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