Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go/nautobotop/api/v1alpha1/nautobot_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type NautobotSpec struct {
TenantGroupRef []ConfigMapRef `json:"tenantGroupRef,omitempty"`
TenantRef []ConfigMapRef `json:"tenantRef,omitempty"`
DeviceRef []ConfigMapRef `json:"deviceRef,omitempty"`
PermissionGroupRef []ConfigMapRef `json:"permissionGroupRef,omitempty"`
}

// NautobotStatus defines the observed state of Nautobot.
Expand Down
1 change: 1 addition & 0 deletions go/nautobotop/internal/controller/dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func TestTopologicalSort(t *testing.T) {
{Name: "namespace", DependsOn: []string{"location", "tenant"}},
{Name: "vlan", DependsOn: []string{"vlanGroup", "location", "tenant", "role"}},
{Name: "prefix", DependsOn: []string{"namespace", "rir", "location", "vlan", "tenant", "role"}},
{Name: "permissionGroup"},
},
want: []string{
"clusterGroup", "clusterType", "deviceType", "locationTypes",
Expand Down
18 changes: 18 additions & 0 deletions go/nautobotop/internal/controller/nautobot_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

nbClient "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/sync"
syncadmin "github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/sync/admin"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Expand Down Expand Up @@ -116,6 +117,7 @@ func (r *NautobotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
{name: "cluster", dependsOn: []string{"clusterType", "clusterGroup", "location", "device"}, configRefs: nautobotCR.Spec.ClusterRef, syncFunc: r.syncCluster},
// Depends on: namespace, rir, location, vlan, tenant, role
{name: "prefix", dependsOn: []string{"namespace", "rir", "location", "vlan", "tenant", "role"}, configRefs: nautobotCR.Spec.PrefixRef, syncFunc: r.syncPrefix},
{name: "permissionGroup", configRefs: nautobotCR.Spec.PermissionGroupRef, syncFunc: r.syncPermissionGroup},
}

// Resolve execution order using topological sort (Kahn's algorithm)
Expand Down Expand Up @@ -298,6 +300,22 @@ func (r *NautobotReconciler) syncRack(ctx context.Context,
return nil
}

func (r *NautobotReconciler) syncPermissionGroup(ctx context.Context,
nautobotClient *nbClient.NautobotClient,
permissionGroupData map[string]string,
) error {
log := logf.FromContext(ctx)
log.Info("syncing permission groups", "count", len(permissionGroupData))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think log statements should be at the end of the code. In case of count zero, since this function is later on calling SyncAll, (len(permissionGroupData)) at the end should be logged.

if len(permissionGroupData) == 0 {
return nil
}
syncSvc := syncadmin.NewPermissionGroupSync(nautobotClient)
if err := syncSvc.SyncAll(ctx, permissionGroupData); err != nil {
return fmt.Errorf("failed to sync permission groups: %w", err)
}
return nil
}

func (r *NautobotReconciler) syncLocation(ctx context.Context,
nautobotClient *nbClient.NautobotClient,
locationType map[string]string,
Expand Down
78 changes: 78 additions & 0 deletions go/nautobotop/internal/nautobot/admin/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package admin

import (
"context"
"net/http"

"github.com/charmbracelet/log"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A suggestion, we should be using "k8s.io/klog/v2". This has more chance of being maintained and up to date with latest vulnerabilities.
Reference Cluster API vSphere

nb "github.com/nautobot/go-nautobot/v3"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers"
)

type GroupService struct {
client *client.NautobotClient
}

func NewGroupService(nautobotClient *client.NautobotClient) *GroupService {
return &GroupService{
client: nautobotClient,
}
}

func (s *GroupService) Create(ctx context.Context, req nb.GroupRequest) (*nb.Group, error) {
group, resp, err := s.client.APIClient.UsersAPI.UsersGroupsCreate(ctx).GroupRequest(req).Execute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again an observation! This calls multiple nested functions. We should try to make this more modular.

if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("createNewGroup", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this different from logging?

return nil, err
}
log.Info("CreateGroup", "created group", group.Name)
return group, nil
}

func (s *GroupService) GetByName(ctx context.Context, name string) *nb.Group {
list, resp, err := s.client.APIClient.UsersAPI.UsersGroupsList(ctx).Name([]string{name}).Execute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same observation, this call should be more modular.

if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("GetGroupByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString)
return nil
}
if list == nil || len(list.Results) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks slightly strange, if list is nil, it should be returned as an empty list instead of nil.

return nil
}
return &list.Results[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only first element is to be returned, line 35 should be calling a function where it gets that one particular element. We should not be processing the entire list here.

}

func (s *GroupService) ListAll(ctx context.Context) []nb.Group {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function name should be ListAllGroups for better readability.

return helpers.PaginatedList(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return should not hold a function call.

ctx,
func(ctx context.Context, limit, offset int32) ([]nb.Group, int32, *http.Response, error) {
list, resp, err := s.client.APIClient.UsersAPI.UsersGroupsList(ctx).
Limit(limit).
Offset(offset).
Execute()
if err != nil {
return nil, 0, resp, err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning 4 items separately,nil, 0, resp, errshould be wrapped inside a structure and that structure should be returned.

}
if list == nil {
return nil, 0, resp, nil
}
return list.Results, list.Count, resp, nil
},
s.client.AddReport,
"ListAllGroups",
)
}

// GetOrCreate fetches a group by name or creates it if it doesn't exist.
func (s *GroupService) GetOrCreate(ctx context.Context, name string) (*nb.Group, error) {
existing := s.GetByName(ctx, name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existing , err:= s.GetByName(ctx, name). This is more common practice in golang.

if existing != nil {
return existing, nil
}

log.Info("group not found, creating", "name", name)
req := nb.GroupRequest{Name: name}
return s.Create(ctx, req)
}
155 changes: 155 additions & 0 deletions go/nautobotop/internal/nautobot/admin/permission.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package admin

import (
"context"
"net/http"

"github.com/charmbracelet/log"
nb "github.com/nautobot/go-nautobot/v3"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/client"
"github.com/rackerlabs/understack/go/nautobotop/internal/nautobot/helpers"
)

type PermissionService struct {
client *client.NautobotClient
}

func NewPermissionService(nautobotClient *client.NautobotClient) *PermissionService {
return &PermissionService{
client: nautobotClient,
}
}

func (s *PermissionService) Create(ctx context.Context, req nb.ObjectPermissionRequest) (*nb.ObjectPermission, error) {
perm, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsCreate(ctx).ObjectPermissionRequest(req).Execute()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, we can make this call more modular.

if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("createNewPermission", "failed to create", "model", req.Name, "error", err.Error(), "response_body", bodyString)
return nil, err
}
log.Info("CreatePermission", "created permission", perm.Name)
return perm, nil
}

func (s *PermissionService) GetByName(ctx context.Context, name string) *nb.ObjectPermission {
list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx).Name([]string{name}).Execute()
if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("GetPermissionByName", "failed to get", "name", name, "error", err.Error(), "response_body", bodyString)
return nil
}
if list == nil || len(list.Results) == 0 {
return nil
}
return &list.Results[0]
}

