diff --git a/api/v1alpha1/user_types.go b/api/v1alpha1/user_types.go index faf317660..ddccb96e2 100644 --- a/api/v1alpha1/user_types.go +++ b/api/v1alpha1/user_types.go @@ -42,6 +42,12 @@ type UserResourceSpec struct { // enabled defines whether a user is enabled or disabled // +optional Enabled *bool `json:"enabled,omitempty"` + + // passwordRef is a reference to a Secret containing the password + // for this user. The Secret must contain a key named "password". + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="passwordRef is immutable" + PasswordRef KubernetesNameRef `json:"passwordRef,omitempty"` } // UserFilter defines an existing resource by its properties @@ -81,4 +87,9 @@ type UserResourceStatus struct { // enabled defines whether a user is enabled or disabled // +optional Enabled bool `json:"enabled,omitempty"` + + // passwordExpiresAt is the timestamp at which the user's password expires. + // +kubebuilder:validation:MaxLength:=1024 + // +optional + PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"` } diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 9250f4e36..e13e9899b 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -11393,7 +11393,15 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceSpec(ref c Format: "", }, }, + "passwordRef": { + SchemaProps: spec.SchemaProps{ + Description: "passwordRef is a reference to a Secret containing the password for this user. The Secret must contain a key named \"password\".", + Type: []string{"string"}, + Format: "", + }, + }, }, + Required: []string{"passwordRef"}, }, }, } @@ -11441,6 +11449,13 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceStatus(ref Format: "", }, }, + "passwordExpiresAt": { + SchemaProps: spec.SchemaProps{ + Description: "passwordExpiresAt is the timestamp at which the user's password expires.", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, diff --git a/config/crd/bases/openstack.k-orc.cloud_users.yaml b/config/crd/bases/openstack.k-orc.cloud_users.yaml index 2b7480257..201bb5eba 100644 --- a/config/crd/bases/openstack.k-orc.cloud_users.yaml +++ b/config/crd/bases/openstack.k-orc.cloud_users.yaml @@ -186,6 +186,18 @@ spec: minLength: 1 pattern: ^[^,]+$ type: string + passwordRef: + description: |- + passwordRef is a reference to a Secret containing the password + for this user. The Secret must contain a key named "password". + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: passwordRef is immutable + rule: self == oldSelf + required: + - passwordRef type: object required: - cloudCredentialsRef @@ -313,6 +325,11 @@ spec: not be unique. maxLength: 1024 type: string + passwordExpiresAt: + description: passwordExpiresAt is the timestamp at which the user's + password expires. + maxLength: 1024 + type: string type: object type: object required: diff --git a/config/samples/openstack_v1alpha1_user.yaml b/config/samples/openstack_v1alpha1_user.yaml index 09067e614..2e6371f2f 100644 --- a/config/samples/openstack_v1alpha1_user.yaml +++ b/config/samples/openstack_v1alpha1_user.yaml @@ -1,5 +1,35 @@ --- apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: user-sample +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: user-sample +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-sample +type: Opaque +stringData: + password: "TestPassword" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: name: user-sample @@ -9,4 +39,9 @@ spec: secretName: openstack-clouds managementPolicy: managed resource: - description: Sample User + name: user-sample + description: User sample + domainRef: user-sample + defaultProjectRef: user-sample + enabled: true + passwordRef: user-sample diff --git a/internal/controllers/user/actuator.go b/internal/controllers/user/actuator.go index 391205112..4b4683fe5 100644 --- a/internal/controllers/user/actuator.go +++ b/internal/controllers/user/actuator.go @@ -135,6 +135,26 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT defaultProjectID = ptr.Deref(project.Status.ID, "") } } + + var password string + { + secret, secretReconcileStatus := dependency.FetchDependency( + ctx, actuator.k8sClient, obj.Namespace, + &resource.PasswordRef, "Secret", + func(*corev1.Secret) bool { return true }, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(secretReconcileStatus) + if secretReconcileStatus == nil { + passwordBytes, ok := secret.Data["password"] + if !ok { + reconcileStatus = reconcileStatus.WithReconcileStatus( + progress.NewReconcileStatus().WithProgressMessage("Password secret does not contain \"password\" key")) + } else { + password = string(passwordBytes) + } + } + } + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { return nil, reconcileStatus } @@ -144,6 +164,7 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT DomainID: domainID, Enabled: resource.Enabled, DefaultProjectID: defaultProjectID, + Password: password, } osResource, err := actuator.osClient.CreateUser(ctx, createOpts) diff --git a/internal/controllers/user/controller.go b/internal/controllers/user/controller.go index 4e432c0c7..f69818726 100644 --- a/internal/controllers/user/controller.go +++ b/internal/controllers/user/controller.go @@ -20,6 +20,7 @@ import ( "context" "errors" + corev1 "k8s.io/api/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -86,6 +87,17 @@ var domainImportDependency = dependency.NewDependency[*orcv1alpha1.UserList, *or }, ) +var passwordDependency = dependency.NewDependency[*orcv1alpha1.UserList, *corev1.Secret]( + "spec.resource.passwordRef", + func(user *orcv1alpha1.User) []string { + resource := user.Spec.Resource + if resource == nil { + return nil + } + return []string{string(resource.PasswordRef)} + }, +) + // SetupWithManager sets up the controller with the Manager. func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { log := ctrl.LoggerFrom(ctx) @@ -106,8 +118,14 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr return err } + passwordWatchEventHandler, err := passwordDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + builder := ctrl.NewControllerManagedBy(mgr). WithOptions(options). + For(&orcv1alpha1.User{}). Watches(&orcv1alpha1.Domain{}, domainWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), ). @@ -118,12 +136,20 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler, builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})), ). - For(&orcv1alpha1.User{}) + // XXX: This is a general watch on secrets. A general watch on secrets + // is undesirable because: + // - It requires problematic RBAC + // - Secrets are arbitrarily large, and we don't want to cache their contents + // + // These will require separate solutions. For the latter we should + // probably use a MetadataOnly watch on secrets. + Watches(&corev1.Secret{}, passwordWatchEventHandler) if err := errors.Join( domainDependency.AddToManager(ctx, mgr), projectDependency.AddToManager(ctx, mgr), domainImportDependency.AddToManager(ctx, mgr), + passwordDependency.AddToManager(ctx, mgr), credentialsDependency.AddToManager(ctx, mgr), credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), ); err != nil { diff --git a/internal/controllers/user/status.go b/internal/controllers/user/status.go index 0d0f8da51..e412d66fd 100644 --- a/internal/controllers/user/status.go +++ b/internal/controllers/user/status.go @@ -17,6 +17,8 @@ limitations under the License. package user import ( + "time" + "github.com/go-logr/logr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -62,5 +64,9 @@ func (userStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResou resourceStatus.WithDefaultProjectID(osResource.DefaultProjectID) } + if !osResource.PasswordExpiresAt.IsZero() { + resourceStatus.WithPasswordExpiresAt(osResource.PasswordExpiresAt.Format(time.RFC3339)) + } + statusApply.WithResource(resourceStatus) } diff --git a/internal/controllers/user/tests/user-create-full/00-assert.yaml b/internal/controllers/user/tests/user-create-full/00-assert.yaml index 0e9bd2a17..b91d5b2dd 100644 --- a/internal/controllers/user/tests/user-create-full/00-assert.yaml +++ b/internal/controllers/user/tests/user-create-full/00-assert.yaml @@ -35,3 +35,5 @@ assertAll: - celExpr: "user.status.id != ''" - celExpr: "user.status.resource.domainID == domain.status.id" - celExpr: "user.status.resource.defaultProjectID == project.status.id" + # passwordExpiresAt depends on the Keystone security_compliance + # configuration and is not asserted here. diff --git a/internal/controllers/user/tests/user-create-full/00-create-resource.yaml b/internal/controllers/user/tests/user-create-full/00-create-resource.yaml index 4df449bda..53d6869bb 100644 --- a/internal/controllers/user/tests/user-create-full/00-create-resource.yaml +++ b/internal/controllers/user/tests/user-create-full/00-create-resource.yaml @@ -21,6 +21,14 @@ spec: managementPolicy: managed resource: {} --- +apiVersion: v1 +kind: Secret +metadata: + name: user-create-full +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: @@ -35,4 +43,5 @@ spec: description: User from "create full" test domainRef: user-create-full defaultProjectRef: user-create-full - enabled: true \ No newline at end of file + enabled: true + passwordRef: user-create-full diff --git a/internal/controllers/user/tests/user-create-minimal/00-assert.yaml b/internal/controllers/user/tests/user-create-minimal/00-assert.yaml index 950d429bd..f8ffcb148 100644 --- a/internal/controllers/user/tests/user-create-minimal/00-assert.yaml +++ b/internal/controllers/user/tests/user-create-minimal/00-assert.yaml @@ -27,3 +27,6 @@ assertAll: - celExpr: "!has(user.status.resource.description)" - celExpr: "user.status.resource.domainID == 'default'" - celExpr: "!has(user.status.resource.defaultProjectID)" + # passwordExpiresAt depends on the Keystone security_compliance + # configuration and is not asserted here. + diff --git a/internal/controllers/user/tests/user-create-minimal/00-create-resource.yaml b/internal/controllers/user/tests/user-create-minimal/00-create-resource.yaml index c3d2147bf..72545e48c 100644 --- a/internal/controllers/user/tests/user-create-minimal/00-create-resource.yaml +++ b/internal/controllers/user/tests/user-create-minimal/00-create-resource.yaml @@ -1,4 +1,12 @@ --- +apiVersion: v1 +kind: Secret +metadata: + name: user-create-minimal +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: @@ -8,4 +16,5 @@ spec: cloudName: openstack-admin secretName: openstack-clouds managementPolicy: managed - resource: {} \ No newline at end of file + resource: + passwordRef: user-create-minimal \ No newline at end of file diff --git a/internal/controllers/user/tests/user-dependency/00-assert.yaml b/internal/controllers/user/tests/user-dependency/00-assert.yaml index 388f70d82..f45da298d 100644 --- a/internal/controllers/user/tests/user-dependency/00-assert.yaml +++ b/internal/controllers/user/tests/user-dependency/00-assert.yaml @@ -42,4 +42,19 @@ status: - type: Progressing message: Waiting for Project/user-dependency to be created status: "True" + reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: user-dependency-no-password +status: + conditions: + - type: Available + message: Waiting for Secret/user-dependency-password to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for Secret/user-dependency-password to be created + status: "True" reason: Progressing \ No newline at end of file diff --git a/internal/controllers/user/tests/user-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/user/tests/user-dependency/00-create-resources-missing-deps.yaml index c5b59dafa..c06e90511 100644 --- a/internal/controllers/user/tests/user-dependency/00-create-resources-missing-deps.yaml +++ b/internal/controllers/user/tests/user-dependency/00-create-resources-missing-deps.yaml @@ -10,6 +10,7 @@ spec: managementPolicy: managed resource: domainRef: user-dependency + passwordRef: user-dependency-password-existing --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User @@ -22,6 +23,7 @@ spec: managementPolicy: managed resource: defaultProjectRef: user-dependency + passwordRef: user-dependency-password-existing --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User @@ -32,4 +34,17 @@ spec: cloudName: openstack-admin secretName: user-dependency managementPolicy: managed - resource: {} \ No newline at end of file + resource: + passwordRef: user-dependency-password-existing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: user-dependency-no-password +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + passwordRef: user-dependency-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-dependency/00-secret.yaml b/internal/controllers/user/tests/user-dependency/00-secret.yaml index 082860af5..1e9d5d5fb 100644 --- a/internal/controllers/user/tests/user-dependency/00-secret.yaml +++ b/internal/controllers/user/tests/user-dependency/00-secret.yaml @@ -3,4 +3,12 @@ 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 \ No newline at end of file + namespaced: true +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-dependency-password-existing +type: Opaque +stringData: + password: "TestPassword" \ No newline at end of file diff --git a/internal/controllers/user/tests/user-dependency/01-assert.yaml b/internal/controllers/user/tests/user-dependency/01-assert.yaml index 30bfee417..83de36848 100644 --- a/internal/controllers/user/tests/user-dependency/01-assert.yaml +++ b/internal/controllers/user/tests/user-dependency/01-assert.yaml @@ -33,6 +33,21 @@ apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: name: user-dependency-no-project +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: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: user-dependency-no-password status: conditions: - type: Available diff --git a/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml b/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml index 4a292db93..2823b9d99 100644 --- a/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml +++ b/internal/controllers/user/tests/user-dependency/01-create-dependencies.yaml @@ -25,4 +25,12 @@ spec: cloudName: openstack-admin secretName: openstack-clouds managementPolicy: managed - resource: {} \ No newline at end of file + resource: {} +--- +apiVersion: v1 +kind: Secret +metadata: + name: user-dependency-password +type: Opaque +stringData: + password: "TestPassword" diff --git a/internal/controllers/user/tests/user-dependency/04-delete-resources.yaml b/internal/controllers/user/tests/user-dependency/04-delete-resources.yaml index 8054e0ebc..e0787a012 100644 --- a/internal/controllers/user/tests/user-dependency/04-delete-resources.yaml +++ b/internal/controllers/user/tests/user-dependency/04-delete-resources.yaml @@ -10,4 +10,7 @@ delete: name: user-dependency-no-domain - apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User - name: user-dependency-no-project \ No newline at end of file + name: user-dependency-no-project +- apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: User + name: user-dependency-no-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-import-dependency/00-import-resource.yaml b/internal/controllers/user/tests/user-import-dependency/00-import-resource.yaml index 0681c805b..6001315f4 100644 --- a/internal/controllers/user/tests/user-import-dependency/00-import-resource.yaml +++ b/internal/controllers/user/tests/user-import-dependency/00-import-resource.yaml @@ -1,4 +1,12 @@ --- +apiVersion: v1 +kind: Secret +metadata: + name: user-import-dependency-password +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: Domain metadata: diff --git a/internal/controllers/user/tests/user-import-dependency/01-create-trap-resource.yaml b/internal/controllers/user/tests/user-import-dependency/01-create-trap-resource.yaml index 7154af7ba..48536462a 100644 --- a/internal/controllers/user/tests/user-import-dependency/01-create-trap-resource.yaml +++ b/internal/controllers/user/tests/user-import-dependency/01-create-trap-resource.yaml @@ -21,4 +21,5 @@ spec: secretName: openstack-clouds managementPolicy: managed resource: - domainRef: user-import-dependency-not-this-one \ No newline at end of file + domainRef: user-import-dependency-not-this-one + passwordRef: user-import-dependency-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-import-dependency/02-create-resource.yaml b/internal/controllers/user/tests/user-import-dependency/02-create-resource.yaml index ea64cab75..51c32bb06 100644 --- a/internal/controllers/user/tests/user-import-dependency/02-create-resource.yaml +++ b/internal/controllers/user/tests/user-import-dependency/02-create-resource.yaml @@ -20,4 +20,5 @@ spec: secretName: openstack-clouds managementPolicy: managed resource: - domainRef: user-import-dependency-external \ No newline at end of file + domainRef: user-import-dependency-external + passwordRef: user-import-dependency-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-import-error/00-create-resources.yaml b/internal/controllers/user/tests/user-import-error/00-create-resources.yaml index 6f4e6c034..10e809d1a 100644 --- a/internal/controllers/user/tests/user-import-error/00-create-resources.yaml +++ b/internal/controllers/user/tests/user-import-error/00-create-resources.yaml @@ -10,6 +10,14 @@ spec: managementPolicy: managed resource: {} --- +apiVersion: v1 +kind: Secret +metadata: + name: user-import-error-password +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: @@ -22,7 +30,7 @@ spec: resource: description: User from "import error" test domainRef: user-import-error-domain - + passwordRef: user-import-error-password --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User @@ -35,4 +43,5 @@ spec: managementPolicy: managed resource: description: User from "import error" test - domainRef: user-import-error-domain \ No newline at end of file + domainRef: user-import-error-domain + passwordRef: user-import-error-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-import/00-import-resource.yaml b/internal/controllers/user/tests/user-import/00-import-resource.yaml index d8b7199fa..a80dc427a 100644 --- a/internal/controllers/user/tests/user-import/00-import-resource.yaml +++ b/internal/controllers/user/tests/user-import/00-import-resource.yaml @@ -1,4 +1,12 @@ --- +apiVersion: v1 +kind: Secret +metadata: + name: user-import-password +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: Domain metadata: diff --git a/internal/controllers/user/tests/user-import/01-create-trap-resource.yaml b/internal/controllers/user/tests/user-import/01-create-trap-resource.yaml index ea393341f..18ae8d89a 100644 --- a/internal/controllers/user/tests/user-import/01-create-trap-resource.yaml +++ b/internal/controllers/user/tests/user-import/01-create-trap-resource.yaml @@ -13,4 +13,5 @@ spec: managementPolicy: managed resource: description: User user-import-external from "user-import" test - domainRef: user-import-external \ No newline at end of file + domainRef: user-import-external + passwordRef: user-import-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-import/02-create-resource.yaml b/internal/controllers/user/tests/user-import/02-create-resource.yaml index 43a43ef04..ca3cb03fc 100644 --- a/internal/controllers/user/tests/user-import/02-create-resource.yaml +++ b/internal/controllers/user/tests/user-import/02-create-resource.yaml @@ -10,4 +10,5 @@ spec: managementPolicy: managed resource: description: User user-import-external from "user-import" test - domainRef: user-import-external \ No newline at end of file + domainRef: user-import-external + passwordRef: user-import-password \ No newline at end of file diff --git a/internal/controllers/user/tests/user-update/00-assert.yaml b/internal/controllers/user/tests/user-update/00-assert.yaml index 1cd41ff5a..c7a2749fc 100644 --- a/internal/controllers/user/tests/user-update/00-assert.yaml +++ b/internal/controllers/user/tests/user-update/00-assert.yaml @@ -10,7 +10,8 @@ assertAll: - celExpr: "!has(user.status.resource.description)" - celExpr: "user.status.resource.domainID == 'default'" - celExpr: "!has(user.status.resource.defaultProjectID)" - - celExpr: "!has(user.status.resource.passwordExpiresAt)" + # passwordExpiresAt depends on the Keystone security_compliance + # configuration and is not asserted here. --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User diff --git a/internal/controllers/user/tests/user-update/00-minimal-resource.yaml b/internal/controllers/user/tests/user-update/00-minimal-resource.yaml index 02960585c..d980e382a 100644 --- a/internal/controllers/user/tests/user-update/00-minimal-resource.yaml +++ b/internal/controllers/user/tests/user-update/00-minimal-resource.yaml @@ -1,4 +1,12 @@ --- +apiVersion: v1 +kind: Secret +metadata: + name: user-update +type: Opaque +stringData: + password: "TestPassword" +--- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User metadata: @@ -8,4 +16,5 @@ spec: cloudName: openstack-admin secretName: openstack-clouds managementPolicy: managed - resource: {} \ No newline at end of file + resource: + passwordRef: user-update \ No newline at end of file diff --git a/internal/controllers/user/tests/user-update/01-updated-resource.yaml b/internal/controllers/user/tests/user-update/01-updated-resource.yaml index 4cbafe8c3..dea4f5476 100644 --- a/internal/controllers/user/tests/user-update/01-updated-resource.yaml +++ b/internal/controllers/user/tests/user-update/01-updated-resource.yaml @@ -7,4 +7,4 @@ spec: resource: name: user-update-updated description: user-update-updated - enabled: false \ No newline at end of file + enabled: false diff --git a/internal/controllers/user/tests/user-update/02-assert.yaml b/internal/controllers/user/tests/user-update/02-assert.yaml index 1c70b64e1..c2c14d837 100644 --- a/internal/controllers/user/tests/user-update/02-assert.yaml +++ b/internal/controllers/user/tests/user-update/02-assert.yaml @@ -8,7 +8,8 @@ resourceRefs: ref: user assertAll: - celExpr: "!has(user.status.resource.description)" - - celExpr: "!has(user.status.resource.passwordExpiresAt)" + # passwordExpiresAt depends on the Keystone security_compliance + # configuration and is not asserted here. --- apiVersion: openstack.k-orc.cloud/v1alpha1 kind: User diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go index ed4b86a2e..bd0bab7c6 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go @@ -30,6 +30,7 @@ type UserResourceSpecApplyConfiguration struct { DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` DefaultProjectRef *apiv1alpha1.KubernetesNameRef `json:"defaultProjectRef,omitempty"` Enabled *bool `json:"enabled,omitempty"` + PasswordRef *apiv1alpha1.KubernetesNameRef `json:"passwordRef,omitempty"` } // UserResourceSpecApplyConfiguration constructs a declarative configuration of the UserResourceSpec type for use with @@ -77,3 +78,11 @@ func (b *UserResourceSpecApplyConfiguration) WithEnabled(value bool) *UserResour b.Enabled = &value return b } + +// WithPasswordRef sets the PasswordRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PasswordRef field is set to the value of the last call. +func (b *UserResourceSpecApplyConfiguration) WithPasswordRef(value apiv1alpha1.KubernetesNameRef) *UserResourceSpecApplyConfiguration { + b.PasswordRef = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go index 05093ff79..c23b0b6cf 100644 --- a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go +++ b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go @@ -21,11 +21,12 @@ package v1alpha1 // UserResourceStatusApplyConfiguration represents a declarative configuration of the UserResourceStatus type for use // with apply. type UserResourceStatusApplyConfiguration struct { - Name *string `json:"name,omitempty"` - Description *string `json:"description,omitempty"` - DomainID *string `json:"domainID,omitempty"` - DefaultProjectID *string `json:"defaultProjectID,omitempty"` - Enabled *bool `json:"enabled,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + DomainID *string `json:"domainID,omitempty"` + DefaultProjectID *string `json:"defaultProjectID,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + PasswordExpiresAt *string `json:"passwordExpiresAt,omitempty"` } // UserResourceStatusApplyConfiguration constructs a declarative configuration of the UserResourceStatus type for use with @@ -73,3 +74,11 @@ func (b *UserResourceStatusApplyConfiguration) WithEnabled(value bool) *UserReso b.Enabled = &value return b } + +// WithPasswordExpiresAt sets the PasswordExpiresAt field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the PasswordExpiresAt field is set to the value of the last call. +func (b *UserResourceStatusApplyConfiguration) WithPasswordExpiresAt(value string) *UserResourceStatusApplyConfiguration { + b.PasswordExpiresAt = &value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 216436517..c641e66f4 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -3409,6 +3409,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: passwordRef + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.UserResourceStatus map: fields: @@ -3427,6 +3430,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: name type: scalar: string + - name: passwordExpiresAt + type: + scalar: string - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.UserSpec map: fields: diff --git a/test/apivalidations/user_test.go b/test/apivalidations/user_test.go index cc7024b9e..15057eb85 100644 --- a/test/apivalidations/user_test.go +++ b/test/apivalidations/user_test.go @@ -41,7 +41,8 @@ func userStub(namespace *corev1.Namespace) *orcv1alpha1.User { } func testUserResource() *applyconfigv1alpha1.UserResourceSpecApplyConfiguration { - return applyconfigv1alpha1.UserResourceSpec() + return applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("user-password") } func baseUserPatch(user client.Object) *applyconfigv1alpha1.UserApplyConfiguration { @@ -95,10 +96,12 @@ var _ = Describe("ORC User API validations", func() { user := userStub(namespace) patch := baseUserPatch(user) patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("user-password"). WithDomainRef("domain-a")) Expect(applyObj(ctx, user, patch)).To(Succeed()) patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("user-password"). WithDomainRef("domain-b")) Expect(applyObj(ctx, user, patch)).To(MatchError(ContainSubstring("domainRef is immutable"))) }) @@ -107,11 +110,25 @@ var _ = Describe("ORC User API validations", func() { user := userStub(namespace) patch := baseUserPatch(user) patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("user-password"). WithDefaultProjectRef("project-a")) Expect(applyObj(ctx, user, patch)).To(Succeed()) patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("user-password"). WithDefaultProjectRef("project-b")) Expect(applyObj(ctx, user, patch)).To(MatchError(ContainSubstring("defaultProjectRef is immutable"))) }) + + It("should have immutable passwordRef", func(ctx context.Context) { + user := userStub(namespace) + patch := baseUserPatch(user) + patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("password-a")) + Expect(applyObj(ctx, user, patch)).To(Succeed()) + + patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec(). + WithPasswordRef("password-b")) + Expect(applyObj(ctx, user, patch)).To(MatchError(ContainSubstring("passwordRef is immutable"))) + }) }) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 8d22de9fc..07dbfbe88 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -4436,6 +4436,7 @@ _Appears in:_ | `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| | `defaultProjectRef` _[KubernetesNameRef](#kubernetesnameref)_ | defaultProjectRef is a reference to the Default Project which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| | `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | Optional: \{\}
| +| `passwordRef` _[KubernetesNameRef](#kubernetesnameref)_ | passwordRef is a reference to a Secret containing the password
for this user. The Secret must contain a key named "password". | | MaxLength: 253
MinLength: 1
Required: \{\}
| #### UserResourceStatus @@ -4456,6 +4457,7 @@ _Appears in:_ | `domainID` _string_ | domainID is the ID of the Domain to which the resource is associated. | | MaxLength: 1024
Optional: \{\}
| | `defaultProjectID` _string_ | defaultProjectID is the ID of the Default Project to which the user is associated with. | | MaxLength: 1024
Optional: \{\}
| | `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | Optional: \{\}
| +| `passwordExpiresAt` _string_ | passwordExpiresAt is the timestamp at which the user's password expires. | | MaxLength: 1024
Optional: \{\}
| #### UserSpec