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 6f5bf196d..05c878671 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3745,6 +3745,243 @@ 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.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **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.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + 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..03d1eee39 --- /dev/null +++ b/api/v1alpha1/zz_generated.roleassignment-resource.go @@ -0,0 +1,179 @@ +// 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 +// +kubebuilder:validation:MaxProperties:=1 +type RoleAssignmentImport struct { + // id contains the unique identifier of an existing OpenStack resource. Note + // that when specifying an import by ID, the resource MUST already exist. + // The ORC object will enter an error state if the resource does not exist. + // +kubebuilder:validation:Format:=uuid + // +kubebuilder:validation:MaxLength:=36 + // +optional + ID *string `json:"id,omitempty"` //nolint:kubeapilinter + + // 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"` + + // id is the unique identifier of the OpenStack resource. + // +kubebuilder:validation:MaxLength:=1024 + // +optional + ID *string `json:"id,omitempty"` + + // 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="ID",type="string",JSONPath=".status.id",description="Resource ID" +// +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 557911992..5f8061c70 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -158,6 +158,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), @@ -7125,6 +7133,381 @@ 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{ + "id": { + SchemaProps: spec.SchemaProps{ + Description: "id contains the unique identifier of an existing OpenStack resource. Note that when specifying an import by ID, the resource MUST already exist. The ORC object will enter an error state if the resource does not exist.", + Type: []string{"string"}, + Format: "", + }, + }, + "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"), + }, + }, + }, + }, + }, + "id": { + SchemaProps: spec.SchemaProps{ + Description: "id is the unique identifier of the OpenStack resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "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/main.go b/cmd/resource-generator/main.go index 2ff83df6e..7e1bd4147 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -124,6 +124,10 @@ var resources []templateFields = []templateFields{ { Name: "Role", }, + { + Name: "RoleAssignment", + IsNotNamed: 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..2c3123c35 --- /dev/null +++ b/config/crd/bases/openstack.k-orc.cloud_roleassignments.yaml @@ -0,0 +1,348 @@ +--- +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: Resource ID + jsonPath: .status.id + name: ID + type: string + - 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. + maxProperties: 1 + 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 + id: + description: |- + id contains the unique identifier of an existing OpenStack resource. Note + that when specifying an import by ID, the resource MUST already exist. + The ORC object will enter an error state if the resource does not exist. + format: uuid + maxLength: 36 + type: string + 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 + id: + description: id is the unique identifier of the OpenStack resource. + maxLength: 1024 + type: string + 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/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..a608fef87 --- /dev/null +++ b/config/samples/openstack_v1alpha1_roleassignment.yaml @@ -0,0 +1,30 @@ +--- +# Example: Assign the 'member' role to a user on a project +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: bob-member +spec: + cloudCredentialsRef: + cloudName: devstack-admin # Requires admin credentials + secretName: openstack-clouds + managementPolicy: managed + resource: + roleRef: assign-role-test # Reference to Role object + userRef: bob # Reference to User object + projectRef: assign-role-test # Reference to Project object +--- +# Example: Assign the 'admin' role to a group on a domain +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: admins-admin-on-default +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + roleRef: admin + groupRef: admins + domainRef: default diff --git a/internal/controllers/roleassignment/actuator.go b/internal/controllers/roleassignment/actuator.go new file mode 100644 index 000000000..9a4d4a0b0 --- /dev/null +++ b/internal/controllers/roleassignment/actuator.go @@ -0,0 +1,493 @@ +/* +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" + "strings" + + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles" + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "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/interfaces" + "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 types +type ( + osResourceT = roles.RoleAssignment + + createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT] + deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT] + helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT] +) + +type roleassignmentActuator struct { + osClient osclients.RoleAssignmentClient + k8sClient client.Client +} + +var _ createResourceActuator = roleassignmentActuator{} +var _ deleteResourceActuator = roleassignmentActuator{} + +// GetResourceID creates a synthetic ID from the tuple (role, actor, scope). +// OpenStack doesn't assign IDs to role assignments - they're identified by the tuple. +// Format: role::user::project: +// or: role::group::domain: +func (roleassignmentActuator) GetResourceID(osResource *osResourceT) string { + var parts []string + + // Role + parts = append(parts, "role", osResource.Role.ID) + + // Actor (user or group) + if osResource.User.ID != "" { + parts = append(parts, "user", osResource.User.ID) + } else if osResource.Group.ID != "" { + parts = append(parts, "group", osResource.Group.ID) + } + + // Scope (project or domain) + if osResource.Scope.Project.ID != "" { + parts = append(parts, "project", osResource.Scope.Project.ID) + } else if osResource.Scope.Domain.ID != "" { + parts = append(parts, "domain", osResource.Scope.Domain.ID) + } + + return strings.Join(parts, ":") +} + +// GetOSResourceByID queries for the role assignment by parsing the synthetic ID back to tuple components. +// The ID format is: role::user::project: (or group/domain variants) +func (actuator roleassignmentActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { + if id == "" { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, + "cannot retrieve role assignment with empty ID")) + } + + // Parse the synthetic ID back to components + parts := strings.Split(id, ":") + if len(parts) < 4 { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, + "invalid role assignment ID format")) + } + + // Build query options from parsed ID + listOpts := roles.ListAssignmentsOpts{ + Effective: ptr.To(false), + } + + // Parse key:value pairs + for i := 0; i < len(parts)-1; i += 2 { + key := parts[i] + value := parts[i+1] + + switch key { + case "role": + listOpts.RoleID = value + case "user": + listOpts.UserID = value + case "group": + listOpts.GroupID = value + case "project": + listOpts.ScopeProjectID = value + case "domain": + listOpts.ScopeDomainID = value + } + } + + // Query with exact filters - should return exactly one result + for assignment, err := range actuator.osClient.ListRoleAssignments(ctx, listOpts) { + if err != nil { + return nil, progress.WrapError(err) + } + return assignment, nil + } + + // Not found + return nil, 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, _ := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, &resourceSpec.RoleRef, "Role", + func(dep *orcv1alpha1.Role) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + if role == nil { + return nil, false // Not ready + } + roleID = ptr.Deref(role.Status.ID, "") + + // Actor dependency (user XOR group) + if resourceSpec.UserRef != nil { + user, _ := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.UserRef, "User", + func(dep *orcv1alpha1.User) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + if user == nil { + return nil, false // Not ready + } + userID = ptr.Deref(user.Status.ID, "") + } else { + group, _ := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.GroupRef, "Group", + func(dep *orcv1alpha1.Group) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + if group == nil { + return nil, false // Not ready + } + groupID = ptr.Deref(group.Status.ID, "") + } + + // Scope dependency (project XOR domain) + if resourceSpec.ProjectRef != nil { + project, _ := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.ProjectRef, "Project", + func(dep *orcv1alpha1.Project) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + if project == nil { + return nil, false // Not ready + } + projectID = ptr.Deref(project.Status.ID, "") + } else { + domain, _ := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.DomainRef, "Domain", + func(dep *orcv1alpha1.Domain) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + if domain == nil { + return nil, false // Not ready + } + domainID = ptr.Deref(domain.Status.ID, "") + } + + // Build query - only set fields that have values + listOpts := roles.ListAssignmentsOpts{ + RoleID: roleID, + Effective: ptr.To(false), // Only list direct assignments, not inherited/group-derived + } + + // Set actor (user OR group, never both) + if userID != "" { + listOpts.UserID = userID + } else if groupID != "" { + listOpts.GroupID = groupID + } + + // Set scope (project OR domain, never both) + if projectID != "" { + listOpts.ScopeProjectID = projectID + } else if domainID != "" { + listOpts.ScopeDomainID = domainID + } + + return actuator.osClient.ListRoleAssignments(ctx, listOpts), 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 + } + + // Build query - only set fields that have values (filter fields are optional) + listOpts := roles.ListAssignmentsOpts{ + Effective: ptr.To(false), // Only list direct assignments, not inherited/group-derived + } + + 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 actuator.osClient.ListRoleAssignments(ctx, listOpts), 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 + listOpts := roles.ListAssignmentsOpts{ + RoleID: roleID, + Effective: ptr.To(false), // Only list direct assignments, not inherited/group-derived + } + + // Set actor (user OR group, never both) + if userID != "" { + listOpts.UserID = userID + } else if groupID != "" { + listOpts.GroupID = groupID + } + + // Set scope (project OR domain, never both) + if projectID != "" { + listOpts.ScopeProjectID = projectID + } else if domainID != "" { + listOpts.ScopeDomainID = domainID + } + + // Get the first matching assignment to return + for assignment, err := range actuator.osClient.ListRoleAssignments(ctx, listOpts) { + if err != nil { + return nil, progress.WrapError(err) + } + return assignment, 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")) +} + +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)) +} + +type roleassignmentHelperFactory struct{} + +var _ helperFactory = roleassignmentHelperFactory{} + +func newActuator(ctx context.Context, orcObject *orcv1alpha1.RoleAssignment, controller interfaces.ResourceController) (roleassignmentActuator, progress.ReconcileStatus) { + log := ctrl.LoggerFrom(ctx) + + // Ensure credential secrets exist and have our finalizer + _, reconcileStatus := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { + return roleassignmentActuator{}, reconcileStatus + } + + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), 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: controller.GetK8sClient(), + }, nil +} + +func (roleassignmentHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { + return roleassignmentAdapter{obj} +} + +func (roleassignmentHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} + +func (roleassignmentHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} diff --git a/internal/controllers/roleassignment/actuator_test.go b/internal/controllers/roleassignment/actuator_test.go new file mode 100644 index 000000000..c37d536c8 --- /dev/null +++ b/internal/controllers/roleassignment/actuator_test.go @@ -0,0 +1,20 @@ +/* +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 + +// NOTE: RoleAssignment is fully immutable, so there are no update functions to test. +// Tests for creation and deletion logic should be added as KUTTL E2E tests in the tests/ directory. diff --git a/internal/controllers/roleassignment/controller.go b/internal/controllers/roleassignment/controller.go new file mode 100644 index 000000000..631ea1236 --- /dev/null +++ b/internal/controllers/roleassignment/controller.go @@ -0,0 +1,282 @@ +/* +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/controllers/generic/reconciler" + "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 + } + + r := reconciler.NewController(controllerName, mgr.GetClient(), c.scopeFactory, roleassignmentHelperFactory{}, roleassignmentStatusWriter{}) + return builder.Complete(&r) +} diff --git a/internal/controllers/roleassignment/status.go b/internal/controllers/roleassignment/status.go new file mode 100644 index 000000000..c7587b320 --- /dev/null +++ b/internal/controllers/roleassignment/status.go @@ -0,0 +1,84 @@ +/* +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 we just check if osResource exists. +func (roleassignmentStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.RoleAssignment, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) { + if osResource == nil { + // Check if we have any status IDs set (indicates we may have created it but can't find it) + 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 + } + return metav1.ConditionTrue, nil +} + +// ApplyResourceStatus extracts the role assignment details and applies them to status. +func (roleassignmentStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { + resourceStatus := orcapplyconfigv1alpha1.RoleAssignmentResourceStatus() + + // Extract role ID + if osResource.Role.ID != "" { + resourceStatus.WithRoleID(osResource.Role.ID) + } + + // Extract actor ID (user XOR group) + if osResource.User.ID != "" { + resourceStatus.WithUserID(osResource.User.ID) + } + if osResource.Group.ID != "" { + resourceStatus.WithGroupID(osResource.Group.ID) + } + + // Extract scope ID (project XOR domain) + 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-minimal/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-assert.yaml new file mode 100644 index 000000000..65c71f85d --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-assert.yaml @@ -0,0 +1,76 @@ +--- +# Assert Role is available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Role +metadata: + name: roleassignment-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-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-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-minimal +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-minimal + ref: roleassignment +assertAll: + # Verify synthetic ID is set (format: role::user::project:) + - celExpr: "roleassignment.status.id != ''" + - celExpr: "roleassignment.status.id.startsWith('role:')" + # Verify all component IDs are populated + - 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-minimal/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-create-resource.yaml new file mode 100644 index 000000000..4032195dd --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-create-resource.yaml @@ -0,0 +1,54 @@ +--- +# Create a test role +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Role +metadata: + name: roleassignment-test-role +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-test-role +--- +# Create a test user +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: roleassignment-test-user +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-test-user +--- +# Create a test project +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: roleassignment-test-project +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-test-project +--- +# Create the role assignment +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: roleassignment-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + roleRef: roleassignment-test-role + userRef: roleassignment-test-user + projectRef: roleassignment-test-project diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-secret.yaml new file mode 100644 index 000000000..99d9cec3d --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/00-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: openstack-clouds +type: Opaque +stringData: + clouds.yaml: | + clouds: + openstack-admin: + auth: + auth_url: ${E2E_OS_AUTH_URL} + project_name: ${E2E_OS_PROJECT_NAME} + username: ${E2E_OS_USERNAME} + password: ${E2E_OS_PASSWORD} + domain_name: ${E2E_OS_DOMAIN_NAME} + user_domain_name: ${E2E_OS_USER_DOMAIN_NAME} + project_domain_name: ${E2E_OS_PROJECT_DOMAIN_NAME} + region_name: ${E2E_OS_REGION_NAME} diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-minimal/01-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/01-assert.yaml new file mode 100644 index 000000000..c034cffb2 --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/01-assert.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +# Verify RoleAssignment is deleted +- script: "! kubectl get roleassignment roleassignment-create-minimal --namespace $NAMESPACE" + skipLogOutput: true +--- +# Verify dependencies still exist (deletion guard should keep them) +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Role +metadata: + name: roleassignment-test-role +status: + conditions: + - type: Available + status: "True" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: roleassignment-test-user +status: + conditions: + - type: Available + status: "True" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: roleassignment-test-project +status: + conditions: + - type: Available + status: "True" diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-minimal/01-delete-roleassignment.yaml b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/01-delete-roleassignment.yaml new file mode 100644 index 000000000..1f02da2fc --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/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-minimal diff --git a/internal/controllers/roleassignment/tests/roleassignment-create-minimal/README.md b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/README.md new file mode 100644 index 000000000..3b92d0da2 --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-create-minimal/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..ea04127cd --- /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: WaitingForDependency 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..99d9cec3d --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/00-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: openstack-clouds +type: Opaque +stringData: + clouds.yaml: | + clouds: + openstack-admin: + auth: + auth_url: ${E2E_OS_AUTH_URL} + project_name: ${E2E_OS_PROJECT_NAME} + username: ${E2E_OS_USERNAME} + password: ${E2E_OS_PASSWORD} + domain_name: ${E2E_OS_DOMAIN_NAME} + user_domain_name: ${E2E_OS_USER_DOMAIN_NAME} + project_domain_name: ${E2E_OS_PROJECT_DOMAIN_NAME} + region_name: ${E2E_OS_REGION_NAME} 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..7521a48ca --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/01-assert.yaml @@ -0,0 +1,64 @@ +--- +# Verify dependencies are Available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Role +metadata: + name: roleassignment-dep-role +status: + conditions: + - type: Available + status: "True" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: User +metadata: + name: roleassignment-dep-user +status: + conditions: + - type: Available + status: "True" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: roleassignment-dep-project +status: + conditions: + - type: Available + status: "True" +--- +# 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..806667ee9 --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-assert.yaml @@ -0,0 +1,21 @@ +--- +# 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" +--- +# Verify RoleAssignment still Available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: roleassignment-dependency +status: + conditions: + - type: Available + status: "True" 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..612feaecf --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-dependency/02-delete-dependencies.yaml @@ -0,0 +1,8 @@ +--- +# Try to delete a dependency (should be blocked by finalizer) +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: roleassignment-dep-project 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-group-domain/00-assert.yaml b/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-assert.yaml new file mode 100644 index 000000000..b25ed3e6b --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-assert.yaml @@ -0,0 +1,35 @@ +--- +# Assert RoleAssignment is available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: roleassignment-group-domain +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +# Validate status has group and domain IDs (not user and project) +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: RoleAssignment + name: roleassignment-group-domain + ref: roleassignment +assertAll: + # Verify synthetic ID format for group+domain + - celExpr: "roleassignment.status.id != ''" + - celExpr: "roleassignment.status.id.contains(':group:')" + - celExpr: "roleassignment.status.id.contains(':domain:')" + # Verify component IDs + - celExpr: "roleassignment.status.resource.roleID != ''" + - celExpr: "roleassignment.status.resource.groupID != ''" + - celExpr: "roleassignment.status.resource.domainID != ''" + # Verify user and project are NOT set + - 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-group-domain/00-create-resource.yaml b/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-create-resource.yaml new file mode 100644 index 000000000..c8261b43f --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-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-role +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-gd-role +--- +# Create a test group +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Group +metadata: + name: roleassignment-gd-group +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-gd-group +--- +# Create a test domain +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: roleassignment-gd-domain +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + name: roleassignment-gd-domain +--- +# Create role assignment (group on domain) +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: RoleAssignment +metadata: + name: roleassignment-group-domain +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + roleRef: roleassignment-gd-role + groupRef: roleassignment-gd-group + domainRef: roleassignment-gd-domain diff --git a/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-secret.yaml b/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-secret.yaml new file mode 100644 index 000000000..99d9cec3d --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-group-domain/00-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: openstack-clouds +type: Opaque +stringData: + clouds.yaml: | + clouds: + openstack-admin: + auth: + auth_url: ${E2E_OS_AUTH_URL} + project_name: ${E2E_OS_PROJECT_NAME} + username: ${E2E_OS_USERNAME} + password: ${E2E_OS_PASSWORD} + domain_name: ${E2E_OS_DOMAIN_NAME} + user_domain_name: ${E2E_OS_USER_DOMAIN_NAME} + project_domain_name: ${E2E_OS_PROJECT_DOMAIN_NAME} + region_name: ${E2E_OS_REGION_NAME} diff --git a/internal/controllers/roleassignment/tests/roleassignment-group-domain/README.md b/internal/controllers/roleassignment/tests/roleassignment-group-domain/README.md new file mode 100644 index 000000000..0174b48c5 --- /dev/null +++ b/internal/controllers/roleassignment/tests/roleassignment-group-domain/README.md @@ -0,0 +1,11 @@ +# Test RoleAssignment with Group and Domain + +## Step 00 + +Create a RoleAssignment that assigns a role to a group on a domain (instead of user on project). + +Verify the XOR validation works and the assignment is created correctly. + +## Reference + +https://k-orc.cloud/development/writing-tests/ diff --git a/internal/controllers/roleassignment/zz_generated.adapter.go b/internal/controllers/roleassignment/zz_generated.adapter.go new file mode 100644 index 000000000..53f478a76 --- /dev/null +++ b/internal/controllers/roleassignment/zz_generated.adapter.go @@ -0,0 +1,78 @@ +// 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 f.Status.ID +} + +func (f adapterT) GetResourceSpec() *resourceSpecT { + return f.Spec.Resource +} + +func (f adapterT) GetImportID() *string { + if f.Spec.Import == nil { + return nil + } + return f.Spec.Import.ID +} + +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..364d27bcf --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentimport.go @@ -0,0 +1,48 @@ +/* +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 { + ID *string `json:"id,omitempty"` + Filter *RoleAssignmentFilterApplyConfiguration `json:"filter,omitempty"` +} + +// RoleAssignmentImportApplyConfiguration constructs a declarative configuration of the RoleAssignmentImport type for use with +// apply. +func RoleAssignmentImport() *RoleAssignmentImportApplyConfiguration { + return &RoleAssignmentImportApplyConfiguration{} +} + +// WithID sets the ID 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 ID field is set to the value of the last call. +func (b *RoleAssignmentImportApplyConfiguration) WithID(value string) *RoleAssignmentImportApplyConfiguration { + b.ID = &value + return b +} + +// 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..1bb4a0d56 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/roleassignmentstatus.go @@ -0,0 +1,66 @@ +/* +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"` + ID *string `json:"id,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 +} + +// WithID sets the ID 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 ID field is set to the value of the last call. +func (b *RoleAssignmentStatusApplyConfiguration) WithID(value string) *RoleAssignmentStatusApplyConfiguration { + b.ID = &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 *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 419e61894..34eda1487 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -2067,6 +2067,126 @@ 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: id + type: + scalar: string +- 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: id + type: + scalar: string + - 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 618c6c9b7..6cd89eefd 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -264,6 +264,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..7a40f65a2 --- /dev/null +++ b/test/apivalidations/roleassignment_test.go @@ -0,0 +1,176 @@ +/* +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" + roleassignmentID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" +) + +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") +} + +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().WithID(roleassignmentID) +} + +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 roleRef", func(ctx context.Context) { + obj := roleassignmentStub(namespace) + patch := baseRoleAssignmentPatch(obj) + patch.Spec.WithResource(testRoleAssignmentResource(). + WithRoleRef("role-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testRoleAssignmentResource(). + WithRoleRef("role-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("roleRef is immutable"))) + }) + + It("should have immutable userRef", func(ctx context.Context) { + obj := roleassignmentStub(namespace) + patch := baseRoleAssignmentPatch(obj) + patch.Spec.WithResource(testRoleAssignmentResource(). + WithUserRef("user-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testRoleAssignmentResource(). + WithUserRef("user-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("userRef is immutable"))) + }) + + It("should have immutable groupRef", func(ctx context.Context) { + obj := roleassignmentStub(namespace) + patch := baseRoleAssignmentPatch(obj) + patch.Spec.WithResource(testRoleAssignmentResource(). + WithGroupRef("group-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testRoleAssignmentResource(). + WithGroupRef("group-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("groupRef is immutable"))) + }) + + It("should have immutable projectRef", func(ctx context.Context) { + obj := roleassignmentStub(namespace) + patch := baseRoleAssignmentPatch(obj) + patch.Spec.WithResource(testRoleAssignmentResource(). + WithProjectRef("project-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testRoleAssignmentResource(). + WithProjectRef("project-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("projectRef is immutable"))) + }) + + It("should have immutable domainRef", func(ctx context.Context) { + obj := roleassignmentStub(namespace) + patch := baseRoleAssignmentPatch(obj) + patch.Spec.WithResource(testRoleAssignmentResource(). + WithDomainRef("domain-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testRoleAssignmentResource(). + WithDomainRef("domain-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("domainRef 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 12e26692e..9522d11db 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) @@ -512,6 +513,7 @@ _Appears in:_ - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec) +- [RoleAssignmentSpec](#roleassignmentspec) - [RoleSpec](#rolespec) - [RouterSpec](#routerspec) - [SecurityGroupSpec](#securitygroupspec) @@ -2156,6 +2158,8 @@ _Appears in:_ - [PortResourceSpec](#portresourcespec) - [ProjectFilter](#projectfilter) - [ProjectResourceSpec](#projectresourcespec) +- [RoleAssignmentFilter](#roleassignmentfilter) +- [RoleAssignmentResourceSpec](#roleassignmentresourcespec) - [RoleFilter](#rolefilter) - [RoleResourceSpec](#roleresourcespec) - [RouterFilter](#routerfilter) @@ -2230,6 +2234,7 @@ _Appears in:_ - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec) +- [RoleAssignmentSpec](#roleassignmentspec) - [RoleSpec](#rolespec) - [RouterSpec](#routerspec) - [SecurityGroupSpec](#securitygroupspec) @@ -2269,6 +2274,7 @@ _Appears in:_ - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec) +- [RoleAssignmentSpec](#roleassignmentspec) - [RoleSpec](#rolespec) - [RouterSpec](#routerspec) - [SecurityGroupSpec](#securitygroupspec) @@ -3046,6 +3052,149 @@ 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:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [RoleAssignmentSpec](#roleassignmentspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `id` _string_ | id contains the unique identifier of an existing OpenStack resource. Note
that when specifying an import by ID, the resource MUST already exist.
The ORC object will enter an error state if the resource does not exist. | | Format: uuid
MaxLength: 36
Optional: \{\}
| +| `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. | | MaxProperties: 1
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: \{\}
| +| `id` _string_ | id is the unique identifier of the OpenStack resource. | | MaxLength: 1024
Optional: \{\}
| +| `resource` _[RoleAssignmentResourceStatus](#roleassignmentresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| + + #### RoleFilter