Skip to content
Draft
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
17 changes: 17 additions & 0 deletions api/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
25 changes: 25 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

35 changes: 35 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.

17 changes: 17 additions & 0 deletions config/crd/bases/openstack.k-orc.cloud_users.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
40 changes: 38 additions & 2 deletions config/samples/openstack_v1alpha1_user.yaml
Original file line number Diff line number Diff line change
@@ -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
91 changes: 91 additions & 0 deletions internal/controllers/user/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand All @@ -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
}

Expand All @@ -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(
Expand All @@ -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()
}

Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading