diff --git a/ops/chart/crds/organization-crd.yaml b/ops/chart/crds/organization-crd.yaml index 9ec2e9e..10d96e5 100644 --- a/ops/chart/crds/organization-crd.yaml +++ b/ops/chart/crds/organization-crd.yaml @@ -34,6 +34,22 @@ spec: spec: description: OrganizationSpec defines the desired state of Organization properties: + organizationAdmin: + properties: + email: + type: string + firstName: + type: string + lastName: + type: string + userName: + type: string + required: + - email + - firstName + - lastName + - userName + type: object zitadelClusterRef: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' @@ -73,11 +89,15 @@ spec: type: object x-kubernetes-map-type: atomic required: + - organizationAdmin - zitadelClusterRef type: object status: description: OrganizationStatus defines the observed state of Organization properties: + AdminId: + default: "" + type: string conditions: description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying @@ -153,6 +173,7 @@ spec: default: "" type: string required: + - AdminId - orgId type: object type: object diff --git a/src/api/v1alpha1/organization_types.go b/src/api/v1alpha1/organization_types.go index 0f9f16a..0a74374 100644 --- a/src/api/v1alpha1/organization_types.go +++ b/src/api/v1alpha1/organization_types.go @@ -26,6 +26,13 @@ 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 OrganizationAdmin struct { + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + UserName string `json:"userName"` +} + // OrganizationSpec defines the desired state of Organization type OrganizationSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster @@ -33,6 +40,7 @@ type OrganizationSpec struct { // +kubebuilder:validation:Required // +operator-sdk:csv:customresourcedefinitions:type=spec ZitadelClusterRef ZitadelClusterRef `json:"zitadelClusterRef" webhook:"inmutable"` + OrganizationAdmin OrganizationAdmin `json:"organizationAdmin"` } // OrganizationStatus defines the observed state of Organization @@ -45,6 +53,8 @@ type OrganizationStatus struct { Conditions []metav1.Condition `json:"conditions,omitempty"` // +kubebuilder:default="" OrgId string `json:"orgId"` + // +kubebuilder:default="" + AdminId string `json:"AdminId"` } func (d *OrganizationStatus) SetCondition(condition metav1.Condition) { diff --git a/src/api/v1alpha1/zz_generated.deepcopy.go b/src/api/v1alpha1/zz_generated.deepcopy.go index 2f53b23..8c52878 100644 --- a/src/api/v1alpha1/zz_generated.deepcopy.go +++ b/src/api/v1alpha1/zz_generated.deepcopy.go @@ -448,6 +448,21 @@ func (in *Organization) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OrganizationAdmin) DeepCopyInto(out *OrganizationAdmin) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationAdmin. +func (in *OrganizationAdmin) DeepCopy() *OrganizationAdmin { + if in == nil { + return nil + } + out := new(OrganizationAdmin) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OrganizationList) DeepCopyInto(out *OrganizationList) { *out = *in @@ -500,6 +515,7 @@ func (in *OrganizationRef) DeepCopy() *OrganizationRef { func (in *OrganizationSpec) DeepCopyInto(out *OrganizationSpec) { *out = *in out.ZitadelClusterRef = in.ZitadelClusterRef + out.OrganizationAdmin = in.OrganizationAdmin } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OrganizationSpec. diff --git a/src/config/crd/bases/zitadel.topmanage.com_organizations.yaml b/src/config/crd/bases/zitadel.topmanage.com_organizations.yaml index 53f5eea..bd29e5f 100644 --- a/src/config/crd/bases/zitadel.topmanage.com_organizations.yaml +++ b/src/config/crd/bases/zitadel.topmanage.com_organizations.yaml @@ -35,6 +35,22 @@ spec: spec: description: OrganizationSpec defines the desired state of Organization properties: + organizationAdmin: + properties: + email: + type: string + firstName: + type: string + lastName: + type: string + userName: + type: string + required: + - email + - firstName + - lastName + - userName + type: object zitadelClusterRef: description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster Important: Run "make" to regenerate code after modifying this file' @@ -74,11 +90,15 @@ spec: type: object x-kubernetes-map-type: atomic required: + - organizationAdmin - zitadelClusterRef type: object status: description: OrganizationStatus defines the observed state of Organization properties: + AdminId: + default: "" + type: string conditions: description: 'INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run "make" to regenerate code after modifying @@ -154,6 +174,7 @@ spec: default: "" type: string required: + - AdminId - orgId type: object type: object diff --git a/src/internal/controller/organization_controller.go b/src/internal/controller/organization_controller.go index d8de823..2bf9d0a 100644 --- a/src/internal/controller/organization_controller.go +++ b/src/internal/controller/organization_controller.go @@ -26,6 +26,7 @@ import ( condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/controller/zitadel" "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" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" @@ -90,7 +91,28 @@ func newWrappedOrganizationReconciler(client client.Client, refResolver *zitadel } } +type orgReconcilePhase struct { + Name string + Reconcile func(context.Context, *management.Client) error +} + func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClient *management.Client) error { + phases := []orgReconcilePhase{ + { + Name: "organization", + Reconcile: wr.reconcileOrg, + }, + } + for _, p := range phases { + err := p.Reconcile(ctx, ztdClient) + if err != nil { + return err + } + } + return nil +} + +func (wr *wrappedOrganizationReconciler) reconcileOrg(ctx context.Context, ztdClient *management.Client) error { zitadelCluster, err := wr.refResolver.ZitadelCluster(ctx, &wr.organization.Spec.ZitadelClusterRef, wr.organization.Namespace) if err != nil { return err @@ -103,7 +125,6 @@ func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClien return fmt.Errorf("Error getting org: %v", err) } } - // TODO: add initial user if orgRes == nil { resp, err := ztdClient.AddOrg(ctx, &pb.AddOrgRequest{ Name: strings.ToLower(wr.organization.Name), @@ -120,6 +141,70 @@ func (wr *wrappedOrganizationReconciler) Reconcile(ctx context.Context, ztdClien return wr.Client.Status().Patch(ctx, wr.organization, patch) } +func (wr *wrappedOrganizationReconciler) reconcileInitialAdmin(ctx context.Context, ztdClient *management.Client) error { + zitadelCluster, err := wr.refResolver.ZitadelCluster(ctx, &wr.organization.Spec.ZitadelClusterRef, wr.organization.Namespace) + if err != nil { + return err + } + adminUser, err := ztdClient.GetUserByLoginNameGlobal(ctx, &pb.GetUserByLoginNameGlobalRequest{ + LoginName: strings.ToLower(fmt.Sprintf("%s@%s.%s", wr.organization.Spec.OrganizationAdmin.UserName, wr.organization.Name, zitadelCluster.Spec.Host)), + }) + if err != nil { + if !strings.Contains(err.Error(), "could not be found") { + return fmt.Errorf("Error getting admin user: %v", err) + } + } + ctx = middleware.SetOrgID(ctx, wr.organization.Status.OrgId) + var userid string + if adminUser == nil { + resp, err := ztdClient.AddHumanUser(ctx, &pb.AddHumanUserRequest{ + UserName: wr.organization.Spec.OrganizationAdmin.UserName, + Profile: &pb.AddHumanUserRequest_Profile{ + FirstName: wr.organization.Spec.OrganizationAdmin.FirstName, + LastName: wr.organization.Spec.OrganizationAdmin.LastName, + }, + Email: &pb.AddHumanUserRequest_Email{ + Email: wr.organization.Spec.OrganizationAdmin.Email, + IsEmailVerified: false, + }, + }) + userid = resp.UserId + if err != nil { + return fmt.Errorf("Error adding human user: %v", err) + } + { + if _, err := ztdClient.AddOrgMember(ctx, &pb.AddOrgMemberRequest{ + UserId: userid, + Roles: []string{ + "ORG_OWNER", + }, + }); err != nil { + if !strings.Contains(err.Error(), "Errors.Org.Member.RolesNotChanged") { + return fmt.Errorf("Error adding org member: %v", err) + } + } + } + } else { + userid = adminUser.User.Id + } + + { + if _, err := ztdClient.UpdateOrgMember(ctx, &pb.UpdateOrgMemberRequest{ + UserId: userid, + Roles: []string{ + "ORG_OWNER", + }, + }); err != nil { + if !strings.Contains(err.Error(), "Errors.Org.Member.RolesNotChanged") { + return fmt.Errorf("Error updating org member: %v", err) + } + } + } + patch := client.MergeFrom(wr.organization.DeepCopy()) + wr.organization.Status.AdminId = userid + return wr.Status().Patch(ctx, wr.organization, patch) +} + func (wr *wrappedOrganizationReconciler) PatchStatus(ctx context.Context, patcher condition.Patcher) error { patch := client.MergeFrom(wr.organization.DeepCopy()) patcher(&wr.organization.Status) diff --git a/src/internal/controller/zitadelcluster_controller.go b/src/internal/controller/zitadelcluster_controller.go index 3159fd6..33e7a48 100644 --- a/src/internal/controller/zitadelcluster_controller.go +++ b/src/internal/controller/zitadelcluster_controller.go @@ -27,7 +27,6 @@ import ( "time" zitadelv1alpha1 "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/api/v1alpha1" - "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/admin" builder "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/builder" condition "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/condition" "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/configuration" @@ -40,13 +39,10 @@ import ( systemapiaccount "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/systemapi" zitadelClient "bitbucket.org/topmanage-software-engineering/zitadel-k8s-operator/src/pkg/zitadel" "github.com/hashicorp/go-multierror" - "github.com/zitadel/zitadel-go/v2/pkg/client/middleware" "github.com/zitadel/zitadel-go/v2/pkg/client/system" adm "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/admin" authn "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/authn" - "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/management" pb "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/system" - "github.com/zitadel/zitadel-go/v2/pkg/client/zitadel/user" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -153,10 +149,6 @@ func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque Name: "DefaultInstance", Reconcile: r.reconcileDefaultInstance, }, - { - Name: "DefaultOrgManifest", - Reconcile: r.reconcileOrgManifest, - }, { Name: "SMTPConfig", Reconcile: r.reconcileSMTPConfig, @@ -165,14 +157,6 @@ func (r *ZitadelClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque Name: "DomainPolicyConfig", Reconcile: r.reconcileDomainPolicy, }, - { - Name: "InitialAdminSecret", - Reconcile: r.reconcileInitialAdminPassword, - }, - { - Name: "InitialAdmin", - Reconcile: r.reconcileInitialHumanUser, - }, } for _, p := range phases { @@ -461,31 +445,6 @@ func (r *ZitadelClusterReconciler) reconcileDefaultInstance(ctx context.Context, return ctrl.Result{}, nil } -func (r *ZitadelClusterReconciler) reconcileOrgManifest(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { - key := types.NamespacedName{ - Name: zitadel.Spec.FirstOrgName, - Namespace: zitadel.Namespace, - } - desiredOrganization, err := r.Builder.BuildOrganization(builder.OrganizationOpts{ - Key: key, - Zitadel: zitadel, - }, zitadel) - if err != nil { - return ctrl.Result{}, fmt.Errorf("error building default organization: %v", err) - } - - var existingOrganization zitadelv1alpha1.Organization - if err := r.Get(ctx, key, &existingOrganization); err != nil { - if !errors.IsNotFound(err) { - return ctrl.Result{}, fmt.Errorf("error getting Organization: %v", err) - } - if err := r.Create(ctx, desiredOrganization); err != nil { - return ctrl.Result{}, fmt.Errorf("error creating Organization: %v", err) - } - } - return ctrl.Result{}, nil -} - func (r *ZitadelClusterReconciler) reconcileSMTPConfig(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { adminClient, err := zitadelClient.NewAdminClient(ctx, zitadel, *r.RefResolver) if err != nil { @@ -575,115 +534,6 @@ func (r *ZitadelClusterReconciler) reconcileDomainPolicy(ctx context.Context, zi return ctrl.Result{}, nil } -func (r *ZitadelClusterReconciler) reconcileInitialAdminPassword(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { - secretName := admin.AdminPasswordSecretName(zitadel) - key := types.NamespacedName{ - Name: secretName, - Namespace: zitadel.Namespace, - } - _, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, admin.Key, zitadel) - - if err != nil { - return ctrl.Result{}, err - } - return ctrl.Result{}, nil -} - -func (r *ZitadelClusterReconciler) reconcileInitialHumanUser(ctx context.Context, zitadel *zitadelv1alpha1.ZitadelCluster) (ctrl.Result, error) { - managementClient, err := zitadelClient.NewClient(ctx, zitadel, *r.RefResolver) - if err != nil { - return ctrl.Result{}, err - } - defer managementClient.Connection.Close() - - secretName := admin.AdminPasswordSecretName(zitadel) - key := types.NamespacedName{ - Name: secretName, - Namespace: zitadel.Namespace, - } - password, err := r.SecretReconciler.ReconcileRandomPassword(ctx, key, admin.Key, zitadel) - if err != nil { - return ctrl.Result{}, err - } - - org, err := managementClient.GetMyOrg(ctx, &management.GetMyOrgRequest{}) - if err != nil { - return ctrl.Result{}, fmt.Errorf("Error getting org: %v", err) - } - adminUser, err := managementClient.GetUserByLoginNameGlobal(ctx, &management.GetUserByLoginNameGlobalRequest{ - LoginName: strings.ToLower(fmt.Sprintf("%s@%s.%s", admin.AccountName, zitadel.Spec.FirstOrgName, zitadel.Spec.Host)), - }) - if err != nil { - if !strings.Contains(err.Error(), "could not be found") { - return ctrl.Result{}, fmt.Errorf("Error getting admin user: %v", err) - } - } - var userid string - if adminUser == nil { - resp, err := managementClient.AddHumanUser(middleware.SetOrgID(ctx, org.Org.Id), &management.AddHumanUserRequest{ - UserName: admin.AccountName, - Profile: &management.AddHumanUserRequest_Profile{ - FirstName: admin.AccountName, - LastName: admin.AccountName, - NickName: admin.AccountName, - DisplayName: admin.AccountName, - Gender: user.Gender_GENDER_DIVERSE, - PreferredLanguage: "en", - }, - InitialPassword: password, - Email: &management.AddHumanUserRequest_Email{ - Email: "dev@topmanage.com", - IsEmailVerified: true, - }, - }) - userid = resp.UserId - if err != nil { - return ctrl.Result{}, fmt.Errorf("Error adding human user: %v", err) - } - { - if _, err := managementClient.AddOrgMember(middleware.SetOrgID(ctx, org.Org.Id), &management.AddOrgMemberRequest{ - UserId: userid, - Roles: []string{ - "ORG_OWNER", - }, - }); err != nil { - if !strings.Contains(err.Error(), "Errors.Org.Member.RolesNotChanged") { - return ctrl.Result{}, fmt.Errorf("Error adding org member: %v", err) - } - } - } - } else { - userid = adminUser.User.Id - } - - { - if _, err := managementClient.SetHumanPassword(middleware.SetOrgID(ctx, org.Org.Id), &management.SetHumanPasswordRequest{ - UserId: userid, - Password: password, - NoChangeRequired: true, - }); err != nil { - return ctrl.Result{}, fmt.Errorf("Error setting password for member: %v", err) - } - } - { - if _, err := managementClient.UpdateOrgMember(middleware.SetOrgID(ctx, org.Org.Id), &management.UpdateOrgMemberRequest{ - UserId: userid, - Roles: []string{ - "ORG_OWNER", - }, - }); err != nil { - if !strings.Contains(err.Error(), "Errors.Org.Member.RolesNotChanged") { - return ctrl.Result{}, fmt.Errorf("Error updating org member: %v", err) - } - } - - } - - patch := client.MergeFrom(zitadel.DeepCopy()) - zitadel.Status.InitialAdminId = userid - return ctrl.Result{}, r.Status().Patch(ctx, zitadel, patch) -} - func GetIssuer(zitadel *zitadelv1alpha1.ZitadelCluster) string { scheme := "http" if zitadel.Spec.ExternalSecure {