Skip to content
Open
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
11 changes: 11 additions & 0 deletions api/v1alpha1/user_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ type UserResourceSpec struct {
// enabled defines whether a user is enabled or disabled
// +optional
Enabled *bool `json:"enabled,omitempty"`

// 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"`
}

// UserFilter defines an existing resource by its properties
Expand Down Expand Up @@ -81,4 +87,9 @@ type UserResourceStatus struct {
// enabled defines whether a user is enabled or disabled
// +optional
Enabled bool `json:"enabled,omitempty"`

// passwordExpiresAt is the timestamp at which the user's password expires.
// +kubebuilder:validation:MaxLength:=1024
// +optional
PasswordExpiresAt string `json:"passwordExpiresAt,omitempty"`
}
15 changes: 15 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
passwordRef:
description: |-
passwordRef is a reference to a Secret containing the password
for this user. The Secret must contain a key named "password".
maxLength: 253
minLength: 1
type: string
x-kubernetes-validations:
- message: passwordRef is immutable
rule: self == oldSelf
required:
- passwordRef
type: object
required:
- cloudCredentialsRef
Expand Down Expand Up @@ -313,6 +325,11 @@ spec:
not be unique.
maxLength: 1024
type: string
passwordExpiresAt:
description: passwordExpiresAt is the timestamp at which the user's
password expires.
maxLength: 1024
type: string
type: object
type: object
required:
Expand Down
37 changes: 36 additions & 1 deletion config/samples/openstack_v1alpha1_user.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Domain
metadata:
name: user-sample
spec:
cloudCredentialsRef:
cloudName: openstack-admin
secretName: openstack-clouds
managementPolicy: managed
resource: {}
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: Project
metadata:
name: user-sample
spec:
cloudCredentialsRef:
cloudName: openstack-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
Expand All @@ -9,4 +39,9 @@ spec:
secretName: openstack-clouds
managementPolicy: managed
resource:
description: Sample User
name: user-sample
description: User sample
domainRef: user-sample
defaultProjectRef: user-sample
enabled: true
passwordRef: user-sample
21 changes: 21 additions & 0 deletions internal/controllers/user/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,26 @@ func (actuator userActuator) CreateResource(ctx context.Context, obj orcObjectPT
defaultProjectID = ptr.Deref(project.Status.ID, "")
}
}

var password string
{
secret, secretReconcileStatus := dependency.FetchDependency(
ctx, actuator.k8sClient, obj.Namespace,
&resource.PasswordRef, "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)
}
}
}

if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
return nil, reconcileStatus
}
Expand All @@ -144,6 +164,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 Down
28 changes: 27 additions & 1 deletion internal/controllers/user/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -86,6 +87,17 @@ var domainImportDependency = dependency.NewDependency[*orcv1alpha1.UserList, *or
},
)

var passwordDependency = dependency.NewDependency[*orcv1alpha1.UserList, *corev1.Secret](
"spec.resource.passwordRef",
func(user *orcv1alpha1.User) []string {
resource := user.Spec.Resource
if resource == nil {
return nil
}
return []string{string(resource.PasswordRef)}
},
)

// 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)
Expand All @@ -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{})),
).
Expand All @@ -118,12 +136,20 @@ func (c userReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctr
Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler,
builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})),
).
For(&orcv1alpha1.User{})
// XXX: This is a general watch on secrets. A general watch on secrets
// is undesirable because:
// - It requires problematic RBAC
// - Secrets are arbitrarily large, and we don't want to cache their contents
//
// These will require separate solutions. For the latter we should
// probably use a MetadataOnly 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 {
Expand Down
6 changes: 6 additions & 0 deletions internal/controllers/user/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package user

import (
"time"

"github.com/go-logr/logr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Expand Down Expand Up @@ -62,5 +64,9 @@ func (userStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResou
resourceStatus.WithDefaultProjectID(osResource.DefaultProjectID)
}

if !osResource.PasswordExpiresAt.IsZero() {
resourceStatus.WithPasswordExpiresAt(osResource.PasswordExpiresAt.Format(time.RFC3339))
}

statusApply.WithResource(resourceStatus)
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ assertAll:
- celExpr: "user.status.id != ''"
- celExpr: "user.status.resource.domainID == domain.status.id"
- celExpr: "user.status.resource.defaultProjectID == project.status.id"
# passwordExpiresAt depends on the Keystone security_compliance
# configuration and is not asserted here.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ spec:
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
metadata:
Expand All @@ -35,4 +43,5 @@ spec:
description: User from "create full" test
domainRef: user-create-full
defaultProjectRef: user-create-full
enabled: true
enabled: true
passwordRef: user-create-full
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ assertAll:
- celExpr: "!has(user.status.resource.description)"
- celExpr: "user.status.resource.domainID == 'default'"
- celExpr: "!has(user.status.resource.defaultProjectID)"
# passwordExpiresAt depends on the Keystone security_compliance
# configuration and is not asserted here.

Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
---
apiVersion: v1
kind: Secret
metadata:
name: user-create-minimal
type: Opaque
stringData:
password: "TestPassword"
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
metadata:
Expand All @@ -8,4 +16,5 @@ spec:
cloudName: openstack-admin
secretName: openstack-clouds
managementPolicy: managed
resource: {}
resource:
passwordRef: user-create-minimal
15 changes: 15 additions & 0 deletions internal/controllers/user/tests/user-dependency/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,19 @@ status:
- type: Progressing
message: Waiting for Project/user-dependency to be created
status: "True"
reason: Progressing
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
metadata:
name: user-dependency-no-password
status:
conditions:
- type: Available
message: Waiting for Secret/user-dependency-password to be created
status: "False"
reason: Progressing
- type: Progressing
message: Waiting for Secret/user-dependency-password to be created
status: "True"
reason: Progressing
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ spec:
managementPolicy: managed
resource:
domainRef: user-dependency
passwordRef: user-dependency-password-existing
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
Expand All @@ -22,6 +23,7 @@ spec:
managementPolicy: managed
resource:
defaultProjectRef: user-dependency
passwordRef: user-dependency-password-existing
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
Expand All @@ -32,4 +34,17 @@ spec:
cloudName: openstack-admin
secretName: user-dependency
managementPolicy: managed
resource: {}
resource:
passwordRef: user-dependency-password-existing
---
apiVersion: openstack.k-orc.cloud/v1alpha1
kind: User
metadata:
name: user-dependency-no-password
spec:
cloudCredentialsRef:
cloudName: openstack-admin
secretName: openstack-clouds
managementPolicy: managed
resource:
passwordRef: user-dependency-password
10 changes: 9 additions & 1 deletion internal/controllers/user/tests/user-dependency/00-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ 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
namespaced: true
---
apiVersion: v1
kind: Secret
metadata:
name: user-dependency-password-existing
type: Opaque
stringData:
password: "TestPassword"
Loading
Loading