Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion api/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ type UserResourceSpec struct {
// 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"`
}

Expand Down Expand Up @@ -92,4 +91,10 @@ type UserResourceStatus struct {
// +kubebuilder:validation:MaxLength:=1024
// +optional
PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"`

// appliedPasswordRef is the name of the Secret containing the
// password that was last applied to the OpenStack resource.
// +kubebuilder:validation:MaxLength=1024
// +optional
AppliedPasswordRef string `json:"appliedPasswordRef,omitempty"`
}
7 changes: 7 additions & 0 deletions cmd/models-schema/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions config/crd/bases/openstack.k-orc.cloud_users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,6 @@ spec:
maxLength: 253
minLength: 1
type: string
x-kubernetes-validations:
- message: passwordRef is immutable
rule: self == oldSelf
required:
- passwordRef
type: object
Expand Down Expand Up @@ -302,6 +299,12 @@ spec:
description: resource contains the observed state of the OpenStack
resource.
properties:
appliedPasswordRef:
description: |-
appliedPasswordRef is the name of the Secret containing the
password that was last applied to the OpenStack resource.
maxLength: 1024
type: string
defaultProjectID:
description: defaultProjectID is the ID of the Default Project
to which the user is associated with.
Expand Down
73 changes: 73 additions & 0 deletions internal/controllers/user/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -31,8 +32,10 @@ import (
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
"github.com/k-orc/openstack-resource-controller/v2/internal/logging"
"github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
"github.com/k-orc/openstack-resource-controller/v2/internal/util/applyconfigs"
"github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
)

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

func (actuator userActuator) reconcilePassword(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
log := ctrl.LoggerFrom(ctx)
resource := obj.Spec.Resource
if resource == nil {
return nil
}

currentRef := string(resource.PasswordRef)
var lastAppliedRef string
if obj.Status.Resource != nil {
lastAppliedRef = obj.Status.Resource.AppliedPasswordRef
}

if lastAppliedRef == currentRef {
return nil
}

// Read the password from the referenced Secret
secret, secretRS := dependency.FetchDependency(
ctx, actuator.k8sClient, obj.Namespace,
&resource.PasswordRef, "Secret",
func(*corev1.Secret) bool { return true },
)
if secretRS != nil {
return secretRS
}

passwordBytes, ok := secret.Data["password"]
if !ok {
return progress.NewReconcileStatus().WithProgressMessage("Password secret does not contain \"password\" key")
}
password := string(passwordBytes)

// Only call UpdateUser if this is not the first reconcile after creation.
// CreateResource already set the initial password.
if lastAppliedRef != "" {
log.V(logging.Info).Info("Updating password")
_, err := actuator.osClient.UpdateUser(ctx, osResource.ID, users.UpdateOpts{
Password: password,
})

if orcerrors.IsConflict(err) {
err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)
}
if err != nil {
return progress.WrapError(err)
}
}

// Update the lastAppliedPasswordRef status field via a MergePatch.
// MergePatch sets only the specified fields without claiming SSA
// ownership, so the main SSA status update won't remove this field.
statusApply := orcapplyconfigv1alpha1.UserResourceStatus().
WithAppliedPasswordRef(currentRef)
applyConfig := orcapplyconfigv1alpha1.User(obj.Name, obj.Namespace).
WithUID(obj.UID).
WithStatus(orcapplyconfigv1alpha1.UserStatus().
WithResource(statusApply))
if err := actuator.k8sClient.Status().Patch(ctx, obj,
applyconfigs.Patch(types.MergePatchType, applyConfig)); err != nil {
return progress.WrapError(err)
}

if lastAppliedRef != "" {
return progress.NeedsRefresh()
}
return nil
}

func (actuator userActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
log := ctrl.LoggerFrom(ctx)
resource := obj.Spec.Resource
Expand Down Expand Up @@ -259,6 +331,7 @@ func handleEnabledUpdate(updateOpts *users.UpdateOpts, resource *resourceSpecT,

func (actuator userActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) {
return []resourceReconciler{
actuator.reconcilePassword,
actuator.updateResource,
}, nil
}
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/user/tests/user-update/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ assertAll:
- celExpr: "!has(user.status.resource.defaultProjectID)"
# passwordExpiresAt depends on the Keystone security_compliance
# configuration and is not asserted here.
- celExpr: "user.status.resource.appliedPasswordRef == 'user-update'"
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
Expand Down
1 change: 1 addition & 0 deletions internal/controllers/user/tests/user-update/01-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ status:
name: user-update-updated
description: user-update-updated
enabled: false
appliedPasswordRef: user-update-password-updated
conditions:
- type: Available
status: "True"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
---
apiVersion: v1
kind: Secret
metadata:
name: user-update-password-updated
type: Opaque
stringData:
password: "TestPasswordUpdated"
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
metadata:
Expand All @@ -8,3 +16,4 @@ spec:
name: user-update-updated
description: user-update-updated
enabled: false
passwordRef: user-update-password-updated
1 change: 1 addition & 0 deletions internal/controllers/user/tests/user-update/02-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ assertAll:
- celExpr: "!has(user.status.resource.description)"
# passwordExpiresAt depends on the Keystone security_compliance
# configuration and is not asserted here.
- celExpr: "user.status.resource.appliedPasswordRef == 'user-update'"
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
Expand Down
2 changes: 1 addition & 1 deletion internal/controllers/user/tests/user-update/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Create a User using only mandatory fields.

## Step 01

Update all mutable fields.
Update all mutable fields, including passwordRef (pointing to a new Secret).

## Step 02

Expand Down
21 changes: 15 additions & 6 deletions pkg/clients/applyconfiguration/api/v1alpha1/userresourcestatus.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pkg/clients/applyconfiguration/internal/internal.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions test/apivalidations/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ var _ = Describe("ORC User API validations", func() {
Expect(applyObj(ctx, user, patch)).To(MatchError(ContainSubstring("defaultProjectRef is immutable")))
})

It("should have immutable passwordRef", func(ctx context.Context) {
It("should have mutable passwordRef", func(ctx context.Context) {
user := userStub(namespace)
patch := baseUserPatch(user)
patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec().
Expand All @@ -129,6 +129,6 @@ var _ = Describe("ORC User API validations", func() {

patch.Spec.WithResource(applyconfigv1alpha1.UserResourceSpec().
WithPasswordRef("password-b"))
Expect(applyObj(ctx, user, patch)).To(MatchError(ContainSubstring("passwordRef is immutable")))
Expect(applyObj(ctx, user, patch)).To(Succeed())
})
})
1 change: 1 addition & 0 deletions website/docs/crd-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -4458,6 +4458,7 @@ _Appears in:_
| `defaultProjectID` _string_ | defaultProjectID is the ID of the Default Project to which the user is associated with. | | MaxLength: 1024 <br />Optional: \{\} <br /> |
| `enabled` _boolean_ | enabled defines whether a user is enabled or disabled | | Optional: \{\} <br /> |
| `passwordExpiresAt` _string_ | passwordExpiresAt is the timestamp at which the user's password expires. | | MaxLength: 1024 <br />Optional: \{\} <br /> |
| `appliedPasswordRef` _string_ | appliedPasswordRef is the name of the Secret containing the<br />password that was last applied to the OpenStack resource. | | MaxLength: 1024 <br />Optional: \{\} <br /> |


#### UserSpec
Expand Down
Loading