Skip to content

Commit 28a09a4

Browse files
committed
user: implement password mutability via passwordRef
Make the passwordRef field mutable so users can update passwords by pointing to a different Secret. Track the applied password reference in a new status field (appliedPasswordRef) to detect when an update is needed. The reconciler compares spec.resource.passwordRef with status.resource.appliedPasswordRef. On first reconcile after creation, it sets the status field without calling UpdateUser (CreateResource already set the initial password). On subsequent changes, it reads the new Secret, calls UpdateUser, and updates the status field via a MergePatch that coexists with the main SSA status update.
1 parent f721c2e commit 28a09a4

13 files changed

Lines changed: 126 additions & 13 deletions

File tree

api/v1alpha1/user_types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ type UserResourceSpec struct {
4646
// passwordRef is a reference to a Secret containing the password
4747
// for this user. The Secret must contain a key named "password".
4848
// +required
49-
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="passwordRef is immutable"
5049
PasswordRef KubernetesNameRef `json:"passwordRef,omitempty"`
5150
}
5251

@@ -92,4 +91,10 @@ type UserResourceStatus struct {
9291
// +kubebuilder:validation:MaxLength:=1024
9392
// +optional
9493
PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"`
94+
95+
// appliedPasswordRef is the name of the Secret containing the
96+
// password that was last applied to the OpenStack resource.
97+
// +kubebuilder:validation:MaxLength=1024
98+
// +optional
99+
AppliedPasswordRef string `json:"appliedPasswordRef,omitempty"`
95100
}

cmd/models-schema/zz_generated.openapi.go

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/openstack.k-orc.cloud_users.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,6 @@ spec:
193193
maxLength: 253
194194
minLength: 1
195195
type: string
196-
x-kubernetes-validations:
197-
- message: passwordRef is immutable
198-
rule: self == oldSelf
199196
required:
200197
- passwordRef
201198
type: object
@@ -302,6 +299,12 @@ spec:
302299
description: resource contains the observed state of the OpenStack
303300
resource.
304301
properties:
302+
appliedPasswordRef:
303+
description: |-
304+
appliedPasswordRef is the name of the Secret containing the
305+
password that was last applied to the OpenStack resource.
306+
maxLength: 1024
307+
type: string
305308
defaultProjectID:
306309
description: defaultProjectID is the ID of the Default Project
307310
to which the user is associated with.

internal/controllers/user/actuator.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users"
2424
corev1 "k8s.io/api/core/v1"
25+
"k8s.io/apimachinery/pkg/types"
2526
"k8s.io/utils/ptr"
2627
ctrl "sigs.k8s.io/controller-runtime"
2728
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -31,8 +32,10 @@ import (
3132
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
3233
"github.com/k-orc/openstack-resource-controller/v2/internal/logging"
3334
"github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
35+
"github.com/k-orc/openstack-resource-controller/v2/internal/util/applyconfigs"
3436
"github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
3537
orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
38+
orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
3639
)
3740

3841
// OpenStack resource types
@@ -183,6 +186,75 @@ func (actuator userActuator) DeleteResource(ctx context.Context, _ orcObjectPT,
183186
return progress.WrapError(actuator.osClient.DeleteUser(ctx, resource.ID))
184187
}
185188

