diff --git a/PROJECT b/PROJECT
index 73daa42d0..cd63db01a 100644
--- a/PROJECT
+++ b/PROJECT
@@ -112,6 +112,14 @@ resources:
kind: Role
path: github.com/k-orc/openstack-resource-controller/api/v1alpha1
version: v1alpha1
+- api:
+ crdVersion: v1
+ namespaced: true
+ domain: k-orc.cloud
+ group: openstack
+ kind: RoleAssignment
+ path: github.com/k-orc/openstack-resource-controller/api/v1alpha1
+ version: v1alpha1
- api:
crdVersion: v1
namespaced: true
diff --git a/api/v1alpha1/roleassignment_types.go b/api/v1alpha1/roleassignment_types.go
new file mode 100644
index 000000000..b2a947975
--- /dev/null
+++ b/api/v1alpha1/roleassignment_types.go
@@ -0,0 +1,104 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+// RoleAssignmentResourceSpec defines the desired role assignment.
+// A role assignment grants a role to a user or group on a project or domain.
+// Role assignments are immutable once created and identified by the combination
+// of (role, actor, scope) rather than a separate ID.
+// +kubebuilder:validation:XValidation:rule="(has(self.userRef) && !has(self.groupRef)) || (!has(self.userRef) && has(self.groupRef))",message="exactly one of userRef or groupRef is required"
+// +kubebuilder:validation:XValidation:rule="(has(self.projectRef) && !has(self.domainRef)) || (!has(self.projectRef) && has(self.domainRef))",message="exactly one of projectRef or domainRef is required"
+// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="RoleAssignmentResourceSpec is immutable"
+type RoleAssignmentResourceSpec struct {
+ // roleRef references the Role being assigned.
+ // +required
+ RoleRef KubernetesNameRef `json:"roleRef,omitempty"`
+
+ // userRef references the User receiving the role assignment.
+ // Exactly one of userRef or groupRef must be specified.
+ // +optional
+ UserRef *KubernetesNameRef `json:"userRef,omitempty"`
+
+ // groupRef references the Group receiving the role assignment.
+ // Exactly one of userRef or groupRef must be specified.
+ // +optional
+ GroupRef *KubernetesNameRef `json:"groupRef,omitempty"`
+
+ // projectRef references the Project scope for the assignment.
+ // Exactly one of projectRef or domainRef must be specified.
+ // +optional
+ ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
+
+ // domainRef references the Domain scope for the assignment.
+ // Exactly one of projectRef or domainRef must be specified.
+ // +optional
+ DomainRef *KubernetesNameRef `json:"domainRef,omitempty"`
+}
+
+// RoleAssignmentFilter defines import filter criteria for existing role assignments.
+// +kubebuilder:validation:MinProperties:=1
+type RoleAssignmentFilter struct {
+ // roleRef filters by the referenced Role.
+ // +optional
+ RoleRef *KubernetesNameRef `json:"roleRef,omitempty"`
+
+ // userRef filters by the referenced User.
+ // +optional
+ UserRef *KubernetesNameRef `json:"userRef,omitempty"`
+
+ // groupRef filters by the referenced Group.
+ // +optional
+ GroupRef *KubernetesNameRef `json:"groupRef,omitempty"`
+
+ // projectRef filters by the referenced Project scope.
+ // +optional
+ ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"`
+
+ // domainRef filters by the referenced Domain scope.
+ // +optional
+ DomainRef *KubernetesNameRef `json:"domainRef,omitempty"`
+}
+
+// RoleAssignmentResourceStatus represents the observed state of the role assignment.
+// Note: Role assignments do not have a unique ID in OpenStack - they are identified
+// by the combination of role, actor (user/group), and scope (project/domain).
+type RoleAssignmentResourceStatus struct {
+ // roleID is the OpenStack ID of the assigned role.
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ RoleID string `json:"roleID,omitempty"`
+
+ // userID is the OpenStack ID of the user (if actorType is User).
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ UserID string `json:"userID,omitempty"`
+
+ // groupID is the OpenStack ID of the group (if actorType is Group).
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ GroupID string `json:"groupID,omitempty"`
+
+ // projectID is the OpenStack ID of the project scope (if scopeType is Project).
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ ProjectID string `json:"projectID,omitempty"`
+
+ // domainID is the OpenStack ID of the domain scope (if scopeType is Domain).
+ // +kubebuilder:validation:MaxLength=1024
+ // +optional
+ DomainID string `json:"domainID,omitempty"`
+}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 09694efa4..23b45f76e 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -3782,6 +3782,233 @@ func (in *Role) DeepCopyObject() runtime.Object {
return nil
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignment) DeepCopyInto(out *RoleAssignment) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignment.
+func (in *RoleAssignment) DeepCopy() *RoleAssignment {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignment)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *RoleAssignment) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentFilter) DeepCopyInto(out *RoleAssignmentFilter) {
+ *out = *in
+ if in.RoleRef != nil {
+ in, out := &in.RoleRef, &out.RoleRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.UserRef != nil {
+ in, out := &in.UserRef, &out.UserRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.GroupRef != nil {
+ in, out := &in.GroupRef, &out.GroupRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.ProjectRef != nil {
+ in, out := &in.ProjectRef, &out.ProjectRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.DomainRef != nil {
+ in, out := &in.DomainRef, &out.DomainRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentFilter.
+func (in *RoleAssignmentFilter) DeepCopy() *RoleAssignmentFilter {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentFilter)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentImport) DeepCopyInto(out *RoleAssignmentImport) {
+ *out = *in
+ if in.Filter != nil {
+ in, out := &in.Filter, &out.Filter
+ *out = new(RoleAssignmentFilter)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentImport.
+func (in *RoleAssignmentImport) DeepCopy() *RoleAssignmentImport {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentImport)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentList) DeepCopyInto(out *RoleAssignmentList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]RoleAssignment, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentList.
+func (in *RoleAssignmentList) DeepCopy() *RoleAssignmentList {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *RoleAssignmentList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentResourceSpec) DeepCopyInto(out *RoleAssignmentResourceSpec) {
+ *out = *in
+ if in.UserRef != nil {
+ in, out := &in.UserRef, &out.UserRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.GroupRef != nil {
+ in, out := &in.GroupRef, &out.GroupRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.ProjectRef != nil {
+ in, out := &in.ProjectRef, &out.ProjectRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+ if in.DomainRef != nil {
+ in, out := &in.DomainRef, &out.DomainRef
+ *out = new(KubernetesNameRef)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentResourceSpec.
+func (in *RoleAssignmentResourceSpec) DeepCopy() *RoleAssignmentResourceSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentResourceSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentResourceStatus) DeepCopyInto(out *RoleAssignmentResourceStatus) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentResourceStatus.
+func (in *RoleAssignmentResourceStatus) DeepCopy() *RoleAssignmentResourceStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentResourceStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentSpec) DeepCopyInto(out *RoleAssignmentSpec) {
+ *out = *in
+ if in.Import != nil {
+ in, out := &in.Import, &out.Import
+ *out = new(RoleAssignmentImport)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.Resource != nil {
+ in, out := &in.Resource, &out.Resource
+ *out = new(RoleAssignmentResourceSpec)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ManagedOptions != nil {
+ in, out := &in.ManagedOptions, &out.ManagedOptions
+ *out = new(ManagedOptions)
+ **out = **in
+ }
+ out.CloudCredentialsRef = in.CloudCredentialsRef
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentSpec.
+func (in *RoleAssignmentSpec) DeepCopy() *RoleAssignmentSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleAssignmentStatus) DeepCopyInto(out *RoleAssignmentStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Resource != nil {
+ in, out := &in.Resource, &out.Resource
+ *out = new(RoleAssignmentResourceStatus)
+ **out = **in
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleAssignmentStatus.
+func (in *RoleAssignmentStatus) DeepCopy() *RoleAssignmentStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(RoleAssignmentStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RoleFilter) DeepCopyInto(out *RoleFilter) {
*out = *in
diff --git a/api/v1alpha1/zz_generated.roleassignment-resource.go b/api/v1alpha1/zz_generated.roleassignment-resource.go
new file mode 100644
index 000000000..0453da8a4
--- /dev/null
+++ b/api/v1alpha1/zz_generated.roleassignment-resource.go
@@ -0,0 +1,165 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// RoleAssignmentImport specifies an existing resource which will be imported instead of
+// creating a new one
+// +kubebuilder:validation:MinProperties:=1
+type RoleAssignmentImport struct {
+
+ // filter contains a resource query which is expected to return a single
+ // result. The controller will continue to retry if filter returns no
+ // results. If filter returns multiple results the controller will set an
+ // error state and will not continue to retry.
+ // +optional
+ Filter *RoleAssignmentFilter `json:"filter,omitempty"`
+}
+
+// RoleAssignmentSpec defines the desired state of an ORC object.
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'managed' ? has(self.resource) : true",message="resource must be specified when policy is managed"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'managed' ? !has(self.__import__) : true",message="import may not be specified when policy is managed"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'unmanaged' ? !has(self.resource) : true",message="resource may not be specified when policy is unmanaged"
+// +kubebuilder:validation:XValidation:rule="self.managementPolicy == 'unmanaged' ? has(self.__import__) : true",message="import must be specified when policy is unmanaged"
+// +kubebuilder:validation:XValidation:rule="has(self.managedOptions) ? self.managementPolicy == 'managed' : true",message="managedOptions may only be provided when policy is managed"
+type RoleAssignmentSpec struct {
+ // import refers to an existing OpenStack resource which will be imported instead of
+ // creating a new one.
+ // +optional
+ Import *RoleAssignmentImport `json:"import,omitempty"`
+
+ // resource specifies the desired state of the resource.
+ //
+ // resource may not be specified if the management policy is `unmanaged`.
+ //
+ // resource must be specified if the management policy is `managed`.
+ // +optional
+ Resource *RoleAssignmentResourceSpec `json:"resource,omitempty"`
+
+ // managementPolicy defines how ORC will treat the object. Valid values are
+ // `managed`: ORC will create, update, and delete the resource; `unmanaged`:
+ // ORC will import an existing resource, and will not apply updates to it or
+ // delete it.
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="managementPolicy is immutable"
+ // +kubebuilder:default:=managed
+ // +optional
+ ManagementPolicy ManagementPolicy `json:"managementPolicy,omitempty"`
+
+ // managedOptions specifies options which may be applied to managed objects.
+ // +optional
+ ManagedOptions *ManagedOptions `json:"managedOptions,omitempty"`
+
+ // cloudCredentialsRef points to a secret containing OpenStack credentials
+ // +required
+ CloudCredentialsRef CloudCredentialsReference `json:"cloudCredentialsRef,omitzero"`
+}
+
+// RoleAssignmentStatus defines the observed state of an ORC resource.
+type RoleAssignmentStatus struct {
+ // conditions represents the observed status of the object.
+ // Known .status.conditions.type are: "Available", "Progressing"
+ //
+ // Available represents the availability of the OpenStack resource. If it is
+ // true then the resource is ready for use.
+ //
+ // Progressing indicates whether the controller is still attempting to
+ // reconcile the current state of the OpenStack resource to the desired
+ // state. Progressing will be False either because the desired state has
+ // been achieved, or because some terminal error prevents it from ever being
+ // achieved and the controller is no longer attempting to reconcile. If
+ // Progressing is True, an observer waiting on the resource should continue
+ // to wait.
+ //
+ // +kubebuilder:validation:MaxItems:=32
+ // +patchMergeKey=type
+ // +patchStrategy=merge
+ // +listType=map
+ // +listMapKey=type
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+
+ // resource contains the observed state of the OpenStack resource.
+ // +optional
+ Resource *RoleAssignmentResourceStatus `json:"resource,omitempty"`
+}
+
+var _ ObjectWithConditions = &RoleAssignment{}
+
+func (i *RoleAssignment) GetConditions() []metav1.Condition {
+ return i.Status.Conditions
+}
+
+// +genclient
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:categories=openstack
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type=='Available')].status",description="Availability status of resource"
+// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].message",description="Message describing current progress status"
+
+// RoleAssignment is the Schema for an ORC resource.
+type RoleAssignment struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // metadata contains the object metadata
+ // +optional
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ // spec specifies the desired state of the resource.
+ // +required
+ Spec RoleAssignmentSpec `json:"spec,omitzero"`
+
+ // status defines the observed state of the resource.
+ // +optional
+ Status RoleAssignmentStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// RoleAssignmentList contains a list of RoleAssignment.
+type RoleAssignmentList struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // metadata contains the list metadata
+ // +optional
+ metav1.ListMeta `json:"metadata,omitempty"`
+
+ // items contains a list of RoleAssignment.
+ // +required
+ Items []RoleAssignment `json:"items"`
+}
+
+func (l *RoleAssignmentList) GetItems() []RoleAssignment {
+ return l.Items
+}
+
+func init() {
+ SchemeBuilder.Register(&RoleAssignment{}, &RoleAssignmentList{})
+}
+
+func (i *RoleAssignment) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) {
+ if i == nil {
+ return nil, nil
+ }
+
+ return &i.Namespace, &i.Spec.CloudCredentialsRef
+}
+
+var _ CloudCredentialsRefProvider = &RoleAssignment{}
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
index c8a624acc..8ec80848a 100644
--- a/cmd/manager/main.go
+++ b/cmd/manager/main.go
@@ -41,6 +41,7 @@ import (
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/port"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/project"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/role"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/roleassignment"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/router"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/routerinterface"
"github.com/k-orc/openstack-resource-controller/v2/internal/controllers/securitygroup"
@@ -136,6 +137,7 @@ func main() {
keypair.New(scopeFactory),
group.New(scopeFactory),
role.New(scopeFactory),
+ roleassignment.New(scopeFactory),
}
restConfig := ctrl.GetConfigOrDie()
diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go
index 3e7cff7c6..c9bee77d9 100644
--- a/cmd/models-schema/zz_generated.openapi.go
+++ b/cmd/models-schema/zz_generated.openapi.go
@@ -159,6 +159,14 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ProjectStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ProjectStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ProviderPropertiesStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ProviderPropertiesStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Role": schema_openstack_resource_controller_v2_api_v1alpha1_Role(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignment": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignment(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentFilter": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentFilter(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentImport": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentImport(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentList": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentList(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentResourceSpec(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentResourceStatus(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentSpec": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentSpec(ref),
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentStatus": schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentStatus(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleFilter": schema_openstack_resource_controller_v2_api_v1alpha1_RoleFilter(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleImport": schema_openstack_resource_controller_v2_api_v1alpha1_RoleImport(ref),
"github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleList": schema_openstack_resource_controller_v2_api_v1alpha1_RoleList(ref),
@@ -7190,6 +7198,367 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_Role(ref common.Refere
}
}
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignment(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignment is the Schema for an ORC resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Description: "metadata contains the object metadata",
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
+ },
+ },
+ "spec": {
+ SchemaProps: spec.SchemaProps{
+ Description: "spec specifies the desired state of the resource.",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentSpec"),
+ },
+ },
+ "status": {
+ SchemaProps: spec.SchemaProps{
+ Description: "status defines the observed state of the resource.",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentStatus"),
+ },
+ },
+ },
+ Required: []string{"spec"},
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentFilter defines import filter criteria for existing role assignments.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "roleRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "roleRef filters by the referenced Role.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "userRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "userRef filters by the referenced User.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "groupRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "groupRef filters by the referenced Group.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectRef filters by the referenced Project scope.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "domainRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "domainRef filters by the referenced Domain scope.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentImport(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentImport specifies an existing resource which will be imported instead of creating a new one",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "filter": {
+ SchemaProps: spec.SchemaProps{
+ Description: "filter contains a resource query which is expected to return a single result. The controller will continue to retry if filter returns no results. If filter returns multiple results the controller will set an error state and will not continue to retry.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentFilter"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentFilter"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentList(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentList contains a list of RoleAssignment.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Description: "metadata contains the list metadata",
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
+ },
+ },
+ "items": {
+ SchemaProps: spec.SchemaProps{
+ Description: "items contains a list of RoleAssignment.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignment"),
+ },
+ },
+ },
+ },
+ },
+ },
+ Required: []string{"items"},
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignment", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentResourceSpec defines the desired role assignment. A role assignment grants a role to a user or group on a project or domain. Role assignments are immutable once created and identified by the combination of (role, actor, scope) rather than a separate ID.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "roleRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "roleRef references the Role being assigned.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "userRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "userRef references the User receiving the role assignment. Exactly one of userRef or groupRef must be specified.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "groupRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "groupRef references the Group receiving the role assignment. Exactly one of userRef or groupRef must be specified.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectRef references the Project scope for the assignment. Exactly one of projectRef or domainRef must be specified.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "domainRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "domainRef references the Domain scope for the assignment. Exactly one of projectRef or domainRef must be specified.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ Required: []string{"roleRef"},
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentResourceStatus represents the observed state of the role assignment. Note: Role assignments do not have a unique ID in OpenStack - they are identified by the combination of role, actor (user/group), and scope (project/domain).",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "roleID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "roleID is the OpenStack ID of the assigned role.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "userID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "userID is the OpenStack ID of the user (if actorType is User).",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "groupID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "groupID is the OpenStack ID of the group (if actorType is Group).",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "projectID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "projectID is the OpenStack ID of the project scope (if scopeType is Project).",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "domainID": {
+ SchemaProps: spec.SchemaProps{
+ Description: "domainID is the OpenStack ID of the domain scope (if scopeType is Domain).",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentSpec defines the desired state of an ORC object.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "import": {
+ SchemaProps: spec.SchemaProps{
+ Description: "import refers to an existing OpenStack resource which will be imported instead of creating a new one.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentImport"),
+ },
+ },
+ "resource": {
+ SchemaProps: spec.SchemaProps{
+ Description: "resource specifies the desired state of the resource.\n\nresource may not be specified if the management policy is `unmanaged`.\n\nresource must be specified if the management policy is `managed`.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceSpec"),
+ },
+ },
+ "managementPolicy": {
+ SchemaProps: spec.SchemaProps{
+ Description: "managementPolicy defines how ORC will treat the object. Valid values are `managed`: ORC will create, update, and delete the resource; `unmanaged`: ORC will import an existing resource, and will not apply updates to it or delete it.",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "managedOptions": {
+ SchemaProps: spec.SchemaProps{
+ Description: "managedOptions specifies options which may be applied to managed objects.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"),
+ },
+ },
+ "cloudCredentialsRef": {
+ SchemaProps: spec.SchemaProps{
+ Description: "cloudCredentialsRef points to a secret containing OpenStack credentials",
+ Default: map[string]interface{}{},
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference"),
+ },
+ },
+ },
+ Required: []string{"cloudCredentialsRef"},
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceSpec"},
+ }
+}
+
+func schema_openstack_resource_controller_v2_api_v1alpha1_RoleAssignmentStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "RoleAssignmentStatus defines the observed state of an ORC resource.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "conditions": {
+ VendorExtensible: spec.VendorExtensible{
+ Extensions: spec.Extensions{
+ "x-kubernetes-list-map-keys": []interface{}{
+ "type",
+ },
+ "x-kubernetes-list-type": "map",
+ "x-kubernetes-patch-merge-key": "type",
+ "x-kubernetes-patch-strategy": "merge",
+ },
+ },
+ SchemaProps: spec.SchemaProps{
+ Description: "conditions represents the observed status of the object. Known .status.conditions.type are: \"Available\", \"Progressing\"\n\nAvailable represents the availability of the OpenStack resource. If it is true then the resource is ready for use.\n\nProgressing indicates whether the controller is still attempting to reconcile the current state of the OpenStack resource to the desired state. Progressing will be False either because the desired state has been achieved, or because some terminal error prevents it from ever being achieved and the controller is no longer attempting to reconcile. If Progressing is True, an observer waiting on the resource should continue to wait.",
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Condition"),
+ },
+ },
+ },
+ },
+ },
+ "resource": {
+ SchemaProps: spec.SchemaProps{
+ Description: "resource contains the observed state of the OpenStack resource.",
+ Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceStatus"),
+ },
+ },
+ },
+ },
+ },
+ Dependencies: []string{
+ "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.RoleAssignmentResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"},
+ }
+}
+
func schema_openstack_resource_controller_v2_api_v1alpha1_RoleFilter(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
diff --git a/cmd/resource-generator/data/adapter.template b/cmd/resource-generator/data/adapter.template
index 7bec457ee..dca45ba90 100644
--- a/cmd/resource-generator/data/adapter.template
+++ b/cmd/resource-generator/data/adapter.template
@@ -55,7 +55,11 @@ func (f adapterT) GetManagedOptions() *orcv1alpha1.ManagedOptions {
}
func (f adapterT) GetStatusID() *string {
+{{- if .NoResourceID }}
+ return nil
+{{- else }}
return f.Status.ID
+{{- end }}
}
func (f adapterT) GetResourceSpec() *resourceSpecT {
@@ -63,10 +67,14 @@ func (f adapterT) GetResourceSpec() *resourceSpecT {
}
func (f adapterT) GetImportID() *string {
+{{- if .NoResourceID }}
+ return nil
+{{- else }}
if f.Spec.Import == nil {
return nil
}
return f.Spec.Import.ID
+{{- end }}
}
func (f adapterT) GetImportFilter() *filterT {
diff --git a/cmd/resource-generator/data/api.template b/cmd/resource-generator/data/api.template
index 9abb00d7a..04e5e0bed 100644
--- a/cmd/resource-generator/data/api.template
+++ b/cmd/resource-generator/data/api.template
@@ -23,8 +23,11 @@ import (
// {{ .Name }}Import specifies an existing resource which will be imported instead of
// creating a new one
// +kubebuilder:validation:MinProperties:=1
+{{- if not .NoResourceID }}
// +kubebuilder:validation:MaxProperties:=1
+{{- end }}
type {{ .Name }}Import struct {
+{{- if not .NoResourceID }}
{{- if .UsesNameAsID }}
// id contains the name of an existing resource. Note: This resource uses
// the resource name as the unique identifier, not a UUID.
@@ -41,6 +44,7 @@ type {{ .Name }}Import struct {
// +kubebuilder:validation:MaxLength:=36
// +optional
ID *string `json:"id,omitempty"` //nolint:kubeapilinter
+{{- end }}
{{- end }}
// filter contains a resource query which is expected to return a single
@@ -118,11 +122,13 @@ type {{ .Name }}Status struct {
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+{{- if not .NoResourceID }}
// id is the unique identifier of the OpenStack resource.
// +kubebuilder:validation:MaxLength:=1024
// +optional
ID *string `json:"id,omitempty"`
+{{- end }}
// resource contains the observed state of the OpenStack resource.
// +optional
@@ -143,7 +149,9 @@ func (i *{{ .Name }}) GetConditions() []metav1.Condition {
// +kubebuilder:object:root=true
// +kubebuilder:resource:categories=openstack
// +kubebuilder:subresource:status
+{{- if not .NoResourceID }}
// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="Resource ID"
+{{- end }}
// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type=='Available')].status",description="Availability status of resource"
{{- range .AdditionalPrintColumns }}
// +kubebuilder:printcolumn:name="{{ .Name }}",type="{{ .Type }}",JSONPath="{{ .JSONPath }}",description="{{ .Description }}"
diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go
index 2ff83df6e..bc32a1244 100644
--- a/cmd/resource-generator/main.go
+++ b/cmd/resource-generator/main.go
@@ -67,6 +67,10 @@ type templateFields struct {
// When true, the UUID validation will be omitted from the Import.ID field.
// Default is false (uses UUID).
UsesNameAsID bool
+ // NoResourceID indicates this is a relationship resource without an
+ // OpenStack-assigned ID. When true, the generator omits import.id,
+ // status.ID, and the ID print column from the generated API types.
+ NoResourceID bool
}
var resources []templateFields = []templateFields{
@@ -124,6 +128,11 @@ var resources []templateFields = []templateFields{
{
Name: "Role",
},
+ {
+ Name: "RoleAssignment",
+ IsNotNamed: true,
+ NoResourceID: true,
+ },
{
Name: "Router",
ExistingOSClient: true,
diff --git a/config/crd/bases/openstack.k-orc.cloud_roleassignments.yaml b/config/crd/bases/openstack.k-orc.cloud_roleassignments.yaml
new file mode 100644
index 000000000..3668ad03e
--- /dev/null
+++ b/config/crd/bases/openstack.k-orc.cloud_roleassignments.yaml
@@ -0,0 +1,331 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.20.1
+ name: roleassignments.openstack.k-orc.cloud
+spec:
+ group: openstack.k-orc.cloud
+ names:
+ categories:
+ - openstack
+ kind: RoleAssignment
+ listKind: RoleAssignmentList
+ plural: roleassignments
+ singular: roleassignment
+ scope: Namespaced
+ versions:
+ - additionalPrinterColumns:
+ - description: Availability status of resource
+ jsonPath: .status.conditions[?(@.type=='Available')].status
+ name: Available
+ type: string
+ - description: Message describing current progress status
+ jsonPath: .status.conditions[?(@.type=='Progressing')].message
+ name: Message
+ type: string
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: RoleAssignment is the Schema for an ORC resource.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: spec specifies the desired state of the resource.
+ properties:
+ cloudCredentialsRef:
+ description: cloudCredentialsRef points to a secret containing OpenStack
+ credentials
+ properties:
+ cloudName:
+ description: cloudName specifies the name of the entry in the
+ clouds.yaml file to use.
+ maxLength: 256
+ minLength: 1
+ type: string
+ secretName:
+ description: |-
+ secretName is the name of a secret in the same namespace as the resource being provisioned.
+ The secret must contain a key named `clouds.yaml` which contains an OpenStack clouds.yaml file.
+ The secret may optionally contain a key named `cacert` containing a PEM-encoded CA certificate.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - cloudName
+ - secretName
+ type: object
+ import:
+ description: |-
+ import refers to an existing OpenStack resource which will be imported instead of
+ creating a new one.
+ minProperties: 1
+ properties:
+ filter:
+ description: |-
+ filter contains a resource query which is expected to return a single
+ result. The controller will continue to retry if filter returns no
+ results. If filter returns multiple results the controller will set an
+ error state and will not continue to retry.
+ minProperties: 1
+ properties:
+ domainRef:
+ description: domainRef filters by the referenced Domain scope.
+ maxLength: 253
+ minLength: 1
+ type: string
+ groupRef:
+ description: groupRef filters by the referenced Group.
+ maxLength: 253
+ minLength: 1
+ type: string
+ projectRef:
+ description: projectRef filters by the referenced Project
+ scope.
+ maxLength: 253
+ minLength: 1
+ type: string
+ roleRef:
+ description: roleRef filters by the referenced Role.
+ maxLength: 253
+ minLength: 1
+ type: string
+ userRef:
+ description: userRef filters by the referenced User.
+ maxLength: 253
+ minLength: 1
+ type: string
+ type: object
+ type: object
+ managedOptions:
+ description: managedOptions specifies options which may be applied
+ to managed objects.
+ properties:
+ onDelete:
+ default: delete
+ description: |-
+ onDelete specifies the behaviour of the controller when the ORC
+ object is deleted. Options are `delete` - delete the OpenStack resource;
+ `detach` - do not delete the OpenStack resource. If not specified, the
+ default is `delete`.
+ enum:
+ - delete
+ - detach
+ type: string
+ type: object
+ managementPolicy:
+ default: managed
+ description: |-
+ managementPolicy defines how ORC will treat the object. Valid values are
+ `managed`: ORC will create, update, and delete the resource; `unmanaged`:
+ ORC will import an existing resource, and will not apply updates to it or
+ delete it.
+ enum:
+ - managed
+ - unmanaged
+ type: string
+ x-kubernetes-validations:
+ - message: managementPolicy is immutable
+ rule: self == oldSelf
+ resource:
+ description: |-
+ resource specifies the desired state of the resource.
+
+ resource may not be specified if the management policy is `unmanaged`.
+
+ resource must be specified if the management policy is `managed`.
+ properties:
+ domainRef:
+ description: |-
+ domainRef references the Domain scope for the assignment.
+ Exactly one of projectRef or domainRef must be specified.
+ maxLength: 253
+ minLength: 1
+ type: string
+ groupRef:
+ description: |-
+ groupRef references the Group receiving the role assignment.
+ Exactly one of userRef or groupRef must be specified.
+ maxLength: 253
+ minLength: 1
+ type: string
+ projectRef:
+ description: |-
+ projectRef references the Project scope for the assignment.
+ Exactly one of projectRef or domainRef must be specified.
+ maxLength: 253
+ minLength: 1
+ type: string
+ roleRef:
+ description: roleRef references the Role being assigned.
+ maxLength: 253
+ minLength: 1
+ type: string
+ userRef:
+ description: |-
+ userRef references the User receiving the role assignment.
+ Exactly one of userRef or groupRef must be specified.
+ maxLength: 253
+ minLength: 1
+ type: string
+ required:
+ - roleRef
+ type: object
+ x-kubernetes-validations:
+ - message: exactly one of userRef or groupRef is required
+ rule: (has(self.userRef) && !has(self.groupRef)) || (!has(self.userRef)
+ && has(self.groupRef))
+ - message: exactly one of projectRef or domainRef is required
+ rule: (has(self.projectRef) && !has(self.domainRef)) || (!has(self.projectRef)
+ && has(self.domainRef))
+ - message: RoleAssignmentResourceSpec is immutable
+ rule: self == oldSelf
+ required:
+ - cloudCredentialsRef
+ type: object
+ x-kubernetes-validations:
+ - message: resource must be specified when policy is managed
+ rule: 'self.managementPolicy == ''managed'' ? has(self.resource) : true'
+ - message: import may not be specified when policy is managed
+ rule: 'self.managementPolicy == ''managed'' ? !has(self.__import__)
+ : true'
+ - message: resource may not be specified when policy is unmanaged
+ rule: 'self.managementPolicy == ''unmanaged'' ? !has(self.resource)
+ : true'
+ - message: import must be specified when policy is unmanaged
+ rule: 'self.managementPolicy == ''unmanaged'' ? has(self.__import__)
+ : true'
+ - message: managedOptions may only be provided when policy is managed
+ rule: 'has(self.managedOptions) ? self.managementPolicy == ''managed''
+ : true'
+ status:
+ description: status defines the observed state of the resource.
+ properties:
+ conditions:
+ description: |-
+ conditions represents the observed status of the object.
+ Known .status.conditions.type are: "Available", "Progressing"
+
+ Available represents the availability of the OpenStack resource. If it is
+ true then the resource is ready for use.
+
+ Progressing indicates whether the controller is still attempting to
+ reconcile the current state of the OpenStack resource to the desired
+ state. Progressing will be False either because the desired state has
+ been achieved, or because some terminal error prevents it from ever being
+ achieved and the controller is no longer attempting to reconcile. If
+ Progressing is True, an observer waiting on the resource should continue
+ to wait.
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ maxItems: 32
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ resource:
+ description: resource contains the observed state of the OpenStack
+ resource.
+ properties:
+ domainID:
+ description: domainID is the OpenStack ID of the domain scope
+ (if scopeType is Domain).
+ maxLength: 1024
+ type: string
+ groupID:
+ description: groupID is the OpenStack ID of the group (if actorType
+ is Group).
+ maxLength: 1024
+ type: string
+ projectID:
+ description: projectID is the OpenStack ID of the project scope
+ (if scopeType is Project).
+ maxLength: 1024
+ type: string
+ roleID:
+ description: roleID is the OpenStack ID of the assigned role.
+ maxLength: 1024
+ type: string
+ userID:
+ description: userID is the OpenStack ID of the user (if actorType
+ is User).
+ maxLength: 1024
+ type: string
+ type: object
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 85a318b42..23338effb 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -16,6 +16,7 @@ resources:
- bases/openstack.k-orc.cloud_ports.yaml
- bases/openstack.k-orc.cloud_projects.yaml
- bases/openstack.k-orc.cloud_roles.yaml
+- bases/openstack.k-orc.cloud_roleassignments.yaml
- bases/openstack.k-orc.cloud_routers.yaml
- bases/openstack.k-orc.cloud_routerinterfaces.yaml
- bases/openstack.k-orc.cloud_securitygroups.yaml
diff --git a/config/manifests/bases/orc.clusterserviceversion.yaml b/config/manifests/bases/orc.clusterserviceversion.yaml
index 0b7164e78..ef49c8611 100644
--- a/config/manifests/bases/orc.clusterserviceversion.yaml
+++ b/config/manifests/bases/orc.clusterserviceversion.yaml
@@ -19,6 +19,16 @@ spec:
apiservicedefinitions: {}
customresourcedefinitions:
owned:
+ - description: AddressScope is the Schema for an ORC resource.
+ displayName: Address Scope
+ kind: AddressScope
+ name: addressscopes.openstack.k-orc.cloud
+ version: v1alpha1
+ - description: ApplicationCredential is the Schema for an ORC resource.
+ displayName: Application Credential
+ kind: ApplicationCredential
+ name: applicationcredentials.openstack.k-orc.cloud
+ version: v1alpha1
- description: Domain is the Schema for an ORC resource.
displayName: Domain
kind: Domain
@@ -69,6 +79,11 @@ spec:
kind: Project
name: projects.openstack.k-orc.cloud
version: v1alpha1
+ - description: RoleAssignment is the Schema for an ORC resource.
+ displayName: Role Assignment
+ kind: RoleAssignment
+ name: roleassignments.openstack.k-orc.cloud
+ version: v1alpha1
- description: Role is the Schema for an ORC resource.
displayName: Role
kind: Role
@@ -109,6 +124,16 @@ spec:
kind: Subnet
name: subnets.openstack.k-orc.cloud
version: v1alpha1
+ - description: Trunk is the Schema for an ORC resource.
+ displayName: Trunk
+ kind: Trunk
+ name: trunks.openstack.k-orc.cloud
+ version: v1alpha1
+ - description: User is the Schema for an ORC resource.
+ displayName: User
+ kind: User
+ name: users.openstack.k-orc.cloud
+ version: v1alpha1
- description: Volume is the Schema for an ORC resource.
displayName: Volume
kind: Volume
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 3b67eb9fa..1f7a41a45 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -29,6 +29,7 @@ rules:
- networks
- ports
- projects
+ - roleassignments
- roles
- routerinterfaces
- routers
@@ -64,6 +65,7 @@ rules:
- networks/status
- ports/status
- projects/status
+ - roleassignments/status
- roles/status
- routerinterfaces/status
- routers/status
diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml
index 4b86755db..b20a311e7 100644
--- a/config/samples/kustomization.yaml
+++ b/config/samples/kustomization.yaml
@@ -14,6 +14,7 @@ resources:
- openstack_v1alpha1_port.yaml
- openstack_v1alpha1_project.yaml
- openstack_v1alpha1_role.yaml
+- openstack_v1alpha1_roleassignment.yaml
- openstack_v1alpha1_router.yaml
- openstack_v1alpha1_routerinterface.yaml
- openstack_v1alpha1_securitygroup.yaml
diff --git a/config/samples/openstack_v1alpha1_roleassignment.yaml b/config/samples/openstack_v1alpha1_roleassignment.yaml
new file mode 100644
index 000000000..876c748b9
--- /dev/null
+++ b/config/samples/openstack_v1alpha1_roleassignment.yaml
@@ -0,0 +1,49 @@
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-sample-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-sample-role
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-sample-group
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-sample-group
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-sample-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-sample-project
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-sample
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-sample-role
+ groupRef: roleassignment-sample-group
+ projectRef: roleassignment-sample-project
diff --git a/internal/controllers/generic/interfaces/status.go b/internal/controllers/generic/interfaces/status.go
index b577a364f..6f7088760 100644
--- a/internal/controllers/generic/interfaces/status.go
+++ b/internal/controllers/generic/interfaces/status.go
@@ -35,9 +35,17 @@ type ORCApplyConfig[objectApplyPT any, statusApplyPT ORCStatusApplyConfig[status
}
// ORCStatusApplyConfig is an interface implemented by the status of any apply
-// configuration for an ORC API object. It has Conditions and an ID field.
+// configuration for an ORC API object.
type ORCStatusApplyConfig[statusApplyPT any] interface {
WithConditions(...*applyconfigv1.ConditionApplyConfiguration) statusApplyPT
+}
+
+// ORCStatusApplyConfigWithID extends ORCStatusApplyConfig with an ID field.
+// This is required by resources that have an OpenStack-assigned ID stored in
+// status.id. Resources without an ID (e.g. relationship resources like
+// RoleAssignment) use only ORCStatusApplyConfig.
+type ORCStatusApplyConfigWithID[statusApplyPT any] interface {
+ ORCStatusApplyConfig[statusApplyPT]
WithID(id string) statusApplyPT
}
diff --git a/internal/controllers/generic/reconciler/controller.go b/internal/controllers/generic/reconciler/controller.go
index 7571519cd..d74a6f18d 100644
--- a/internal/controllers/generic/reconciler/controller.go
+++ b/internal/controllers/generic/reconciler/controller.go
@@ -51,7 +51,7 @@ func NewController[
objectApplyPT interfaces.ORCApplyConfig[objectApplyPT, statusApplyPT],
statusApplyPT interface {
*statusApplyT
- interfaces.ORCStatusApplyConfig[statusApplyPT]
+ interfaces.ORCStatusApplyConfigWithID[statusApplyPT]
}, statusApplyT any,
osResourceT any,
](
@@ -80,7 +80,7 @@ type Controller[
objectApplyPT interfaces.ORCApplyConfig[objectApplyPT, statusApplyPT],
statusApplyPT interface {
*statusApplyT
- interfaces.ORCStatusApplyConfig[statusApplyPT]
+ interfaces.ORCStatusApplyConfigWithID[statusApplyPT]
},
statusApplyT any,
osResourceT any,
diff --git a/internal/controllers/generic/status/status.go b/internal/controllers/generic/status/status.go
index 4776d16ae..990eb1ed0 100644
--- a/internal/controllers/generic/status/status.go
+++ b/internal/controllers/generic/status/status.go
@@ -41,7 +41,7 @@ func SetStatusID[
objectApplyPT interfaces.ORCApplyConfig[objectApplyPT, statusApplyPT],
statusApplyPT interface {
*statusApplyT
- interfaces.ORCStatusApplyConfig[statusApplyPT]
+ interfaces.ORCStatusApplyConfigWithID[statusApplyPT]
},
statusApplyT any,
osResourcePT any,
diff --git a/internal/controllers/roleassignment/actuator.go b/internal/controllers/roleassignment/actuator.go
new file mode 100644
index 000000000..d988815e4
--- /dev/null
+++ b/internal/controllers/roleassignment/actuator.go
@@ -0,0 +1,350 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ "context"
+ "iter"
+
+ "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
+)
+
+// OpenStack resource type
+type osResourceT = roles.RoleAssignment
+
+type roleassignmentActuator struct {
+ osClient osclients.RoleAssignmentClient
+ k8sClient client.Client
+}
+
+// buildListOpts constructs a ListAssignmentsOpts from component IDs.
+// Only non-empty fields are set, so this works for both exact queries
+// (all fields populated) and partial filter queries.
+func buildListOpts(roleID, userID, groupID, projectID, domainID string) roles.ListAssignmentsOpts {
+ // Note: Don't set Effective parameter - it can cause issues with group assignments
+ listOpts := roles.ListAssignmentsOpts{}
+
+ if roleID != "" {
+ listOpts.RoleID = roleID
+ }
+ if userID != "" {
+ listOpts.UserID = userID
+ }
+ if groupID != "" {
+ listOpts.GroupID = groupID
+ }
+ if projectID != "" {
+ listOpts.ScopeProjectID = projectID
+ }
+ if domainID != "" {
+ listOpts.ScopeDomainID = domainID
+ }
+
+ return listOpts
+}
+
+// GetResourceByComponents queries for the role assignment by its tuple (role, actor, scope).
+// OpenStack doesn't assign IDs to role assignments - they're identified by this tuple.
+// Exactly one of userID/groupID must be set, and exactly one of projectID/domainID must be set.
+func (actuator roleassignmentActuator) GetResourceByComponents(
+ ctx context.Context,
+ roleID string,
+ userID string,
+ groupID string,
+ projectID string,
+ domainID string,
+) (*osResourceT, progress.ReconcileStatus) {
+ listOpts := buildListOpts(roleID, userID, groupID, projectID, domainID)
+
+ // Query with exact filters - should return exactly one result
+ osResource, err := atMostOne(actuator.osClient.ListRoleAssignments(ctx, listOpts),
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError,
+ "found more than one matching role assignment for the same (role, actor, scope) tuple"))
+ if err != nil {
+ return nil, progress.WrapError(err)
+ }
+ return osResource, nil
+}
+
+func (actuator roleassignmentActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) {
+ resourceSpec := orcObject.Spec.Resource
+ if resourceSpec == nil {
+ return nil, false
+ }
+
+ // Fetch all dependencies to build the exact filter
+ var roleID, userID, groupID, projectID, domainID string
+
+ // Role dependency (required)
+ role, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, orcObject.Namespace, &resourceSpec.RoleRef, "Role",
+ func(dep *orcv1alpha1.Role) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, false // Not ready
+ }
+ roleID = ptr.Deref(role.Status.ID, "")
+
+ // Actor dependency (user XOR group)
+ if resourceSpec.UserRef != nil {
+ user, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.UserRef, "User",
+ func(dep *orcv1alpha1.User) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, false // Not ready
+ }
+ userID = ptr.Deref(user.Status.ID, "")
+ } else {
+ group, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.GroupRef, "Group",
+ func(dep *orcv1alpha1.Group) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, false // Not ready
+ }
+ groupID = ptr.Deref(group.Status.ID, "")
+ }
+
+ // Scope dependency (project XOR domain)
+ if resourceSpec.ProjectRef != nil {
+ project, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.ProjectRef, "Project",
+ func(dep *orcv1alpha1.Project) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, false // Not ready
+ }
+ projectID = ptr.Deref(project.Status.ID, "")
+ } else {
+ domain, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.DomainRef, "Domain",
+ func(dep *orcv1alpha1.Domain) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ if needsReschedule, _ := rs.NeedsReschedule(); needsReschedule {
+ return nil, false // Not ready
+ }
+ domainID = ptr.Deref(domain.Status.ID, "")
+ }
+
+ return actuator.osClient.ListRoleAssignments(ctx, buildListOpts(roleID, userID, groupID, projectID, domainID)), true
+}
+
+func (actuator roleassignmentActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) {
+ var reconcileStatus progress.ReconcileStatus
+
+ // Build ListAssignmentsOpts from filter references
+ var roleID, userID, groupID, projectID, domainID string
+
+ if filter.RoleRef != nil {
+ role, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace, filter.RoleRef, "Role",
+ func(dep *orcv1alpha1.Role) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+ if role != nil && role.Status.ID != nil {
+ roleID = *role.Status.ID
+ }
+ }
+
+ if filter.UserRef != nil {
+ user, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace, filter.UserRef, "User",
+ func(dep *orcv1alpha1.User) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+ if user != nil && user.Status.ID != nil {
+ userID = *user.Status.ID
+ }
+ }
+
+ if filter.GroupRef != nil {
+ group, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace, filter.GroupRef, "Group",
+ func(dep *orcv1alpha1.Group) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+ if group != nil && group.Status.ID != nil {
+ groupID = *group.Status.ID
+ }
+ }
+
+ if filter.ProjectRef != nil {
+ project, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace, filter.ProjectRef, "Project",
+ func(dep *orcv1alpha1.Project) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+ if project != nil && project.Status.ID != nil {
+ projectID = *project.Status.ID
+ }
+ }
+
+ if filter.DomainRef != nil {
+ domain, rs := dependency.FetchDependency(
+ ctx, actuator.k8sClient, obj.Namespace, filter.DomainRef, "Domain",
+ func(dep *orcv1alpha1.Domain) bool { return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(rs)
+ if domain != nil && domain.Status.ID != nil {
+ domainID = *domain.Status.ID
+ }
+ }
+
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return nil, reconcileStatus
+ }
+
+ return actuator.osClient.ListRoleAssignments(ctx, buildListOpts(roleID, userID, groupID, projectID, domainID)), nil
+}
+
+func (actuator roleassignmentActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) {
+ resource := obj.Spec.Resource
+
+ if resource == nil {
+ // Should have been caught by API validation
+ return nil, progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set"))
+ }
+ var reconcileStatus progress.ReconcileStatus
+
+ // Fetch role dependency (required)
+ role, roleDepRS := roleDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Role) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(roleDepRS)
+ var roleID string
+ if role != nil {
+ roleID = ptr.Deref(role.Status.ID, "")
+ }
+
+ // Fetch actor dependency (user XOR group)
+ var userID, groupID string
+ if resource.UserRef != nil {
+ user, userDepRS := userDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.User) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(userDepRS)
+ if user != nil {
+ userID = ptr.Deref(user.Status.ID, "")
+ }
+ } else {
+ group, groupDepRS := groupDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Group) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(groupDepRS)
+ if group != nil {
+ groupID = ptr.Deref(group.Status.ID, "")
+ }
+ }
+
+ // Fetch scope dependency (project XOR domain)
+ var projectID, domainID string
+ if resource.ProjectRef != nil {
+ project, projectDepRS := projectDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Project) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(projectDepRS)
+ if project != nil {
+ projectID = ptr.Deref(project.Status.ID, "")
+ }
+ } else {
+ domain, domainDepRS := domainDependency.GetDependency(
+ ctx, actuator.k8sClient, obj, func(dep *orcv1alpha1.Domain) bool {
+ return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil
+ },
+ )
+ reconcileStatus = reconcileStatus.WithReconcileStatus(domainDepRS)
+ if domain != nil {
+ domainID = ptr.Deref(domain.Status.ID, "")
+ }
+ }
+
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return nil, reconcileStatus
+ }
+
+ // Build AssignOpts
+ assignOpts := roles.AssignOpts{
+ UserID: userID,
+ GroupID: groupID,
+ ProjectID: projectID,
+ DomainID: domainID,
+ }
+
+ // Assign the role (idempotent - returns 204 even if already exists)
+ err := actuator.osClient.AssignRole(ctx, roleID, assignOpts)
+ if err != nil {
+ if !orcerrors.IsRetryable(err) {
+ err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating role assignment: "+err.Error(), err)
+ }
+ return nil, progress.WrapError(err)
+ }
+
+ // Verify the assignment was created by listing with exact filters
+ osResource, verifyErr := atMostOne(actuator.osClient.ListRoleAssignments(ctx, buildListOpts(roleID, userID, groupID, projectID, domainID)),
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError,
+ "found more than one matching role assignment after creation"))
+ if verifyErr != nil {
+ return nil, progress.WrapError(verifyErr)
+ }
+ if osResource == nil {
+ // This shouldn't happen - we just assigned it
+ return nil, progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError,
+ "role assignment succeeded but could not be found in OpenStack"))
+ }
+ return osResource, nil
+}
+
+func (actuator roleassignmentActuator) DeleteResource(ctx context.Context, _ orcObjectPT, osResource *osResourceT) progress.ReconcileStatus {
+ // Build UnassignOpts from the osResource
+ unassignOpts := roles.UnassignOpts{
+ UserID: osResource.User.ID,
+ GroupID: osResource.Group.ID,
+ ProjectID: osResource.Scope.Project.ID,
+ DomainID: osResource.Scope.Domain.ID,
+ }
+
+ return progress.WrapError(actuator.osClient.UnassignRole(ctx, osResource.Role.ID, unassignOpts))
+}
diff --git a/internal/controllers/roleassignment/actuator_test.go b/internal/controllers/roleassignment/actuator_test.go
new file mode 100644
index 000000000..2ddc49467
--- /dev/null
+++ b/internal/controllers/roleassignment/actuator_test.go
@@ -0,0 +1,418 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "iter"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/utils/ptr"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/osclients"
+)
+
+var (
+ errNotImplemented = errors.New("not implemented")
+ errTest = errors.New("test error")
+)
+
+const (
+ testNamespace = "test-ns"
+ testRoleName = "test-role"
+ testUserName = "test-user"
+ testGroupName = "test-group"
+ testProjectName = "test-project"
+ testDomainName = "test-domain"
+)
+
+// mockRoleAssignmentClient is a simple mock that returns pre-configured assignments.
+type mockRoleAssignmentClient struct {
+ assignments []roles.RoleAssignment
+}
+
+var _ osclients.RoleAssignmentClient = mockRoleAssignmentClient{}
+
+func (m mockRoleAssignmentClient) ListRoleAssignments(_ context.Context, _ roles.ListAssignmentsOpts) iter.Seq2[*roles.RoleAssignment, error] {
+ return func(yield func(*roles.RoleAssignment, error) bool) {
+ for i := range m.assignments {
+ if !yield(&m.assignments[i], nil) {
+ return
+ }
+ }
+ }
+}
+
+func (m mockRoleAssignmentClient) AssignRole(_ context.Context, _ string, _ roles.AssignOpts) error {
+ return errNotImplemented
+}
+
+func (m mockRoleAssignmentClient) UnassignRole(_ context.Context, _ string, _ roles.UnassignOpts) error {
+ return errNotImplemented
+}
+
+// Test result type and check helpers
+
+type raResult struct {
+ assignment *roles.RoleAssignment
+ err error
+}
+
+type checkFunc func([]raResult) error
+
+func checks(fns ...checkFunc) []checkFunc { return fns }
+
+func noError(results []raResult) error {
+ for _, result := range results {
+ if result.err != nil {
+ return fmt.Errorf("unexpected error: %w", result.err)
+ }
+ }
+ return nil
+}
+
+func wantError(wantErr error) checkFunc {
+ return func(results []raResult) error {
+ for _, result := range results {
+ if result.err != nil && errors.Is(result.err, wantErr) {
+ return nil
+ }
+ }
+ return fmt.Errorf("expected error %v not found in results", wantErr)
+ }
+}
+
+func findsN(wantN int) checkFunc {
+ return func(results []raResult) error {
+ found := len(results)
+ if found != wantN {
+ return fmt.Errorf("expected %d results, got %d", wantN, found)
+ }
+ return nil
+ }
+}
+
+// availableCondition returns an Available=True condition for test objects.
+func availableCondition() metav1.Condition {
+ return metav1.Condition{
+ Type: orcv1alpha1.ConditionAvailable,
+ Status: metav1.ConditionTrue,
+ LastTransitionTime: metav1.Now(),
+ Reason: "Available",
+ }
+}
+
+// newFakeK8sClient creates a fake k8s client with the given objects and ORC scheme.
+func newFakeK8sClient(objects ...client.Object) client.Client {
+ scheme := runtime.NewScheme()
+ _ = orcv1alpha1.AddToScheme(scheme)
+
+ return fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithObjects(objects...).
+ Build()
+}
+
+// availableRole returns a Role object that is available with the given status ID.
+func availableRole(statusID string) *orcv1alpha1.Role {
+ return &orcv1alpha1.Role{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testRoleName,
+ Namespace: testNamespace,
+ },
+ Status: orcv1alpha1.RoleStatus{
+ Conditions: []metav1.Condition{availableCondition()},
+ ID: ptr.To(statusID),
+ },
+ }
+}
+
+// availableUser returns a User object that is available with the given status ID.
+func availableUser(statusID string) *orcv1alpha1.User {
+ return &orcv1alpha1.User{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testUserName,
+ Namespace: testNamespace,
+ },
+ Status: orcv1alpha1.UserStatus{
+ Conditions: []metav1.Condition{availableCondition()},
+ ID: ptr.To(statusID),
+ },
+ }
+}
+
+// availableGroup returns a Group object that is available with the given status ID.
+func availableGroup(statusID string) *orcv1alpha1.Group {
+ return &orcv1alpha1.Group{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testGroupName,
+ Namespace: testNamespace,
+ },
+ Status: orcv1alpha1.GroupStatus{
+ Conditions: []metav1.Condition{availableCondition()},
+ ID: ptr.To(statusID),
+ },
+ }
+}
+
+// availableProject returns a Project object that is available with the given status ID.
+func availableProject(statusID string) *orcv1alpha1.Project {
+ return &orcv1alpha1.Project{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testProjectName,
+ Namespace: testNamespace,
+ },
+ Status: orcv1alpha1.ProjectStatus{
+ Conditions: []metav1.Condition{availableCondition()},
+ ID: ptr.To(statusID),
+ },
+ }
+}
+
+// availableDomain returns a Domain object that is available with the given status ID.
+func availableDomain(statusID string) *orcv1alpha1.Domain {
+ return &orcv1alpha1.Domain{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: testDomainName,
+ Namespace: testNamespace,
+ },
+ Status: orcv1alpha1.DomainStatus{
+ Conditions: []metav1.Condition{availableCondition()},
+ ID: ptr.To(statusID),
+ },
+ }
+}
+
+func TestListOSResourcesForAdoption(t *testing.T) {
+ userProjectAssignment := roles.RoleAssignment{
+ Role: roles.AssignedRole{ID: "role-id-1"},
+ User: roles.User{ID: "user-id-1"},
+ Scope: roles.Scope{Project: roles.Project{ID: "project-id-1"}},
+ }
+
+ groupDomainAssignment := roles.RoleAssignment{
+ Role: roles.AssignedRole{ID: "role-id-2"},
+ Group: roles.Group{ID: "group-id-2"},
+ Scope: roles.Scope{Domain: roles.Domain{ID: "domain-id-2"}},
+ }
+
+ for _, tc := range [...]struct {
+ name string
+ orcObject *orcv1alpha1.RoleAssignment
+ k8sObjects []client.Object
+ osClient osclients.RoleAssignmentClient
+ wantAdopt bool
+ checks []checkFunc
+ }{
+ {
+ name: "returns false when spec.resource is nil",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{},
+ },
+ osClient: mockRoleAssignmentClient{},
+ wantAdopt: false,
+ },
+ {
+ name: "user+project scope, all deps available, match found",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ UserRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-user"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-1"),
+ availableUser("user-id-1"),
+ availableProject("project-id-1"),
+ },
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{userProjectAssignment}},
+ wantAdopt: true,
+ checks: checks(noError, findsN(1)),
+ },
+ {
+ name: "group+domain scope, all deps available, match found",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ GroupRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-group"),
+ DomainRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-domain"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-2"),
+ availableGroup("group-id-2"),
+ availableDomain("domain-id-2"),
+ },
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{groupDomainAssignment}},
+ wantAdopt: true,
+ checks: checks(noError, findsN(1)),
+ },
+ {
+ name: "all deps available, no matches from OS",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ UserRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-user"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-1"),
+ availableUser("user-id-2"),
+ availableProject("project-id-2"),
+ },
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{}},
+ wantAdopt: true,
+ checks: checks(noError, findsN(0)),
+ },
+ {
+ name: "role dependency not found, returns false",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "missing-role",
+ UserRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-user"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ // role is missing
+ availableUser("user-id-1"),
+ availableProject("project-id-1"),
+ },
+ // OS client has a match — must NOT be queried
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{userProjectAssignment}},
+ wantAdopt: false,
+ },
+ {
+ name: "user dependency not ready, returns false",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ UserRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-user"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-1"),
+ // user exists but is not available (no Available condition, no Status.ID)
+ &orcv1alpha1.User{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-user", Namespace: testNamespace},
+ },
+ availableProject("project-id-1"),
+ },
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{userProjectAssignment}},
+ wantAdopt: false,
+ },
+ {
+ name: "project dependency not found, returns false",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ GroupRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-group"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("missing-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-2"),
+ availableGroup("group-id-2"),
+ // project is missing
+ },
+ osClient: mockRoleAssignmentClient{assignments: []roles.RoleAssignment{groupDomainAssignment}},
+ wantAdopt: false,
+ },
+ {
+ name: "OS client returns error",
+ orcObject: &orcv1alpha1.RoleAssignment{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-ra", Namespace: testNamespace},
+ Spec: orcv1alpha1.RoleAssignmentSpec{
+ Resource: &orcv1alpha1.RoleAssignmentResourceSpec{
+ RoleRef: "test-role",
+ UserRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-user"),
+ ProjectRef: ptr.To[orcv1alpha1.KubernetesNameRef]("test-project"),
+ },
+ },
+ },
+ k8sObjects: []client.Object{
+ availableRole("role-id-1"),
+ availableUser("user-id-1"),
+ availableProject("project-id-1"),
+ },
+ osClient: osclients.NewRoleAssignmentErrorClient(errTest),
+ wantAdopt: true,
+ checks: checks(wantError(errTest)),
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ ctx := context.Background()
+
+ k8sClient := newFakeK8sClient(tc.k8sObjects...)
+
+ actuator := roleassignmentActuator{
+ osClient: tc.osClient,
+ k8sClient: k8sClient,
+ }
+
+ resourceIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, tc.orcObject)
+ if canAdopt != tc.wantAdopt {
+ t.Fatalf("canAdopt = %v, want %v", canAdopt, tc.wantAdopt)
+ }
+
+ if !canAdopt {
+ return
+ }
+
+ var results []raResult
+ for assignment, err := range resourceIter {
+ results = append(results, raResult{assignment, err})
+ }
+
+ for _, check := range tc.checks {
+ if e := check(results); e != nil {
+ t.Error(e)
+ }
+ }
+ })
+ }
+}
diff --git a/internal/controllers/roleassignment/controller.go b/internal/controllers/roleassignment/controller.go
new file mode 100644
index 000000000..9051fc97f
--- /dev/null
+++ b/internal/controllers/roleassignment/controller.go
@@ -0,0 +1,285 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ "context"
+ "errors"
+
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/controller"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/scope"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/credentials"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ "github.com/k-orc/openstack-resource-controller/v2/pkg/predicates"
+)
+
+const controllerName = "roleassignment"
+
+// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=roleassignments,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=roleassignments/status,verbs=get;update;patch
+
+type roleassignmentReconcilerConstructor struct {
+ scopeFactory scope.Factory
+}
+
+func New(scopeFactory scope.Factory) interfaces.Controller {
+ return roleassignmentReconcilerConstructor{scopeFactory: scopeFactory}
+}
+
+func (roleassignmentReconcilerConstructor) GetName() string {
+ return controllerName
+}
+
+var roleDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Role](
+ "spec.resource.roleRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Resource
+ if resource == nil {
+ return nil
+ }
+ return []string{string(resource.RoleRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var userDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.User](
+ "spec.resource.userRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Resource
+ if resource == nil || resource.UserRef == nil {
+ return nil
+ }
+ return []string{string(*resource.UserRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var groupDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Group](
+ "spec.resource.groupRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Resource
+ if resource == nil || resource.GroupRef == nil {
+ return nil
+ }
+ return []string{string(*resource.GroupRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var projectDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Project](
+ "spec.resource.projectRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Resource
+ if resource == nil || resource.ProjectRef == nil {
+ return nil
+ }
+ return []string{string(*resource.ProjectRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var domainDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Domain](
+ "spec.resource.domainRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Resource
+ if resource == nil || resource.DomainRef == nil {
+ return nil
+ }
+ return []string{string(*resource.DomainRef)}
+ },
+ finalizer, externalObjectFieldOwner,
+)
+
+var roleImportDependency = dependency.NewDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Role](
+ "spec.import.filter.roleRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.RoleRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.RoleRef)}
+ },
+)
+
+var userImportDependency = dependency.NewDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.User](
+ "spec.import.filter.userRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.UserRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.UserRef)}
+ },
+)
+
+var groupImportDependency = dependency.NewDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Group](
+ "spec.import.filter.groupRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.GroupRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.GroupRef)}
+ },
+)
+
+var projectImportDependency = dependency.NewDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Project](
+ "spec.import.filter.projectRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.ProjectRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.ProjectRef)}
+ },
+)
+
+var domainImportDependency = dependency.NewDependency[*orcv1alpha1.RoleAssignmentList, *orcv1alpha1.Domain](
+ "spec.import.filter.domainRef",
+ func(roleassignment *orcv1alpha1.RoleAssignment) []string {
+ resource := roleassignment.Spec.Import
+ if resource == nil || resource.Filter == nil || resource.Filter.DomainRef == nil {
+ return nil
+ }
+ return []string{string(*resource.Filter.DomainRef)}
+ },
+)
+
+// SetupWithManager sets up the controller with the Manager.
+func (c roleassignmentReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
+ log := ctrl.LoggerFrom(ctx)
+ k8sClient := mgr.GetClient()
+
+ roleWatchEventHandler, err := roleDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ userWatchEventHandler, err := userDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ groupWatchEventHandler, err := groupDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ projectWatchEventHandler, err := projectDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ domainWatchEventHandler, err := domainDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ roleImportWatchEventHandler, err := roleImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ userImportWatchEventHandler, err := userImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ groupImportWatchEventHandler, err := groupImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ projectImportWatchEventHandler, err := projectImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ domainImportWatchEventHandler, err := domainImportDependency.WatchEventHandler(log, k8sClient)
+ if err != nil {
+ return err
+ }
+
+ builder := ctrl.NewControllerManagedBy(mgr).
+ WithOptions(options).
+ Watches(&orcv1alpha1.Role{}, roleWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Role{})),
+ ).
+ Watches(&orcv1alpha1.User{}, userWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.User{})),
+ ).
+ Watches(&orcv1alpha1.Group{}, groupWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Group{})),
+ ).
+ Watches(&orcv1alpha1.Project{}, projectWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Project{})),
+ ).
+ Watches(&orcv1alpha1.Domain{}, domainWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Role{}, roleImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Role{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.User{}, userImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.User{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Group{}, groupImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Group{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Project{}, projectImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Project{})),
+ ).
+ // A second watch is necessary because we need a different handler that omits deletion guards
+ Watches(&orcv1alpha1.Domain{}, domainImportWatchEventHandler,
+ builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Domain{})),
+ ).
+ For(&orcv1alpha1.RoleAssignment{})
+
+ if err := errors.Join(
+ roleDependency.AddToManager(ctx, mgr),
+ userDependency.AddToManager(ctx, mgr),
+ groupDependency.AddToManager(ctx, mgr),
+ projectDependency.AddToManager(ctx, mgr),
+ domainDependency.AddToManager(ctx, mgr),
+ roleImportDependency.AddToManager(ctx, mgr),
+ userImportDependency.AddToManager(ctx, mgr),
+ groupImportDependency.AddToManager(ctx, mgr),
+ projectImportDependency.AddToManager(ctx, mgr),
+ domainImportDependency.AddToManager(ctx, mgr),
+ credentialsDependency.AddToManager(ctx, mgr),
+ credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency),
+ ); err != nil {
+ return err
+ }
+
+ // Custom reconciler for role assignments (relationships, not resources with IDs)
+ reconciler := &roleassignmentReconciler{
+ client: mgr.GetClient(),
+ scopeFactory: c.scopeFactory,
+ }
+ return builder.Complete(reconciler)
+}
diff --git a/internal/controllers/roleassignment/reconciler.go b/internal/controllers/roleassignment/reconciler.go
new file mode 100644
index 000000000..2e2bd5ba0
--- /dev/null
+++ b/internal/controllers/roleassignment/reconciler.go
@@ -0,0 +1,420 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ "context"
+ "fmt"
+ "iter"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/status"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/logging"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/scope"
+ orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/finalizers"
+ orcstrings "github.com/k-orc/openstack-resource-controller/v2/internal/util/strings"
+)
+
+const (
+ // The time to wait before reconciling again when we are waiting for some change in OpenStack
+ externalUpdatePollingPeriod = 15 * time.Second
+)
+
+// roleassignmentReconciler reconciles RoleAssignment objects.
+// Unlike other ORC resources, role assignments are relationships (not resources with IDs),
+// so this uses a custom reconciler instead of the generic framework.
+type roleassignmentReconciler struct {
+ client client.Client
+ scopeFactory scope.Factory
+
+ statusWriter roleassignmentStatusWriter
+}
+
+func (r *roleassignmentReconciler) GetName() string { return controllerName }
+func (r *roleassignmentReconciler) GetK8sClient() client.Client { return r.client }
+func (r *roleassignmentReconciler) GetScopeFactory() scope.Factory { return r.scopeFactory }
+
+// Reconcile is the main entry point for reconciliation.
+// It fetches the RoleAssignment object and routes to either reconcileNormal or reconcileDelete.
+func (r *roleassignmentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ orcObject := new(orcObjectT)
+ err := r.client.Get(ctx, req.NamespacedName, orcObject)
+ if err != nil {
+ if apierrors.IsNotFound(err) {
+ // Object deleted, nothing to do
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, err
+ }
+
+ log := ctrl.LoggerFrom(ctx)
+
+ // Check if object is being deleted
+ if !orcObject.GetDeletionTimestamp().IsZero() {
+ return r.reconcileDelete(ctx, orcObject).Return(log)
+ }
+
+ return r.reconcileNormal(ctx, orcObject).Return(log)
+}
+
+// shouldReconcile determines if reconciliation should proceed based on the Progressing condition.
+// Returns true if:
+// - Progressing condition is not present
+// - Progressing condition is True
+// - Progressing condition is False but observedGeneration is stale
+func shouldReconcile(obj orcObjectPT) bool {
+ progressing := meta.FindStatusCondition(obj.GetConditions(), orcv1alpha1.ConditionProgressing)
+ if progressing == nil {
+ return true
+ }
+
+ if progressing.Status == metav1.ConditionTrue {
+ return true
+ }
+
+ return progressing.ObservedGeneration != obj.GetGeneration()
+}
+
+// reconcileNormal handles the normal reconciliation flow:
+// 1. Check if we should reconcile (based on Progressing condition)
+// 2. Create actuator (OpenStack client)
+// 3. Get or create the role assignment
+// 4. Update status
+func (r *roleassignmentReconciler) reconcileNormal(ctx context.Context, orcObject orcObjectPT) (reconcileStatus progress.ReconcileStatus) {
+ log := ctrl.LoggerFrom(ctx)
+
+ // Check if we should skip reconciliation
+ if !shouldReconcile(orcObject) {
+ log.V(logging.Verbose).Info("Status is up to date: not reconciling")
+ return reconcileStatus
+ }
+
+ log.V(logging.Verbose).Info("Reconciling role assignment")
+
+ var osResource *osResourceT
+
+ // Ensure we always update status at the end
+ defer func() {
+ reconcileStatus = reconcileStatus.WithReconcileStatus(
+ status.UpdateStatus(ctx, r, r.statusWriter, orcObject, osResource, reconcileStatus))
+ }()
+
+ // Phase 3: Add finalizer if not present
+ if !controllerutil.ContainsFinalizer(orcObject, finalizer) {
+ patch := finalizers.SetFinalizerPatch(orcObject, finalizer)
+ if err := r.client.Patch(ctx, orcObject, patch, client.ForceOwnership, orcstrings.GetSSAFieldOwnerWithTxn(controllerName, orcstrings.SSATransactionFinalizer)); err != nil {
+ return progress.WrapError(fmt.Errorf("setting finalizer: %w", err))
+ }
+ }
+
+ // Phase 3: Create actuator
+ actuator, actuatorRS := r.newActuator(ctx, orcObject)
+ if needsReschedule, err := actuatorRS.NeedsReschedule(); needsReschedule {
+ if err == nil {
+ log.V(logging.Verbose).Info("Waiting on events before creation")
+ }
+ return actuatorRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ // Phase 4: Check if role assignment exists using Status.Resource components
+ if orcObject.Status.Resource != nil {
+ statusResource := orcObject.Status.Resource
+ // If we have all components in status, try to fetch the role assignment
+ if statusResource.RoleID != "" &&
+ (statusResource.UserID != "" || statusResource.GroupID != "") &&
+ (statusResource.ProjectID != "" || statusResource.DomainID != "") {
+
+ osResource, getRS := actuator.GetResourceByComponents(
+ ctx,
+ statusResource.RoleID,
+ statusResource.UserID,
+ statusResource.GroupID,
+ statusResource.ProjectID,
+ statusResource.DomainID,
+ )
+ if needsReschedule, _ := getRS.NeedsReschedule(); needsReschedule {
+ return getRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ if osResource != nil {
+ log.V(logging.Verbose).Info("Got existing role assignment")
+ return reconcileStatus
+ }
+
+ // Status was fully populated but the resource no longer
+ // exists in OpenStack. GetResourceByComponents uses a
+ // LIST query which returns (nil, nil) for empty results
+ // rather than a 404 error, so we detect deletion here.
+ return progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "role assignment has been deleted from OpenStack"))
+ }
+ }
+
+ // Phase 5: Import by filter
+ if importSpec := orcObject.Spec.Import; importSpec != nil {
+ if filter := importSpec.Filter; filter != nil {
+ resourceIter, importRS := actuator.ListOSResourcesForImport(ctx, orcObject, *filter)
+ if needsReschedule, _ := importRS.NeedsReschedule(); needsReschedule {
+ return importRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ var err error
+ osResource, err = atMostOne(resourceIter,
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration,
+ "found more than one matching OpenStack resource during import"))
+ if err != nil {
+ return progress.WrapError(err)
+ }
+
+ if osResource == nil {
+ return progress.WaitingOnOpenStack(progress.WaitingOnCreation, externalUpdatePollingPeriod)
+ }
+
+ log.V(logging.Info).Info("Imported role assignment")
+ return reconcileStatus
+ }
+ }
+
+ // Phase 6: Adoption - check for existing resource before creating
+ if orcObject.Spec.ManagementPolicy == orcv1alpha1.ManagementPolicyUnmanaged {
+ // We never create an unmanaged resource
+ // API validation should have ensured that one of the above functions returned
+ return progress.WrapError(
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Not creating unmanaged resource"))
+ }
+
+ resourceIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject)
+ if canAdopt {
+ var err error
+ osResource, err = atMostOne(resourceIter,
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration,
+ "found more than one matching OpenStack resource during adoption"))
+ if err != nil {
+ return progress.WrapError(err)
+ }
+ if osResource != nil {
+ log.V(logging.Info).Info("Adopted previously created resource")
+ return reconcileStatus
+ }
+ }
+
+ // Phase 7: Fetch dependencies and create role assignment
+ log.V(logging.Info).Info("Creating resource")
+ osResource, createRS := actuator.CreateResource(ctx, orcObject)
+ if needsReschedule, err := createRS.NeedsReschedule(); needsReschedule {
+ if err == nil {
+ log.V(logging.Verbose).Info("Waiting on dependencies or creation")
+ }
+ return createRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ if osResource == nil {
+ return reconcileStatus.WithError(fmt.Errorf("osResource is not set, but no wait events or error"))
+ }
+
+ log.V(logging.Info).Info("Role assignment created or adopted")
+ return reconcileStatus
+}
+
+// atMostOne returns the first element from the iterator, or nil if it's empty.
+// It returns multipleErr if the iterator yields more than one element.
+func atMostOne(resourceIter iter.Seq2[*osResourceT, error], multipleErr error) (*osResourceT, error) {
+ next, stop := iter.Pull2(resourceIter)
+ defer stop()
+
+ // Try to fetch the first result
+ osResource, err, ok := next()
+ if err != nil {
+ return nil, err
+ } else if !ok {
+ // No first result
+ return nil, nil
+ }
+
+ // Check that there are no other results
+ _, err, ok = next()
+ if err != nil {
+ return nil, err
+ } else if ok {
+ return nil, multipleErr
+ }
+
+ return osResource, nil
+}
+
+// reconcileDelete handles deletion of the RoleAssignment:
+// 1. Check finalizer
+// 2. Fetch the role assignment (using Status.Resource components)
+// 3. Check management policy
+// 4. Delete from OpenStack
+// 5. Remove finalizer
+func (r *roleassignmentReconciler) reconcileDelete(ctx context.Context, orcObject orcObjectPT) (reconcileStatus progress.ReconcileStatus) {
+ log := ctrl.LoggerFrom(ctx)
+ log.V(logging.Verbose).Info("Reconciling role assignment delete")
+
+ var osResource *osResourceT
+ deleted := false
+
+ // Update status unless we've removed the finalizer
+ defer func() {
+ if !deleted {
+ reconcileStatus = reconcileStatus.WithReconcileStatus(
+ status.UpdateStatus(ctx, r, r.statusWriter, orcObject, osResource, reconcileStatus))
+ }
+ }()
+
+ // Check if our finalizer is present
+ var foundFinalizer bool
+ for _, f := range orcObject.GetFinalizers() {
+ if f == finalizer {
+ foundFinalizer = true
+ } else {
+ reconcileStatus = reconcileStatus.WaitingOnFinalizer(f)
+ }
+ }
+
+ // Cleanup not required if our finalizer is not present
+ if !foundFinalizer {
+ return reconcileStatus
+ }
+
+ if needsReschedule, err := reconcileStatus.NeedsReschedule(); needsReschedule {
+ if err == nil {
+ log.V(logging.Verbose).Info("Deferring resource cleanup due to remaining external finalizers")
+ }
+ return reconcileStatus
+ }
+
+ removeFinalizer := func(reconcileStatus progress.ReconcileStatus) progress.ReconcileStatus {
+ if err := r.client.Patch(ctx, orcObject, finalizers.RemoveFinalizerPatch(orcObject), orcstrings.GetSSAFieldOwnerWithTxn(controllerName, orcstrings.SSATransactionFinalizer)); err != nil {
+ return reconcileStatus.WithError(fmt.Errorf("removing finalizer: %w", err))
+ }
+ deleted = true
+ return reconcileStatus
+ }
+
+ // Check management policy
+ managementPolicy := orcObject.Spec.ManagementPolicy
+ managedOptions := orcObject.Spec.ManagedOptions
+ if managementPolicy == orcv1alpha1.ManagementPolicyUnmanaged || managedOptions.GetOnDelete() == orcv1alpha1.OnDeleteDetach {
+ logPolicy := []any{"managementPolicy", managementPolicy}
+ if managementPolicy == orcv1alpha1.ManagementPolicyManaged {
+ logPolicy = append(logPolicy, "onDelete", managedOptions.GetOnDelete())
+ }
+ log.V(logging.Verbose).Info("Not deleting OpenStack resource due to policy", logPolicy...)
+ return removeFinalizer(reconcileStatus)
+ }
+
+ // Create actuator for OpenStack operations
+ actuator, actuatorRS := r.newActuator(ctx, orcObject)
+ if needsReschedule, err := actuatorRS.NeedsReschedule(); needsReschedule {
+ if err == nil {
+ log.V(logging.Verbose).Info("Waiting on events before deletion")
+ }
+ return actuatorRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ // Fetch the role assignment using Status.Resource components
+ if orcObject.Status.Resource != nil {
+ statusResource := orcObject.Status.Resource
+ if statusResource.RoleID != "" &&
+ (statusResource.UserID != "" || statusResource.GroupID != "") &&
+ (statusResource.ProjectID != "" || statusResource.DomainID != "") {
+
+ var getRS progress.ReconcileStatus
+ osResource, getRS = actuator.GetResourceByComponents(
+ ctx,
+ statusResource.RoleID,
+ statusResource.UserID,
+ statusResource.GroupID,
+ statusResource.ProjectID,
+ statusResource.DomainID,
+ )
+ if needsReschedule, err := getRS.NeedsReschedule(); needsReschedule {
+ // NotFound is our success condition for delete
+ if err == nil || !orcerrors.IsNotFound(err) {
+ return getRS.WithReconcileStatus(reconcileStatus)
+ }
+ osResource = nil
+ }
+ }
+ }
+
+ // If status was never populated, check for orphaned resources via adoption
+ if osResource == nil && orcObject.Status.Resource == nil {
+ resourceIter, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObject)
+ if canAdopt {
+ var err error
+ osResource, err = atMostOne(resourceIter,
+ orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration,
+ "found more than one matching OpenStack resource during adoption"))
+ if err != nil {
+ return reconcileStatus.WithError(err)
+ }
+ }
+ }
+
+ if osResource == nil {
+ log.V(logging.Info).Info("Role assignment deletion confirmed")
+ return removeFinalizer(reconcileStatus)
+ }
+
+ log.V(logging.Info).Info("Deleting role assignment from OpenStack")
+ deleteRS := actuator.DeleteResource(ctx, orcObject, osResource)
+ if needsReschedule, _ := deleteRS.NeedsReschedule(); needsReschedule {
+ return deleteRS.WithReconcileStatus(reconcileStatus)
+ }
+
+ log.V(logging.Info).Info("Role assignment deletion confirmed")
+ return removeFinalizer(reconcileStatus)
+}
+
+// newActuator creates a roleassignmentActuator with OpenStack client setup.
+func (r *roleassignmentReconciler) newActuator(ctx context.Context, orcObject orcObjectPT) (roleassignmentActuator, progress.ReconcileStatus) {
+ log := ctrl.LoggerFrom(ctx)
+
+ // Ensure credential secrets exist and have our finalizer
+ _, reconcileStatus := credentialsDependency.GetDependencies(ctx, r.client, orcObject, func(*corev1.Secret) bool { return true })
+ if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule {
+ return roleassignmentActuator{}, reconcileStatus
+ }
+
+ clientScope, err := r.scopeFactory.NewClientScopeFromObject(ctx, r.client, log, orcObject)
+ if err != nil {
+ return roleassignmentActuator{}, progress.WrapError(err)
+ }
+ osClient, err := clientScope.NewRoleAssignmentClient()
+ if err != nil {
+ return roleassignmentActuator{}, progress.WrapError(err)
+ }
+
+ return roleassignmentActuator{
+ osClient: osClient,
+ k8sClient: r.client,
+ }, nil
+}
diff --git a/internal/controllers/roleassignment/status.go b/internal/controllers/roleassignment/status.go
new file mode 100644
index 000000000..2a938e9e7
--- /dev/null
+++ b/internal/controllers/roleassignment/status.go
@@ -0,0 +1,83 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ "github.com/go-logr/logr"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress"
+ orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+)
+
+type roleassignmentStatusWriter struct{}
+
+type objectApplyT = orcapplyconfigv1alpha1.RoleAssignmentApplyConfiguration
+type statusApplyT = orcapplyconfigv1alpha1.RoleAssignmentStatusApplyConfiguration
+
+var _ interfaces.ResourceStatusWriter[*orcv1alpha1.RoleAssignment, *osResourceT, *objectApplyT, *statusApplyT] = roleassignmentStatusWriter{}
+
+func (roleassignmentStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT {
+ return orcapplyconfigv1alpha1.RoleAssignment(name, namespace)
+}
+
+// ResourceAvailableStatus returns the availability status of the role assignment.
+// Role assignments don't have Status.ID, so availability is based on osResource
+// presence and status component fields.
+func (roleassignmentStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.RoleAssignment, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) {
+ if osResource != nil {
+ return metav1.ConditionTrue, nil
+ }
+
+ // If we previously observed component IDs but can't fetch the resource now,
+ // report Unknown since we can't confirm availability.
+ if orcObject.Status.Resource != nil &&
+ (orcObject.Status.Resource.RoleID != "" ||
+ orcObject.Status.Resource.UserID != "" ||
+ orcObject.Status.Resource.GroupID != "" ||
+ orcObject.Status.Resource.ProjectID != "" ||
+ orcObject.Status.Resource.DomainID != "") {
+ return metav1.ConditionUnknown, nil
+ }
+
+ return metav1.ConditionFalse, nil
+}
+
+// ApplyResourceStatus writes the role assignment component IDs to status.
+func (roleassignmentStatusWriter) ApplyResourceStatus(_ logr.Logger, osResource *osResourceT, statusApply *statusApplyT) {
+ resourceStatus := orcapplyconfigv1alpha1.RoleAssignmentResourceStatus()
+
+ if osResource.Role.ID != "" {
+ resourceStatus.WithRoleID(osResource.Role.ID)
+ }
+ if osResource.User.ID != "" {
+ resourceStatus.WithUserID(osResource.User.ID)
+ }
+ if osResource.Group.ID != "" {
+ resourceStatus.WithGroupID(osResource.Group.ID)
+ }
+ if osResource.Scope.Project.ID != "" {
+ resourceStatus.WithProjectID(osResource.Scope.Project.ID)
+ }
+ if osResource.Scope.Domain.ID != "" {
+ resourceStatus.WithDomainID(osResource.Scope.Domain.ID)
+ }
+
+ statusApply.WithResource(resourceStatus)
+}
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-assert.yaml
new file mode 100644
index 000000000..594f22e3f
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-assert.yaml
@@ -0,0 +1,75 @@
+---
+# Assert Role is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gd-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Group is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gd-test-group
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Domain is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-gd-test-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert RoleAssignment is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-group-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Validate RoleAssignment status fields
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-group-domain
+ ref: roleassignment
+assertAll:
+ # Verify status.id is NOT set (role assignments use component-based identification)
+ - celExpr: "!has(roleassignment.status.id) || roleassignment.status.id == ''"
+ # Verify all component IDs are populated in status.resource
+ - celExpr: "roleassignment.status.resource.roleID != ''"
+ - celExpr: "roleassignment.status.resource.groupID != ''"
+ - celExpr: "roleassignment.status.resource.domainID != ''"
+ # Verify user and project are not set (since we used group and domain)
+ - celExpr: "!has(roleassignment.status.resource.userID) || roleassignment.status.resource.userID == ''"
+ - celExpr: "!has(roleassignment.status.resource.projectID) || roleassignment.status.resource.projectID == ''"
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-create-resource.yaml
new file mode 100644
index 000000000..a0fae8d95
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-create-resource.yaml
@@ -0,0 +1,54 @@
+---
+# Create a test role
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gd-test-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gd-test-role
+---
+# Create a test group
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gd-test-group
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gd-test-group
+---
+# Create a test domain
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-gd-test-domain
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gd-test-domain
+---
+# Create role assignment (group on domain)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-group-domain
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-gd-test-role
+ groupRef: roleassignment-gd-test-group
+ domainRef: roleassignment-gd-test-domain
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-secret.yaml
new file mode 100644
index 000000000..f0fb63e85
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/00-secret.yaml
@@ -0,0 +1,5 @@
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-assert.yaml
new file mode 100644
index 000000000..5470f499b
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-assert.yaml
@@ -0,0 +1,9 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Domain
+ name: roleassignment-gd-test-domain
+ ref: domain
+assertAll:
+ - celExpr: "domain.status.resource.enabled == false"
\ No newline at end of file
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-disable-domain.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-disable-domain.yaml
new file mode 100644
index 000000000..fccf56038
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/01-disable-domain.yaml
@@ -0,0 +1,7 @@
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-gd-test-domain
+spec:
+ resource:
+ enabled: false
\ No newline at end of file
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-assert.yaml
new file mode 100644
index 000000000..64fc769bf
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-assert.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Verify RoleAssignment is deleted
+- script: "! kubectl get roleassignment roleassignment-create-group-domain --namespace $NAMESPACE"
+ skipLogOutput: true
+---
+# Verify dependencies still exist (deletion guard should keep them)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gd-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gd-test-group
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-gd-test-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-delete-roleassignment.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-delete-roleassignment.yaml
new file mode 100644
index 000000000..e3554c56b
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/02-delete-roleassignment.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-group-domain
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/README.md b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/README.md
new file mode 100644
index 000000000..ec03542bb
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-domain/README.md
@@ -0,0 +1,15 @@
+# Create a RoleAssignment for Group on Domain
+
+## Step 00
+
+Create dependencies (Role, Group, Domain) and a RoleAssignment that assigns a role to a group on a domain.
+
+Verify that the observed state corresponds to the spec and the role assignment exists in OpenStack.
+
+## Step 01
+
+Delete the RoleAssignment and verify it's removed from OpenStack.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-minimal
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-assert.yaml
new file mode 100644
index 000000000..680b4e462
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-assert.yaml
@@ -0,0 +1,75 @@
+---
+# Assert Role is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gp-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Group is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gp-test-group
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Project is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-gp-test-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert RoleAssignment is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-group-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Validate RoleAssignment status fields
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-group-project
+ ref: roleassignment
+assertAll:
+ # Verify status.id is NOT set (role assignments use component-based identification)
+ - celExpr: "!has(roleassignment.status.id) || roleassignment.status.id == ''"
+ # Verify all component IDs are populated in status.resource
+ - celExpr: "roleassignment.status.resource.roleID != ''"
+ - celExpr: "roleassignment.status.resource.groupID != ''"
+ - celExpr: "roleassignment.status.resource.projectID != ''"
+ # Verify user and domain are not set (since we used group and project)
+ - celExpr: "!has(roleassignment.status.resource.userID) || roleassignment.status.resource.userID == ''"
+ - celExpr: "!has(roleassignment.status.resource.domainID) || roleassignment.status.resource.domainID == ''"
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-create-resource.yaml
new file mode 100644
index 000000000..229f73ac2
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-create-resource.yaml
@@ -0,0 +1,54 @@
+---
+# Create a test role
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gp-test-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gp-test-role
+---
+# Create a test group
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gp-test-group
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gp-test-group
+---
+# Create a test project
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-gp-test-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-gp-test-project
+---
+# Create role assignment (group on project)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-group-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-gp-test-role
+ groupRef: roleassignment-gp-test-group
+ projectRef: roleassignment-gp-test-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-secret.yaml
new file mode 100644
index 000000000..f0fb63e85
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/00-secret.yaml
@@ -0,0 +1,5 @@
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-assert.yaml
new file mode 100644
index 000000000..774133d60
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-assert.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Verify RoleAssignment is deleted
+- script: "! kubectl get roleassignment roleassignment-create-group-project --namespace $NAMESPACE"
+ skipLogOutput: true
+---
+# Verify dependencies still exist (deletion guard should keep them)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-gp-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Group
+metadata:
+ name: roleassignment-gp-test-group
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-gp-test-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-delete-roleassignment.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-delete-roleassignment.yaml
new file mode 100644
index 000000000..3e76e9a0f
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/01-delete-roleassignment.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-group-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-group-project/README.md b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/README.md
new file mode 100644
index 000000000..5ea31a789
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-group-project/README.md
@@ -0,0 +1,15 @@
+# Create a RoleAssignment for Group on Project
+
+## Step 00
+
+Create dependencies (Role, Group, Project) and a RoleAssignment that assigns a role to a group on a project.
+
+Verify that the observed state corresponds to the spec and the role assignment exists in OpenStack.
+
+## Step 01
+
+Delete the RoleAssignment and verify it's removed from OpenStack.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-minimal
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-assert.yaml
new file mode 100644
index 000000000..9907fe691
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-assert.yaml
@@ -0,0 +1,75 @@
+---
+# Assert Role is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-ud-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert User is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-ud-test-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Domain is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-ud-test-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert RoleAssignment is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-user-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Validate RoleAssignment status fields
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-user-domain
+ ref: roleassignment
+assertAll:
+ # Verify status.id is NOT set (role assignments use component-based identification)
+ - celExpr: "!has(roleassignment.status.id) || roleassignment.status.id == ''"
+ # Verify all component IDs are populated in status.resource
+ - celExpr: "roleassignment.status.resource.roleID != ''"
+ - celExpr: "roleassignment.status.resource.userID != ''"
+ - celExpr: "roleassignment.status.resource.domainID != ''"
+ # Verify group and project are not set (since we used user and domain)
+ - celExpr: "!has(roleassignment.status.resource.groupID) || roleassignment.status.resource.groupID == ''"
+ - celExpr: "!has(roleassignment.status.resource.projectID) || roleassignment.status.resource.projectID == ''"
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-create-resource.yaml
new file mode 100644
index 000000000..844b43af0
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-create-resource.yaml
@@ -0,0 +1,54 @@
+---
+# Create a test role
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-ud-test-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-ud-test-role
+---
+# Create a test user
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-ud-test-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-ud-test-user
+---
+# Create a test domain
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-ud-test-domain
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-ud-test-domain
+---
+# Create the role assignment
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-user-domain
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-ud-test-role
+ userRef: roleassignment-ud-test-user
+ domainRef: roleassignment-ud-test-domain
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-secret.yaml
new file mode 100644
index 000000000..f0fb63e85
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/00-secret.yaml
@@ -0,0 +1,5 @@
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-assert.yaml
new file mode 100644
index 000000000..49610f398
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-assert.yaml
@@ -0,0 +1,9 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Domain
+ name: roleassignment-ud-test-domain
+ ref: domain
+assertAll:
+ - celExpr: "domain.status.resource.enabled == false"
\ No newline at end of file
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-disable-domain.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-disable-domain.yaml
new file mode 100644
index 000000000..09053901a
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/01-disable-domain.yaml
@@ -0,0 +1,7 @@
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-ud-test-domain
+spec:
+ resource:
+ enabled: false
\ No newline at end of file
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-assert.yaml
new file mode 100644
index 000000000..fee740b41
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-assert.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Verify RoleAssignment is deleted
+- script: "! kubectl get roleassignment roleassignment-create-user-domain --namespace $NAMESPACE"
+ skipLogOutput: true
+---
+# Verify dependencies still exist (deletion guard should keep them)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-ud-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-ud-test-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Domain
+metadata:
+ name: roleassignment-ud-test-domain
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-delete-roleassignment.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-delete-roleassignment.yaml
new file mode 100644
index 000000000..d2ad8ee20
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/02-delete-roleassignment.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-user-domain
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/README.md b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/README.md
new file mode 100644
index 000000000..d74af937a
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-domain/README.md
@@ -0,0 +1,15 @@
+# Create a RoleAssignment for User on Domain
+
+## Step 00
+
+Create dependencies (Role, User, Domain) and a RoleAssignment that assigns a role to a user on a domain.
+
+Verify that the observed state corresponds to the spec and the role assignment exists in OpenStack.
+
+## Step 01
+
+Delete the RoleAssignment and verify it's removed from OpenStack.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-minimal
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-assert.yaml
new file mode 100644
index 000000000..7a56b4d4c
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-assert.yaml
@@ -0,0 +1,75 @@
+---
+# Assert Role is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-up-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert User is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-up-test-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert Project is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-up-test-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Assert RoleAssignment is available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-user-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Validate RoleAssignment status fields
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-user-project
+ ref: roleassignment
+assertAll:
+ # Verify status.id is NOT set (role assignments use component-based identification)
+ - celExpr: "!has(roleassignment.status.id) || roleassignment.status.id == ''"
+ # Verify all component IDs are populated in status.resource
+ - celExpr: "roleassignment.status.resource.roleID != ''"
+ - celExpr: "roleassignment.status.resource.userID != ''"
+ - celExpr: "roleassignment.status.resource.projectID != ''"
+ # Verify group and domain are not set (since we used user and project)
+ - celExpr: "!has(roleassignment.status.resource.groupID) || roleassignment.status.resource.groupID == ''"
+ - celExpr: "!has(roleassignment.status.resource.domainID) || roleassignment.status.resource.domainID == ''"
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-create-resource.yaml
new file mode 100644
index 000000000..dbc89aed6
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-create-resource.yaml
@@ -0,0 +1,54 @@
+---
+# Create a test role
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-up-test-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-up-test-role
+---
+# Create a test user
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-up-test-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-up-test-user
+---
+# Create a test project
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-up-test-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-up-test-project
+---
+# Create the role assignment
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-create-user-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-up-test-role
+ userRef: roleassignment-up-test-user
+ projectRef: roleassignment-up-test-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-secret.yaml
new file mode 100644
index 000000000..f0fb63e85
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/00-secret.yaml
@@ -0,0 +1,5 @@
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-assert.yaml
new file mode 100644
index 000000000..607c22ab4
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-assert.yaml
@@ -0,0 +1,47 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Verify RoleAssignment is deleted
+- script: "! kubectl get roleassignment roleassignment-create-user-project --namespace $NAMESPACE"
+ skipLogOutput: true
+---
+# Verify dependencies still exist (deletion guard should keep them)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-up-test-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-up-test-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-up-test-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-delete-roleassignment.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-delete-roleassignment.yaml
new file mode 100644
index 000000000..9b20de4ef
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/01-delete-roleassignment.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-create-user-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-user-project/README.md b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/README.md
new file mode 100644
index 000000000..3b92d0da2
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-create-user-project/README.md
@@ -0,0 +1,15 @@
+# Create a RoleAssignment with minimum options
+
+## Step 00
+
+Create dependencies (Role, User, Project) and a minimal RoleAssignment that assigns a role to a user on a project.
+
+Verify that the observed state corresponds to the spec and the role assignment exists in OpenStack.
+
+## Step 01
+
+Delete the RoleAssignment and verify it's removed from OpenStack.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#create-minimal
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-assert.yaml
new file mode 100644
index 000000000..448d7ee63
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-assert.yaml
@@ -0,0 +1,13 @@
+---
+# Verify RoleAssignment is Progressing (waiting for dependencies)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-dependency
+status:
+ conditions:
+ - type: Available
+ status: "False"
+ - type: Progressing
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-create-resources-missing-deps.yaml
new file mode 100644
index 000000000..412216039
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-create-resources-missing-deps.yaml
@@ -0,0 +1,15 @@
+---
+# Create RoleAssignment with missing dependencies
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-dependency
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-dep-role
+ userRef: roleassignment-dep-user
+ projectRef: roleassignment-dep-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-secret.yaml
new file mode 100644
index 000000000..f0fb63e85
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-secret.yaml
@@ -0,0 +1,5 @@
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/01-assert.yaml
new file mode 100644
index 000000000..7ddf9a725
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/01-assert.yaml
@@ -0,0 +1,76 @@
+---
+# Verify dependencies are Available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-dep-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-dep-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-dep-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Verify RoleAssignment is now Available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-dependency
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Verify deletion guard finalizers are set on dependencies
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Role
+ name: roleassignment-dep-role
+ ref: role
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: User
+ name: roleassignment-dep-user
+ ref: user
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Project
+ name: roleassignment-dep-project
+ ref: project
+assertAll:
+ # Check that deletion guard finalizers are present
+ - celExpr: "role.metadata.finalizers.exists(f, f.startsWith('openstack.k-orc.cloud/roleassignment'))"
+ - celExpr: "user.metadata.finalizers.exists(f, f.startsWith('openstack.k-orc.cloud/roleassignment'))"
+ - celExpr: "project.metadata.finalizers.exists(f, f.startsWith('openstack.k-orc.cloud/roleassignment'))"
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/01-create-dependencies.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/01-create-dependencies.yaml
new file mode 100644
index 000000000..a113632a8
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/01-create-dependencies.yaml
@@ -0,0 +1,37 @@
+---
+# Create the dependencies
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-dep-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-dep-role
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-dep-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-dep-user
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-dep-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-dep-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/02-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-assert.yaml
new file mode 100644
index 000000000..f1c77d3b5
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-assert.yaml
@@ -0,0 +1,29 @@
+---
+# Verify Project still exists (deletion blocked by finalizer)
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-dep-project
+ # deletionTimestamp should be set, but resource should still exist
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+# Verify RoleAssignment still Available
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-dependency
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/02-delete-dependencies.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-delete-dependencies.yaml
new file mode 100644
index 000000000..3ea65dfe7
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-delete-dependencies.yaml
@@ -0,0 +1,6 @@
+---
+# Try to delete a dependency (should be blocked by finalizer)
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ - command: kubectl delete project roleassignment-dep-project --namespace $NAMESPACE --wait=false
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/03-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/03-assert.yaml
new file mode 100644
index 000000000..1a99c8e08
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/03-assert.yaml
@@ -0,0 +1,10 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+# Verify RoleAssignment is deleted
+- script: "! kubectl get roleassignment roleassignment-dependency --namespace $NAMESPACE"
+ skipLogOutput: true
+# Verify Project can now be deleted (finalizer removed)
+- script: "! kubectl get project roleassignment-dep-project --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/03-delete-resources.yaml b/internal/controllers/roleassignment/tests/roleassignment-dependency/03-delete-resources.yaml
new file mode 100644
index 000000000..cc50ec3db
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/03-delete-resources.yaml
@@ -0,0 +1,8 @@
+---
+# Delete RoleAssignment first
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-dependency
diff --git a/internal/controllers/roleassignment/tests/roleassignment-dependency/README.md b/internal/controllers/roleassignment/tests/roleassignment-dependency/README.md
new file mode 100644
index 000000000..49d537638
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/README.md
@@ -0,0 +1,23 @@
+# Test RoleAssignment dependency handling
+
+## Step 00
+
+Create a RoleAssignment that references Role, User, and Project that don't exist yet.
+Verify that it enters Progressing state waiting for dependencies.
+
+## Step 01
+
+Create the dependencies and verify the RoleAssignment becomes Available.
+
+## Step 02
+
+Try to delete a dependency (Project) while it's still referenced by the RoleAssignment.
+Verify the deletion is blocked by the finalizer.
+
+## Step 03
+
+Delete the RoleAssignment first, then verify dependencies can be deleted.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#dependencies
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-assert.yaml
new file mode 100644
index 000000000..c90c27041
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-assert.yaml
@@ -0,0 +1,43 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-dep-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-dep-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep
+status:
+ conditions:
+ - type: Available
+ message: |-
+ Waiting for Role/roleassignment-import-dep-role to be ready
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: |-
+ Waiting for Role/roleassignment-import-dep-role to be ready
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-import-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-import-resource.yaml
new file mode 100644
index 000000000..f795c68ae
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-import-resource.yaml
@@ -0,0 +1,56 @@
+---
+# Unmanaged Role that imports by name. No matching OpenStack role exists yet,
+# so this will stay in Progressing state.
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-dep-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ name: roleassignment-import-dep-ext-role
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-dep-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-dep-user
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-dep-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-dep-project
+---
+# Import RoleAssignment referencing the unmanaged Role. Since the Role is not
+# yet available, this should wait on the dependency.
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ roleRef: roleassignment-import-dep-role
+ userRef: roleassignment-import-dep-user
+ projectRef: roleassignment-import-dep-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-assert.yaml
new file mode 100644
index 000000000..105272ace
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-assert.yaml
@@ -0,0 +1,30 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep-trap
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep
+status:
+ conditions:
+ - type: Available
+ message: |-
+ Waiting for Role/roleassignment-import-dep-role to be ready
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: |-
+ Waiting for Role/roleassignment-import-dep-role to be ready
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-create-trap-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-create-trap-resource.yaml
new file mode 100644
index 000000000..7b4db41fa
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/01-create-trap-resource.yaml
@@ -0,0 +1,28 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-dep-trap-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-dep-trap-role
+---
+# This role assignment uses a different role but the same user and project.
+# It should not be picked by the import filter.
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep-trap
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-dep-trap-role
+ userRef: roleassignment-import-dep-user
+ projectRef: roleassignment-import-dep-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-assert.yaml
new file mode 100644
index 000000000..4e44d0fa3
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-assert.yaml
@@ -0,0 +1,39 @@
+---
+# Verify the imported role assignment matches the created one
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import-dep
+ ref: importedRA
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import-dep-external
+ ref: externalRA
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: Role
+ name: roleassignment-import-dep-role
+ ref: role
+assertAll:
+ # Import should have same component IDs as external
+ - celExpr: "importedRA.status.resource.roleID == externalRA.status.resource.roleID"
+ - celExpr: "importedRA.status.resource.userID == externalRA.status.resource.userID"
+ - celExpr: "importedRA.status.resource.projectID == externalRA.status.resource.projectID"
+ # The roleID should match the unmanaged Role's imported status.id
+ - celExpr: "importedRA.status.resource.roleID == role.status.id"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep
+status:
+ conditions:
+ - type: Available
+ message: OpenStack resource is available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ message: OpenStack resource is up to date
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-create-resource.yaml
new file mode 100644
index 000000000..1165c7af5
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/02-create-resource.yaml
@@ -0,0 +1,29 @@
+---
+# Create the managed Role that satisfies the unmanaged Role's import filter.
+# The unmanaged Role imports by filter name: roleassignment-import-dep-ext-role
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-dep-ext-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-dep-ext-role
+---
+# Create the role assignment matching the import filter
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-dep-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-dep-ext-role
+ userRef: roleassignment-import-dep-user
+ projectRef: roleassignment-import-dep-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-assert.yaml
new file mode 100644
index 000000000..396d7efe5
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-assert.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+- script: "! kubectl get role.openstack.k-orc.cloud roleassignment-import-dep-role --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-delete-import-dependencies.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-delete-import-dependencies.yaml
new file mode 100644
index 000000000..e1b01ffab
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/03-delete-import-dependencies.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+ # We should be able to delete the import dependencies
+ - command: kubectl delete role.openstack.k-orc.cloud roleassignment-import-dep-role
+ namespaced: true
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-assert.yaml
new file mode 100644
index 000000000..9ac45ea07
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-assert.yaml
@@ -0,0 +1,6 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+commands:
+- script: "! kubectl get roleassignment roleassignment-import-dep --namespace $NAMESPACE"
+ skipLogOutput: true
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-delete-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-delete-resource.yaml
new file mode 100644
index 000000000..4c0c62425
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/04-delete-resource.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+delete:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import-dep
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-dependency/README.md b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/README.md
new file mode 100644
index 000000000..b804056ba
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-dependency/README.md
@@ -0,0 +1,32 @@
+# Check dependency handling for imported RoleAssignment
+
+## Step 00
+
+Create an unmanaged Role importing by filter (name that doesn't exist yet),
+managed User and Project dependencies, and an unmanaged RoleAssignment
+importing by filter with roleRef pointing to the unmanaged Role.
+Verify the RoleAssignment is waiting for the Role dependency to be ready.
+
+## Step 01
+
+Create a trap RoleAssignment with a different role but the same user and
+project, and verify that it is not being imported.
+
+## Step 02
+
+Create a managed Role matching the unmanaged Role's import filter and a
+managed RoleAssignment matching the import filter. Verify the imported
+RoleAssignment is available with correct component IDs.
+
+## Step 03
+
+Delete the import dependency (the unmanaged Role) and verify ORC does not
+prevent deletion. Import dependencies should not have deletion guards.
+
+## Step 04
+
+Delete the imported RoleAssignment and verify it's gone.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import-dependency
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-assert.yaml
new file mode 100644
index 000000000..44cbe53fb
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-assert.yaml
@@ -0,0 +1,78 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-err-role-1
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-err-role-2
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-err-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-err-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-err-1
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-err-2
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/00-create-resources.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-create-resources.yaml
new file mode 100644
index 000000000..f5f499b58
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-create-resources.yaml
@@ -0,0 +1,76 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-err-role-1
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-err-role-1
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-err-role-2
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-err-role-2
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-err-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-err-user
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-err-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-err-project
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-err-1
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-err-role-1
+ userRef: roleassignment-import-err-user
+ projectRef: roleassignment-import-err-project
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-err-2
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-err-role-2
+ userRef: roleassignment-import-err-user
+ projectRef: roleassignment-import-err-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-error/01-assert.yaml
new file mode 100644
index 000000000..1f7e3a893
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/01-assert.yaml
@@ -0,0 +1,15 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-error
+status:
+ conditions:
+ - type: Available
+ message: found more than one matching OpenStack resource during import
+ status: "False"
+ reason: InvalidConfiguration
+ - type: Progressing
+ message: found more than one matching OpenStack resource during import
+ status: "False"
+ reason: InvalidConfiguration
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/01-import-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import-error/01-import-resource.yaml
new file mode 100644
index 000000000..030399b68
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/01-import-resource.yaml
@@ -0,0 +1,14 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-error
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ userRef: roleassignment-import-err-user
+ projectRef: roleassignment-import-err-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import-error/README.md b/internal/controllers/roleassignment/tests/roleassignment-import-error/README.md
new file mode 100644
index 000000000..ffe332e6d
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import-error/README.md
@@ -0,0 +1,17 @@
+# Import RoleAssignment Error
+
+## Step 00
+
+Create dependencies (User, two Roles, a Project) as managed resources, and
+two managed RoleAssignments assigning each role to the same user on the same
+project.
+
+## Step 01
+
+Import an unmanaged RoleAssignment using a filter that specifies only userRef
+and projectRef. Both role assignments match the filter, causing a terminal
+error because more than one matching resource was found.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import-error
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/00-assert.yaml
new file mode 100644
index 000000000..175a5e837
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/00-assert.yaml
@@ -0,0 +1,54 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-role
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-user
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-project
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import
+status:
+ conditions:
+ - type: Available
+ message: Waiting for OpenStack resource to be created externally
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for OpenStack resource to be created externally
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/00-import-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/00-import-resource.yaml
new file mode 100644
index 000000000..e3e0c8df5
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/00-import-resource.yaml
@@ -0,0 +1,51 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-role
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: User
+metadata:
+ name: roleassignment-import-user
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-user
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Project
+metadata:
+ name: roleassignment-import-project
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-project
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: unmanaged
+ import:
+ filter:
+ roleRef: roleassignment-import-role
+ userRef: roleassignment-import-user
+ projectRef: roleassignment-import-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/00-secret.yaml
new file mode 100644
index 000000000..045711ee7
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/00-secret.yaml
@@ -0,0 +1,6 @@
+---
+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
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/01-assert.yaml
new file mode 100644
index 000000000..02fb6038b
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/01-assert.yaml
@@ -0,0 +1,28 @@
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-trap
+status:
+ conditions:
+ - type: Available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ status: "False"
+ reason: Success
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import
+status:
+ conditions:
+ - type: Available
+ message: Waiting for OpenStack resource to be created externally
+ status: "False"
+ reason: Progressing
+ - type: Progressing
+ message: Waiting for OpenStack resource to be created externally
+ status: "True"
+ reason: Progressing
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/01-create-trap-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/01-create-trap-resource.yaml
new file mode 100644
index 000000000..6bc4c7745
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/01-create-trap-resource.yaml
@@ -0,0 +1,29 @@
+---
+# Create a different role to use in the trap
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Role
+metadata:
+ name: roleassignment-import-trap-role
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ name: roleassignment-import-trap-role
+---
+# This role assignment uses a different role but the same user and project.
+# It should not be picked by the import filter which specifies a different roleRef.
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-trap
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-trap-role
+ userRef: roleassignment-import-user
+ projectRef: roleassignment-import-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/02-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/02-assert.yaml
new file mode 100644
index 000000000..9552fa89e
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/02-assert.yaml
@@ -0,0 +1,39 @@
+---
+# Verify the imported role assignment matches the created one
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+resourceRefs:
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import
+ ref: importedRA
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import-external
+ ref: externalRA
+ - apiVersion: openstack.k-orc.cloud/v1alpha1
+ kind: RoleAssignment
+ name: roleassignment-import-trap
+ ref: trapRA
+assertAll:
+ # Import should have same component IDs as external
+ - celExpr: "importedRA.status.resource.roleID == externalRA.status.resource.roleID"
+ - celExpr: "importedRA.status.resource.userID == externalRA.status.resource.userID"
+ - celExpr: "importedRA.status.resource.projectID == externalRA.status.resource.projectID"
+ # Import should not have picked the trap (different role ID)
+ - celExpr: "importedRA.status.resource.roleID != trapRA.status.resource.roleID"
+---
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import
+status:
+ conditions:
+ - type: Available
+ message: OpenStack resource is available
+ status: "True"
+ reason: Success
+ - type: Progressing
+ message: OpenStack resource is up to date
+ status: "False"
+ reason: Success
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/02-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-import/02-create-resource.yaml
new file mode 100644
index 000000000..e01728436
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/02-create-resource.yaml
@@ -0,0 +1,15 @@
+---
+# Create the role assignment matching the import filter
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: RoleAssignment
+metadata:
+ name: roleassignment-import-external
+spec:
+ cloudCredentialsRef:
+ cloudName: openstack-admin
+ secretName: openstack-clouds
+ managementPolicy: managed
+ resource:
+ roleRef: roleassignment-import-role
+ userRef: roleassignment-import-user
+ projectRef: roleassignment-import-project
diff --git a/internal/controllers/roleassignment/tests/roleassignment-import/README.md b/internal/controllers/roleassignment/tests/roleassignment-import/README.md
new file mode 100644
index 000000000..ac505ff33
--- /dev/null
+++ b/internal/controllers/roleassignment/tests/roleassignment-import/README.md
@@ -0,0 +1,23 @@
+# Import RoleAssignment
+
+## Step 00
+
+Create dependencies (Role, User, Project) as managed resources, and an
+unmanaged RoleAssignment importing by filter that references all three.
+Verify that the import RoleAssignment is waiting for the external resource
+to be created in OpenStack.
+
+## Step 01
+
+Create a trap RoleAssignment using a different role but the same user and
+project, and verify that it is not being imported by the filter.
+
+## Step 02
+
+Create a managed RoleAssignment matching the import filter and verify that
+the imported RoleAssignment picks it up with the correct component IDs.
+Also verify that the imported RoleAssignment didn't pick the trap.
+
+## Reference
+
+https://k-orc.cloud/development/writing-tests/#import
diff --git a/internal/controllers/roleassignment/zz_generated.adapter.go b/internal/controllers/roleassignment/zz_generated.adapter.go
new file mode 100644
index 000000000..9941f472b
--- /dev/null
+++ b/internal/controllers/roleassignment/zz_generated.adapter.go
@@ -0,0 +1,75 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces"
+)
+
+// Fundamental types
+type (
+ orcObjectT = orcv1alpha1.RoleAssignment
+ orcObjectListT = orcv1alpha1.RoleAssignmentList
+ resourceSpecT = orcv1alpha1.RoleAssignmentResourceSpec
+ filterT = orcv1alpha1.RoleAssignmentFilter
+)
+
+// Derived types
+type (
+ orcObjectPT = *orcObjectT
+ adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT]
+ adapterT = roleassignmentAdapter
+)
+
+type roleassignmentAdapter struct {
+ *orcv1alpha1.RoleAssignment
+}
+
+var _ adapterI = &adapterT{}
+
+func (f adapterT) GetObject() orcObjectPT {
+ return f.RoleAssignment
+}
+
+func (f adapterT) GetManagementPolicy() orcv1alpha1.ManagementPolicy {
+ return f.Spec.ManagementPolicy
+}
+
+func (f adapterT) GetManagedOptions() *orcv1alpha1.ManagedOptions {
+ return f.Spec.ManagedOptions
+}
+
+func (f adapterT) GetStatusID() *string {
+ return nil
+}
+
+func (f adapterT) GetResourceSpec() *resourceSpecT {
+ return f.Spec.Resource
+}
+
+func (f adapterT) GetImportID() *string {
+ return nil
+}
+
+func (f adapterT) GetImportFilter() *filterT {
+ if f.Spec.Import == nil {
+ return nil
+ }
+ return f.Spec.Import.Filter
+}
diff --git a/internal/controllers/roleassignment/zz_generated.controller.go b/internal/controllers/roleassignment/zz_generated.controller.go
new file mode 100644
index 000000000..469e96460
--- /dev/null
+++ b/internal/controllers/roleassignment/zz_generated.controller.go
@@ -0,0 +1,45 @@
+// Code generated by resource-generator. DO NOT EDIT.
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package roleassignment
+
+import (
+ corev1 "k8s.io/api/core/v1"
+
+ "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency"
+ orcstrings "github.com/k-orc/openstack-resource-controller/v2/internal/util/strings"
+)
+
+var (
+ // NOTE: controllerName must be defined in any controller using this template
+
+ // finalizer is the string this controller adds to an object's Finalizers
+ finalizer = orcstrings.GetFinalizerName(controllerName)
+
+ // externalObjectFieldOwner is the field owner we use when using
+ // server-side-apply on objects we don't control
+ externalObjectFieldOwner = orcstrings.GetSSAFieldOwner(controllerName)
+
+ credentialsDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *corev1.Secret](
+ "spec.cloudCredentialsRef.secretName",
+ func(obj orcObjectPT) []string {
+ return []string{obj.Spec.CloudCredentialsRef.SecretName}
+ },
+ finalizer, externalObjectFieldOwner,
+ dependency.OverrideDependencyName("credentials"),
+ )
+)
diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go
index 766500c8f..57088736b 100644
--- a/internal/osclients/mock/doc.go
+++ b/internal/osclients/mock/doc.go
@@ -56,6 +56,9 @@ import (
//go:generate mockgen -package mock -destination=role.go -source=../role.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock RoleClient
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt role.go > _role.go && mv _role.go role.go"
+//go:generate mockgen -package mock -destination=roleassignment.go -source=../roleassignment.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock RoleAssignmentClient
+//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt roleassignment.go > _roleassignment.go && mv _roleassignment.go roleassignment.go"
+
//go:generate mockgen -package mock -destination=service.go -source=../service.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock ServiceClient
//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt service.go > _service.go && mv _service.go service.go"
diff --git a/internal/osclients/mock/roleassignment.go b/internal/osclients/mock/roleassignment.go
new file mode 100644
index 000000000..fa513aba8
--- /dev/null
+++ b/internal/osclients/mock/roleassignment.go
@@ -0,0 +1,100 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+// Code generated by MockGen. DO NOT EDIT.
+// Source: ../roleassignment.go
+//
+// Generated by this command:
+//
+// mockgen -package mock -destination=roleassignment.go -source=../roleassignment.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock RoleAssignmentClient
+//
+
+// Package mock is a generated GoMock package.
+package mock
+
+import (
+ context "context"
+ iter "iter"
+ reflect "reflect"
+
+ roles "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles"
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockRoleAssignmentClient is a mock of RoleAssignmentClient interface.
+type MockRoleAssignmentClient struct {
+ ctrl *gomock.Controller
+ recorder *MockRoleAssignmentClientMockRecorder
+ isgomock struct{}
+}
+
+// MockRoleAssignmentClientMockRecorder is the mock recorder for MockRoleAssignmentClient.
+type MockRoleAssignmentClientMockRecorder struct {
+ mock *MockRoleAssignmentClient
+}
+
+// NewMockRoleAssignmentClient creates a new mock instance.
+func NewMockRoleAssignmentClient(ctrl *gomock.Controller) *MockRoleAssignmentClient {
+ mock := &MockRoleAssignmentClient{ctrl: ctrl}
+ mock.recorder = &MockRoleAssignmentClientMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockRoleAssignmentClient) EXPECT() *MockRoleAssignmentClientMockRecorder {
+ return m.recorder
+}
+
+// AssignRole mocks base method.
+func (m *MockRoleAssignmentClient) AssignRole(ctx context.Context, roleID string, opts roles.AssignOpts) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AssignRole", ctx, roleID, opts)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// AssignRole indicates an expected call of AssignRole.
+func (mr *MockRoleAssignmentClientMockRecorder) AssignRole(ctx, roleID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignRole", reflect.TypeOf((*MockRoleAssignmentClient)(nil).AssignRole), ctx, roleID, opts)
+}
+
+// ListRoleAssignments mocks base method.
+func (m *MockRoleAssignmentClient) ListRoleAssignments(ctx context.Context, listOpts roles.ListAssignmentsOpts) iter.Seq2[*roles.RoleAssignment, error] {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "ListRoleAssignments", ctx, listOpts)
+ ret0, _ := ret[0].(iter.Seq2[*roles.RoleAssignment, error])
+ return ret0
+}
+
+// ListRoleAssignments indicates an expected call of ListRoleAssignments.
+func (mr *MockRoleAssignmentClientMockRecorder) ListRoleAssignments(ctx, listOpts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRoleAssignments", reflect.TypeOf((*MockRoleAssignmentClient)(nil).ListRoleAssignments), ctx, listOpts)
+}
+
+// UnassignRole mocks base method.
+func (m *MockRoleAssignmentClient) UnassignRole(ctx context.Context, roleID string, opts roles.UnassignOpts) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UnassignRole", ctx, roleID, opts)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// UnassignRole indicates an expected call of UnassignRole.
+func (mr *MockRoleAssignmentClientMockRecorder) UnassignRole(ctx, roleID, opts any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnassignRole", reflect.TypeOf((*MockRoleAssignmentClient)(nil).UnassignRole), ctx, roleID, opts)
+}
diff --git a/internal/osclients/roleassignment.go b/internal/osclients/roleassignment.go
new file mode 100644
index 000000000..bf83a75b2
--- /dev/null
+++ b/internal/osclients/roleassignment.go
@@ -0,0 +1,86 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package osclients
+
+import (
+ "context"
+ "fmt"
+ "iter"
+
+ "github.com/gophercloud/gophercloud/v2"
+ "github.com/gophercloud/gophercloud/v2/openstack"
+ "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles"
+ "github.com/gophercloud/utils/v2/openstack/clientconfig"
+)
+
+type RoleAssignmentClient interface {
+ ListRoleAssignments(ctx context.Context, listOpts roles.ListAssignmentsOpts) iter.Seq2[*roles.RoleAssignment, error]
+ AssignRole(ctx context.Context, roleID string, opts roles.AssignOpts) error
+ UnassignRole(ctx context.Context, roleID string, opts roles.UnassignOpts) error
+}
+
+type roleassignmentClient struct{ client *gophercloud.ServiceClient }
+
+// NewRoleAssignmentClient returns a new OpenStack Identity client for role assignments.
+func NewRoleAssignmentClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (RoleAssignmentClient, error) {
+ client, err := openstack.NewIdentityV3(providerClient, gophercloud.EndpointOpts{
+ Region: providerClientOpts.RegionName,
+ Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType),
+ })
+
+ if err != nil {
+ return nil, fmt.Errorf("failed to create role assignment service client: %v", err)
+ }
+
+ return &roleassignmentClient{client}, nil
+}
+
+func (c roleassignmentClient) ListRoleAssignments(ctx context.Context, listOpts roles.ListAssignmentsOpts) iter.Seq2[*roles.RoleAssignment, error] {
+ pager := roles.ListAssignments(c.client, listOpts)
+ return func(yield func(*roles.RoleAssignment, error) bool) {
+ _ = pager.EachPage(ctx, yieldPage(roles.ExtractRoleAssignments, yield))
+ }
+}
+
+func (c roleassignmentClient) AssignRole(ctx context.Context, roleID string, opts roles.AssignOpts) error {
+ return roles.Assign(ctx, c.client, roleID, opts).ExtractErr()
+}
+
+func (c roleassignmentClient) UnassignRole(ctx context.Context, roleID string, opts roles.UnassignOpts) error {
+ return roles.Unassign(ctx, c.client, roleID, opts).ExtractErr()
+}
+
+type roleassignmentErrorClient struct{ error }
+
+// NewRoleAssignmentErrorClient returns a RoleAssignmentClient in which every method returns the given error.
+func NewRoleAssignmentErrorClient(e error) RoleAssignmentClient {
+ return roleassignmentErrorClient{e}
+}
+
+func (e roleassignmentErrorClient) ListRoleAssignments(_ context.Context, _ roles.ListAssignmentsOpts) iter.Seq2[*roles.RoleAssignment, error] {
+ return func(yield func(*roles.RoleAssignment, error) bool) {
+ yield(nil, e.error)
+ }
+}
+
+func (e roleassignmentErrorClient) AssignRole(_ context.Context, _ string, _ roles.AssignOpts) error {
+ return e.error
+}
+
+func (e roleassignmentErrorClient) UnassignRole(_ context.Context, _ string, _ roles.UnassignOpts) error {
+ return e.error
+}
diff --git a/internal/scope/mock.go b/internal/scope/mock.go
index 256fa2e1d..fdf984782 100644
--- a/internal/scope/mock.go
+++ b/internal/scope/mock.go
@@ -45,6 +45,7 @@ type MockScopeFactory struct {
KeyPairClient *mock.MockKeyPairClient
NetworkClient *mock.MockNetworkClient
RoleClient *mock.MockRoleClient
+ RoleAssignmentClient *mock.MockRoleAssignmentClient
ServiceClient *mock.MockServiceClient
UserClient *mock.MockUserClient
VolumeClient *mock.MockVolumeClient
@@ -65,6 +66,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory {
keypairClient := mock.NewMockKeyPairClient(mockCtrl)
networkClient := mock.NewMockNetworkClient(mockCtrl)
roleClient := mock.NewMockRoleClient(mockCtrl)
+ roleassignmentClient := mock.NewMockRoleAssignmentClient(mockCtrl)
serviceClient := mock.NewMockServiceClient(mockCtrl)
userClient := mock.NewMockUserClient(mockCtrl)
volumeClient := mock.NewMockVolumeClient(mockCtrl)
@@ -82,6 +84,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory {
KeyPairClient: keypairClient,
NetworkClient: networkClient,
RoleClient: roleClient,
+ RoleAssignmentClient: roleassignmentClient,
ServiceClient: serviceClient,
UserClient: userClient,
VolumeClient: volumeClient,
@@ -152,6 +155,10 @@ func (f *MockScopeFactory) NewRoleClient() (osclients.RoleClient, error) {
return f.RoleClient, nil
}
+func (f *MockScopeFactory) NewRoleAssignmentClient() (osclients.RoleAssignmentClient, error) {
+ return f.RoleAssignmentClient, nil
+}
+
func (f *MockScopeFactory) NewEndpointClient() (osclients.EndpointClient, error) {
return f.EndpointClient, nil
}
diff --git a/internal/scope/provider.go b/internal/scope/provider.go
index aadd5c5ff..c6fd90099 100644
--- a/internal/scope/provider.go
+++ b/internal/scope/provider.go
@@ -197,6 +197,10 @@ func (s *providerScope) NewRoleClient() (clients.RoleClient, error) {
return clients.NewRoleClient(s.providerClient, s.providerClientOpts)
}
+func (s *providerScope) NewRoleAssignmentClient() (clients.RoleAssignmentClient, error) {
+ return clients.NewRoleAssignmentClient(s.providerClient, s.providerClientOpts)
+}
+
func (s *providerScope) ExtractToken() (*tokens.Token, error) {
client, err := openstack.NewIdentityV3(s.providerClient, gophercloud.EndpointOpts{})
if err != nil {
diff --git a/internal/scope/scope.go b/internal/scope/scope.go
index d8426fd62..63d6f649d 100644
--- a/internal/scope/scope.go
+++ b/internal/scope/scope.go
@@ -59,6 +59,7 @@ type Scope interface {
NewKeyPairClient() (osclients.KeyPairClient, error)
NewNetworkClient() (osclients.NetworkClient, error)
NewRoleClient() (osclients.RoleClient, error)
+ NewRoleAssignmentClient() (osclients.RoleAssignmentClient, error)
NewServiceClient() (osclients.ServiceClient, error)
NewUserClient() (osclients.UserClient, error)
NewVolumeClient() (osclients.VolumeClient, error)
diff --git a/kuttl-test.yaml b/kuttl-test.yaml
index d58526493..99d71a82c 100644
--- a/kuttl-test.yaml
+++ b/kuttl-test.yaml
@@ -15,6 +15,7 @@ testDirs:
- ./internal/controllers/port/tests/
- ./internal/controllers/project/tests/
- ./internal/controllers/role/tests/
+- ./internal/controllers/roleassignment/tests/
- ./internal/controllers/router/tests/
- ./internal/controllers/routerinterface/tests/
- ./internal/controllers/securitygroup/tests/
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignment.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignment.go
new file mode 100644
index 000000000..26462d14d
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignment.go
@@ -0,0 +1,281 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ internal "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/internal"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ managedfields "k8s.io/apimachinery/pkg/util/managedfields"
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// RoleAssignmentApplyConfiguration represents a declarative configuration of the RoleAssignment type for use
+// with apply.
+type RoleAssignmentApplyConfiguration struct {
+ v1.TypeMetaApplyConfiguration `json:",inline"`
+ *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+ Spec *RoleAssignmentSpecApplyConfiguration `json:"spec,omitempty"`
+ Status *RoleAssignmentStatusApplyConfiguration `json:"status,omitempty"`
+}
+
+// RoleAssignment constructs a declarative configuration of the RoleAssignment type for use with
+// apply.
+func RoleAssignment(name, namespace string) *RoleAssignmentApplyConfiguration {
+ b := &RoleAssignmentApplyConfiguration{}
+ b.WithName(name)
+ b.WithNamespace(namespace)
+ b.WithKind("RoleAssignment")
+ b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1")
+ return b
+}
+
+// ExtractRoleAssignment extracts the applied configuration owned by fieldManager from
+// roleAssignment. If no managedFields are found in roleAssignment for fieldManager, a
+// RoleAssignmentApplyConfiguration is returned with only the Name, Namespace (if applicable),
+// APIVersion and Kind populated. It is possible that no managed fields were found for because other
+// field managers have taken ownership of all the fields previously owned by fieldManager, or because
+// the fieldManager never owned fields any fields.
+// roleAssignment must be a unmodified RoleAssignment API object that was retrieved from the Kubernetes API.
+// ExtractRoleAssignment provides a way to perform a extract/modify-in-place/apply workflow.
+// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously
+// applied if another fieldManager has updated or force applied any of the previously applied fields.
+// Experimental!
+func ExtractRoleAssignment(roleAssignment *apiv1alpha1.RoleAssignment, fieldManager string) (*RoleAssignmentApplyConfiguration, error) {
+ return extractRoleAssignment(roleAssignment, fieldManager, "")
+}
+
+// ExtractRoleAssignmentStatus is the same as ExtractRoleAssignment except
+// that it extracts the status subresource applied configuration.
+// Experimental!
+func ExtractRoleAssignmentStatus(roleAssignment *apiv1alpha1.RoleAssignment, fieldManager string) (*RoleAssignmentApplyConfiguration, error) {
+ return extractRoleAssignment(roleAssignment, fieldManager, "status")
+}
+
+func extractRoleAssignment(roleAssignment *apiv1alpha1.RoleAssignment, fieldManager string, subresource string) (*RoleAssignmentApplyConfiguration, error) {
+ b := &RoleAssignmentApplyConfiguration{}
+ err := managedfields.ExtractInto(roleAssignment, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignment"), fieldManager, b, subresource)
+ if err != nil {
+ return nil, err
+ }
+ b.WithName(roleAssignment.Name)
+ b.WithNamespace(roleAssignment.Namespace)
+
+ b.WithKind("RoleAssignment")
+ b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1")
+ return b, nil
+}
+func (b RoleAssignmentApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithKind(value string) *RoleAssignmentApplyConfiguration {
+ b.TypeMetaApplyConfiguration.Kind = &value
+ return b
+}
+
+// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the APIVersion field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithAPIVersion(value string) *RoleAssignmentApplyConfiguration {
+ b.TypeMetaApplyConfiguration.APIVersion = &value
+ return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithName(value string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Name = &value
+ return b
+}
+
+// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GenerateName field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithGenerateName(value string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.GenerateName = &value
+ return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithNamespace(value string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Namespace = &value
+ return b
+}
+
+// WithUID sets the UID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UID field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithUID(value types.UID) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.UID = &value
+ return b
+}
+
+// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ResourceVersion field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithResourceVersion(value string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+ return b
+}
+
+// WithGeneration sets the Generation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Generation field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithGeneration(value int64) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Generation = &value
+ return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CreationTimestamp field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithCreationTimestamp(value metav1.Time) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+ return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+ return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+ return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *RoleAssignmentApplyConfiguration) WithLabels(entries map[string]string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Labels[k] = v
+ }
+ return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *RoleAssignmentApplyConfiguration) WithAnnotations(entries map[string]string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Annotations[k] = v
+ }
+ return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *RoleAssignmentApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithOwnerReferences")
+ }
+ b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+ }
+ return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *RoleAssignmentApplyConfiguration) WithFinalizers(values ...string) *RoleAssignmentApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+ }
+ return b
+}
+
+func (b *RoleAssignmentApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+ if b.ObjectMetaApplyConfiguration == nil {
+ b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+ }
+}
+
+// WithSpec sets the Spec field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Spec field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithSpec(value *RoleAssignmentSpecApplyConfiguration) *RoleAssignmentApplyConfiguration {
+ b.Spec = value
+ return b
+}
+
+// WithStatus sets the Status field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Status field is set to the value of the last call.
+func (b *RoleAssignmentApplyConfiguration) WithStatus(value *RoleAssignmentStatusApplyConfiguration) *RoleAssignmentApplyConfiguration {
+ b.Status = value
+ return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *RoleAssignmentApplyConfiguration) GetKind() *string {
+ return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *RoleAssignmentApplyConfiguration) GetAPIVersion() *string {
+ return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *RoleAssignmentApplyConfiguration) GetName() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *RoleAssignmentApplyConfiguration) GetNamespace() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentfilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentfilter.go
new file mode 100644
index 000000000..5367d30da
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentfilter.go
@@ -0,0 +1,79 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// RoleAssignmentFilterApplyConfiguration represents a declarative configuration of the RoleAssignmentFilter type for use
+// with apply.
+type RoleAssignmentFilterApplyConfiguration struct {
+ RoleRef *apiv1alpha1.KubernetesNameRef `json:"roleRef,omitempty"`
+ UserRef *apiv1alpha1.KubernetesNameRef `json:"userRef,omitempty"`
+ GroupRef *apiv1alpha1.KubernetesNameRef `json:"groupRef,omitempty"`
+ ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"`
+ DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"`
+}
+
+// RoleAssignmentFilterApplyConfiguration constructs a declarative configuration of the RoleAssignmentFilter type for use with
+// apply.
+func RoleAssignmentFilter() *RoleAssignmentFilterApplyConfiguration {
+ return &RoleAssignmentFilterApplyConfiguration{}
+}
+
+// WithRoleRef sets the RoleRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the RoleRef field is set to the value of the last call.
+func (b *RoleAssignmentFilterApplyConfiguration) WithRoleRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentFilterApplyConfiguration {
+ b.RoleRef = &value
+ return b
+}
+
+// WithUserRef sets the UserRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UserRef field is set to the value of the last call.
+func (b *RoleAssignmentFilterApplyConfiguration) WithUserRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentFilterApplyConfiguration {
+ b.UserRef = &value
+ return b
+}
+
+// WithGroupRef sets the GroupRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GroupRef field is set to the value of the last call.
+func (b *RoleAssignmentFilterApplyConfiguration) WithGroupRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentFilterApplyConfiguration {
+ b.GroupRef = &value
+ return b
+}
+
+// WithProjectRef sets the ProjectRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ProjectRef field is set to the value of the last call.
+func (b *RoleAssignmentFilterApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentFilterApplyConfiguration {
+ b.ProjectRef = &value
+ return b
+}
+
+// WithDomainRef sets the DomainRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DomainRef field is set to the value of the last call.
+func (b *RoleAssignmentFilterApplyConfiguration) WithDomainRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentFilterApplyConfiguration {
+ b.DomainRef = &value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentimport.go
new file mode 100644
index 000000000..172fa19d9
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentimport.go
@@ -0,0 +1,39 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// RoleAssignmentImportApplyConfiguration represents a declarative configuration of the RoleAssignmentImport type for use
+// with apply.
+type RoleAssignmentImportApplyConfiguration struct {
+ Filter *RoleAssignmentFilterApplyConfiguration `json:"filter,omitempty"`
+}
+
+// RoleAssignmentImportApplyConfiguration constructs a declarative configuration of the RoleAssignmentImport type for use with
+// apply.
+func RoleAssignmentImport() *RoleAssignmentImportApplyConfiguration {
+ return &RoleAssignmentImportApplyConfiguration{}
+}
+
+// WithFilter sets the Filter field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Filter field is set to the value of the last call.
+func (b *RoleAssignmentImportApplyConfiguration) WithFilter(value *RoleAssignmentFilterApplyConfiguration) *RoleAssignmentImportApplyConfiguration {
+ b.Filter = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcespec.go
new file mode 100644
index 000000000..680572620
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcespec.go
@@ -0,0 +1,79 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// RoleAssignmentResourceSpecApplyConfiguration represents a declarative configuration of the RoleAssignmentResourceSpec type for use
+// with apply.
+type RoleAssignmentResourceSpecApplyConfiguration struct {
+ RoleRef *apiv1alpha1.KubernetesNameRef `json:"roleRef,omitempty"`
+ UserRef *apiv1alpha1.KubernetesNameRef `json:"userRef,omitempty"`
+ GroupRef *apiv1alpha1.KubernetesNameRef `json:"groupRef,omitempty"`
+ ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"`
+ DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"`
+}
+
+// RoleAssignmentResourceSpecApplyConfiguration constructs a declarative configuration of the RoleAssignmentResourceSpec type for use with
+// apply.
+func RoleAssignmentResourceSpec() *RoleAssignmentResourceSpecApplyConfiguration {
+ return &RoleAssignmentResourceSpecApplyConfiguration{}
+}
+
+// WithRoleRef sets the RoleRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the RoleRef field is set to the value of the last call.
+func (b *RoleAssignmentResourceSpecApplyConfiguration) WithRoleRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentResourceSpecApplyConfiguration {
+ b.RoleRef = &value
+ return b
+}
+
+// WithUserRef sets the UserRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UserRef field is set to the value of the last call.
+func (b *RoleAssignmentResourceSpecApplyConfiguration) WithUserRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentResourceSpecApplyConfiguration {
+ b.UserRef = &value
+ return b
+}
+
+// WithGroupRef sets the GroupRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GroupRef field is set to the value of the last call.
+func (b *RoleAssignmentResourceSpecApplyConfiguration) WithGroupRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentResourceSpecApplyConfiguration {
+ b.GroupRef = &value
+ return b
+}
+
+// WithProjectRef sets the ProjectRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ProjectRef field is set to the value of the last call.
+func (b *RoleAssignmentResourceSpecApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentResourceSpecApplyConfiguration {
+ b.ProjectRef = &value
+ return b
+}
+
+// WithDomainRef sets the DomainRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DomainRef field is set to the value of the last call.
+func (b *RoleAssignmentResourceSpecApplyConfiguration) WithDomainRef(value apiv1alpha1.KubernetesNameRef) *RoleAssignmentResourceSpecApplyConfiguration {
+ b.DomainRef = &value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcestatus.go
new file mode 100644
index 000000000..e4e29148e
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentresourcestatus.go
@@ -0,0 +1,75 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// RoleAssignmentResourceStatusApplyConfiguration represents a declarative configuration of the RoleAssignmentResourceStatus type for use
+// with apply.
+type RoleAssignmentResourceStatusApplyConfiguration struct {
+ RoleID *string `json:"roleID,omitempty"`
+ UserID *string `json:"userID,omitempty"`
+ GroupID *string `json:"groupID,omitempty"`
+ ProjectID *string `json:"projectID,omitempty"`
+ DomainID *string `json:"domainID,omitempty"`
+}
+
+// RoleAssignmentResourceStatusApplyConfiguration constructs a declarative configuration of the RoleAssignmentResourceStatus type for use with
+// apply.
+func RoleAssignmentResourceStatus() *RoleAssignmentResourceStatusApplyConfiguration {
+ return &RoleAssignmentResourceStatusApplyConfiguration{}
+}
+
+// WithRoleID sets the RoleID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the RoleID field is set to the value of the last call.
+func (b *RoleAssignmentResourceStatusApplyConfiguration) WithRoleID(value string) *RoleAssignmentResourceStatusApplyConfiguration {
+ b.RoleID = &value
+ return b
+}
+
+// WithUserID sets the UserID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UserID field is set to the value of the last call.
+func (b *RoleAssignmentResourceStatusApplyConfiguration) WithUserID(value string) *RoleAssignmentResourceStatusApplyConfiguration {
+ b.UserID = &value
+ return b
+}
+
+// WithGroupID sets the GroupID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GroupID field is set to the value of the last call.
+func (b *RoleAssignmentResourceStatusApplyConfiguration) WithGroupID(value string) *RoleAssignmentResourceStatusApplyConfiguration {
+ b.GroupID = &value
+ return b
+}
+
+// WithProjectID sets the ProjectID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ProjectID field is set to the value of the last call.
+func (b *RoleAssignmentResourceStatusApplyConfiguration) WithProjectID(value string) *RoleAssignmentResourceStatusApplyConfiguration {
+ b.ProjectID = &value
+ return b
+}
+
+// WithDomainID sets the DomainID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DomainID field is set to the value of the last call.
+func (b *RoleAssignmentResourceStatusApplyConfiguration) WithDomainID(value string) *RoleAssignmentResourceStatusApplyConfiguration {
+ b.DomainID = &value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentspec.go
new file mode 100644
index 000000000..df29d44b1
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentspec.go
@@ -0,0 +1,79 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+)
+
+// RoleAssignmentSpecApplyConfiguration represents a declarative configuration of the RoleAssignmentSpec type for use
+// with apply.
+type RoleAssignmentSpecApplyConfiguration struct {
+ Import *RoleAssignmentImportApplyConfiguration `json:"import,omitempty"`
+ Resource *RoleAssignmentResourceSpecApplyConfiguration `json:"resource,omitempty"`
+ ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"`
+ ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"`
+ CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"`
+}
+
+// RoleAssignmentSpecApplyConfiguration constructs a declarative configuration of the RoleAssignmentSpec type for use with
+// apply.
+func RoleAssignmentSpec() *RoleAssignmentSpecApplyConfiguration {
+ return &RoleAssignmentSpecApplyConfiguration{}
+}
+
+// WithImport sets the Import field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Import field is set to the value of the last call.
+func (b *RoleAssignmentSpecApplyConfiguration) WithImport(value *RoleAssignmentImportApplyConfiguration) *RoleAssignmentSpecApplyConfiguration {
+ b.Import = value
+ return b
+}
+
+// WithResource sets the Resource field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Resource field is set to the value of the last call.
+func (b *RoleAssignmentSpecApplyConfiguration) WithResource(value *RoleAssignmentResourceSpecApplyConfiguration) *RoleAssignmentSpecApplyConfiguration {
+ b.Resource = value
+ return b
+}
+
+// WithManagementPolicy sets the ManagementPolicy field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ManagementPolicy field is set to the value of the last call.
+func (b *RoleAssignmentSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *RoleAssignmentSpecApplyConfiguration {
+ b.ManagementPolicy = &value
+ return b
+}
+
+// WithManagedOptions sets the ManagedOptions field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ManagedOptions field is set to the value of the last call.
+func (b *RoleAssignmentSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *RoleAssignmentSpecApplyConfiguration {
+ b.ManagedOptions = value
+ return b
+}
+
+// WithCloudCredentialsRef sets the CloudCredentialsRef field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CloudCredentialsRef field is set to the value of the last call.
+func (b *RoleAssignmentSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *RoleAssignmentSpecApplyConfiguration {
+ b.CloudCredentialsRef = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentstatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentstatus.go
new file mode 100644
index 000000000..bf5edaa54
--- /dev/null
+++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentstatus.go
@@ -0,0 +1,57 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// RoleAssignmentStatusApplyConfiguration represents a declarative configuration of the RoleAssignmentStatus type for use
+// with apply.
+type RoleAssignmentStatusApplyConfiguration struct {
+ Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
+ Resource *RoleAssignmentResourceStatusApplyConfiguration `json:"resource,omitempty"`
+}
+
+// RoleAssignmentStatusApplyConfiguration constructs a declarative configuration of the RoleAssignmentStatus type for use with
+// apply.
+func RoleAssignmentStatus() *RoleAssignmentStatusApplyConfiguration {
+ return &RoleAssignmentStatusApplyConfiguration{}
+}
+
+// WithConditions adds the given value to the Conditions field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Conditions field.
+func (b *RoleAssignmentStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *RoleAssignmentStatusApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithConditions")
+ }
+ b.Conditions = append(b.Conditions, *values[i])
+ }
+ return b
+}
+
+// WithResource sets the Resource field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Resource field is set to the value of the last call.
+func (b *RoleAssignmentStatusApplyConfiguration) WithResource(value *RoleAssignmentResourceStatusApplyConfiguration) *RoleAssignmentStatusApplyConfiguration {
+ b.Resource = value
+ return b
+}
diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go
index b51e2a458..2e7a00ec6 100644
--- a/pkg/clients/applyconfiguration/internal/internal.go
+++ b/pkg/clients/applyconfiguration/internal/internal.go
@@ -2090,6 +2090,120 @@ var schemaYAML = typed.YAMLObject(`types:
type:
namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleStatus
default: {}
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignment
+ map:
+ fields:
+ - name: apiVersion
+ type:
+ scalar: string
+ - name: kind
+ type:
+ scalar: string
+ - name: metadata
+ type:
+ namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta
+ default: {}
+ - name: spec
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentSpec
+ default: {}
+ - name: status
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentStatus
+ default: {}
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentFilter
+ map:
+ fields:
+ - name: domainRef
+ type:
+ scalar: string
+ - name: groupRef
+ type:
+ scalar: string
+ - name: projectRef
+ type:
+ scalar: string
+ - name: roleRef
+ type:
+ scalar: string
+ - name: userRef
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentImport
+ map:
+ fields:
+ - name: filter
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentFilter
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentResourceSpec
+ map:
+ fields:
+ - name: domainRef
+ type:
+ scalar: string
+ - name: groupRef
+ type:
+ scalar: string
+ - name: projectRef
+ type:
+ scalar: string
+ - name: roleRef
+ type:
+ scalar: string
+ - name: userRef
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentResourceStatus
+ map:
+ fields:
+ - name: domainID
+ type:
+ scalar: string
+ - name: groupID
+ type:
+ scalar: string
+ - name: projectID
+ type:
+ scalar: string
+ - name: roleID
+ type:
+ scalar: string
+ - name: userID
+ type:
+ scalar: string
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentSpec
+ map:
+ fields:
+ - name: cloudCredentialsRef
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.CloudCredentialsReference
+ default: {}
+ - name: import
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentImport
+ - name: managedOptions
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ManagedOptions
+ - name: managementPolicy
+ type:
+ scalar: string
+ - name: resource
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentResourceSpec
+- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentStatus
+ map:
+ fields:
+ - name: conditions
+ type:
+ list:
+ elementType:
+ namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Condition
+ elementRelationship: associative
+ keys:
+ - type
+ - name: resource
+ type:
+ namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleAssignmentResourceStatus
- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.RoleFilter
map:
fields:
diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go
index 73e66b28b..d30735a7c 100644
--- a/pkg/clients/applyconfiguration/utils.go
+++ b/pkg/clients/applyconfiguration/utils.go
@@ -266,6 +266,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &apiv1alpha1.ProviderPropertiesStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Role"):
return &apiv1alpha1.RoleApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignment"):
+ return &apiv1alpha1.RoleAssignmentApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentFilter"):
+ return &apiv1alpha1.RoleAssignmentFilterApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentImport"):
+ return &apiv1alpha1.RoleAssignmentImportApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentResourceSpec"):
+ return &apiv1alpha1.RoleAssignmentResourceSpecApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentResourceStatus"):
+ return &apiv1alpha1.RoleAssignmentResourceStatusApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentSpec"):
+ return &apiv1alpha1.RoleAssignmentSpecApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("RoleAssignmentStatus"):
+ return &apiv1alpha1.RoleAssignmentStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("RoleFilter"):
return &apiv1alpha1.RoleFilterApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("RoleImport"):
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
index a145cab6f..af2dbd9e0 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go
@@ -41,6 +41,7 @@ type OpenstackV1alpha1Interface interface {
PortsGetter
ProjectsGetter
RolesGetter
+ RoleAssignmentsGetter
RoutersGetter
RouterInterfacesGetter
SecurityGroupsGetter
@@ -111,6 +112,10 @@ func (c *OpenstackV1alpha1Client) Roles(namespace string) RoleInterface {
return newRoles(c, namespace)
}
+func (c *OpenstackV1alpha1Client) RoleAssignments(namespace string) RoleAssignmentInterface {
+ return newRoleAssignments(c, namespace)
+}
+
func (c *OpenstackV1alpha1Client) Routers(namespace string) RouterInterface {
return newRouters(c, namespace)
}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
index 87bc2b39d..2b3498411 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_api_client.go
@@ -80,6 +80,10 @@ func (c *FakeOpenstackV1alpha1) Roles(namespace string) v1alpha1.RoleInterface {
return newFakeRoles(c, namespace)
}
+func (c *FakeOpenstackV1alpha1) RoleAssignments(namespace string) v1alpha1.RoleAssignmentInterface {
+ return newFakeRoleAssignments(c, namespace)
+}
+
func (c *FakeOpenstackV1alpha1) Routers(namespace string) v1alpha1.RouterInterface {
return newFakeRouters(c, namespace)
}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_roleassignment.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_roleassignment.go
new file mode 100644
index 000000000..05bbae41c
--- /dev/null
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_roleassignment.go
@@ -0,0 +1,53 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ v1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+ typedapiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset/typed/api/v1alpha1"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// fakeRoleAssignments implements RoleAssignmentInterface
+type fakeRoleAssignments struct {
+ *gentype.FakeClientWithListAndApply[*v1alpha1.RoleAssignment, *v1alpha1.RoleAssignmentList, *apiv1alpha1.RoleAssignmentApplyConfiguration]
+ Fake *FakeOpenstackV1alpha1
+}
+
+func newFakeRoleAssignments(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.RoleAssignmentInterface {
+ return &fakeRoleAssignments{
+ gentype.NewFakeClientWithListAndApply[*v1alpha1.RoleAssignment, *v1alpha1.RoleAssignmentList, *apiv1alpha1.RoleAssignmentApplyConfiguration](
+ fake.Fake,
+ namespace,
+ v1alpha1.SchemeGroupVersion.WithResource("roleassignments"),
+ v1alpha1.SchemeGroupVersion.WithKind("RoleAssignment"),
+ func() *v1alpha1.RoleAssignment { return &v1alpha1.RoleAssignment{} },
+ func() *v1alpha1.RoleAssignmentList { return &v1alpha1.RoleAssignmentList{} },
+ func(dst, src *v1alpha1.RoleAssignmentList) { dst.ListMeta = src.ListMeta },
+ func(list *v1alpha1.RoleAssignmentList) []*v1alpha1.RoleAssignment {
+ return gentype.ToPointerSlice(list.Items)
+ },
+ func(list *v1alpha1.RoleAssignmentList, items []*v1alpha1.RoleAssignment) {
+ list.Items = gentype.FromPointerSlice(items)
+ },
+ ),
+ fake,
+ }
+}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
index de60388f2..7d409e9d8 100644
--- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go
@@ -44,6 +44,8 @@ type ProjectExpansion interface{}
type RoleExpansion interface{}
+type RoleAssignmentExpansion interface{}
+
type RouterExpansion interface{}
type RouterInterfaceExpansion interface{}
diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/roleassignment.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/roleassignment.go
new file mode 100644
index 000000000..37d5f5a96
--- /dev/null
+++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/roleassignment.go
@@ -0,0 +1,74 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ applyconfigurationapiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+ scheme "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset/scheme"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// RoleAssignmentsGetter has a method to return a RoleAssignmentInterface.
+// A group's client should implement this interface.
+type RoleAssignmentsGetter interface {
+ RoleAssignments(namespace string) RoleAssignmentInterface
+}
+
+// RoleAssignmentInterface has methods to work with RoleAssignment resources.
+type RoleAssignmentInterface interface {
+ Create(ctx context.Context, roleAssignment *apiv1alpha1.RoleAssignment, opts v1.CreateOptions) (*apiv1alpha1.RoleAssignment, error)
+ Update(ctx context.Context, roleAssignment *apiv1alpha1.RoleAssignment, opts v1.UpdateOptions) (*apiv1alpha1.RoleAssignment, error)
+ // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+ UpdateStatus(ctx context.Context, roleAssignment *apiv1alpha1.RoleAssignment, opts v1.UpdateOptions) (*apiv1alpha1.RoleAssignment, error)
+ Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+ DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+ Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.RoleAssignment, error)
+ List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.RoleAssignmentList, error)
+ Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+ Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.RoleAssignment, err error)
+ Apply(ctx context.Context, roleAssignment *applyconfigurationapiv1alpha1.RoleAssignmentApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.RoleAssignment, err error)
+ // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus().
+ ApplyStatus(ctx context.Context, roleAssignment *applyconfigurationapiv1alpha1.RoleAssignmentApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.RoleAssignment, err error)
+ RoleAssignmentExpansion
+}
+
+// roleAssignments implements RoleAssignmentInterface
+type roleAssignments struct {
+ *gentype.ClientWithListAndApply[*apiv1alpha1.RoleAssignment, *apiv1alpha1.RoleAssignmentList, *applyconfigurationapiv1alpha1.RoleAssignmentApplyConfiguration]
+}
+
+// newRoleAssignments returns a RoleAssignments
+func newRoleAssignments(c *OpenstackV1alpha1Client, namespace string) *roleAssignments {
+ return &roleAssignments{
+ gentype.NewClientWithListAndApply[*apiv1alpha1.RoleAssignment, *apiv1alpha1.RoleAssignmentList, *applyconfigurationapiv1alpha1.RoleAssignmentApplyConfiguration](
+ "roleassignments",
+ c.RESTClient(),
+ scheme.ParameterCodec,
+ namespace,
+ func() *apiv1alpha1.RoleAssignment { return &apiv1alpha1.RoleAssignment{} },
+ func() *apiv1alpha1.RoleAssignmentList { return &apiv1alpha1.RoleAssignmentList{} },
+ ),
+ }
+}
diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
index b9b415243..0db92472f 100644
--- a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
+++ b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go
@@ -50,6 +50,8 @@ type Interface interface {
Projects() ProjectInformer
// Roles returns a RoleInformer.
Roles() RoleInformer
+ // RoleAssignments returns a RoleAssignmentInformer.
+ RoleAssignments() RoleAssignmentInformer
// Routers returns a RouterInformer.
Routers() RouterInformer
// RouterInterfaces returns a RouterInterfaceInformer.
@@ -150,6 +152,11 @@ func (v *version) Roles() RoleInformer {
return &roleInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}
+// RoleAssignments returns a RoleAssignmentInformer.
+func (v *version) RoleAssignments() RoleAssignmentInformer {
+ return &roleAssignmentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
// Routers returns a RouterInformer.
func (v *version) Routers() RouterInformer {
return &routerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/roleassignment.go b/pkg/clients/informers/externalversions/api/v1alpha1/roleassignment.go
new file mode 100644
index 000000000..d34221cf4
--- /dev/null
+++ b/pkg/clients/informers/externalversions/api/v1alpha1/roleassignment.go
@@ -0,0 +1,102 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+ time "time"
+
+ v2apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ clientset "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset"
+ internalinterfaces "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/informers/externalversions/internalinterfaces"
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/listers/api/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ watch "k8s.io/apimachinery/pkg/watch"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// RoleAssignmentInformer provides access to a shared informer and lister for
+// RoleAssignments.
+type RoleAssignmentInformer interface {
+ Informer() cache.SharedIndexInformer
+ Lister() apiv1alpha1.RoleAssignmentLister
+}
+
+type roleAssignmentInformer struct {
+ factory internalinterfaces.SharedInformerFactory
+ tweakListOptions internalinterfaces.TweakListOptionsFunc
+ namespace string
+}
+
+// NewRoleAssignmentInformer constructs a new informer for RoleAssignment type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewRoleAssignmentInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ return NewFilteredRoleAssignmentInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredRoleAssignmentInformer constructs a new informer for RoleAssignment type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredRoleAssignmentInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+ return cache.NewSharedIndexInformer(
+ &cache.ListWatch{
+ ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().RoleAssignments(namespace).List(context.Background(), options)
+ },
+ WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().RoleAssignments(namespace).Watch(context.Background(), options)
+ },
+ ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().RoleAssignments(namespace).List(ctx, options)
+ },
+ WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&options)
+ }
+ return client.OpenstackV1alpha1().RoleAssignments(namespace).Watch(ctx, options)
+ },
+ },
+ &v2apiv1alpha1.RoleAssignment{},
+ resyncPeriod,
+ indexers,
+ )
+}
+
+func (f *roleAssignmentInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+ return NewFilteredRoleAssignmentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *roleAssignmentInformer) Informer() cache.SharedIndexInformer {
+ return f.factory.InformerFor(&v2apiv1alpha1.RoleAssignment{}, f.defaultInformer)
+}
+
+func (f *roleAssignmentInformer) Lister() apiv1alpha1.RoleAssignmentLister {
+ return apiv1alpha1.NewRoleAssignmentLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go
index 99f589164..a6627b89c 100644
--- a/pkg/clients/informers/externalversions/generic.go
+++ b/pkg/clients/informers/externalversions/generic.go
@@ -79,6 +79,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Projects().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("roles"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Roles().Informer()}, nil
+ case v1alpha1.SchemeGroupVersion.WithResource("roleassignments"):
+ return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().RoleAssignments().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("routers"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Routers().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("routerinterfaces"):
diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go
index 98590bb01..77fd9e47b 100644
--- a/pkg/clients/listers/api/v1alpha1/expansion_generated.go
+++ b/pkg/clients/listers/api/v1alpha1/expansion_generated.go
@@ -122,6 +122,14 @@ type RoleListerExpansion interface{}
// RoleNamespaceLister.
type RoleNamespaceListerExpansion interface{}
+// RoleAssignmentListerExpansion allows custom methods to be added to
+// RoleAssignmentLister.
+type RoleAssignmentListerExpansion interface{}
+
+// RoleAssignmentNamespaceListerExpansion allows custom methods to be added to
+// RoleAssignmentNamespaceLister.
+type RoleAssignmentNamespaceListerExpansion interface{}
+
// RouterListerExpansion allows custom methods to be added to
// RouterLister.
type RouterListerExpansion interface{}
diff --git a/pkg/clients/listers/api/v1alpha1/roleassignment.go b/pkg/clients/listers/api/v1alpha1/roleassignment.go
new file mode 100644
index 000000000..37c6c7b8d
--- /dev/null
+++ b/pkg/clients/listers/api/v1alpha1/roleassignment.go
@@ -0,0 +1,70 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ listers "k8s.io/client-go/listers"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// RoleAssignmentLister helps list RoleAssignments.
+// All objects returned here must be treated as read-only.
+type RoleAssignmentLister interface {
+ // List lists all RoleAssignments in the indexer.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*apiv1alpha1.RoleAssignment, err error)
+ // RoleAssignments returns an object that can list and get RoleAssignments.
+ RoleAssignments(namespace string) RoleAssignmentNamespaceLister
+ RoleAssignmentListerExpansion
+}
+
+// roleAssignmentLister implements the RoleAssignmentLister interface.
+type roleAssignmentLister struct {
+ listers.ResourceIndexer[*apiv1alpha1.RoleAssignment]
+}
+
+// NewRoleAssignmentLister returns a new RoleAssignmentLister.
+func NewRoleAssignmentLister(indexer cache.Indexer) RoleAssignmentLister {
+ return &roleAssignmentLister{listers.New[*apiv1alpha1.RoleAssignment](indexer, apiv1alpha1.Resource("roleassignment"))}
+}
+
+// RoleAssignments returns an object that can list and get RoleAssignments.
+func (s *roleAssignmentLister) RoleAssignments(namespace string) RoleAssignmentNamespaceLister {
+ return roleAssignmentNamespaceLister{listers.NewNamespaced[*apiv1alpha1.RoleAssignment](s.ResourceIndexer, namespace)}
+}
+
+// RoleAssignmentNamespaceLister helps list and get RoleAssignments.
+// All objects returned here must be treated as read-only.
+type RoleAssignmentNamespaceLister interface {
+ // List lists all RoleAssignments in the indexer for a given namespace.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*apiv1alpha1.RoleAssignment, err error)
+ // Get retrieves the RoleAssignment from the indexer for a given namespace and name.
+ // Objects returned here must be treated as read-only.
+ Get(name string) (*apiv1alpha1.RoleAssignment, error)
+ RoleAssignmentNamespaceListerExpansion
+}
+
+// roleAssignmentNamespaceLister implements the RoleAssignmentNamespaceLister
+// interface.
+type roleAssignmentNamespaceLister struct {
+ listers.ResourceIndexer[*apiv1alpha1.RoleAssignment]
+}
diff --git a/test/apivalidations/roleassignment_test.go b/test/apivalidations/roleassignment_test.go
new file mode 100644
index 000000000..a48ba150a
--- /dev/null
+++ b/test/apivalidations/roleassignment_test.go
@@ -0,0 +1,135 @@
+/*
+Copyright The ORC Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package apivalidations
+
+import (
+ "context"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ corev1 "k8s.io/api/core/v1"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
+ applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
+)
+
+const (
+ roleassignmentName = "roleassignment"
+)
+
+func roleassignmentStub(namespace *corev1.Namespace) *orcv1alpha1.RoleAssignment {
+ obj := &orcv1alpha1.RoleAssignment{}
+ obj.Name = roleassignmentName
+ obj.Namespace = namespace.Name
+ return obj
+}
+
+func testRoleAssignmentResource() *applyconfigv1alpha1.RoleAssignmentResourceSpecApplyConfiguration {
+ return applyconfigv1alpha1.RoleAssignmentResourceSpec().
+ WithRoleRef("role").
+ WithUserRef("user").
+ WithProjectRef("project")
+}
+
+func baseRoleAssignmentPatch(obj client.Object) *applyconfigv1alpha1.RoleAssignmentApplyConfiguration {
+ return applyconfigv1alpha1.RoleAssignment(obj.GetName(), obj.GetNamespace()).
+ WithSpec(applyconfigv1alpha1.RoleAssignmentSpec().
+ WithCloudCredentialsRef(testCredentials()))
+}
+
+func testRoleAssignmentImport() *applyconfigv1alpha1.RoleAssignmentImportApplyConfiguration {
+ return applyconfigv1alpha1.RoleAssignmentImport().
+ WithFilter(applyconfigv1alpha1.RoleAssignmentFilter().WithRoleRef("admin"))
+}
+
+var _ = Describe("ORC RoleAssignment API validations", func() {
+ var namespace *corev1.Namespace
+ BeforeEach(func() {
+ namespace = createNamespace()
+ })
+
+ runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.RoleAssignmentApplyConfiguration]{
+ createObject: func(ns *corev1.Namespace) client.Object { return roleassignmentStub(ns) },
+ basePatch: func(obj client.Object) *applyconfigv1alpha1.RoleAssignmentApplyConfiguration {
+ return baseRoleAssignmentPatch(obj)
+ },
+ applyResource: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithResource(testRoleAssignmentResource())
+ },
+ applyImport: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithImport(testRoleAssignmentImport())
+ },
+ applyEmptyImport: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithImport(applyconfigv1alpha1.RoleAssignmentImport())
+ },
+ applyEmptyFilter: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithImport(applyconfigv1alpha1.RoleAssignmentImport().WithFilter(applyconfigv1alpha1.RoleAssignmentFilter()))
+ },
+ applyValidFilter: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithImport(applyconfigv1alpha1.RoleAssignmentImport().WithFilter(applyconfigv1alpha1.RoleAssignmentFilter().WithRoleRef("admin")))
+ },
+ applyManaged: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged)
+ },
+ applyUnmanaged: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged)
+ },
+ applyManagedOptions: func(p *applyconfigv1alpha1.RoleAssignmentApplyConfiguration) {
+ p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach))
+ },
+ getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy {
+ return obj.(*orcv1alpha1.RoleAssignment).Spec.ManagementPolicy
+ },
+ getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete {
+ return obj.(*orcv1alpha1.RoleAssignment).Spec.ManagedOptions.OnDelete
+ },
+ })
+
+ It("should reject a roleassignment without required fields", func(ctx context.Context) {
+ obj := roleassignmentStub(namespace)
+ patch := baseRoleAssignmentPatch(obj)
+ patch.Spec.WithResource(applyconfigv1alpha1.RoleAssignmentResourceSpec())
+ Expect(applyObj(ctx, obj, patch)).NotTo(Succeed())
+ })
+
+ It("should have immutable RoleAssignmentResourceSpec", func(ctx context.Context) {
+ obj := roleassignmentStub(namespace)
+ patch := baseRoleAssignmentPatch(obj)
+ patch.Spec.WithResource(applyconfigv1alpha1.RoleAssignmentResourceSpec().
+ WithRoleRef("role").
+ WithUserRef("user").
+ WithProjectRef("project"))
+ Expect(applyObj(ctx, obj, patch)).To(Succeed())
+
+ // Try to change any field - should fail because entire spec is immutable
+ patch.Spec.WithResource(applyconfigv1alpha1.RoleAssignmentResourceSpec().
+ WithRoleRef("role").
+ WithUserRef("user-changed").
+ WithProjectRef("project"))
+ Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("RoleAssignmentResourceSpec is immutable")))
+ })
+
+ // TODO(scaffolding): Add more resource-specific validation tests.
+ // Some common things to test:
+ // - Immutability of fields with `self == oldSelf` validation
+ // - Enum validation (valid and invalid values)
+ // - Numeric range validation (min/max bounds)
+ // - Tag uniqueness (if the resource has tags with listType=set)
+ // - Format validation (CIDR, UUID, etc.)
+ // - Cross-field validation rules
+})
diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md
index 756e1f09e..9ff67d50d 100644
--- a/website/docs/crd-reference.md
+++ b/website/docs/crd-reference.md
@@ -23,6 +23,7 @@ Package v1alpha1 contains API Schema definitions for the openstack v1alpha1 API
- [Port](#port)
- [Project](#project)
- [Role](#role)
+- [RoleAssignment](#roleassignment)
- [Router](#router)
- [RouterInterface](#routerinterface)
- [SecurityGroup](#securitygroup)
@@ -513,6 +514,7 @@ _Appears in:_
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
+- [RoleAssignmentSpec](#roleassignmentspec)
- [RoleSpec](#rolespec)
- [RouterSpec](#routerspec)
- [SecurityGroupSpec](#securitygroupspec)
@@ -2157,6 +2159,8 @@ _Appears in:_
- [PortResourceSpec](#portresourcespec)
- [ProjectFilter](#projectfilter)
- [ProjectResourceSpec](#projectresourcespec)
+- [RoleAssignmentFilter](#roleassignmentfilter)
+- [RoleAssignmentResourceSpec](#roleassignmentresourcespec)
- [RoleFilter](#rolefilter)
- [RoleResourceSpec](#roleresourcespec)
- [RouterFilter](#routerfilter)
@@ -2232,6 +2236,7 @@ _Appears in:_
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
+- [RoleAssignmentSpec](#roleassignmentspec)
- [RoleSpec](#rolespec)
- [RouterSpec](#routerspec)
- [SecurityGroupSpec](#securitygroupspec)
@@ -2271,6 +2276,7 @@ _Appears in:_
- [NetworkSpec](#networkspec)
- [PortSpec](#portspec)
- [ProjectSpec](#projectspec)
+- [RoleAssignmentSpec](#roleassignmentspec)
- [RoleSpec](#rolespec)
- [RouterSpec](#routerspec)
- [SecurityGroupSpec](#securitygroupspec)
@@ -3068,6 +3074,146 @@ Role is the Schema for an ORC resource.
| `status` _[RoleStatus](#rolestatus)_ | status defines the observed state of the resource. | | Optional: \{\}
|
+#### RoleAssignment
+
+
+
+RoleAssignment is the Schema for an ORC resource.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | |
+| `kind` _string_ | `RoleAssignment` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | Optional: \{\}
|
+| `spec` _[RoleAssignmentSpec](#roleassignmentspec)_ | spec specifies the desired state of the resource. | | Required: \{\}
|
+| `status` _[RoleAssignmentStatus](#roleassignmentstatus)_ | status defines the observed state of the resource. | | Optional: \{\}
|
+
+
+#### RoleAssignmentFilter
+
+
+
+RoleAssignmentFilter defines import filter criteria for existing role assignments.
+
+_Validation:_
+- MinProperties: 1
+
+_Appears in:_
+- [RoleAssignmentImport](#roleassignmentimport)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `roleRef` _[KubernetesNameRef](#kubernetesnameref)_ | roleRef filters by the referenced Role. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `userRef` _[KubernetesNameRef](#kubernetesnameref)_ | userRef filters by the referenced User. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `groupRef` _[KubernetesNameRef](#kubernetesnameref)_ | groupRef filters by the referenced Group. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef filters by the referenced Project scope. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef filters by the referenced Domain scope. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+
+
+#### RoleAssignmentImport
+
+
+
+RoleAssignmentImport specifies an existing resource which will be imported instead of
+creating a new one
+
+_Validation:_
+- MinProperties: 1
+
+_Appears in:_
+- [RoleAssignmentSpec](#roleassignmentspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `filter` _[RoleAssignmentFilter](#roleassignmentfilter)_ | filter contains a resource query which is expected to return a single
result. The controller will continue to retry if filter returns no
results. If filter returns multiple results the controller will set an
error state and will not continue to retry. | | MinProperties: 1
Optional: \{\}
|
+
+
+#### RoleAssignmentResourceSpec
+
+
+
+RoleAssignmentResourceSpec defines the desired role assignment.
+A role assignment grants a role to a user or group on a project or domain.
+Role assignments are immutable once created and identified by the combination
+of (role, actor, scope) rather than a separate ID.
+
+
+
+_Appears in:_
+- [RoleAssignmentSpec](#roleassignmentspec)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `roleRef` _[KubernetesNameRef](#kubernetesnameref)_ | roleRef references the Role being assigned. | | MaxLength: 253
MinLength: 1
Required: \{\}
|
+| `userRef` _[KubernetesNameRef](#kubernetesnameref)_ | userRef references the User receiving the role assignment.
Exactly one of userRef or groupRef must be specified. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `groupRef` _[KubernetesNameRef](#kubernetesnameref)_ | groupRef references the Group receiving the role assignment.
Exactly one of userRef or groupRef must be specified. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef references the Project scope for the assignment.
Exactly one of projectRef or domainRef must be specified. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+| `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef references the Domain scope for the assignment.
Exactly one of projectRef or domainRef must be specified. | | MaxLength: 253
MinLength: 1
Optional: \{\}
|
+
+
+#### RoleAssignmentResourceStatus
+
+
+
+RoleAssignmentResourceStatus represents the observed state of the role assignment.
+Note: Role assignments do not have a unique ID in OpenStack - they are identified
+by the combination of role, actor (user/group), and scope (project/domain).
+
+
+
+_Appears in:_
+- [RoleAssignmentStatus](#roleassignmentstatus)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `roleID` _string_ | roleID is the OpenStack ID of the assigned role. | | MaxLength: 1024
Optional: \{\}
|
+| `userID` _string_ | userID is the OpenStack ID of the user (if actorType is User). | | MaxLength: 1024
Optional: \{\}
|
+| `groupID` _string_ | groupID is the OpenStack ID of the group (if actorType is Group). | | MaxLength: 1024
Optional: \{\}
|
+| `projectID` _string_ | projectID is the OpenStack ID of the project scope (if scopeType is Project). | | MaxLength: 1024
Optional: \{\}
|
+| `domainID` _string_ | domainID is the OpenStack ID of the domain scope (if scopeType is Domain). | | MaxLength: 1024
Optional: \{\}
|
+
+
+#### RoleAssignmentSpec
+
+
+
+RoleAssignmentSpec defines the desired state of an ORC object.
+
+
+
+_Appears in:_
+- [RoleAssignment](#roleassignment)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `import` _[RoleAssignmentImport](#roleassignmentimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MinProperties: 1
Optional: \{\}
|
+| `resource` _[RoleAssignmentResourceSpec](#roleassignmentresourcespec)_ | resource specifies the desired state of the resource.
resource may not be specified if the management policy is `unmanaged`.
resource must be specified if the management policy is `managed`. | | Optional: \{\}
|
+| `managementPolicy` _[ManagementPolicy](#managementpolicy)_ | managementPolicy defines how ORC will treat the object. Valid values are
`managed`: ORC will create, update, and delete the resource; `unmanaged`:
ORC will import an existing resource, and will not apply updates to it or
delete it. | managed | Enum: [managed unmanaged]
Optional: \{\}
|
+| `managedOptions` _[ManagedOptions](#managedoptions)_ | managedOptions specifies options which may be applied to managed objects. | | Optional: \{\}
|
+| `cloudCredentialsRef` _[CloudCredentialsReference](#cloudcredentialsreference)_ | cloudCredentialsRef points to a secret containing OpenStack credentials | | Required: \{\}
|
+
+
+#### RoleAssignmentStatus
+
+
+
+RoleAssignmentStatus defines the observed state of an ORC resource.
+
+
+
+_Appears in:_
+- [RoleAssignment](#roleassignment)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta) array_ | conditions represents the observed status of the object.
Known .status.conditions.type are: "Available", "Progressing"
Available represents the availability of the OpenStack resource. If it is
true then the resource is ready for use.
Progressing indicates whether the controller is still attempting to
reconcile the current state of the OpenStack resource to the desired
state. Progressing will be False either because the desired state has
been achieved, or because some terminal error prevents it from ever being
achieved and the controller is no longer attempting to reconcile. If
Progressing is True, an observer waiting on the resource should continue
to wait. | | MaxItems: 32
Optional: \{\}
|
+| `resource` _[RoleAssignmentResourceStatus](#roleassignmentresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
|
+
+
#### RoleFilter
diff --git a/website/docs/development/godoc/generic-interfaces.md b/website/docs/development/godoc/generic-interfaces.md
index 2d0021572..51536dfde 100644
--- a/website/docs/development/godoc/generic-interfaces.md
+++ b/website/docs/development/godoc/generic-interfaces.md
@@ -15,6 +15,7 @@ import "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/g
- [type DeleteResourceActuator](<#DeleteResourceActuator>)
- [type ORCApplyConfig](<#ORCApplyConfig>)
- [type ORCStatusApplyConfig](<#ORCStatusApplyConfig>)
+- [type ORCStatusApplyConfigWithID](<#ORCStatusApplyConfigWithID>)
- [type ReconcileResourceActuator](<#ReconcileResourceActuator>)
- [type ResourceController](<#ResourceController>)
- [type ResourceHelperFactory](<#ResourceHelperFactory>)
@@ -203,14 +204,25 @@ type ORCApplyConfig[objectApplyPT any, statusApplyPT ORCStatusApplyConfig[status
```
-## type [ORCStatusApplyConfig]()
+## type [ORCStatusApplyConfig]()
-ORCStatusApplyConfig is an interface implemented by the status of any apply configuration for an ORC API object. It has Conditions and an ID field.
+ORCStatusApplyConfig is an interface implemented by the status of any apply configuration for an ORC API object.
```go
type ORCStatusApplyConfig[statusApplyPT any] interface {
WithConditions(...*applyconfigv1.ConditionApplyConfiguration) statusApplyPT
+}
+```
+
+
+## type [ORCStatusApplyConfigWithID]()
+
+ORCStatusApplyConfigWithID extends ORCStatusApplyConfig with an ID field. This is required by resources that have an OpenStack\-assigned ID stored in status.id. Resources without an ID \(e.g. relationship resources like RoleAssignment\) use only ORCStatusApplyConfig.
+
+```go
+type ORCStatusApplyConfigWithID[statusApplyPT any] interface {
WithID(id string) statusApplyPT
+ // contains filtered or unexported methods
}
```
@@ -313,7 +325,7 @@ type ResourceReconciler[orcObjectPT, osResourceT any] func(ctx context.Context,
```
-## type [ResourceStatusWriter]()
+## type [ResourceStatusWriter]()
ResourceStatusWriter defines methods for writing an ORC object status