diff --git a/ops/chart/crds/project-crd.yaml b/ops/chart/crds/project-crd.yaml index 5e893a4..70a7a31 100644 --- a/ops/chart/crds/project-crd.yaml +++ b/ops/chart/crds/project-crd.yaml @@ -79,6 +79,21 @@ spec: type: boolean projectRoleCheck: type: boolean + roles: + items: + properties: + displayName: + type: string + group: + type: string + key: + type: string + required: + - displayName + - group + - key + type: object + type: array required: - organizationRef type: object diff --git a/src/api/v1alpha1/project_types.go b/src/api/v1alpha1/project_types.go index 9318f65..0724451 100644 --- a/src/api/v1alpha1/project_types.go +++ b/src/api/v1alpha1/project_types.go @@ -26,6 +26,12 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +type Role struct { + Key string `json:"key"` + DisplayName string `json:"displayName"` + Group string `json:"group"` +} + // ProjectSpec defines the desired state of Project type ProjectSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -35,6 +41,8 @@ type ProjectSpec struct { // +operator-sdk:csv:customresourcedefinitions:type=spec OrganizationRef OrganizationRef `json:"organizationRef"` // +optional + Roles []Role `json:"roles"` + // +optional ProjectRoleAssertion bool `json:"projectRoleAssertion,omitempty"` // +optional ProjectRoleCheck bool `json:"projectRoleCheck,omitempty"` diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 1efee6e..1c3af72 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -399,7 +399,7 @@ func (in *Project) DeepCopyInto(out *Project) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -473,6 +473,11 @@ func (in *ProjectRef) DeepCopy() *ProjectRef { func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) { *out = *in out.OrganizationRef = in.OrganizationRef + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]Role, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec. @@ -507,6 +512,21 @@ func (in *ProjectStatus) DeepCopy() *ProjectStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Role) DeepCopyInto(out *Role) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. +func (in *Role) DeepCopy() *Role { + if in == nil { + return nil + } + out := new(Role) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ZitadelCluster) DeepCopyInto(out *ZitadelCluster) { *out = *in diff --git a/src/config/crd/bases/zitadel.topmanage.com_projects.yaml b/src/config/crd/bases/zitadel.topmanage.com_projects.yaml index 5ef2577..8e734ae 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_projects.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_projects.yaml @@ -80,6 +80,21 @@ spec: type: boolean projectRoleCheck: type: boolean + roles: + items: + properties: + displayName: + type: string + group: + type: string + key: + type: string + required: + - displayName + - group + - key + type: object + type: array required: - organizationRef type: object diff --git a/src/internal/controller/project_controller.go b/src/internal/controller/project_controller.go index 24b4a5f..d2f021f 100644 --- a/src/internal/controller/project_controller.go +++ b/src/internal/controller/project_controller.go @@ -28,6 +28,7 @@ import ( "github.com/zitadel/zitadel-go/v2/pkg/client/management" "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" + "golang.org/x/exp/maps" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -91,7 +92,32 @@ func newWrappedProjectReconciler(client client.Client, refResolver *zitadelv1alp } } +type projectReconcilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []projectReconcilePhase{ + { + Name: "project", + Reconcile: wr.reconcileProject, + }, + { + Name: "roles", + Reconcile: wr.reconcileRoles, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedProjectReconciler) reconcileProject(ctx context.Context, ztdClient *management.Client) error { org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) if err != nil { return err @@ -138,6 +164,59 @@ func (wr *wrappedProjectReconciler) Reconcile(ctx context.Context, ztdClient *ma return wr.Client.Status().Patch(ctx, wr.project, patch) } +func (wr *wrappedProjectReconciler) reconcileRoles(ctx context.Context, ztdClient *management.Client) error { + org, err := wr.refResolver.OrganizationRef(ctx, &wr.project.Spec.OrganizationRef, wr.project.Namespace) + if err != nil { + return err + } + + resp, err := ztdClient.ListProjectRoles(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.ListProjectRolesRequest{ + ProjectId: wr.project.Status.ProjectId, + }) + if err != nil { + return fmt.Errorf("Could not list project roles: %v", err) + } + roles := map[string]*pb.BulkAddProjectRolesRequest_Role{} + deleteRoles := []*pb.BulkAddProjectRolesRequest_Role{} + for _, role := range wr.project.Spec.Roles { + roles[role.Key] = &pb.BulkAddProjectRolesRequest_Role{ + Key: role.Key, + DisplayName: role.DisplayName, + Group: role.Group, + } + } + + for _, role := range resp.Result { + if r, ok := roles[role.Key]; ok { + if r.DisplayName != role.DisplayName || r.Group != role.Group { + deleteRoles = append(deleteRoles, r) + } else { + delete(roles, role.Key) + } + } else { + delete(roles, role.Key) + } + } + + for _, dRole := range deleteRoles { + if _, err = ztdClient.RemoveProjectRole(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.RemoveProjectRoleRequest{ + ProjectId: wr.project.Status.ProjectId, + RoleKey: dRole.Key, + }); err != nil { + return fmt.Errorf("Error removing project role: %v", err) + } + } + if len(roles) > 0 { + _, err = ztdClient.BulkAddProjectRoles(middleware.SetOrgID(ctx, org.Status.OrgId), &pb.BulkAddProjectRolesRequest{ + ProjectId: wr.project.Status.ProjectId, + Roles: maps.Values(roles)}) + if err != nil { + return fmt.Errorf("Could not add roles to project: %v", err) + } + } + return nil +} + func (wr *wrappedProjectReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { patch := client.MergeFrom(wr.project.DeepCopy()) patcher(&wr.project.Status)