Skip to content

Commit b6fde2a

Browse files
authored
Merge pull request #725 from shiftstack/mutable-password
user: implement password mutability via passwordRef
2 parents 2cca781 + 28a09a4 commit b6fde2a

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)