189+
func (actuator userActuator) reconcilePassword(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
190+
log := ctrl.LoggerFrom(ctx)
191+
resource := obj.Spec.Resource
192+
if resource == nil {
193+
return nil
194+
}
195+
196+
currentRef := string(resource.PasswordRef)
197+
var lastAppliedRef string
198+
if obj.Status.Resource != nil {
199+
lastAppliedRef = obj.Status.Resource.AppliedPasswordRef
200+
}
201+
202+
if lastAppliedRef == currentRef {
203+
return nil
204+
}
205+
206+
// Read the password from the referenced Secret
207+
secret, secretRS := dependency.FetchDependency(
208+
ctx, actuator.k8sClient, obj.Namespace,
209+
&resource.PasswordRef, "Secret",
210+
func(*corev1.Secret) bool { return true },
211+
)
212+
if secretRS != nil {
213+
return secretRS
214+
}
215+
216+
passwordBytes, ok := secret.Data["password"]
217+
if !ok {
218+
return progress.NewReconcileStatus().WithProgressMessage("Password secret does not contain \"password\" key")
219+
}
220+
password := string(passwordBytes)
221+
222+
// Only call UpdateUser if this is not the first reconcile after creation.
223+
// CreateResource already set the initial password.
224+
if lastAppliedRef != "" {
225+
log.V(logging.Info).Info("Updating password")
226+
_, err := actuator.osClient.UpdateUser(ctx, osResource.ID, users.UpdateOpts{
227+
Password: password,
228+
})
229+
230+
if orcerrors.IsConflict(err) {
231+
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)
232+
}
233+
if err != nil {
234+
return progress.WrapError(err)
235+
}
236+
}
237+
238+
// Update the lastAppliedPasswordRef status field via a MergePatch.
239+
// MergePatch sets only the specified fields without claiming SSA
240+
// ownership, so the main SSA status update won't remove this field.
241+
statusApply := orcapplyconfigv1alpha1.UserResourceStatus().
242+
WithAppliedPasswordRef(currentRef)
243+
applyConfig := orcapplyconfigv1alpha1.User(obj.Name, obj.Namespace).
244+
WithUID(obj.UID).
245+
WithStatus(orcapplyconfigv1alpha1.UserStatus().
246+
WithResource(statusApply))
247+
if err := actuator.k8sClient.Status().Patch(ctx, obj,
248+
applyconfigs.Patch(types.MergePatchType, applyConfig)); err != nil {
249+
return progress.WrapError(err)
250+
}
251+
252+
if lastAppliedRef != "" {
253+
return progress.NeedsRefresh()
254+
}
255+
return nil
256+
}
257+
186258
func (actuator userActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
187259
log := ctrl.LoggerFrom(ctx)
188260
resource := obj.Spec.Resource
@@ -259,6 +331,7 @@ func handleEnabledUpdate(updateOpts *users.UpdateOpts, resource *resourceSpecT,
259331

260332
func (actuator userActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
261333
return []resourceReconciler{
334+
actuator.reconcilePassword,
262335
actuator.updateResource,
263336
}, nil
264337
}

internal/controllers/user/tests/user-update/00-assert.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ assertAll:
1212
- celExpr: "!has(user.status.resource.defaultProjectID)"
1313
# passwordExpiresAt depends on the Keystone security_compliance
1414
# configuration and is not asserted here.
15+
- celExpr: "user.status.resource.appliedPasswordRef == 'user-update'"
1516
---
1617
apiVersion: openstack.k-orc.cloud/v1alpha1
1718
kind: User

internal/controllers/user/tests/user-update/01-assert.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ status:
88
name: user-update-updated
99
description: user-update-updated
1010
enabled: false
11+
appliedPasswordRef: user-update-password-updated
1112
conditions:
1213
- type: Available
1314
status: "True"

internal/controllers/user/tests/user-update/01-updated-resource.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
---
2+
apiVersion: v1
3+
kind: Secret
4+
metadata:
5+
name: user-update-password-updated
6+
type: Opaque
7+
stringData:
8+
password: "TestPasswordUpdated"
9+
---
210
apiVersion: openstack.k-orc.cloud/v1alpha1
311
kind: User
412
metadata:
@@ -8,3 +16,4 @@ spec:
816
name: user-update-updated
917
description: user-update-updated
1018
enabled: false
19+
passwordRef: user-update-password-updated

internal/controllers/user/tests/user-update/02-assert.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ assertAll:
1010
- celExpr: "!has(user.status.resource.description)"
1111
# passwordExpiresAt depends on the Keystone security_compliance
1212
# configuration and is not asserted here.
13+
- celExpr: "user.status.resource.appliedPasswordRef == 'user-update'"
1314
---
1415
apiVersion: openstack.k-orc.cloud/v1alpha1
1516
kind: User

internal/controllers/user/tests/user-update/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Create a User using only mandatory fields.
66

77
## Step 01
88

9-
Update all mutable fields.
9+
Update all mutable fields, including passwordRef (pointing to a new Secret).
1010

1111
## Step 02
1212

pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go

Lines changed: 15 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)