diff --git a/api/v1alpha1/user_types.go b/api/v1alpha1/user_types.go
index faf317660..742f055d9 100644
--- a/api/v1alpha1/user_types.go
+++ b/api/v1alpha1/user_types.go
@@ -42,6 +42,18 @@ type UserResourceSpec struct {
// enabled defines whether a user is enabled or disabled
// +optional
Enabled *bool `json:"enabled,omitempty"`
+
+ // password is the password set for the user
+ // +optional
+ Password *PasswordSpec `json:"password,omitempty"`
+}
+
+// +kubebuilder:validation:MinProperties:=1
+// +kubebuilder:validation:MaxProperties:=1
+type PasswordSpec struct {
+ // secretRef is a reference to a Secret containing the password for this user.
+ // +optional
+ SecretRef *KubernetesNameRef `json:"secretRef,omitempty"`
}
// UserFilter defines an existing resource by its properties
@@ -81,4 +93,9 @@ type UserResourceStatus struct {
// enabled defines whether a user is enabled or disabled
// +optional
Enabled bool `json:"enabled,omitempty"`
+
+ // passwordExpiresAt filters the response based on expriing passwords.
+ // +kubebuilder:validation:MaxLength:=255
+ // +optional
+ PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"`
}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 2b3b6c381..e207925b3 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -2559,6 +2559,26 @@ func (in *NeutronStatusMetadata) DeepCopy() *NeutronStatusMetadata {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PasswordSpec) DeepCopyInto(out *PasswordSpec) {
+ *out = *in
+ if in.SecretRef != nil {
+ in, out := &in.SecretRef, &out.SecretRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec.
+func (in *PasswordSpec) DeepCopy() *PasswordSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(PasswordSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Port) DeepCopyInto(out *Port) {
*out = *in
@@ -5697,6 +5717,11 @@ func (in *UserResourceSpec) DeepCopyInto(out *UserResourceSpec) {
*out = new(bool)
**out = **in
}
+ if in.Password != nil {
+ in, out := &in.Password, &out.Password
+ *out = new(PasswordSpec)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserResourceSpec.
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index ee55bc2bb..e95eeb172 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -119,6 +119,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkSpec": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkSpec(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkStatus": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NeutronStatusMetadata": schema_openstack_resource_controller_v2_api_v1alpha1_NeutronStatusMetadata(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec": schema_openstack_resource_controller_v2_api_v1alpha1_PasswordSpec(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Port": schema_openstack_resource_controller_v2_api_v1alpha1_Port(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PortFilter": schema_openstack_resource_controller_v2_api_v1alpha1_PortFilter(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PortImport": schema_openstack_resource_controller_v2_api_v1alpha1_PortImport(ref),
@@ -4872,6 +4873,25 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_NeutronStatusMetadata(
}
}
+func schema_openstack_resource_controller_v2_api_v1alpha1_PasswordSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "secretRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "secretRef is a reference to a Secret containing the password for this user.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
func schema_openstack_resource_controller_v2_api_v1alpha1_Port(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
@@ -11030,9 +11050,17 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceSpec(ref c
Format: "",
},
},
+ "password": {
+ SchemaProps: spec.SchemaProps{
+ Description: "password is the password set for the user",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec"),
+ },
+ },
},
},
},
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.PasswordSpec"},
}
}
@@ -11078,6 +11106,13 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_UserResourceStatus(ref
Format: "",
},
},
+ "passwordExpiresAt": {
+ SchemaProps: spec.SchemaProps{
+ Description: "passwordExpiresAt filters the response based on expriing passwords.",
+ 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 bc8835301..ec3101c1a 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
+ password:
+ description: password is the password set for the user
+ maxProperties: 1
+ minProperties: 1
+ properties:
+ secretRef:
+ description: secretRef is a reference to a Secret containing
+ the password for this user.
+ maxLength: 253
+ minLength: 1
+ type: string
+ type: object
type: object
required:
- cloudCredentialsRef
@@ -313,6 +325,11 @@ spec:
not be unique.
maxLength: 1024
type: string
+ passwordExpiresAt:
+ description: passwordExpiresAt filters the response based on expriing
+ passwords.
+ maxLength: 255
+ 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..d27169d30 100644
--- a/config/samples/openstack_v1alpha1_user.yaml
+++ b/config/samples/openstack_v1alpha1_user.yaml
@@ -1,12 +1,48 @@
---
apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: user-sample
+spec:
+ cloudCredentialsRef:
+ cloudName: devstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource: {}
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: user-sample
+spec:
+ cloudCredentialsRef:
+ cloudName: devstack-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
spec:
cloudCredentialsRef:
- cloudName: openstack-admin
+ cloudName: devstack-admin
secretName: openstack-clouds
managementPolicy: managed
resource:
- description: Sample User
+ name: user-sample
+ description: User sample
+ domainRef: user-sample
+ defaultProjectRef: user-sample
+ enabled: true
+ password:
+ secretRef: user-sample
\ No newline at end of file
diff --git a/internal/controllers/user/actuator.go b/internal/controllers/user/actuator.go
index 391205112..cbf09c89e 100644
--- a/internal/controllers/user/actuator.go
+++ b/internal/controllers/user/actuator.go
@@ -135,6 +135,28 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT
defaultProjectID = ptr.Deref(project.Status.ID, "")
}
}
+
+ var password string
+ var passwordSecretVersion string
+ if resource.Password != nil {
+ secret, secretReconcileStatus := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace,
+ resource.Password.SecretRef, "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)
+ passwordSecretVersion = secret.ResourceVersion
+ }
+ }
+ }
+
if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
return nil, reconcileStatus
}
@@ -144,6 +166,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)
@@ -155,6 +178,15 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT
return nil, progress.WrapError(err)
}
+ // Store the password Secret ResourceVersion after successful creation
+ if passwordSecretVersion != "" {
+ if err := actuator.updatePasswordSecretVersionAnnotation(ctx, obj, passwordSecretVersion); err != nil {
+ log := ctrl.LoggerFrom(ctx)
+ log.Error(err, "Failed to update password secret version annotation after creation")
+ // Don't fail the create just because we couldn't update the annotation
+ }
+ }
+
return osResource, nil
}
@@ -177,6 +209,11 @@ func (actuator userActuator) updateResource(ctx context.Context, obj orcObjectPT
handleDescriptionUpdate(&updateOpts, resource, osResource)
handleEnabledUpdate(&updateOpts, resource, osResource)
+ newSecretVersion, passwordRS := actuator.handlePasswordUpdate(ctx, &updateOpts, obj)
+ if passwordRS != nil {
+ return passwordRS
+ }
+
needsUpdate, err := needsUpdate(updateOpts)
if err != nil {
return progress.WrapError(
@@ -198,6 +235,15 @@ func (actuator userActuator) updateResource(ctx context.Context, obj orcObjectPT
return progress.WrapError(err)
}
+ // If password was updated, store the new Secret ResourceVersion in annotation
+ if newSecretVersion != "" {
+ if err := actuator.updatePasswordSecretVersionAnnotation(ctx, obj, newSecretVersion); err != nil {
+ log.Error(err, "Failed to update password secret version annotation")
+ // Don't fail the reconcile just because we couldn't update the annotation
+ // The password was already updated in OpenStack
+ }
+ }
+
return progress.NeedsRefresh()
}
@@ -236,6 +282,51 @@ func handleEnabledUpdate(updateOpts *users.UpdateOpts, resource *resourceSpecT,
}
}
+func (actuator userActuator) updatePasswordSecretVersionAnnotation(ctx context.Context, obj orcObjectPT, secretVersion string) error {
+ // Create a patch to update just the annotation
+ patch := client.MergeFrom(obj.DeepCopy())
+
+ if obj.Annotations == nil {
+ obj.Annotations = make(map[string]string)
+ }
+ obj.Annotations["openstack.k-orc.cloud/password-secret-version"] = secretVersion
+
+ return actuator.k8sClient.Patch(ctx, obj, patch)
+}
+
+func (actuator userActuator) handlePasswordUpdate(ctx context.Context, updateOpts *users.UpdateOpts, obj orcObjectPT) (secretResourceVersion string, reconcileStatus progress.ReconcileStatus) {
+ resource := obj.Spec.Resource
+ if resource == nil {
+ return "", nil
+ }
+
+ if resource.Password != nil && resource.Password.SecretRef != nil {
+ secret, secretReconcileStatus := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace,
+ resource.Password.SecretRef, "Secret",
+ func(*corev1.Secret) bool { return true },
+ )
+ if secretReconcileStatus != nil {
+ return "", secretReconcileStatus
+ }
+
+ // Check if password Secret has changed by comparing ResourceVersion
+ currentSecretVersion := secret.ResourceVersion
+ storedSecretVersion := obj.Annotations["openstack.k-orc.cloud/password-secret-version"]
+
+ // Only update password if Secret ResourceVersion changed
+ if storedSecretVersion != currentSecretVersion {
+ if passwordBytes, ok := secret.Data["password"]; ok {
+ password := string(passwordBytes)
+ updateOpts.Password = password
+ return currentSecretVersion, nil
+ }
+ }
+ }
+
+ return "", nil
+}
+
func (actuator userActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
return []resourceReconciler{
actuator.updateResource,
diff --git a/internal/controllers/user/controller.go b/internal/controllers/user/controller.go
index 4e432c0c7..2f966f873 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.password.secretRef",
+ func(user *orcv1alpha1.User) []string {
+ resource := user.Spec.Resource
+ if resource == nil || resource.Password == nil || resource.Password.SecretRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Password.SecretRef)}
+ },
+)
+
// 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,14 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr
Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler,
builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})),
).
- For(&orcv1alpha1.User{})
+ // General 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/tests/user-create-full/00-create-resource.yaml b/internal/controllers/user/tests/user-create-full/00-create-resource.yaml
index 4df449bda..e4f5f132f 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
@@ -20,6 +20,14 @@ spec:
secretName: openstack-clouds
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
@@ -35,4 +43,6 @@ 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
+ password:
+ secretRef: user-create-full
\ No newline at end of file
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..02a409a8f 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,5 @@ 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)"
+
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..d47398d55 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-create-full
+type: Opaque
+stringData:
+ password: "TestPassword"
\ No newline at end of file
diff --git a/internal/controllers/user/tests/user-update-password/00-assert.yaml b/internal/controllers/user/tests/user-update-password/00-assert.yaml
new file mode 100644
index 000000000..1565e3db4
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/00-assert.yaml
@@ -0,0 +1,28 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: User
+ name: user-update-password
+ ref: user
+assertAll:
+ - celExpr: "user.status.id != ''"
+ - celExpr: "user.status.resource.domainID == 'default'"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: user-update-password
+status:
+ resource:
+ name: user-update-password
+ enabled: true
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+
diff --git a/internal/controllers/user/tests/user-update-password/00-create-resource.yaml b/internal/controllers/user/tests/user-update-password/00-create-resource.yaml
new file mode 100644
index 000000000..cea90f7bf
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/00-create-resource.yaml
@@ -0,0 +1,13 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: user-update-password
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ password:
+ secretRef: user-update-password
diff --git a/internal/controllers/user/tests/user-update-password/00-secret.yaml b/internal/controllers/user/tests/user-update-password/00-secret.yaml
new file mode 100644
index 000000000..c299398fb
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/00-secret.yaml
@@ -0,0 +1,14 @@
+---
+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
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: user-update-password
+type: Opaque
+stringData:
+ password: "user-update"
diff --git a/internal/controllers/user/tests/user-update-password/01-assert.yaml b/internal/controllers/user/tests/user-update-password/01-assert.yaml
new file mode 100644
index 000000000..7ff211503
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/01-assert.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: user-update-password
+status:
+ resource:
+ name: user-update-password
+ enabled: true
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+
diff --git a/internal/controllers/user/tests/user-update-password/01-update-password.yaml b/internal/controllers/user/tests/user-update-password/01-update-password.yaml
new file mode 100644
index 000000000..c2165ccf6
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/01-update-password.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: user-update-password
+type: Opaque
+stringData:
+ password: "user-update-updated"
diff --git a/internal/controllers/user/tests/user-update-password/02-assert.yaml b/internal/controllers/user/tests/user-update-password/02-assert.yaml
new file mode 100644
index 000000000..7ff211503
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/02-assert.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: user-update-password
+status:
+ resource:
+ name: user-update-password
+ enabled: true
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+
diff --git a/internal/controllers/user/tests/user-update-password/02-revert-password.yaml b/internal/controllers/user/tests/user-update-password/02-revert-password.yaml
new file mode 100644
index 000000000..8f9bd268f
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/02-revert-password.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: user-update-password
+type: Opaque
+stringData:
+ password: "user-update"
diff --git a/internal/controllers/user/tests/user-update-password/README.md b/internal/controllers/user/tests/user-update-password/README.md
new file mode 100644
index 000000000..44a4a6485
--- /dev/null
+++ b/internal/controllers/user/tests/user-update-password/README.md
@@ -0,0 +1,27 @@
+# Update User Password
+
+This test verifies that a User's password can be updated by changing the referenced Secret.
+
+## Step 00
+
+Create a User with a password Secret containing "InitialPassword123".
+
+## Step 01
+
+Update the password Secret to contain "UpdatedPassword456". Verify that the User reconciles and remains Available.
+
+## Step 02
+
+Revert the password Secret back to "InitialPassword123". Verify that the User reconciles and remains Available.
+
+## What This Tests
+
+- Password is set during creation
+- Password can be updated by changing the Secret
+- Password updates trigger reconciliation
+- User remains Available throughout password updates
+- Password can be changed multiple times
+
+## Note
+
+The password value itself is write-only in OpenStack, so we cannot verify the actual password value in the status. This test only verifies that password updates don't cause errors and the User remains Available.
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go
new file mode 100644
index 000000000..4dd652577
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/passwordspec.go
@@ -0,0 +1,43 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// PasswordSpecApplyConfiguration represents a declarative configuration of the PasswordSpec type for use
+// with apply.
+type PasswordSpecApplyConfiguration struct {
+ SecretRef *apiv1alpha1.KubernetesNameRef `json:"secretRef,omitempty"`
+}
+
+// PasswordSpecApplyConfiguration constructs a declarative configuration of the PasswordSpec type for use with
+// apply.
+func PasswordSpec() *PasswordSpecApplyConfiguration {
+ return &PasswordSpecApplyConfiguration{}
+}
+
+// WithSecretRef sets the SecretRef 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 SecretRef field is set to the value of the last call.
+func (b *PasswordSpecApplyConfiguration) WithSecretRef(value apiv1alpha1.KubernetesNameRef) *PasswordSpecApplyConfiguration {
+ b.SecretRef = &value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go
index ed4b86a2e..e7898082e 100644
--- a/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/userresourcespec.go
@@ -25,11 +25,12 @@ import (
// UserResourceSpecApplyConfiguration represents a declarative configuration of the UserResourceSpec type for use
// with apply.
type UserResourceSpecApplyConfiguration struct {
- Name *apiv1alpha1.OpenStackName `json:"name,omitempty"`
- Description *string `json:"description,omitempty"`
- DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"`
- DefaultProjectRef *apiv1alpha1.KubernetesNameRef `json:"defaultProjectRef,omitempty"`
- Enabled *bool `json:"enabled,omitempty"`
+ Name *apiv1alpha1.OpenStackName `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"`
+ DefaultProjectRef *apiv1alpha1.KubernetesNameRef `json:"defaultProjectRef,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ Password *PasswordSpecApplyConfiguration `json:"password,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
}
+
+// WithPassword sets the Password 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 Password field is set to the value of the last call.
+func (b *UserResourceSpecApplyConfiguration) WithPassword(value *PasswordSpecApplyConfiguration) *UserResourceSpecApplyConfiguration {
+ b.Password = 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 3a7e6ae0c..2d11586f0 100644
--- a/pkg/clients/applyconfiguration/internal/internal.go
+++ b/pkg/clients/applyconfiguration/internal/internal.go
@@ -1338,6 +1338,12 @@ var schemaYAML = typed.YAMLObject(`types:
- name: resource
type:
namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.NetworkResourceStatus
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.PasswordSpec
+ map:
+ fields:
+ - name: secretRef
+ type:
+ scalar: string
- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Port
map:
fields:
@@ -3297,6 +3303,9 @@ var schemaYAML = typed.YAMLObject(`types:
- name: name
type:
scalar: string
+ - name: password
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.PasswordSpec
- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.UserResourceStatus
map:
fields:
@@ -3315,6 +3324,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/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go
index 0e7f2efb3..428ca2b9c 100644
--- a/pkg/clients/applyconfiguration/utils.go
+++ b/pkg/clients/applyconfiguration/utils.go
@@ -194,6 +194,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1alpha1.NetworkStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("NeutronStatusMetadata"):
return &apiv1alpha1.NeutronStatusMetadataApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("PasswordSpec"):
+ return &apiv1alpha1.PasswordSpecApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Port"):
return &apiv1alpha1.PortApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("PortFilter"):
diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md
index 5512b3f6b..a8ea51c43 100644
--- a/website/docs/crd-reference.md
+++ b/website/docs/crd-reference.md
@@ -1784,6 +1784,7 @@ _Appears in:_
- [HostID](#hostid)
- [NetworkFilter](#networkfilter)
- [NetworkResourceSpec](#networkresourcespec)
+- [PasswordSpec](#passwordspec)
- [PortFilter](#portfilter)
- [PortResourceSpec](#portresourcespec)
- [RoleFilter](#rolefilter)
@@ -2218,6 +2219,24 @@ _Appears in:_
+#### PasswordSpec
+
+
+
+
+
+_Validation:_
+- MaxProperties: 1
+- MinProperties: 1
+
+_Appears in:_
+- [UserResourceSpec](#userresourcespec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `secretRef` _[KubernetesNameRef](#kubernetesnameref)_ | secretRef is a reference to a Secret containing the password for this user. | | MaxLength: 253
MinLength: 1
|
+
+
#### Port
@@ -4291,6 +4310,7 @@ _Appears in:_
| `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with. | | MaxLength: 253
MinLength: 1
|
| `defaultProjectRef` _[KubernetesNameRef](#kubernetesnameref)_ | defaultProjectRef is a reference to the Default Project which this resource is associated with. | | MaxLength: 253
MinLength: 1
|
| `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | |
+| `password` _[PasswordSpec](#passwordspec)_ | password is the password set for the user | | MaxProperties: 1
MinProperties: 1
|
#### UserResourceStatus
@@ -4311,6 +4331,7 @@ _Appears in:_
| `domainID` _string_ | domainID is the ID of the Domain to which the resource is associated. | | MaxLength: 1024
|
| `defaultProjectID` _string_ | defaultProjectID is the ID of the Default Project to which the user is associated with. | | MaxLength: 1024
|
| `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | |
+| `passwordExpiresAt` _string_ | passwordExpiresAt filters the response based on expriing passwords. | | MaxLength: 255
|
#### UserSpec