diff --git a/api/v1alpha1/group_types.go b/api/v1alpha1/group_types.go index a648447aa..05ea78d50 100644 --- a/api/v1alpha1/group_types.go +++ b/api/v1alpha1/group_types.go @@ -34,6 +34,13 @@ type GroupResourceSpec struct { // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" // +orc:kustomize:ref=Domain DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` + + // userRefs contains ORC Users that belong to this group. + // +kubebuilder:validation:MaxItems:=1024 + // +listType=set + // +optional + // +orc:kustomize:ref=User + UserRefs []KubernetesNameRef `json:"userRefs,omitempty"` } // GroupFilter defines an existing resource by its properties @@ -65,4 +72,11 @@ type GroupResourceStatus struct { // +kubebuilder:validation:MaxLength=1024 // +optional DomainID string `json:"domainID,omitempty"` + + // userIDs lists the IDs of the Users in this group. + // +kubebuilder:validation:MaxItems=1024 + // +kubebuilder:validation:items:MaxLength=1024 + // +optional + // +listType=set + UserIDs []string `json:"userIDs,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 966323690..41774ad7b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2304,6 +2304,11 @@ func (in *GroupResourceSpec) DeepCopyInto(out *GroupResourceSpec) { *out = new(KubernetesNameRef) **out = **in } + if in.UserRefs != nil { + in, out := &in.UserRefs, &out.UserRefs + *out = make([]KubernetesNameRef, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupResourceSpec. @@ -2319,6 +2324,11 @@ func (in *GroupResourceSpec) DeepCopy() *GroupResourceSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupResourceStatus) DeepCopyInto(out *GroupResourceStatus) { *out = *in + if in.UserIDs != nil { + in, out := &in.UserIDs, &out.UserIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupResourceStatus. @@ -2385,7 +2395,7 @@ func (in *GroupStatus) DeepCopyInto(out *GroupStatus) { if in.Resource != nil { in, out := &in.Resource, &out.Resource *out = new(GroupResourceStatus) - **out = **in + (*in).DeepCopyInto(*out) } if in.LastSyncTime != nil { in, out := &in.LastSyncTime, &out.LastSyncTime diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 71e8d1076..0a2f8de23 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -4525,6 +4525,26 @@ func schema_openstack_resource_controller_v3_api_v1alpha1_GroupResourceSpec(ref Format: "", }, }, + "userRefs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "userRefs contains ORC Users that belong to this group.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, }, }, @@ -4559,6 +4579,26 @@ func schema_openstack_resource_controller_v3_api_v1alpha1_GroupResourceStatus(re Format: "", }, }, + "userIDs": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "userIDs lists the IDs of the Users in this group.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, }, }, }, diff --git a/config/crd/bases/openstack.k-orc.cloud_groups.yaml b/config/crd/bases/openstack.k-orc.cloud_groups.yaml index 49e5ad809..7a1cc5bb3 100644 --- a/config/crd/bases/openstack.k-orc.cloud_groups.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_groups.yaml @@ -172,6 +172,15 @@ spec: maxLength: 64 minLength: 1 type: string + userRefs: + description: userRefs contains ORC Users that belong to this group. + items: + maxLength: 253 + minLength: 1 + type: string + maxItems: 1024 + type: array + x-kubernetes-list-type: set type: object resyncPeriod: description: |- @@ -307,6 +316,14 @@ spec: not be unique. maxLength: 1024 type: string + userIDs: + description: userIDs lists the IDs of the Users in this group. + items: + maxLength: 1024 + type: string + maxItems: 1024 + type: array + x-kubernetes-list-type: set type: object type: object required: diff --git a/config/samples/openstack_v1alpha1_group.yaml b/config/samples/openstack_v1alpha1_group.yaml index 50df5cec0..6bae94aee 100644 --- a/config/samples/openstack_v1alpha1_group.yaml +++ b/config/samples/openstack_v1alpha1_group.yaml @@ -21,3 +21,5 @@ spec: resource: description: Sample Group domainRef: group-sample + userRefs: + - user-sample diff --git a/examples/components/kustomizeconfig/kustomizeconfig.yaml b/examples/components/kustomizeconfig/kustomizeconfig.yaml index cca04a65c..ac6c69a1a 100644 --- a/examples/components/kustomizeconfig/kustomizeconfig.yaml +++ b/examples/components/kustomizeconfig/kustomizeconfig.yaml @@ -286,6 +286,8 @@ nameReference: path: spec/import/filter/userRef - kind: ApplicationCredential path: spec/resource/userRef + - kind: Group + path: spec/resource/userRefs[] - kind: RoleAssignment path: spec/import/filter/userRef - kind: RoleAssignment diff --git a/internal/controllers/group/actuator.go b/internal/controllers/group/actuator.go index c26b8825a..7dd39ed91 100644 --- a/internal/controllers/group/actuator.go +++ b/internal/controllers/group/actuator.go @@ -19,9 +19,11 @@ package group import ( "context" "iter" + "slices" "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/groups" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,7 +39,7 @@ import ( // OpenStack resource types type ( - osResourceT = groups.Group + osResourceT = osclients.GroupExt createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT] deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT] @@ -228,9 +230,80 @@ func handleDescriptionUpdate(updateOpts *groups.UpdateOpts, resource *resourceSp } } +func (actuator groupActuator) reconcileUserRefs(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + + orcUsers, usersDepRS := userDependency.RequireDependencies( + ctx, actuator.k8sClient, obj, orcv1alpha1.IsAvailable, + ) + + reconcileStatus := progress.NewReconcileStatus().WithReconcileStatus(usersDepRS) + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { + return reconcileStatus + } + + // This may remove users that were added to the group out-of-band. + toAdd, toDelete := handleUserRefsUpdate(orcUsers, osResource.UserIDs) + + log.V(logging.Debug).Info("user ref changes", "toAdd", toAdd, "toDelete", toDelete) + for existingUserID := range toDelete { + if err := actuator.osClient.RemoveUser(ctx, osResource.ID, existingUserID); err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "error removing user from group: "+err.Error(), err) + } + return progress.WrapError(err) + } + log.V(logging.Debug).Info("user removed from group", "userID", existingUserID) + } + + for expectedUserID := range toAdd { + if err := actuator.osClient.AddUser(ctx, osResource.ID, expectedUserID); err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "error adding user to group: "+err.Error(), err) + } + return progress.WrapError(err) + } + log.V(logging.Debug).Info("user added to group", "userID", expectedUserID) + } + + if len(toAdd) > 0 || len(toDelete) > 0 { + osUsers, err := actuator.osClient.ListUsers(ctx, osResource.ID) + if err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "error listing users in group: "+err.Error(), err) + } + return progress.WrapError(err) + } + + userIDs := make([]string, 0, len(osUsers)) + for _, u := range osUsers { + userIDs = append(userIDs, u.ID) + } + slices.Sort(userIDs) + osResource.UserIDs = userIDs + } + + return nil +} + +func handleUserRefsUpdate(orcUsers map[string]*orcv1alpha1.User, osUsers []string) (sets.Set[string], sets.Set[string]) { + expectedUsersMap := sets.New[string]() + for _, u := range orcUsers { + expectedUsersMap.Insert(ptr.Deref(u.Status.ID, "")) + } + + existingUsersMap := sets.New[string]() + for _, u := range osUsers { + existingUsersMap.Insert(u) + } + + return expectedUsersMap.Difference(existingUsersMap), existingUsersMap.Difference(expectedUsersMap) +} + func (actuator groupActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { return []resourceReconciler{ actuator.updateResource, + actuator.reconcileUserRefs, }, nil } diff --git a/internal/controllers/group/actuator_test.go b/internal/controllers/group/actuator_test.go index c6a47d3d4..85cd985dc 100644 --- a/internal/controllers/group/actuator_test.go +++ b/internal/controllers/group/actuator_test.go @@ -21,6 +21,7 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/groups" orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v3/api/v1alpha1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/utils/ptr" ) @@ -73,7 +74,11 @@ func TestHandleNameUpdate(t *testing.T) { resource.Spec = orcv1alpha1.GroupSpec{ Resource: &orcv1alpha1.GroupResourceSpec{Name: tt.newValue}, } - osResource := &osResourceT{Name: tt.existingValue} + osResource := &osResourceT{ + Group: groups.Group{ + Name: tt.existingValue, + }, + } updateOpts := groups.UpdateOpts{} handleNameUpdate(&updateOpts, resource, osResource) @@ -104,7 +109,10 @@ func TestHandleDescriptionUpdate(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { resource := &orcv1alpha1.GroupResourceSpec{Description: tt.newValue} - osResource := &osResourceT{Description: tt.existingValue} + osResource := &osResourceT{ + Group: groups.Group{ + Description: tt.existingValue}, + } updateOpts := groups.UpdateOpts{} handleDescriptionUpdate(&updateOpts, resource, osResource) @@ -117,3 +125,161 @@ func TestHandleDescriptionUpdate(t *testing.T) { } } + +func TestHandleUserRefsUpdate(t *testing.T) { + userName1 := "user1" + userID1 := "05648b1ac8c44e699de20c378c3f8cc7" + userName2 := "user2" + userID2 := "7df7160f73d64c6ab6a08de445aae10b" + + testCases := []struct { + name string + + orcUsers map[string]*orcv1alpha1.User + osUsers []string + + expectedToAdd sets.Set[string] + expectedToDelete sets.Set[string] + }{ + { + name: "Both empty", + orcUsers: map[string]*orcv1alpha1.User{}, + osUsers: []string{}, + expectedToAdd: sets.New[string](), + expectedToDelete: sets.New[string](), + }, + { + name: "Both nil", + orcUsers: nil, + osUsers: nil, + expectedToAdd: sets.New[string](), + expectedToDelete: sets.New[string](), + }, + { + name: "Equal", + orcUsers: map[string]*orcv1alpha1.User{ + userName1: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID1), + }, + }, + }, + osUsers: []string{ + userID1, + }, + expectedToAdd: sets.New[string](), + expectedToDelete: sets.New[string](), + }, + { + name: "New user", + orcUsers: map[string]*orcv1alpha1.User{ + userName1: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID1), + }, + }, + }, + osUsers: []string{}, + expectedToAdd: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID1) + return s + }(), + expectedToDelete: sets.New[string](), + }, + { + name: "Remove user", + orcUsers: map[string]*orcv1alpha1.User{}, + osUsers: []string{ + userID1, + }, + expectedToAdd: sets.New[string](), + expectedToDelete: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID1) + return s + }(), + }, + { + name: "Add more user", + orcUsers: map[string]*orcv1alpha1.User{ + userName1: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID1), + }, + }, + userName2: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID2), + }, + }, + }, + osUsers: []string{ + userID1, + }, + expectedToAdd: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID2) + return s + }(), + expectedToDelete: sets.New[string](), + }, + { + name: "Delete a user when there are more than one", + orcUsers: map[string]*orcv1alpha1.User{ + userName1: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID1), + }, + }, + }, + osUsers: []string{ + userID1, + userID2, + }, + expectedToAdd: sets.New[string](), + expectedToDelete: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID2) + return s + }(), + }, + { + name: "Add/delete users at the same time", + orcUsers: map[string]*orcv1alpha1.User{ + userName1: { + Status: orcv1alpha1.UserStatus{ + ID: ptr.To(userID1), + }, + }, + }, + osUsers: []string{ + userID2, + }, + expectedToAdd: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID1) + return s + }(), + expectedToDelete: func() sets.Set[string] { + s := sets.New[string]() + s.Insert(userID2) + return s + }(), + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + toAdd, toDelete := handleUserRefsUpdate(tt.orcUsers, tt.osUsers) + + if !tt.expectedToAdd.Equal(toAdd) { + t.Errorf("Expected change: %v, got: %v", tt.expectedToAdd, toAdd) + } + + if !tt.expectedToDelete.Equal(toDelete) { + t.Errorf("Expected change: %v, got: %v", tt.expectedToDelete, toDelete) + } + }) + } +} diff --git a/internal/controllers/group/controller.go b/internal/controllers/group/controller.go index cffe24649..03ece24b9 100644 --- a/internal/controllers/group/controller.go +++ b/internal/controllers/group/controller.go @@ -69,6 +69,24 @@ var domainDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.GroupL finalizer, externalObjectFieldOwner, ) +var userDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.GroupList, *orcv1alpha1.User]( + "spec.resource.userRefs", + func(group *orcv1alpha1.Group) []string { + resource := group.Spec.Resource + if resource == nil || len(resource.UserRefs) == 0 { + return nil + } + + users := make([]string, 0, len(resource.UserRefs)) + for _, u := range resource.UserRefs { + users = append(users, string(u)) + } + + return users + }, + finalizer, externalObjectFieldOwner, +) + var domainImportDependency = dependency.NewDependency[*orcv1alpha1.GroupList, *orcv1alpha1.Domain]( "spec.import.filter.domainRef", func(group *orcv1alpha1.Group) []string { @@ -90,6 +108,11 @@ func (c *groupReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c return err } + userWatchEventHandler, err := userDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + domainImportWatchEventHandler, err := domainImportDependency.WatchEventHandler(log, k8sClient) if err != nil { return err @@ -100,6 +123,9 @@ func (c *groupReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c Watches(&orcv1alpha1.Domain{}, domainWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), ). + Watches(&orcv1alpha1.User{}, userWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.User{})), + ). // A second watch is necessary because we need a different handler that omits deletion guards Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), @@ -108,6 +134,7 @@ func (c *groupReconcilerConstructor) SetupWithManager(ctx context.Context, mgr c if err := errors.Join( domainDependency.AddToManager(ctx, mgr), + userDependency.AddToManager(ctx, mgr), domainImportDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), diff --git a/internal/controllers/group/status.go b/internal/controllers/group/status.go index ae1464bd3..858eee204 100644 --- a/internal/controllers/group/status.go +++ b/internal/controllers/group/status.go @@ -51,6 +51,7 @@ func (groupStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.Group, o func (groupStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { resourceStatus := orcapplyconfigv1alpha1.GroupResourceStatus(). WithDomainID(osResource.DomainID). + WithUserIDs(osResource.UserIDs...). WithName(osResource.Name) if osResource.Description != "" { diff --git a/internal/controllers/group/tests/group-create-full/00-assert.yaml b/internal/controllers/group/tests/group-create-full/00-assert.yaml index 58b22973a..be1c09e61 100644 --- a/internal/controllers/group/tests/group-create-full/00-assert.yaml +++ b/internal/controllers/group/tests/group-create-full/00-assert.yaml @@ -26,7 +26,11 @@ resourceRefs: kind: Domain name: group-create-full ref: domain + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-create-full + ref: user assertAll: - celExpr: "group.status.id != ''" - celExpr: "group.status.resource.domainID == domain.status.id" - + - celExpr: "user.status.id in group.status.resource.userIDs" diff --git a/internal/controllers/group/tests/group-create-full/00-create-resource.yaml b/internal/controllers/group/tests/group-create-full/00-create-resource.yaml index 1edb45a68..e2084d180 100644 --- a/internal/controllers/group/tests/group-create-full/00-create-resource.yaml +++ b/internal/controllers/group/tests/group-create-full/00-create-resource.yaml @@ -5,12 +5,24 @@ metadata: name: group-create-full spec: cloudCredentialsRef: - cloudName: openstack-admin + cloudName: openstack-admin secretName: openstack-clouds managementPolicy: managed resource: {} --- apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: group-create-full +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 kind: Group metadata: name: group-create-full @@ -23,3 +35,5 @@ spec: name: group-create-full-override description: Group from "create full" test domainRef: group-create-full + userRefs: + - group-create-full diff --git a/internal/controllers/group/tests/group-dependency/00-assert.yaml b/internal/controllers/group/tests/group-dependency/00-assert.yaml index dd71e1da9..7184a70b8 100644 --- a/internal/controllers/group/tests/group-dependency/00-assert.yaml +++ b/internal/controllers/group/tests/group-dependency/00-assert.yaml @@ -28,3 +28,19 @@ status: message: Waiting for Domain/group-dependency to be created status: "True" reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-dependency-no-user +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: Waiting for User/group-dependency to be created + status: "True" + reason: Progressing + diff --git a/internal/controllers/group/tests/group-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/group/tests/group-dependency/00-create-resources-missing-deps.yaml index 637bc09bb..642148202 100644 --- a/internal/controllers/group/tests/group-dependency/00-create-resources-missing-deps.yaml +++ b/internal/controllers/group/tests/group-dependency/00-create-resources-missing-deps.yaml @@ -21,3 +21,16 @@ spec: secretName: group-dependency managementPolicy: managed resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-dependency-no-user +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + userRefs: + - group-dependency diff --git a/internal/controllers/group/tests/group-dependency/01-assert.yaml b/internal/controllers/group/tests/group-dependency/01-assert.yaml index 443a85a00..a9f348a52 100644 --- a/internal/controllers/group/tests/group-dependency/01-assert.yaml +++ b/internal/controllers/group/tests/group-dependency/01-assert.yaml @@ -29,6 +29,21 @@ status: status: "False" reason: Success --- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-dependency-no-user +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- apiVersion: kuttl.dev/v1beta1 kind: TestAssert resourceRefs: @@ -40,5 +55,14 @@ resourceRefs: kind: Domain name: group-dependency ref: domain + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-dependency-no-user + ref: group1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-dependency + ref: user assertAll: - - celExpr: "group.status.resource.domainID == domain.status.id" \ No newline at end of file + - celExpr: "group.status.resource.domainID == domain.status.id" + - celExpr: "user.status.id in group1.status.resource.userIDs" diff --git a/internal/controllers/group/tests/group-dependency/01-create-dependencies.yaml b/internal/controllers/group/tests/group-dependency/01-create-dependencies.yaml index 1f49e0737..2d95f2360 100644 --- a/internal/controllers/group/tests/group-dependency/01-create-dependencies.yaml +++ b/internal/controllers/group/tests/group-dependency/01-create-dependencies.yaml @@ -15,3 +15,14 @@ spec: secretName: openstack-clouds managementPolicy: managed resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} diff --git a/internal/controllers/group/tests/group-dependency/03-assert.yaml b/internal/controllers/group/tests/group-dependency/03-assert.yaml index 8b0bf5f5c..e676d60e7 100644 --- a/internal/controllers/group/tests/group-dependency/03-assert.yaml +++ b/internal/controllers/group/tests/group-dependency/03-assert.yaml @@ -10,8 +10,14 @@ resourceRefs: kind: Secret name: group-dependency ref: secret + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-dependency + ref: user assertAll: - celExpr: "domain.metadata.deletionTimestamp != 0" - celExpr: "'openstack.k-orc.cloud/group' in domain.metadata.finalizers" - celExpr: "secret.metadata.deletionTimestamp != 0" - celExpr: "'openstack.k-orc.cloud/group' in secret.metadata.finalizers" + - celExpr: "user.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/group' in user.metadata.finalizers" diff --git a/internal/controllers/group/tests/group-dependency/03-delete-dependencies.yaml b/internal/controllers/group/tests/group-dependency/03-delete-dependencies.yaml index 5ae3d23ae..3a1875a42 100644 --- a/internal/controllers/group/tests/group-dependency/03-delete-dependencies.yaml +++ b/internal/controllers/group/tests/group-dependency/03-delete-dependencies.yaml @@ -7,3 +7,5 @@ commands: namespaced: true - command: kubectl delete secret group-dependency --wait=false namespaced: true + - command: kubectl delete user group-dependency --wait=false + namespaced: true diff --git a/internal/controllers/group/tests/group-dependency/04-assert.yaml b/internal/controllers/group/tests/group-dependency/04-assert.yaml index c45f641a1..5a7b5462d 100644 --- a/internal/controllers/group/tests/group-dependency/04-assert.yaml +++ b/internal/controllers/group/tests/group-dependency/04-assert.yaml @@ -7,3 +7,5 @@ commands: skipLogOutput: true - script: "! kubectl get secret group-dependency --namespace $NAMESPACE" skipLogOutput: true +- script: "! kubectl get user group-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/group/tests/group-dependency/04-delete-resources.yaml b/internal/controllers/group/tests/group-dependency/04-delete-resources.yaml index 6333c53b2..5ac7bd945 100644 --- a/internal/controllers/group/tests/group-dependency/04-delete-resources.yaml +++ b/internal/controllers/group/tests/group-dependency/04-delete-resources.yaml @@ -8,3 +8,6 @@ delete: - apiVersion: openstack.k-orc.cloud/v1alpha1 kind: Group name: group-dependency-no-domain +- apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-dependency-no-user diff --git a/internal/controllers/group/tests/group-update-user/00-assert.yaml b/internal/controllers/group/tests/group-update-user/00-assert.yaml new file mode 100644 index 000000000..49576cc40 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/00-assert.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-update-user + ref: group +assertAll: + - celExpr: "!has(group.status.resource.description)" + - celExpr: "group.status.resource.domainID == 'default'" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +status: + resource: + name: group-update-user + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/group/tests/group-update-user/00-minimal-resource.yaml b/internal/controllers/group/tests/group-update-user/00-minimal-resource.yaml new file mode 100644 index 000000000..548597efa --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/00-minimal-resource.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} diff --git a/internal/controllers/group/tests/group-update-user/00-prerequisites.yaml b/internal/controllers/group/tests/group-update-user/00-prerequisites.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/00-prerequisites.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/group/tests/group-update-user/01-add-user.yaml b/internal/controllers/group/tests/group-update-user/01-add-user.yaml new file mode 100644 index 000000000..0606014fe --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/01-add-user.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-update-user1 +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +spec: + resource: + userRefs: + - group-update-user1 diff --git a/internal/controllers/group/tests/group-update-user/01-assert.yaml b/internal/controllers/group/tests/group-update-user/01-assert.yaml new file mode 100644 index 000000000..d493f49d7 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/01-assert.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-update-user1 +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-update-user + ref: group + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-update-user1 + ref: user1 +assertAll: + - celExpr: "user1.status.id in group.status.resource.userIDs" diff --git a/internal/controllers/group/tests/group-update-user/02-add-another-user.yaml b/internal/controllers/group/tests/group-update-user/02-add-another-user.yaml new file mode 100644 index 000000000..487ce0bf0 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/02-add-another-user.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-update-user2 +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +spec: + resource: + userRefs: + - group-update-user1 + - group-update-user2 diff --git a/internal/controllers/group/tests/group-update-user/02-assert.yaml b/internal/controllers/group/tests/group-update-user/02-assert.yaml new file mode 100644 index 000000000..d229a3945 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/02-assert.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: group-update-user2 +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-update-user + ref: group + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-update-user1 + ref: user1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-update-user2 + ref: user2 +assertAll: + - celExpr: "user1.status.id in group.status.resource.userIDs" + - celExpr: "user2.status.id in group.status.resource.userIDs" diff --git a/internal/controllers/group/tests/group-update-user/03-assert.yaml b/internal/controllers/group/tests/group-update-user/03-assert.yaml new file mode 100644 index 000000000..0720e0269 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/03-assert.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-update-user + ref: group + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-update-user1 + ref: user1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: group-update-user2 + ref: user2 +assertAll: + - celExpr: "!(user1.status.id in group.status.resource.userIDs)" + - celExpr: "user2.status.id in group.status.resource.userIDs" diff --git a/internal/controllers/group/tests/group-update-user/03-delete-a-user.yaml b/internal/controllers/group/tests/group-update-user/03-delete-a-user.yaml new file mode 100644 index 000000000..c679c76e6 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/03-delete-a-user.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +spec: + resource: + userRefs: + - group-update-user2 diff --git a/internal/controllers/group/tests/group-update-user/04-assert.yaml b/internal/controllers/group/tests/group-update-user/04-assert.yaml new file mode 100644 index 000000000..4573ef228 --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/04-assert.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: group-update-user +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Group + name: group-update-user + ref: group +assertAll: + - celExpr: "!has(group.status.resource.userIDs)" diff --git a/internal/controllers/group/tests/group-update-user/04-reverted-resource.yaml b/internal/controllers/group/tests/group-update-user/04-reverted-resource.yaml new file mode 100644 index 000000000..2c6c253ff --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/04-reverted-resource.yaml @@ -0,0 +1,7 @@ +# NOTE: kuttl only does patch updates, which means we can't delete a field. +# We have to use a kubectl apply command instead. +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl replace -f 00-minimal-resource.yaml + namespaced: true diff --git a/internal/controllers/group/tests/group-update-user/README.md b/internal/controllers/group/tests/group-update-user/README.md new file mode 100644 index 000000000..03427fd5e --- /dev/null +++ b/internal/controllers/group/tests/group-update-user/README.md @@ -0,0 +1,21 @@ +# Update Group with Users + +## Step 00 + +Create a Group using only mandatory fields. + +## Step 01 + +Create a User and add this User to the Group. Verify that the user has been added to the group in OpenStack successfully. + +## Step 02 + +Create another User and add this User to the Group. Verify that the user has been added to the group in OpenStack successfully. + +## Step 03 + +Delete one of the Users from the group. Verify that only that specific user is no longer in the group in OpenStack. + +## Step 04 + +Revert the resource to its original value and verify that none of the users remain in the group. diff --git a/internal/osclients/group.go b/internal/osclients/group.go index 1082a98b0..8b6673228 100644 --- a/internal/osclients/group.go +++ b/internal/osclients/group.go @@ -20,19 +20,32 @@ import ( "context" "fmt" "iter" + "slices" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack" "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/groups" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" + "github.com/gophercloud/gophercloud/v2/pagination" "github.com/gophercloud/utils/v2/openstack/clientconfig" ) +type GroupExt struct { + groups.Group + + UserIDs []string +} + type GroupClient interface { - ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*groups.Group, error] - CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.Group, error) + ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*GroupExt, error] + CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*GroupExt, error) DeleteGroup(ctx context.Context, resourceID string) error - GetGroup(ctx context.Context, resourceID string) (*groups.Group, error) - UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.Group, error) + GetGroup(ctx context.Context, resourceID string) (*GroupExt, error) + UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*GroupExt, error) + + AddUser(ctx context.Context, groupID, userID string) error + RemoveUser(ctx context.Context, groupID, userID string) error + ListUsers(ctx context.Context, groupID string) ([]users.User, error) } type groupClient struct{ client *gophercloud.ServiceClient } @@ -51,27 +64,86 @@ func NewGroupClient(providerClient *gophercloud.ProviderClient, providerClientOp return &groupClient{client}, nil } -func (c groupClient) ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*groups.Group, error] { +func (c groupClient) ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*GroupExt, error] { + extractGroupsExt := func(r pagination.Page) ([]GroupExt, error) { + gps, err := groups.ExtractGroups(r) + if err != nil { + return nil, err + } + + groupExts := make([]GroupExt, 0, len(gps)) + for _, g := range gps { + groupExts = append(groupExts, GroupExt{Group: g}) + } + + return groupExts, nil + } pager := groups.List(c.client, listOpts) - return func(yield func(*groups.Group, error) bool) { - _ = pager.EachPage(ctx, yieldPage(groups.ExtractGroups, yield)) + return func(yield func(*GroupExt, error) bool) { + _ = pager.EachPage(ctx, yieldPage(extractGroupsExt, yield)) } } -func (c groupClient) CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.Group, error) { - return groups.Create(ctx, c.client, opts).Extract() +func (c groupClient) CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*GroupExt, error) { + g, err := groups.Create(ctx, c.client, opts).Extract() + if err != nil { + return nil, err + } + return &GroupExt{Group: *g}, nil } func (c groupClient) DeleteGroup(ctx context.Context, resourceID string) error { return groups.Delete(ctx, c.client, resourceID).ExtractErr() } -func (c groupClient) GetGroup(ctx context.Context, resourceID string) (*groups.Group, error) { - return groups.Get(ctx, c.client, resourceID).Extract() +func (c groupClient) GetGroup(ctx context.Context, resourceID string) (*GroupExt, error) { + g, err := groups.Get(ctx, c.client, resourceID).Extract() + if err != nil { + return nil, err + } + + p, err := users.ListInGroup(c.client, resourceID, users.ListOpts{}).AllPages(ctx) + if err != nil { + return nil, err + } + groupUsers, err := users.ExtractUsers(p) + if err != nil { + return nil, err + } + + userIDs := make([]string, 0, len(groupUsers)) + for _, u := range groupUsers { + userIDs = append(userIDs, u.ID) + } + slices.Sort(userIDs) + + return &GroupExt{Group: *g, UserIDs: userIDs}, nil +} + +func (c groupClient) UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*GroupExt, error) { + g, err := groups.Update(ctx, c.client, id, opts).Extract() + if err != nil { + return nil, err + } + + return &GroupExt{Group: *g}, nil +} + +func (c groupClient) AddUser(ctx context.Context, groupID, userID string) error { + return users.AddToGroup(ctx, c.client, groupID, userID).ExtractErr() +} + +func (c groupClient) RemoveUser(ctx context.Context, groupID, userID string) error { + return users.RemoveFromGroup(ctx, c.client, groupID, userID).ExtractErr() } -func (c groupClient) UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.Group, error) { - return groups.Update(ctx, c.client, id, opts).Extract() +func (c groupClient) ListUsers(ctx context.Context, groupID string) ([]users.User, error) { + pager, err := users.ListInGroup(c.client, groupID, users.ListOpts{}).AllPages(ctx) + if err != nil { + return nil, err + } + + return users.ExtractUsers(pager) } type groupErrorClient struct{ error } @@ -81,13 +153,13 @@ func NewGroupErrorClient(e error) GroupClient { return groupErrorClient{e} } -func (e groupErrorClient) ListGroups(_ context.Context, _ groups.ListOptsBuilder) iter.Seq2[*groups.Group, error] { - return func(yield func(*groups.Group, error) bool) { +func (e groupErrorClient) ListGroups(_ context.Context, _ groups.ListOptsBuilder) iter.Seq2[*GroupExt, error] { + return func(yield func(*GroupExt, error) bool) { yield(nil, e.error) } } -func (e groupErrorClient) CreateGroup(_ context.Context, _ groups.CreateOptsBuilder) (*groups.Group, error) { +func (e groupErrorClient) CreateGroup(_ context.Context, _ groups.CreateOptsBuilder) (*GroupExt, error) { return nil, e.error } @@ -95,10 +167,22 @@ func (e groupErrorClient) DeleteGroup(_ context.Context, _ string) error { return e.error } -func (e groupErrorClient) GetGroup(_ context.Context, _ string) (*groups.Group, error) { +func (e groupErrorClient) GetGroup(_ context.Context, _ string) (*GroupExt, error) { return nil, e.error } -func (e groupErrorClient) UpdateGroup(_ context.Context, _ string, _ groups.UpdateOptsBuilder) (*groups.Group, error) { +func (e groupErrorClient) UpdateGroup(_ context.Context, _ string, _ groups.UpdateOptsBuilder) (*GroupExt, error) { + return nil, e.error +} + +func (e groupErrorClient) AddUser(_ context.Context, _, _ string) error { + return e.error +} + +func (e groupErrorClient) RemoveUser(_ context.Context, _, _ string) error { + return e.error +} + +func (e groupErrorClient) ListUsers(_ context.Context, _ string) ([]users.User, error) { return nil, e.error } diff --git a/internal/osclients/mock/group.go b/internal/osclients/mock/group.go index 0612ebed2..1be0ebac2 100644 --- a/internal/osclients/mock/group.go +++ b/internal/osclients/mock/group.go @@ -30,6 +30,8 @@ import ( reflect "reflect" groups "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/groups" + users "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" + osclients "github.com/k-orc/openstack-resource-controller/v3/internal/osclients" gomock "go.uber.org/mock/gomock" ) @@ -57,11 +59,25 @@ func (m *MockGroupClient) EXPECT() *MockGroupClientMockRecorder { return m.recorder } +// AddUser mocks base method. +func (m *MockGroupClient) AddUser(ctx context.Context, groupID, userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddUser", ctx, groupID, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddUser indicates an expected call of AddUser. +func (mr *MockGroupClientMockRecorder) AddUser(ctx, groupID, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUser", reflect.TypeOf((*MockGroupClient)(nil).AddUser), ctx, groupID, userID) +} + // CreateGroup mocks base method. -func (m *MockGroupClient) CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*groups.Group, error) { +func (m *MockGroupClient) CreateGroup(ctx context.Context, opts groups.CreateOptsBuilder) (*osclients.GroupExt, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateGroup", ctx, opts) - ret0, _ := ret[0].(*groups.Group) + ret0, _ := ret[0].(*osclients.GroupExt) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -87,10 +103,10 @@ func (mr *MockGroupClientMockRecorder) DeleteGroup(ctx, resourceID any) *gomock. } // GetGroup mocks base method. -func (m *MockGroupClient) GetGroup(ctx context.Context, resourceID string) (*groups.Group, error) { +func (m *MockGroupClient) GetGroup(ctx context.Context, resourceID string) (*osclients.GroupExt, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetGroup", ctx, resourceID) - ret0, _ := ret[0].(*groups.Group) + ret0, _ := ret[0].(*osclients.GroupExt) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -102,10 +118,10 @@ func (mr *MockGroupClientMockRecorder) GetGroup(ctx, resourceID any) *gomock.Cal } // ListGroups mocks base method. -func (m *MockGroupClient) ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*groups.Group, error] { +func (m *MockGroupClient) ListGroups(ctx context.Context, listOpts groups.ListOptsBuilder) iter.Seq2[*osclients.GroupExt, error] { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListGroups", ctx, listOpts) - ret0, _ := ret[0].(iter.Seq2[*groups.Group, error]) + ret0, _ := ret[0].(iter.Seq2[*osclients.GroupExt, error]) return ret0 } @@ -115,11 +131,40 @@ func (mr *MockGroupClientMockRecorder) ListGroups(ctx, listOpts any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListGroups", reflect.TypeOf((*MockGroupClient)(nil).ListGroups), ctx, listOpts) } +// ListUsers mocks base method. +func (m *MockGroupClient) ListUsers(ctx context.Context, groupID string) ([]users.User, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListUsers", ctx, groupID) + ret0, _ := ret[0].([]users.User) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListUsers indicates an expected call of ListUsers. +func (mr *MockGroupClientMockRecorder) ListUsers(ctx, groupID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockGroupClient)(nil).ListUsers), ctx, groupID) +} + +// RemoveUser mocks base method. +func (m *MockGroupClient) RemoveUser(ctx context.Context, groupID, userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveUser", ctx, groupID, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveUser indicates an expected call of RemoveUser. +func (mr *MockGroupClientMockRecorder) RemoveUser(ctx, groupID, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUser", reflect.TypeOf((*MockGroupClient)(nil).RemoveUser), ctx, groupID, userID) +} + // UpdateGroup mocks base method. -func (m *MockGroupClient) UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*groups.Group, error) { +func (m *MockGroupClient) UpdateGroup(ctx context.Context, id string, opts groups.UpdateOptsBuilder) (*osclients.GroupExt, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateGroup", ctx, id, opts) - ret0, _ := ret[0].(*groups.Group) + ret0, _ := ret[0].(*osclients.GroupExt) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcespec.go index 5780b6886..8eeb890f5 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcespec.go @@ -25,9 +25,10 @@ import ( // GroupResourceSpecApplyConfiguration represents a declarative configuration of the GroupResourceSpec type for use // with apply. type GroupResourceSpecApplyConfiguration struct { - Name *apiv1alpha1.KeystoneName `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` + Name *apiv1alpha1.KeystoneName `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` + UserRefs []apiv1alpha1.KubernetesNameRef `json:"userRefs,omitempty"` } // GroupResourceSpecApplyConfiguration constructs a declarative configuration of the GroupResourceSpec type for use with @@ -59,3 +60,13 @@ func (b *GroupResourceSpecApplyConfiguration) WithDomainRef(value apiv1alpha1.Ku b.DomainRef = &value return b } + +// WithUserRefs adds the given value to the UserRefs field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the UserRefs field. +func (b *GroupResourceSpecApplyConfiguration) WithUserRefs(values ...apiv1alpha1.KubernetesNameRef) *GroupResourceSpecApplyConfiguration { + for i := range values { + b.UserRefs = append(b.UserRefs, values[i]) + } + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcestatus.go index bfe2aa55f..a59906d50 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/groupresourcestatus.go @@ -21,9 +21,10 @@ package v1alpha1 // GroupResourceStatusApplyConfiguration represents a declarative configuration of the GroupResourceStatus type for use // with apply. type GroupResourceStatusApplyConfiguration struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - DomainID *string `json:"domainID,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + DomainID *string `json:"domainID,omitempty"` + UserIDs []string `json:"userIDs,omitempty"` } // GroupResourceStatusApplyConfiguration constructs a declarative configuration of the GroupResourceStatus type for use with @@ -55,3 +56,13 @@ func (b *GroupResourceStatusApplyConfiguration) WithDomainID(value string) *Grou b.DomainID = &value return b } + +// WithUserIDs adds the given value to the UserIDs field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the UserIDs field. +func (b *GroupResourceStatusApplyConfiguration) WithUserIDs(values ...string) *GroupResourceStatusApplyConfiguration { + for i := range values { + b.UserIDs = append(b.UserIDs, values[i]) + } + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index fd0864c1c..6d400f17e 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -1219,6 +1219,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: userRefs + type: + list: + elementType: + scalar: string + elementRelationship: associative - name: com.github.k-orc.openstack-resource-controller.v3.api.v1alpha1.GroupResourceStatus map: fields: @@ -1231,6 +1237,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: userIDs + type: + list: + elementType: + scalar: string + elementRelationship: associative - name: com.github.k-orc.openstack-resource-controller.v3.api.v1alpha1.GroupSpec map: fields: diff --git a/test/apivalidations/group_test.go b/test/apivalidations/group_test.go index 01a0eaac7..bb480c087 100644 --- a/test/apivalidations/group_test.go +++ b/test/apivalidations/group_test.go @@ -108,4 +108,16 @@ var _ = Describe("ORC Group API validations", func() { WithDomainRef("domain-b")) Expect(applyObj(ctx, group, patch)).To(MatchError(ContainSubstring("domainRef is immutable"))) }) + + It("should allow updating userRefs", func(ctx context.Context) { + group := groupStub(namespace) + patch := baseGroupPatch(group) + patch.Spec.WithResource(applyconfigv1alpha1.GroupResourceSpec(). + WithUserRefs("user-1")) + Expect(applyObj(ctx, group, patch)).To(Succeed()) + + patch.Spec.WithResource(applyconfigv1alpha1.GroupResourceSpec(). + WithUserRefs("user-2", "user-3")) + Expect(applyObj(ctx, group, patch)).To(Succeed()) + }) }) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 2c64b0b8a..1401fd5e8 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -1566,6 +1566,7 @@ _Appears in:_ | `name` _[KeystoneName](#keystonename)_ | name will be the name of the created resource. If not specified, the
name of the ORC object will be used. | | MaxLength: 64
MinLength: 1
Optional: \{\}
| | `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| | `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `userRefs` _[KubernetesNameRef](#kubernetesnameref) array_ | userRefs contains ORC Users that belong to this group. | | MaxItems: 1024
MaxLength: 253
MinLength: 1
Optional: \{\}
| #### GroupResourceStatus @@ -1584,6 +1585,7 @@ _Appears in:_ | `name` _string_ | name is a Human-readable name for the resource. Might not be unique. | | MaxLength: 1024
Optional: \{\}
| | `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 1024
Optional: \{\}
| | `domainID` _string_ | domainID is the ID of the Domain to which the resource is associated. | | MaxLength: 1024
Optional: \{\}
| +| `userIDs` _string array_ | userIDs lists the IDs of the Users in this group. | | MaxItems: 1024
items:MaxLength: 1024
Optional: \{\}
| #### GroupSpec