func (s *PermissionService) GetByID(ctx context.Context, id string) *nb.ObjectPermission {
if id == "" {
return nil
}
list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx).Id([]string{id}).Execute()
if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("GetPermissionByID", "failed to get", "id", id, "error", err.Error(), "response_body", bodyString)
return nil
}
if list == nil || len(list.Results) == 0 {
return nil
}
return &list.Results[0]
}

func (s *PermissionService) ListAll(ctx context.Context) []nb.ObjectPermission {
return helpers.PaginatedList(
ctx,
func(ctx context.Context, limit, offset int32) ([]nb.ObjectPermission, int32, *http.Response, error) {
list, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsList(ctx).
Limit(limit).
Offset(offset).
Execute()
if err != nil {
return nil, 0, resp, err
}
if list == nil {
return nil, 0, resp, nil
}
return list.Results, list.Count, resp, nil
},
s.client.AddReport,
"ListAllPermissions",
)
}

// PermissionUpdatePayload represents the desired state of a permission.
type PermissionUpdatePayload struct {
Name string `json:"name"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
Actions interface{} `json:"actions"`
ObjectTypes []string `json:"object_types"`
Constraints interface{} `json:"constraints"`
Groups []GroupRef `json:"groups"`
}

// GroupRef is a minimal group reference for the permission update payload.
type GroupRef struct {
ID int32 `json:"id"`
}

// Update performs a full PUT via the SDK client
func (s *PermissionService) Update(ctx context.Context, id string, payload PermissionUpdatePayload) error {
// Build the SDK request
req := *nb.NewObjectPermissionRequest(payload.ObjectTypes, payload.Name, payload.Actions)
req.Enabled = nb.PtrBool(payload.Enabled)
if payload.Description != "" {
req.Description = nb.PtrString(payload.Description)
}

// Set groups
groups := make([]nb.ApprovalWorkflowStageResponseApprovalWorkflowStage, 0, len(payload.Groups))
for _, g := range payload.Groups {
gid := g.ID
groups = append(groups, nb.ApprovalWorkflowStageResponseApprovalWorkflowStage{
Id: &nb.ApprovalWorkflowApprovalWorkflowDefinitionId{Int32: &gid},
})
}
req.Groups = groups

if payload.Constraints != nil {
req.Constraints = payload.Constraints
} else {
req.AdditionalProperties = map[string]interface{}{
"constraints": nil,
}
}

perm, resp, err := s.client.APIClient.UsersAPI.UsersPermissionsUpdate(ctx, id).ObjectPermissionRequest(req).Execute()
if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("UpdatePermission", "failed to update", "id", id, "model", payload.Name, "error", err.Error(), "response_body", bodyString)
return err
}
log.Info("successfully updated permission", "id", id, "model", perm.GetName())
return nil
}

func (s *PermissionService) Destroy(ctx context.Context, id string) error {
owned, err := s.client.IsCreatedByUser(ctx, id)
if err != nil {
s.client.AddReport("DestroyPermission", "failed to check ownership", "id", id, "error", err.Error())
return err
}
if !owned {
log.Warn("skipping destroy, object not created by user", "id", id, "user", s.client.Username)
return nil
}

resp, err := s.client.APIClient.UsersAPI.UsersPermissionsDestroy(ctx, id).Execute()
if err != nil {
bodyString := helpers.ReadResponseBody(resp)
s.client.AddReport("DestroyPermission", "failed to destroy", "id", id, "error", err.Error(), "response_body", bodyString)
return err
}
return nil
}
48 changes: 48 additions & 0 deletions go/nautobotop/internal/nautobot/models/admin/permissionGroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package admin

// Permission represents a single Nautobot ObjectPermission with its group assignments.
type Permission struct {
Name string `json:"name" yaml:"name"`
Description string `json:"description" yaml:"description"`
Enabled *bool `json:"enabled" yaml:"enabled"`
CanView bool `json:"can_view" yaml:"can_view"`
CanAdd bool `json:"can_add" yaml:"can_add"`
CanChange bool `json:"can_change" yaml:"can_change"`
CanDelete bool `json:"can_delete" yaml:"can_delete"`
AdditionalActions []string `json:"additional_actions" yaml:"additional_actions"`
ObjectTypes []string `json:"object_types" yaml:"object_types"`
Groups []string `json:"groups" yaml:"groups"`
Constraints string `json:"constraints" yaml:"constraints"`
}

// PermissionGroupConfig represents the top-level structure for the permissions configmap.
type PermissionGroupConfig struct {
Permissions []Permission `json:"permissions" yaml:"permissions"`
}

// BuildActions constructs the actions list from the boolean flags and additional actions.
func (p *Permission) BuildActions() []string {
var actions []string
if p.CanView {
actions = append(actions, "view")
}
if p.CanAdd {
actions = append(actions, "add")
}
if p.CanChange {
actions = append(actions, "change")
}
if p.CanDelete {
actions = append(actions, "delete")
}
actions = append(actions, p.AdditionalActions...)
return actions
}

// IsEnabled returns whether the permission is enabled (defaults to true if not set).
func (p *Permission) IsEnabled() bool {
if p.Enabled == nil {
return true
}
return *p.Enabled
}
Loading
Loading