diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 2922eacdf..8c4262995 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -64,7 +64,7 @@ jobs: - name: Run e2e tests run: | - make test-e2e + E2E_KUTTL_DIR=./internal/controllers/limit/tests make test-e2e make test-examples - name: Generate logs on failure diff --git a/PROJECT b/PROJECT index 357849779..b40507be8 100644 --- a/PROJECT +++ b/PROJECT @@ -80,6 +80,14 @@ resources: kind: KeyPair path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: k-orc.cloud + group: openstack + kind: Limit + path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/README.md b/README.md index a5663e5fd..7af379b23 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ kubectl delete -f $ORC_RELEASE | group | | ✔ | ✔ | | image | ✔ | ✔ | ✔ | | keypair | | ◐ | ◐ | +| limit | | | ◐ | | network | | ◐ | ◐ | | port | | ◐ | ◐ | | project | | ◐ | ◐ | diff --git a/api/v1alpha1/limit_types.go b/api/v1alpha1/limit_types.go new file mode 100644 index 000000000..6f1f9b694 --- /dev/null +++ b/api/v1alpha1/limit_types.go @@ -0,0 +1,121 @@ +/* +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 + +// LimitResourceSpec contains the desired state of the resource. +// +kubebuilder:validation:XValidation:rule="has(self.projectRef) || has(self.domainRef)",message="either projectRef or domainRef must be specified" +// +kubebuilder:validation:XValidation:rule="!(has(self.projectRef) && has(self.domainRef))",message="projectRef and domainRef are mutually exclusive" +type LimitResourceSpec struct { + // description is a human-readable description for the resource. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` + + // serviceRef is a reference to the ORC Service which this resource is associated with. + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="serviceRef is immutable" + ServiceRef KubernetesNameRef `json:"serviceRef,omitempty"` + + // projectRef is a reference to the ORC Project which this resource is associated with. + // Either Domain ID or Project ID must be provided. + // https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="projectRef is immutable" + ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` + + // domainRef is a reference to the ORC Domain which this resource is associated with. + // Either Domain ID or Project ID must be provided. + // https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="domainRef is immutable" + DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` + + // resourceName is the name of the resource this limit is associated with. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +kubebuilder:validation:Pattern=`^[\S]+$` + // +required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceName is immutable" + ResourceName string `json:"resourceName,omitempty"` + + // resourceLimit is the override value of the limit. + // +kubebuilder:validation:Minimum=-1 + // +required + ResourceLimit int32 `json:"resourceLimit"` +} + +// LimitFilter defines an existing resource by its properties +// +kubebuilder:validation:MinProperties:=1 +type LimitFilter struct { + // description of the existing resource + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` + + // serviceRef is a reference to the ORC Service which this resource is associated with. + // +optional + ServiceRef *KubernetesNameRef `json:"serviceRef,omitempty"` + + // projectRef is a reference to the ORC Project which this resource is associated with. + // +optional + ProjectRef *KubernetesNameRef `json:"projectRef,omitempty"` + + // domainRef is a reference to the ORC Domain which this resource is associated with. + // +optional + DomainRef *KubernetesNameRef `json:"domainRef,omitempty"` + + // resourceName is the name of the resource this limit is associated with. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +kubebuilder:validation:Pattern=`^[\S]+$` + // +optional + ResourceName string `json:"resourceName,omitempty"` +} + +// LimitResourceStatus represents the observed state of the resource. +type LimitResourceStatus struct { + // description is a human-readable description for the resource. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Description string `json:"description,omitempty"` + + // serviceID is the ID of the Service to which the resource is associated. + // +kubebuilder:validation:MaxLength=1024 + // +optional + ServiceID string `json:"serviceID,omitempty"` + + // projectID is the ID of the Project to which the resource is associated. + // +kubebuilder:validation:MaxLength=1024 + // +optional + ProjectID string `json:"projectID,omitempty"` + + // domainID is the ID of the Domain to which the resource is associated. + // +kubebuilder:validation:MaxLength=1024 + // +optional + DomainID string `json:"domainID,omitempty"` + + // resourceLimit is the override value of the limit. + // +optional + ResourceLimit int32 `json:"resourceLimit,omitempty"` + + // resourceName is the name of the resource this limit is associated with. + // +kubebuilder:validation:MaxLength=1024 + // +optional + ResourceName string `json:"resourceName,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 4e7d7e3c1..4d612438f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -2896,6 +2896,242 @@ func (in *KeyPairStatus) DeepCopy() *KeyPairStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Limit) DeepCopyInto(out *Limit) { + *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 Limit. +func (in *Limit) DeepCopy() *Limit { + if in == nil { + return nil + } + out := new(Limit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Limit) 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 *LimitFilter) DeepCopyInto(out *LimitFilter) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.ServiceRef != nil { + in, out := &in.ServiceRef, &out.ServiceRef + *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 LimitFilter. +func (in *LimitFilter) DeepCopy() *LimitFilter { + if in == nil { + return nil + } + out := new(LimitFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitImport) DeepCopyInto(out *LimitImport) { + *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(LimitFilter) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitImport. +func (in *LimitImport) DeepCopy() *LimitImport { + if in == nil { + return nil + } + out := new(LimitImport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitList) DeepCopyInto(out *LimitList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Limit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitList. +func (in *LimitList) DeepCopy() *LimitList { + if in == nil { + return nil + } + out := new(LimitList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LimitList) 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 *LimitResourceSpec) DeepCopyInto(out *LimitResourceSpec) { + *out = *in + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **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 LimitResourceSpec. +func (in *LimitResourceSpec) DeepCopy() *LimitResourceSpec { + if in == nil { + return nil + } + out := new(LimitResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitResourceStatus) DeepCopyInto(out *LimitResourceStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitResourceStatus. +func (in *LimitResourceStatus) DeepCopy() *LimitResourceStatus { + if in == nil { + return nil + } + out := new(LimitResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitSpec) DeepCopyInto(out *LimitSpec) { + *out = *in + if in.Import != nil { + in, out := &in.Import, &out.Import + *out = new(LimitImport) + (*in).DeepCopyInto(*out) + } + if in.Resource != nil { + in, out := &in.Resource, &out.Resource + *out = new(LimitResourceSpec) + (*in).DeepCopyInto(*out) + } + if in.ManagedOptions != nil { + in, out := &in.ManagedOptions, &out.ManagedOptions + *out = new(ManagedOptions) + **out = **in + } + if in.ResyncPeriod != nil { + in, out := &in.ResyncPeriod, &out.ResyncPeriod + *out = new(v1.Duration) + **out = **in + } + out.CloudCredentialsRef = in.CloudCredentialsRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitSpec. +func (in *LimitSpec) DeepCopy() *LimitSpec { + if in == nil { + return nil + } + out := new(LimitSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LimitStatus) DeepCopyInto(out *LimitStatus) { + *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(LimitResourceStatus) + **out = **in + } + if in.LastSyncTime != nil { + in, out := &in.LastSyncTime, &out.LastSyncTime + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitStatus. +func (in *LimitStatus) DeepCopy() *LimitStatus { + if in == nil { + return nil + } + out := new(LimitStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedOptions) DeepCopyInto(out *ManagedOptions) { *out = *in diff --git a/api/v1alpha1/zz_generated.limit-resource.go b/api/v1alpha1/zz_generated.limit-resource.go new file mode 100644 index 000000000..5c3789c0d --- /dev/null +++ b/api/v1alpha1/zz_generated.limit-resource.go @@ -0,0 +1,197 @@ +// 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" +) + +// LimitImport specifies an existing resource which will be imported instead of +// creating a new one +// +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:MaxProperties:=1 +type LimitImport 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 *LimitFilter `json:"filter,omitempty"` +} + +// LimitSpec 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 LimitSpec struct { + // import refers to an existing OpenStack resource which will be imported instead of + // creating a new one. + // +optional + Import *LimitImport `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 *LimitResourceSpec `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"` + + // resyncPeriod defines how frequently the controller will re-reconcile + // this resource even when no changes have been detected. This overrides + // the global default resync period. The value must be a valid Go duration + // string, e.g. "10m", "1h". Set to "0s" to disable periodic resync for + // this resource. Very low values may cause excessive OpenStack API load. + // +optional + ResyncPeriod *metav1.Duration `json:"resyncPeriod,omitempty"` //nolint:kubeapilinter // metav1.Duration is appropriate for user-facing duration config + + // cloudCredentialsRef points to a secret containing OpenStack credentials + // +required + CloudCredentialsRef CloudCredentialsReference `json:"cloudCredentialsRef,omitzero"` +} + +// LimitStatus defines the observed state of an ORC resource. +type LimitStatus 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 *LimitResourceStatus `json:"resource,omitempty"` + + // lastSyncTime is the timestamp of the last successful reconciliation + // that fetched state from OpenStack. It is updated each time the + // controller successfully reads the resource state from the OpenStack + // API. + // +optional + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` +} + +var _ ObjectWithConditions = &Limit{} + +func (i *Limit) 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="Resource",type="string",JSONPath=".status.resource.resourceName",description="The name of the resource this limit is associated with" +// +kubebuilder:printcolumn:name="Limit",type="integer",JSONPath=".status.resource.resourceLimit",description="The override value of this limit" +// +kubebuilder:printcolumn:name="Policy",type="string",JSONPath=".spec.managementPolicy",description="Whether this limit is imported" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].message",description="Message describing current progress status" + +// Limit is the Schema for an ORC resource. +type Limit 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 LimitSpec `json:"spec,omitzero"` + + // status defines the observed state of the resource. + // +optional + Status LimitStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// LimitList contains a list of Limit. +type LimitList struct { + metav1.TypeMeta `json:",inline"` + + // metadata contains the list metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items contains a list of Limit. + // +required + Items []Limit `json:"items"` +} + +func (l *LimitList) GetItems() []Limit { + return l.Items +} + +func init() { + SchemeBuilder.Register(&Limit{}, &LimitList{}) +} + +func (i *Limit) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) { + if i == nil { + return nil, nil + } + + return &i.Namespace, &i.Spec.CloudCredentialsRef +} + +var _ CloudCredentialsRefProvider = &Limit{} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 6f759c2e5..8fd331df3 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -37,6 +37,7 @@ import ( "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/group" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/image" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/keypair" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/limit" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/network" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/port" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/project" @@ -143,6 +144,7 @@ func main() { group.New(scopeFactory), role.New(scopeFactory), roleassignment.New(scopeFactory), + limit.New(scopeFactory), } restConfig := ctrl.GetConfigOrDie() diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index 29d2fa1b1..4186112d5 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -130,6 +130,14 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairResourceStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairSpec": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.KeyPairStatus": schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Limit": schema_openstack_resource_controller_v2_api_v1alpha1_Limit(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitFilter": schema_openstack_resource_controller_v2_api_v1alpha1_LimitFilter(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitImport": schema_openstack_resource_controller_v2_api_v1alpha1_LimitImport(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitList": schema_openstack_resource_controller_v2_api_v1alpha1_LimitList(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_LimitResourceSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_LimitResourceStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitSpec": schema_openstack_resource_controller_v2_api_v1alpha1_LimitSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitStatus": schema_openstack_resource_controller_v2_api_v1alpha1_LimitStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions": schema_openstack_resource_controller_v2_api_v1alpha1_ManagedOptions(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Network": schema_openstack_resource_controller_v2_api_v1alpha1_Network(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.NetworkFilter": schema_openstack_resource_controller_v2_api_v1alpha1_NetworkFilter(ref), @@ -5348,6 +5356,408 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_KeyPairStatus(ref comm } } +func schema_openstack_resource_controller_v2_api_v1alpha1_Limit(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Limit 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.LimitSpec"), + }, + }, + "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.LimitStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitFilter defines an existing resource by its properties", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description of the existing resource", + Type: []string{"string"}, + Format: "", + }, + }, + "serviceRef": { + SchemaProps: spec.SchemaProps{ + Description: "serviceRef is a reference to the ORC Service which this resource is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "projectRef": { + SchemaProps: spec.SchemaProps{ + Description: "projectRef is a reference to the ORC Project which this resource is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "domainRef": { + SchemaProps: spec.SchemaProps{ + Description: "domainRef is a reference to the ORC Domain which this resource is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceName": { + SchemaProps: spec.SchemaProps{ + Description: "resourceName is the name of the resource this limit is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitImport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitImport 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.LimitFilter"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitFilter"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitList contains a list of Limit.", + 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 Limit.", + 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.Limit"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Limit", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitResourceSpec contains the desired state of the resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "serviceRef": { + SchemaProps: spec.SchemaProps{ + Description: "serviceRef is a reference to the ORC Service which this resource is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "projectRef": { + SchemaProps: spec.SchemaProps{ + Description: "projectRef is a reference to the ORC Project which this resource is associated with. Either Domain ID or Project ID must be provided. https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340", + Type: []string{"string"}, + Format: "", + }, + }, + "domainRef": { + SchemaProps: spec.SchemaProps{ + Description: "domainRef is a reference to the ORC Domain which this resource is associated with. Either Domain ID or Project ID must be provided. https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceName": { + SchemaProps: spec.SchemaProps{ + Description: "resourceName is the name of the resource this limit is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceLimit": { + SchemaProps: spec.SchemaProps{ + Description: "resourceLimit is the override value of the limit.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"serviceRef", "resourceName", "resourceLimit"}, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitResourceStatus represents the observed state of the resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "serviceID": { + SchemaProps: spec.SchemaProps{ + Description: "serviceID is the ID of the Service to which the resource is associated.", + Type: []string{"string"}, + Format: "", + }, + }, + "projectID": { + SchemaProps: spec.SchemaProps{ + Description: "projectID is the ID of the Project to which the resource is associated.", + Type: []string{"string"}, + Format: "", + }, + }, + "domainID": { + SchemaProps: spec.SchemaProps{ + Description: "domainID is the ID of the Domain to which the resource is associated.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceLimit": { + SchemaProps: spec.SchemaProps{ + Description: "resourceLimit is the override value of the limit.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "resourceName": { + SchemaProps: spec.SchemaProps{ + Description: "resourceName is the name of the resource this limit is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitSpec 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.LimitImport"), + }, + }, + "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.LimitResourceSpec"), + }, + }, + "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"), + }, + }, + "resyncPeriod": { + SchemaProps: spec.SchemaProps{ + Description: "resyncPeriod defines how frequently the controller will re-reconcile this resource even when no changes have been detected. This overrides the global default resync period. The value must be a valid Go duration string, e.g. \"10m\", \"1h\". Set to \"0s\" to disable periodic resync for this resource. Very low values may cause excessive OpenStack API load.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, + "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.LimitImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitResourceSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_LimitStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "LimitStatus 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.LimitResourceStatus"), + }, + }, + "lastSyncTime": { + SchemaProps: spec.SchemaProps{ + Description: "lastSyncTime is the timestamp of the last successful reconciliation that fetched state from OpenStack. It is updated each time the controller successfully reads the resource state from the OpenStack API.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.LimitResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_ManagedOptions(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 c5d6bc042..41f117142 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -189,6 +189,30 @@ var resources []templateFields = []templateFields{ { Name: "ApplicationCredential", }, + { + Name: "Limit", + IsNotNamed: true, + AdditionalPrintColumns: []additionalPrintColumn{ + { + Name: "Resource", + Type: "string", + JSONPath: ".status.resource.resourceName", + Description: "The name of the resource this limit is associated with", + }, + { + Name: "Limit", + Type: "integer", + JSONPath: ".status.resource.resourceLimit", + Description: "The override value of this limit", + }, + { + Name: "Policy", + Type: "string", + JSONPath: ".spec.managementPolicy", + Description: "Whether this limit is imported", + }, + }, + }, } // These resources won't be generated diff --git a/config/crd/bases/openstack.k-orc.cloud_limits.yaml b/config/crd/bases/openstack.k-orc.cloud_limits.yaml new file mode 100644 index 000000000..9ce13acb1 --- /dev/null +++ b/config/crd/bases/openstack.k-orc.cloud_limits.yaml @@ -0,0 +1,402 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: limits.openstack.k-orc.cloud +spec: + group: openstack.k-orc.cloud + names: + categories: + - openstack + kind: Limit + listKind: LimitList + plural: limits + singular: limit + 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: The name of the resource this limit is associated with + jsonPath: .status.resource.resourceName + name: Resource + type: string + - description: The override value of this limit + jsonPath: .status.resource.resourceLimit + name: Limit + type: integer + - description: Whether this limit is imported + jsonPath: .spec.managementPolicy + name: Policy + type: string + - description: Message describing current progress status + jsonPath: .status.conditions[?(@.type=='Progressing')].message + name: Message + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Limit 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: + description: + description: description of the existing resource + maxLength: 255 + minLength: 1 + type: string + domainRef: + description: domainRef is a reference to the ORC Domain which + this resource is associated with. + maxLength: 253 + minLength: 1 + type: string + projectRef: + description: projectRef is a reference to the ORC Project + which this resource is associated with. + maxLength: 253 + minLength: 1 + type: string + resourceName: + description: resourceName is the name of the resource this + limit is associated with. + maxLength: 255 + minLength: 1 + pattern: ^[\S]+$ + type: string + serviceRef: + description: serviceRef is a reference to the ORC Service + which this resource is associated with. + 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: + description: + description: description is a human-readable description for the + resource. + maxLength: 255 + minLength: 1 + type: string + domainRef: + description: |- + domainRef is a reference to the ORC Domain which this resource is associated with. + Either Domain ID or Project ID must be provided. + https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: domainRef is immutable + rule: self == oldSelf + projectRef: + description: |- + projectRef is a reference to the ORC Project which this resource is associated with. + Either Domain ID or Project ID must be provided. + https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: projectRef is immutable + rule: self == oldSelf + resourceLimit: + description: resourceLimit is the override value of the limit. + format: int32 + minimum: -1 + type: integer + resourceName: + description: resourceName is the name of the resource this limit + is associated with. + maxLength: 255 + minLength: 1 + pattern: ^[\S]+$ + type: string + x-kubernetes-validations: + - message: resourceName is immutable + rule: self == oldSelf + serviceRef: + description: serviceRef is a reference to the ORC Service which + this resource is associated with. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: serviceRef is immutable + rule: self == oldSelf + required: + - resourceLimit + - resourceName + - serviceRef + type: object + x-kubernetes-validations: + - message: either projectRef or domainRef must be specified + rule: has(self.projectRef) || has(self.domainRef) + - message: projectRef and domainRef are mutually exclusive + rule: '!(has(self.projectRef) && has(self.domainRef))' + resyncPeriod: + description: |- + resyncPeriod defines how frequently the controller will re-reconcile + this resource even when no changes have been detected. This overrides + the global default resync period. The value must be a valid Go duration + string, e.g. "10m", "1h". Set to "0s" to disable periodic resync for + this resource. Very low values may cause excessive OpenStack API load. + type: string + 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 + lastSyncTime: + description: |- + lastSyncTime is the timestamp of the last successful reconciliation + that fetched state from OpenStack. It is updated each time the + controller successfully reads the resource state from the OpenStack + API. + format: date-time + type: string + resource: + description: resource contains the observed state of the OpenStack + resource. + properties: + description: + description: description is a human-readable description for the + resource. + maxLength: 1024 + type: string + domainID: + description: domainID is the ID of the Domain to which the resource + is associated. + maxLength: 1024 + type: string + projectID: + description: projectID is the ID of the Project to which the resource + is associated. + maxLength: 1024 + type: string + resourceLimit: + description: resourceLimit is the override value of the limit. + format: int32 + type: integer + resourceName: + description: resourceName is the name of the resource this limit + is associated with. + maxLength: 1024 + type: string + serviceID: + description: serviceID is the ID of the Service to which the resource + is associated. + 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 9c133d11c..852e50d7b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -12,6 +12,7 @@ resources: - bases/openstack.k-orc.cloud_groups.yaml - bases/openstack.k-orc.cloud_images.yaml - bases/openstack.k-orc.cloud_keypairs.yaml +- bases/openstack.k-orc.cloud_limits.yaml - bases/openstack.k-orc.cloud_networks.yaml - bases/openstack.k-orc.cloud_ports.yaml - bases/openstack.k-orc.cloud_projects.yaml diff --git a/config/manifests/bases/orc.clusterserviceversion.yaml b/config/manifests/bases/orc.clusterserviceversion.yaml index 410c09b66..93f793a9e 100644 --- a/config/manifests/bases/orc.clusterserviceversion.yaml +++ b/config/manifests/bases/orc.clusterserviceversion.yaml @@ -64,6 +64,11 @@ spec: kind: KeyPair name: keypairs.openstack.k-orc.cloud version: v1alpha1 + - description: Limit is the Schema for an ORC resource. + displayName: Limit + kind: Limit + name: limits.openstack.k-orc.cloud + version: v1alpha1 - description: Network is the Schema for an ORC resource. displayName: Network kind: Network diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index a04a488e2..96176412b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -26,6 +26,7 @@ rules: - groups - images - keypairs + - limits - networks - ports - projects @@ -63,6 +64,7 @@ rules: - groups/status - images/status - keypairs/status + - limits/status - networks/status - ports/status - projects/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index ceb05e15e..cbcb54b8e 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -10,6 +10,7 @@ resources: - openstack_v1alpha1_group.yaml - openstack_v1alpha1_image.yaml - openstack_v1alpha1_keypair.yaml +- openstack_v1alpha1_limit.yaml - openstack_v1alpha1_network.yaml - openstack_v1alpha1_port.yaml - openstack_v1alpha1_project.yaml diff --git a/config/samples/openstack_v1alpha1_limit.yaml b/config/samples/openstack_v1alpha1_limit.yaml new file mode 100644 index 000000000..6f76912b0 --- /dev/null +++ b/config/samples/openstack_v1alpha1_limit.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-managed +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Nova limit servers for project demo + serviceRef: nova + projectRef: demo # Either projectRef or domainRef must be provided. + domainRef: default + resourceName: servers + resourceLimit: 10 diff --git a/internal/controllers/limit/actuator.go b/internal/controllers/limit/actuator.go new file mode 100644 index 000000000..81514bd0f --- /dev/null +++ b/internal/controllers/limit/actuator.go @@ -0,0 +1,416 @@ +/* +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 limit + +import ( + "context" + "errors" + "iter" + + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/limits" + 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/logging" + "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" +) + +var ( + errInvalidDomainRefUpdate = errors.New("limit cannot be updated with domainRef when projectRef has been used") + errInvalidProjectRefUpdate = errors.New("limit cannot be updated with projectRef when domainRef has been used") +) + +// OpenStack resource types +type ( + osResourceT = limits.Limit + + createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT] + deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT] + resourceReconciler = interfaces.ResourceReconciler[orcObjectPT, osResourceT] + helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT] +) + +type limitActuator struct { + osClient osclients.LimitClient + k8sClient client.Client +} + +var _ createResourceActuator = limitActuator{} +var _ deleteResourceActuator = limitActuator{} + +func (limitActuator) GetResourceID(osResource *osResourceT) string { + return osResource.ID +} + +func (actuator limitActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { + resource, err := actuator.osClient.GetLimit(ctx, id) + if err != nil { + return nil, progress.WrapError(err) + } + return resource, nil +} + +func (actuator limitActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) { + resourceSpec := orcObject.Spec.Resource + if resourceSpec == nil { + return nil, false + } + + var rs progress.ReconcileStatus + + svc, rs1 := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, &resourceSpec.ServiceRef, "Service", + func(dep *orcv1alpha1.Service) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + rs = rs.WithReconcileStatus(rs1) + + project, rs1 := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.ProjectRef, "Project", + func(dep *orcv1alpha1.Project) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + rs = rs.WithReconcileStatus(rs1) + + domain, rs1 := dependency.FetchDependency( + ctx, actuator.k8sClient, orcObject.Namespace, resourceSpec.DomainRef, "Domain", + func(dep *orcv1alpha1.Domain) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil + }, + ) + rs = rs.WithReconcileStatus(rs1) + + if needsReschedule, err := rs.NeedsReschedule(); needsReschedule { + if err != nil { + ctrl.LoggerFrom(ctx).Info("fetch dependency before listing limit for adoption", "error", err) + } + + return nil, false + } + + var filters []osclients.ResourceFilter[osResourceT] + + if resourceSpec.Description != nil { + filters = append(filters, func(ort *osResourceT) bool { + return ort.Description == *resourceSpec.Description + }) + } + + listOpts := limits.ListOpts{ + ServiceID: ptr.Deref(svc.Status.ID, ""), + ProjectID: ptr.Deref(project.Status.ID, ""), + DomainID: ptr.Deref(domain.Status.ID, ""), + ResourceName: resourceSpec.ResourceName, + } + + return actuator.listOSResources(ctx, filters, listOpts), true +} + +func (actuator limitActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { + var reconcileStatus progress.ReconcileStatus + + service, rs := dependency.FetchDependency[*orcv1alpha1.Service]( + ctx, actuator.k8sClient, obj.Namespace, + filter.ServiceRef, "Service", + orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(rs) + + project, rs := dependency.FetchDependency[*orcv1alpha1.Project]( + ctx, actuator.k8sClient, obj.Namespace, + filter.ProjectRef, "Project", + orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(rs) + + domain, rs := dependency.FetchDependency[*orcv1alpha1.Domain]( + ctx, actuator.k8sClient, obj.Namespace, + filter.DomainRef, "Domain", + orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(rs) + + if needsReschedule, err := reconcileStatus.NeedsReschedule(); needsReschedule { + if err != nil { + ctrl.LoggerFrom(ctx).Info("fetch dependency before listing limit for import", "error", err) + } + + return nil, reconcileStatus + } + + var filters []osclients.ResourceFilter[osResourceT] + + if filter.Description != nil { + filters = append(filters, func(ort *osResourceT) bool { + return ort.Description == *filter.Description + }) + } + + listOpts := limits.ListOpts{ + ServiceID: ptr.Deref(service.Status.ID, ""), + ProjectID: ptr.Deref(project.Status.ID, ""), + DomainID: ptr.Deref(domain.Status.ID, ""), + ResourceName: filter.ResourceName, + } + + return actuator.listOSResources(ctx, filters, listOpts), reconcileStatus +} + +func (actuator limitActuator) listOSResources(ctx context.Context, filters []osclients.ResourceFilter[osResourceT], listOpts limits.ListOptsBuilder) iter.Seq2[*limits.Limit, error] { + logger := ctrl.LoggerFrom(ctx) + logger.V(logging.Debug).Info("list option", "listOpts", listOpts) + resultLimits := actuator.osClient.ListLimits(ctx, listOpts) + + t := []struct { + l *limits.Limit + err error + }{} + + // For debugging CI issue + for l, err := range resultLimits { + logger.V(logging.Debug).Info("dump limit", "limit", l, "error", err) + t = append(t, struct { + l *limits.Limit + err error + }{ + l, err, + }) + } + + return osclients.Filter(func(yield func(*limits.Limit, error) bool) { + for _, m := range t { + if !yield(m.l, m.err) { + return + } + } + + }, filters...) +} + +func (actuator limitActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) { + logger := ctrl.LoggerFrom(ctx).WithValues("limitName", obj.Name) + 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 + + var serviceID string + service, serviceDepRS := serviceDependency.GetDependency( + ctx, actuator.k8sClient, obj, orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(serviceDepRS) + if service != nil { + serviceID = ptr.Deref(service.Status.ID, "") + } + + var projectID string + if resource.ProjectRef != nil { + project, projectDepRS := projectDependency.GetDependency( + ctx, actuator.k8sClient, obj, orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(projectDepRS) + if project != nil { + projectID = ptr.Deref(project.Status.ID, "") + } + } + + var domainID string + if resource.DomainRef != nil { + domain, domainDepRS := domainDependency.GetDependency( + ctx, actuator.k8sClient, obj, orcv1alpha1.IsAvailable, + ) + reconcileStatus = reconcileStatus.WithReconcileStatus(domainDepRS) + if domain != nil { + domainID = ptr.Deref(domain.Status.ID, "") + } + } + if needsReschedule, err := reconcileStatus.NeedsReschedule(); needsReschedule { + if err != nil { + logger.Info("fetch dependency before creating limit", "error", err) + } + + return nil, reconcileStatus + } + createOpts := limits.CreateOpts{ + Description: ptr.Deref(resource.Description, ""), + ServiceID: serviceID, + ProjectID: projectID, + DomainID: domainID, + ResourceName: resource.ResourceName, + ResourceLimit: int(resource.ResourceLimit), + } + + osResource, err := actuator.osClient.CreateLimit(ctx, limits.BatchCreateOpts{createOpts}) + if err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration creating resource: "+err.Error(), err) + } + return nil, progress.WrapError(err) + } + + logger.Info("limit created", "createOpts", createOpts) + + return osResource, nil +} + +func (actuator limitActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus { + return progress.WrapError(actuator.osClient.DeleteLimit(ctx, resource.ID)) +} + +func (actuator limitActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + resource := obj.Spec.Resource + if resource == nil { + // Should have been caught by API validation + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Update requested, but spec.resource is not set")) + } + + if err := validateUpdate(resource, osResource); err != nil { + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)) + } + + updateOpts := limits.UpdateOpts{} + + // There seems to be a bug that keystone doesn't clear the description field when receiving a PATCH request with an empty description. + // This will cause the `Progressing` condition stuck with `Resource status will be refreshed`. + // Tested with `openstack limit set --description ""` + handleDescriptionUpdate(&updateOpts, resource, osResource) + // The same issue exists with resourceLimit. Updating resourceLimit with 0 doesn't work. + handleResourceLimitUpdate(&updateOpts, resource, osResource) + + needsUpdate, err := needsUpdate(updateOpts) + if err != nil { + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err)) + } + if !needsUpdate { + log.V(logging.Debug).Info("No changes") + return nil + } + + _, err = actuator.osClient.UpdateLimit(ctx, osResource.ID, updateOpts) + + if err != nil { + if !orcerrors.IsRetryable(err) { + err = orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration updating resource: "+err.Error(), err) + } + return progress.WrapError(err) + } + + return progress.NeedsRefresh() +} + +func needsUpdate(updateOpts limits.UpdateOpts) (bool, error) { + updateOptsMap, err := updateOpts.ToLimitUpdateMap() + if err != nil { + return false, err + } + + updateMap, ok := updateOptsMap["limit"].(map[string]any) + if !ok { + updateMap = make(map[string]any) + } + + return len(updateMap) > 0, nil +} + +func handleDescriptionUpdate(updateOpts *limits.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + description := ptr.Deref(resource.Description, "") + if osResource.Description != description { + updateOpts.Description = &description + } +} + +func handleResourceLimitUpdate(updateOpts *limits.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + rl := int(resource.ResourceLimit) + if osResource.ResourceLimit != rl { + updateOpts.ResourceLimit = &rl + } +} + +func validateUpdate(resource *resourceSpecT, osResource *osResourceT) error { + if resource.DomainRef != nil && osResource.ProjectID != "" { + return errInvalidDomainRefUpdate + } + + if resource.ProjectRef != nil && osResource.DomainID != "" { + return errInvalidProjectRefUpdate + } + + return nil +} + +func (actuator limitActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { + return []resourceReconciler{ + actuator.updateResource, + }, nil +} + +type limitHelperFactory struct{} + +var _ helperFactory = limitHelperFactory{} + +func newActuator(ctx context.Context, orcObject *orcv1alpha1.Limit, controller interfaces.ResourceController) (limitActuator, 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 limitActuator{}, reconcileStatus + } + + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) + if err != nil { + return limitActuator{}, progress.WrapError(err) + } + osClient, err := clientScope.NewLimitClient() + if err != nil { + return limitActuator{}, progress.WrapError(err) + } + + return limitActuator{ + osClient: osClient, + k8sClient: controller.GetK8sClient(), + }, nil +} + +func (limitHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { + return limitAdapter{obj} +} + +func (limitHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} + +func (limitHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} diff --git a/internal/controllers/limit/actuator_test.go b/internal/controllers/limit/actuator_test.go new file mode 100644 index 000000000..52656e8fe --- /dev/null +++ b/internal/controllers/limit/actuator_test.go @@ -0,0 +1,166 @@ +/* +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 limit + +import ( + "testing" + + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/limits" + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + "k8s.io/utils/ptr" +) + +func TestNeedsUpdate(t *testing.T) { + testCases := []struct { + name string + updateOpts limits.UpdateOpts + expectChange bool + }{ + { + name: "Empty base opts", + updateOpts: limits.UpdateOpts{}, + expectChange: false, + }, + { + name: "Updated opts with description", + updateOpts: limits.UpdateOpts{Description: ptr.To("updated")}, + expectChange: true, + }, + { + name: "Updated opts with resourceLimit", + updateOpts: limits.UpdateOpts{ResourceLimit: ptr.To(-1)}, + expectChange: true, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got, _ := needsUpdate(tt.updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + } +} + +func TestHandleDescriptionUpdate(t *testing.T) { + ptrToDescription := ptr.To[string] + testCases := []struct { + name string + newValue *string + existingValue string + expectChange bool + }{ + {name: "Identical", newValue: ptrToDescription("desc"), existingValue: "desc", expectChange: false}, + {name: "Different", newValue: ptrToDescription("new-desc"), existingValue: "desc", expectChange: true}, + {name: "No value provided, existing is set", newValue: nil, existingValue: "desc", expectChange: true}, + {name: "No value provided, existing is empty", newValue: nil, existingValue: "", expectChange: false}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + resource := &orcv1alpha1.LimitResourceSpec{Description: tt.newValue} + osResource := &osResourceT{Description: tt.existingValue} + + updateOpts := limits.UpdateOpts{} + handleDescriptionUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + + } +} + +func TestHandleResourceLimitUpdate(t *testing.T) { + testCases := []struct { + name string + newValue int32 + existingValue int + expectChange bool + }{ + {name: "Identical", newValue: -1, existingValue: -1, expectChange: false}, + {name: "Different", newValue: -1, existingValue: 10, expectChange: true}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + resource := &orcv1alpha1.LimitResourceSpec{ResourceLimit: tt.newValue} + osResource := &osResourceT{ResourceLimit: tt.existingValue} + + updateOpts := limits.UpdateOpts{} + handleResourceLimitUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + } +} + +func TestValidateUpdate(t *testing.T) { + testCases := []struct { + name string + resource *orcv1alpha1.LimitResourceSpec + osResource *osResourceT + expectError error + }{ + { + name: "Update domainRef", + resource: &orcv1alpha1.LimitResourceSpec{ + DomainRef: ptr.To(orcv1alpha1.KubernetesNameRef("domain-ref")), + }, + osResource: &osResourceT{ + ProjectID: "12312312312", + }, + expectError: errInvalidDomainRefUpdate, + }, + { + name: "Update projectRef", + resource: &orcv1alpha1.LimitResourceSpec{ + ProjectRef: ptr.To(orcv1alpha1.KubernetesNameRef("project-ref")), + }, + osResource: &osResourceT{ + DomainID: "default", + }, + expectError: errInvalidProjectRefUpdate, + }, + { + name: "Normal update", + resource: &orcv1alpha1.LimitResourceSpec{ + ProjectRef: ptr.To(orcv1alpha1.KubernetesNameRef("project-ref")), + }, + osResource: &osResourceT{ + ProjectID: "12312312312", + }, + expectError: nil, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := validateUpdate(tt.resource, tt.osResource) + + if got != tt.expectError { + t.Errorf("Expected error: %v, got: %v", tt.expectError, got) + } + }) + } +} diff --git a/internal/controllers/limit/controller.go b/internal/controllers/limit/controller.go new file mode 100644 index 000000000..a26b263e6 --- /dev/null +++ b/internal/controllers/limit/controller.go @@ -0,0 +1,204 @@ +/* +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 limit + +import ( + "context" + "errors" + "time" + + 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 = "limit" + +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=limits,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=limits/status,verbs=get;update;patch + +type limitReconcilerConstructor struct { + scopeFactory scope.Factory + defaultResyncPeriod time.Duration +} + +func New(scopeFactory scope.Factory) interfaces.Controller { + return &limitReconcilerConstructor{scopeFactory: scopeFactory} +} + +func (limitReconcilerConstructor) GetName() string { + return controllerName +} + +func (c *limitReconcilerConstructor) SetDefaultResyncPeriod(d time.Duration) { + c.defaultResyncPeriod = d +} + +var serviceDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LimitList, *orcv1alpha1.Service]( + "spec.resource.serviceRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.Spec.Resource + if resource == nil { + return nil + } + return []string{string(resource.ServiceRef)} + }, + finalizer, externalObjectFieldOwner, +) + +var projectDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LimitList, *orcv1alpha1.Project]( + "spec.resource.projectRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.Spec.Resource + if resource == nil || resource.ProjectRef == nil { + return nil + } + return []string{string(*resource.ProjectRef)} + }, + finalizer, externalObjectFieldOwner, +) + +var domainDependency = dependency.NewDeletionGuardDependency[*orcv1alpha1.LimitList, *orcv1alpha1.Domain]( + "spec.resource.domainRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.Spec.Resource + if resource == nil || resource.DomainRef == nil { + return nil + } + return []string{string(*resource.DomainRef)} + }, + finalizer, externalObjectFieldOwner, +) + +var serviceImportDependency = dependency.NewDependency[*orcv1alpha1.LimitList, *orcv1alpha1.Service]( + "spec.import.filter.serviceRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.Spec.Import + if resource == nil || resource.Filter == nil || resource.Filter.ServiceRef == nil { + return nil + } + return []string{string(*resource.Filter.ServiceRef)} + }, +) + +var projectImportDependency = dependency.NewDependency[*orcv1alpha1.LimitList, *orcv1alpha1.Project]( + "spec.import.filter.projectRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.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.LimitList, *orcv1alpha1.Domain]( + "spec.import.filter.domainRef", + func(limit *orcv1alpha1.Limit) []string { + resource := limit.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 *limitReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) + k8sClient := mgr.GetClient() + + serviceWatchEventHandler, err := serviceDependency.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 + } + + serviceImportWatchEventHandler, err := serviceImportDependency.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.Service{}, serviceWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Service{})), + ). + 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.Service{}, serviceImportWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.Service{})), + ). + // 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.Limit{}) + + if err := errors.Join( + serviceDependency.AddToManager(ctx, mgr), + projectDependency.AddToManager(ctx, mgr), + domainDependency.AddToManager(ctx, mgr), + serviceImportDependency.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, limitHelperFactory{}, limitStatusWriter{}, c.defaultResyncPeriod) + return builder.Complete(&r) +} diff --git a/internal/controllers/limit/status.go b/internal/controllers/limit/status.go new file mode 100644 index 000000000..0d48c71e0 --- /dev/null +++ b/internal/controllers/limit/status.go @@ -0,0 +1,76 @@ +/* +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 limit + +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 limitStatusWriter struct{} + +type objectApplyT = orcapplyconfigv1alpha1.LimitApplyConfiguration +type statusApplyT = orcapplyconfigv1alpha1.LimitStatusApplyConfiguration + +var _ interfaces.ResourceStatusWriter[*orcv1alpha1.Limit, *osResourceT, *objectApplyT, *statusApplyT] = limitStatusWriter{} + +func (limitStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT { + return orcapplyconfigv1alpha1.Limit(name, namespace) +} + +func (limitStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.Limit, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) { + if osResource == nil { + if orcObject.Status.ID == nil { + return metav1.ConditionFalse, nil + } else { + return metav1.ConditionUnknown, nil + } + } + return metav1.ConditionTrue, nil +} + +func (limitStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { + resourceStatus := orcapplyconfigv1alpha1.LimitResourceStatus(). + WithResourceLimit(int32(osResource.ResourceLimit)) + + if osResource.Description != "" { + resourceStatus.WithDescription(osResource.Description) + } + + if osResource.ResourceName != "" { + resourceStatus.WithResourceName(osResource.ResourceName) + } + + if osResource.ServiceID != "" { + resourceStatus.WithServiceID(osResource.ServiceID) + } + + if osResource.ProjectID != "" { + resourceStatus.WithProjectID(osResource.ProjectID) + } + + if osResource.DomainID != "" { + resourceStatus.WithDomainID(osResource.DomainID) + } + + statusApply.WithResource(resourceStatus) +} diff --git a/internal/controllers/limit/tests/limit-create-full/00-assert.yaml b/internal/controllers/limit/tests/limit-create-full/00-assert.yaml new file mode 100644 index 000000000..edcab9759 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-full/00-assert.yaml @@ -0,0 +1,116 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-create-full +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-create-full +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-full-1 +status: + resource: + description: Limit from "create full" test + resourceName: limit-create-full-1 + resourceLimit: 50 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-full-2 +status: + resource: + description: Limit from "create full" test + resourceName: limit-create-full-2 + resourceLimit: 10 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-create-full-1 && + openstack registered limit list --resource-name limit-create-full-2 +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-create-full-1 + ref: limit1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-create-full-2 + ref: limit2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Service + name: service-limit-create-full + ref: service + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: project-limit-create-full + ref: project + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Domain + name: domain-limit-create-full + ref: domain +assertAll: + - celExpr: "limit1.status.id != ''" + - celExpr: "limit2.status.id != ''" + - celExpr: "limit1.status.resource.serviceID == service.status.id" + - celExpr: "limit2.status.resource.serviceID == service.status.id" + - celExpr: "limit1.status.resource.projectID == project.status.id" + - celExpr: "limit2.status.resource.domainID == domain.status.id" diff --git a/internal/controllers/limit/tests/limit-create-full/00-create-resource.yaml b/internal/controllers/limit/tests/limit-create-full/00-create-resource.yaml new file mode 100644 index 000000000..6ededc7d3 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-full/00-create-resource.yaml @@ -0,0 +1,79 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-create-full + labels: + test-case: limit-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-create-full" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-create-full + labels: + test-case: limit-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-create-full +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-create-full + labels: + test-case: limit-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +# Use projectRef +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-full-1 + labels: + test-case: limit-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit from "create full" test + serviceRef: service-limit-create-full + projectRef: project-limit-create-full + resourceName: limit-create-full-1 + resourceLimit: 50 +--- +# Use domainRef +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-full-2 + labels: + test-case: limit-create-full +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit from "create full" test + serviceRef: service-limit-create-full + domainRef: domain-limit-create-full + resourceName: limit-create-full-2 + resourceLimit: 10 diff --git a/internal/controllers/limit/tests/limit-create-full/00-secret.yaml b/internal/controllers/limit/tests/limit-create-full/00-secret.yaml new file mode 100644 index 000000000..11be38a4e --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-full/00-secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true diff --git a/internal/controllers/limit/tests/limit-create-full/00-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-create-full/00-setup-cleanup.yaml new file mode 100644 index 000000000..9a94d37c9 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-full/00-setup-cleanup.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-create-full + - name: REGISTER_LIMITS + value: service-limit-create-full/limit-create-full-1/100,service-limit-create-full/limit-create-full-2/100 + - name: DOMAINS + value: domain-limit-create-full + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-create-full/README.md b/internal/controllers/limit/tests/limit-create-full/README.md new file mode 100644 index 000000000..69cf7f8ed --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-full/README.md @@ -0,0 +1,7 @@ +# Create Limits with all the options + +## Step 00 + +Create Limits using all available fields, and verify that the observed state corresponds to the spec. + +Because `projectRef` and `domainRef` are mutual exclusive, two limits are created for test. diff --git a/internal/controllers/limit/tests/limit-create-minimal/00-assert.yaml b/internal/controllers/limit/tests/limit-create-minimal/00-assert.yaml new file mode 100644 index 000000000..b600c76bb --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/00-assert.yaml @@ -0,0 +1,118 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-create-minimal +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-create-minimal +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-create-minimal +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-minimal-1 +status: + resource: + resourceName: limit-create-minimal-1 + resourceLimit: 50 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-minimal-2 +status: + resource: + resourceName: limit-create-minimal-2 + resourceLimit: 10 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-create-minimal-1 && + openstack registered limit list --resource-name limit-create-minimal-2 +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-create-minimal-1 + ref: limit1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-create-minimal-2 + ref: limit2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Service + name: service-limit-create-minimal + ref: service + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: project-limit-create-minimal + ref: project + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Domain + name: domain-limit-create-minimal + ref: domain +assertAll: + - celExpr: "limit1.status.id != ''" + - celExpr: "!has(limit1.status.resource.description) || limit1.status.resource.description == ''" + - celExpr: "limit2.status.id != ''" + - celExpr: "!has(limit2.status.resource.description) || limit2.status.resource.description == ''" + - celExpr: "limit1.status.resource.serviceID == service.status.id" + - celExpr: "limit2.status.resource.serviceID == service.status.id" + - celExpr: "limit1.status.resource.projectID == project.status.id" + - celExpr: "limit2.status.resource.domainID == domain.status.id" diff --git a/internal/controllers/limit/tests/limit-create-minimal/00-create-resource.yaml b/internal/controllers/limit/tests/limit-create-minimal/00-create-resource.yaml new file mode 100644 index 000000000..f6fd63464 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/00-create-resource.yaml @@ -0,0 +1,77 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-create-minimal + labels: + test-case: limit-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-create-minimal" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-create-minimal + labels: + test-case: limit-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-create-minimal +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-create-minimal + labels: + test-case: limit-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +# Use projectRef +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-minimal-1 + labels: + test-case: limit-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + serviceRef: service-limit-create-minimal + projectRef: project-limit-create-minimal + resourceName: limit-create-minimal-1 + resourceLimit: 50 +--- +# Use domainRef +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-create-minimal-2 + labels: + test-case: limit-create-minimal +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + serviceRef: service-limit-create-minimal + domainRef: domain-limit-create-minimal + resourceName: limit-create-minimal-2 + resourceLimit: 10 diff --git a/internal/controllers/limit/tests/limit-create-minimal/00-secret.yaml b/internal/controllers/limit/tests/limit-create-minimal/00-secret.yaml new file mode 100644 index 000000000..11be38a4e --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/00-secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true diff --git a/internal/controllers/limit/tests/limit-create-minimal/00-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-create-minimal/00-setup-cleanup.yaml new file mode 100644 index 000000000..6e9f85959 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/00-setup-cleanup.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-create-minimal + - name: REGISTER_LIMITS + value: service-limit-create-minimal/limit-create-minimal-1/100,service-limit-create-minimal/limit-create-minimal-2/100 + - name: DOMAINS + value: domain-limit-create-minimal + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-create-minimal/01-assert.yaml b/internal/controllers/limit/tests/limit-create-minimal/01-assert.yaml new file mode 100644 index 000000000..d1b3792dc --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/01-assert.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: v1 + kind: Secret + name: openstack-clouds + ref: secret + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Service + name: service-limit-create-minimal + ref: service + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: project-limit-create-minimal + ref: project + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Domain + name: domain-limit-create-minimal + ref: domain +assertAll: + - celExpr: "secret.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in secret.metadata.finalizers" + - celExpr: "service.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in service.metadata.finalizers" + - celExpr: "project.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in project.metadata.finalizers" + - celExpr: "domain.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in domain.metadata.finalizers" diff --git a/internal/controllers/limit/tests/limit-create-minimal/01-delete-resources.yaml b/internal/controllers/limit/tests/limit-create-minimal/01-delete-resources.yaml new file mode 100644 index 000000000..7a171582b --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/01-delete-resources.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We expect the deletion to hang due to the finalizer, so use --wait=false + - command: kubectl delete secret openstack-clouds --wait=false + namespaced: true + - command: kubectl delete service.openstack.k-orc.cloud,project,domain -l test-case=limit-create-minimal --wait=false + namespaced: true diff --git a/internal/controllers/limit/tests/limit-create-minimal/README.md b/internal/controllers/limit/tests/limit-create-minimal/README.md new file mode 100644 index 000000000..bec2f2d29 --- /dev/null +++ b/internal/controllers/limit/tests/limit-create-minimal/README.md @@ -0,0 +1,9 @@ +# Create Limits with the minimum options + +## Step 00 + +Create two minimal Limits that set only the required fields (serviceRef/projectRef/resourceName/resourceLimit and serviceRef/domainRef/resourceName/resourceLimit), and verify that the observed state corresponds to the spec. `projectRef` and `domainRef` are mutual exclusive, so the only field that is really optional is the description field. + +## Step 01 + +Try deleting the secret and other dependencies ensure that they are not deleted thanks to the finalizer. diff --git a/internal/controllers/limit/tests/limit-dependency/00-assert.yaml b/internal/controllers/limit/tests/limit-dependency/00-assert.yaml new file mode 100644 index 000000000..ce3dcf71d --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/00-assert.yaml @@ -0,0 +1,55 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-1 +status: + conditions: + - type: Available + message: |- + Waiting for Service/service-limit-dependency to be created + Waiting for Project/project-limit-dependency to be created + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for Service/service-limit-dependency to be created + Waiting for Project/project-limit-dependency to be created + status: "True" + reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-2 +status: + conditions: + - type: Available + message: |- + Waiting for Service/service-limit-dependency to be created + Waiting for Domain/domain-limit-dependency to be created + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for Service/service-limit-dependency to be created + Waiting for Domain/domain-limit-dependency to be created + status: "True" + reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-3 +status: + conditions: + - type: Available + message: |- + Waiting for Secret/limit-dependency to be created + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for Secret/limit-dependency to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/limit/tests/limit-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/limit/tests/limit-dependency/00-create-resources-missing-deps.yaml new file mode 100644 index 000000000..f0af4725b --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/00-create-resources-missing-deps.yaml @@ -0,0 +1,54 @@ +--- +# No project and service available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-1 + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + serviceRef: service-limit-dependency + projectRef: project-limit-dependency + resourceName: limit-dependency-1 + resourceLimit: 50 +--- +# No domain and service available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-2 + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + serviceRef: service-limit-dependency + domainRef: domain-limit-dependency + resourceName: limit-dependency-2 + resourceLimit: 10 +--- +# No secret, service and domain available +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-3 + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: limit-dependency + managementPolicy: managed + resource: + serviceRef: service-limit-dependency + domainRef: domain-limit-dependency + resourceName: limit-dependency-3 + resourceLimit: 5 diff --git a/internal/controllers/limit/tests/limit-dependency/00-secret.yaml b/internal/controllers/limit/tests/limit-dependency/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/limit/tests/limit-dependency/01-assert.yaml b/internal/controllers/limit/tests/limit-dependency/01-assert.yaml new file mode 100644 index 000000000..0db242380 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/01-assert.yaml @@ -0,0 +1,102 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-1 +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-2 +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-dependency-3 +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-dependency-1 && + openstack registered limit list --resource-name limit-dependency-2 && + openstack registered limit list --resource-name limit-dependency-3 diff --git a/internal/controllers/limit/tests/limit-dependency/01-create-dependencies.yaml b/internal/controllers/limit/tests/limit-dependency/01-create-dependencies.yaml new file mode 100644 index 000000000..7b6bada28 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/01-create-dependencies.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic limit-dependency --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-dependency + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-dependency" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-dependency + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-dependency +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-dependency + labels: + test-case: limit-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} diff --git a/internal/controllers/limit/tests/limit-dependency/01-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-dependency/01-setup-cleanup.yaml new file mode 100644 index 000000000..e64093bda --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/01-setup-cleanup.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-dependency + - name: REGISTER_LIMITS + value: service-limit-dependency/limit-dependency-1/100,service-limit-dependency/limit-dependency-2/100,service-limit-dependency/limit-dependency-3/100 + - name: DOMAINS + value: domain-limit-dependency + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-dependency/02-assert.yaml b/internal/controllers/limit/tests/limit-dependency/02-assert.yaml new file mode 100644 index 000000000..8ebf5f754 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/02-assert.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-dependency +status: + resource: + enabled: false diff --git a/internal/controllers/limit/tests/limit-dependency/02-disable-domain.yaml b/internal/controllers/limit/tests/limit-dependency/02-disable-domain.yaml new file mode 100644 index 000000000..1ca9a5f9a --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/02-disable-domain.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl patch domain domain-limit-dependency -p '{"spec":{"resource":{"enabled":false}}}' --type=merge + namespaced: true diff --git a/internal/controllers/limit/tests/limit-dependency/03-assert.yaml b/internal/controllers/limit/tests/limit-dependency/03-assert.yaml new file mode 100644 index 000000000..3173eed73 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/03-assert.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Service + name: service-limit-dependency + ref: service + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: project-limit-dependency + ref: project + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Domain + name: domain-limit-dependency + ref: domain + - apiVersion: v1 + kind: Secret + name: limit-dependency + ref: secret +assertAll: + - celExpr: "service.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in service.metadata.finalizers" + - celExpr: "project.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in project.metadata.finalizers" + - celExpr: "domain.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in domain.metadata.finalizers" + - celExpr: "secret.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/limit' in secret.metadata.finalizers" diff --git a/internal/controllers/limit/tests/limit-dependency/03-delete-dependencies.yaml b/internal/controllers/limit/tests/limit-dependency/03-delete-dependencies.yaml new file mode 100644 index 000000000..68870a152 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/03-delete-dependencies.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We expect the deletion to hang due to the finalizer, so use --wait=false + - command: kubectl delete service.openstack.k-orc.cloud,project,domain -l test-case=limit-dependency --wait=false + namespaced: true + - command: kubectl delete secret limit-dependency --wait=false + namespaced: true diff --git a/internal/controllers/limit/tests/limit-dependency/04-assert.yaml b/internal/controllers/limit/tests/limit-dependency/04-assert.yaml new file mode 100644 index 000000000..38b2770fe --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/04-assert.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + # Dependencies that were prevented deletion before should now be gone + - script: "! kubectl get service.openstack.k-orc.cloud service-limit-dependency --namespace $NAMESPACE" + skipLogOutput: true + - script: "! kubectl get project.openstack.k-orc.cloud project-limit-dependency --namespace $NAMESPACE" + skipLogOutput: true + - script: "! kubectl get domain.openstack.k-orc.cloud domain-limit-dependency --namespace $NAMESPACE" + skipLogOutput: true + - script: "! kubectl get secret limit-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/limit/tests/limit-dependency/04-delete-resources.yaml b/internal/controllers/limit/tests/limit-dependency/04-delete-resources.yaml new file mode 100644 index 000000000..71595ce0f --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/04-delete-resources.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-dependency-1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-dependency-2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-dependency-3 diff --git a/internal/controllers/limit/tests/limit-dependency/README.md b/internal/controllers/limit/tests/limit-dependency/README.md new file mode 100644 index 000000000..58a4efa03 --- /dev/null +++ b/internal/controllers/limit/tests/limit-dependency/README.md @@ -0,0 +1,21 @@ +# Creation and deletion dependencies + +## Step 00 + +Create Limits referencing non-existing resources. Each Limit is dependent on some other non-existing resource(project/domain/secret). Verify that the Limits are waiting for the needed resources to be created externally. + +## Step 01 + +Create the missing dependencies and verify all the Limits are available. + +## Step 02 + +Disable domain to facilitate the deletion. + +## Step 03 + +Delete all the dependencies and check that ORC prevents deletion since there is still a resource that depends on them. + +## Step 04 + +Delete the Limits and validate that all resources are gone. diff --git a/internal/controllers/limit/tests/limit-import-dependency/00-assert.yaml b/internal/controllers/limit/tests/limit-import-dependency/00-assert.yaml new file mode 100644 index 000000000..b0758f632 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/00-assert.yaml @@ -0,0 +1,47 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-dependency-imported +status: + conditions: + - type: Available + message: |- + Waiting for OpenStack resource to be created externally + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for OpenStack resource to be created externally + status: "True" + reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-dependency +status: + conditions: + - type: Available + message: |- + Waiting for Project/project-limit-import-dependency-imported to be ready + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for Project/project-limit-import-dependency-imported to be ready + status: "True" + reason: Progressing diff --git a/internal/controllers/limit/tests/limit-import-dependency/00-import-resource.yaml b/internal/controllers/limit/tests/limit-import-dependency/00-import-resource.yaml new file mode 100644 index 000000000..3a5935f49 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/00-import-resource.yaml @@ -0,0 +1,46 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import-dependency + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-import-dependency" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-dependency-imported + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + name: project-limit-import-dependency +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-dependency + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + serviceRef: service-limit-import-dependency + projectRef: project-limit-import-dependency-imported + resourceName: limit-import-dependency diff --git a/internal/controllers/limit/tests/limit-import-dependency/00-secret.yaml b/internal/controllers/limit/tests/limit-import-dependency/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/limit/tests/limit-import-dependency/01-assert.yaml b/internal/controllers/limit/tests/limit-import-dependency/01-assert.yaml new file mode 100644 index 000000000..f8a8c3231 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/01-assert.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-dependency +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-dependency-external +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-dependency +status: + resource: + resourceName: limit-import-dependency + resourceLimit: 50 + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-import-dependency +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import-dependency + ref: limit1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import-dependency-external + ref: limit2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Project + name: project-limit-import-dependency + ref: project +assertAll: + - celExpr: "limit1.status.id == limit2.status.id" + - celExpr: "limit1.status.resource.projectID == project.status.id" diff --git a/internal/controllers/limit/tests/limit-import-dependency/01-create-resource.yaml b/internal/controllers/limit/tests/limit-import-dependency/01-create-resource.yaml new file mode 100644 index 000000000..e95ebcea5 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/01-create-resource.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import-dependency + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-dependency + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-import-dependency +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-dependency-external + labels: + test-case: limit-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + serviceRef: service-limit-import-dependency + projectRef: project-limit-import-dependency + resourceName: limit-import-dependency + resourceLimit: 50 diff --git a/internal/controllers/limit/tests/limit-import-dependency/01-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-import-dependency/01-setup-cleanup.yaml new file mode 100644 index 000000000..126cdb01f --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/01-setup-cleanup.yaml @@ -0,0 +1,76 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create configmap common --from-file=../utils + namespaced: true +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-import-dependency + - name: REGISTER_LIMITS + value: service-limit-import-dependency/limit-import-dependency/100 + - name: DOMAINS + value: domain-limit-import-dependency + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-import-dependency/02-assert.yaml b/internal/controllers/limit/tests/limit-import-dependency/02-assert.yaml new file mode 100644 index 000000000..23cf7be57 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/02-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: "! kubectl get project.openstack.k-orc.cloud project-limit-import-dependency-imported --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/limit/tests/limit-import-dependency/02-delete-import-dependencies.yaml b/internal/controllers/limit/tests/limit-import-dependency/02-delete-import-dependencies.yaml new file mode 100644 index 000000000..662b7e010 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/02-delete-import-dependencies.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We should be able to delete the import dependencies + - command: kubectl delete project.openstack.k-orc.cloud project-limit-import-dependency-imported + namespaced: true diff --git a/internal/controllers/limit/tests/limit-import-dependency/03-assert.yaml b/internal/controllers/limit/tests/limit-import-dependency/03-assert.yaml new file mode 100644 index 000000000..60b888c8b --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/03-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: "! kubectl get limit.openstack.k-orc.cloud limit-import-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/limit/tests/limit-import-dependency/03-delete-resource.yaml b/internal/controllers/limit/tests/limit-import-dependency/03-delete-resource.yaml new file mode 100644 index 000000000..d620fe2ef --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/03-delete-resource.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import-dependency diff --git a/internal/controllers/limit/tests/limit-import-dependency/README.md b/internal/controllers/limit/tests/limit-import-dependency/README.md new file mode 100644 index 000000000..3ae461cfa --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-dependency/README.md @@ -0,0 +1,21 @@ +# Check dependency handling for imported Limit + +## Step 00 + +Import a Limit that references other imported resource(projectRef). The referenced imported resource(project) has no matching resources yet. +Verify the Limit is waiting for the dependency to be ready. + +## Step 01 + +Create the referenced resource(projectRef) and a Limit matching the import filters. + +Verify that the observed status on the imported Limit corresponds to the spec of the created Limit. + +## Step 02 + +Delete the referenced imported resources and check that ORC does not prevent deletion. The OpenStack resources still exist because they +were imported resources and we only deleted the ORC representation of it. + +## Step 03 + +Delete the Limit and validate that all resources are gone. diff --git a/internal/controllers/limit/tests/limit-import-error/00-assert.yaml b/internal/controllers/limit/tests/limit-import-error/00-assert.yaml new file mode 100644 index 000000000..69ddc5942 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/00-assert.yaml @@ -0,0 +1,98 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import-error +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import-error +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-error-1 +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-error-2 +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error-1 +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error-2 +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-import-error diff --git a/internal/controllers/limit/tests/limit-import-error/00-create-resources.yaml b/internal/controllers/limit/tests/limit-import-error/00-create-resources.yaml new file mode 100644 index 000000000..dc6922fc3 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/00-create-resources.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import-error + labels: + test-case: limit-import-error +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-import-error" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import-error + labels: + test-case: limit-import-error +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-error-1 + labels: + test-case: limit-import-error +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-import-error +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import-error-2 + labels: + test-case: limit-import-error +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-import-error +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error-1 +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit from "import error" test for project-limit-import-error-1 + serviceRef: service-limit-import-error + projectRef: project-limit-import-error-1 + resourceName: limit-import-error + resourceLimit: 50 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error-2 +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit from "import error" test for project-limit-import-error-2 + serviceRef: service-limit-import-error + projectRef: project-limit-import-error-2 + resourceName: limit-import-error + resourceLimit: 60 diff --git a/internal/controllers/limit/tests/limit-import-error/00-secret.yaml b/internal/controllers/limit/tests/limit-import-error/00-secret.yaml new file mode 100644 index 000000000..11be38a4e --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/00-secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true diff --git a/internal/controllers/limit/tests/limit-import-error/00-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-import-error/00-setup-cleanup.yaml new file mode 100644 index 000000000..2ff526a47 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/00-setup-cleanup.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-import-error + - name: REGISTER_LIMITS + value: service-limit-import-error/limit-import-error/100 + - name: DOMAINS + value: domain-limit-import-error + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-import-error/01-assert.yaml b/internal/controllers/limit/tests/limit-import-error/01-assert.yaml new file mode 100644 index 000000000..ce718a593 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/01-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error +status: + conditions: + - type: Available + message: found more than one matching OpenStack resource during import + status: "False" + reason: InvalidConfiguration + - type: Progressing + message: found more than one matching OpenStack resource during import + status: "False" + reason: InvalidConfiguration diff --git a/internal/controllers/limit/tests/limit-import-error/01-import-resource.yaml b/internal/controllers/limit/tests/limit-import-error/01-import-resource.yaml new file mode 100644 index 000000000..ee0185907 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/01-import-resource.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-error +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + resourceName: limit-import-error diff --git a/internal/controllers/limit/tests/limit-import-error/README.md b/internal/controllers/limit/tests/limit-import-error/README.md new file mode 100644 index 000000000..435200dac --- /dev/null +++ b/internal/controllers/limit/tests/limit-import-error/README.md @@ -0,0 +1,9 @@ +# Import Limit with more than one matching resources + +## Step 00 + +Create two managed Limits with the same resource name. + +## Step 01 + +Ensure that an imported Limit with a filter matching the resource returns an error. diff --git a/internal/controllers/limit/tests/limit-import/00-assert.yaml b/internal/controllers/limit/tests/limit-import/00-assert.yaml new file mode 100644 index 000000000..27d6dacd5 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/00-assert.yaml @@ -0,0 +1,71 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import +status: + conditions: + - type: Available + message: Waiting for OpenStack resource to be created externally + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for OpenStack resource to be created externally + status: "True" + reason: Progressing +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-import && + openstack registered limit list --resource-name limit-import-nonexistent-resource diff --git a/internal/controllers/limit/tests/limit-import/00-import-resource.yaml b/internal/controllers/limit/tests/limit-import/00-import-resource.yaml new file mode 100644 index 000000000..148c1eab9 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/00-import-resource.yaml @@ -0,0 +1,58 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-import + labels: + test-case: limit-import +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-import" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-import + labels: + test-case: limit-import +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-import +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-import + labels: + test-case: limit-import +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import + labels: + test-case: limit-import +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + serviceRef: service-limit-import + projectRef: project-limit-import + resourceName: limit-import diff --git a/internal/controllers/limit/tests/limit-import/00-secret.yaml b/internal/controllers/limit/tests/limit-import/00-secret.yaml new file mode 100644 index 000000000..11be38a4e --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/00-secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true diff --git a/internal/controllers/limit/tests/limit-import/00-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-import/00-setup-cleanup.yaml new file mode 100644 index 000000000..d0e47346a --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/00-setup-cleanup.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-import + - name: REGISTER_LIMITS + value: service-limit-import/limit-import/100,service-limit-import/limit-import-nonexistent-resource/100 + - name: DOMAINS + value: domain-limit-import + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-import/01-assert.yaml b/internal/controllers/limit/tests/limit-import/01-assert.yaml new file mode 100644 index 000000000..9c46ed88b --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/01-assert.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-external-not-this-one +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success + resource: + description: Limit limit-import-external from "limit-import" test + resourceName: limit-import-nonexistent-resource + resourceLimit: 55 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import +status: + conditions: + - type: Available + message: Waiting for OpenStack resource to be created externally + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for OpenStack resource to be created externally + status: "True" + reason: Progressing diff --git a/internal/controllers/limit/tests/limit-import/01-create-trap-resource.yaml b/internal/controllers/limit/tests/limit-import/01-create-trap-resource.yaml new file mode 100644 index 000000000..fd172dc0b --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/01-create-trap-resource.yaml @@ -0,0 +1,19 @@ +--- +# This `limit-import-external-not-this-one` resource serves two purposes: +# - ensure that we can successfully create another resource which name is a substring of it (i.e. it's not being adopted) +# - ensure that importing a resource which name is a substring of it will not pick this one. +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-external-not-this-one +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit limit-import-external from "limit-import" test + serviceRef: service-limit-import + projectRef: project-limit-import + resourceName: limit-import-nonexistent-resource + resourceLimit: 55 diff --git a/internal/controllers/limit/tests/limit-import/02-assert.yaml b/internal/controllers/limit/tests/limit-import/02-assert.yaml new file mode 100644 index 000000000..ca4d5fab2 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/02-assert.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import + ref: limit0 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import-external + ref: limit1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-import-external-not-this-one + ref: limit2 +assertAll: + - celExpr: "limit0.status.id == limit1.status.id" + - celExpr: "limit1.status.id != limit2.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success + resource: + description: Limit limit-import-external from "limit-import" test + resourceName: limit-import + resourceLimit: 42 diff --git a/internal/controllers/limit/tests/limit-import/02-create-resource.yaml b/internal/controllers/limit/tests/limit-import/02-create-resource.yaml new file mode 100644 index 000000000..96138f305 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/02-create-resource.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-import-external +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: Limit limit-import-external from "limit-import" test + serviceRef: service-limit-import + projectRef: project-limit-import + resourceName: limit-import + resourceLimit: 42 diff --git a/internal/controllers/limit/tests/limit-import/README.md b/internal/controllers/limit/tests/limit-import/README.md new file mode 100644 index 000000000..1efe51eb7 --- /dev/null +++ b/internal/controllers/limit/tests/limit-import/README.md @@ -0,0 +1,14 @@ +# Import Limit + +## Step 00 + +Import a limit that matches all fields in the filter, and verify it is waiting for the external resource to be created. + +## Step 01 + +Create a limit whose resource name is a superstring of the one specified in the import filter, otherwise matching the filter, and verify that it's not being imported. + +## Step 02 + +Create a limit matching the filter and verify that the observed status on the imported limit corresponds to the spec of the created limit. +Also, confirm that it does not adopt any limit whose name is a superstring of its own. diff --git a/internal/controllers/limit/tests/limit-update/00-assert.yaml b/internal/controllers/limit/tests/limit-update/00-assert.yaml new file mode 100644 index 000000000..10eacfb0a --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/00-assert.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown +status: + readyReplicas: 1 +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-update +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-update +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-update +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +status: + resource: + description: some description + resourceName: limit-update + resourceLimit: 50 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +collectors: + - selector: app=setup-teardown +commands: + - script: |- + cd $(dirname ${E2E_KUTTL_OSCLOUDS}) + export OS_CLOUD=openstack-admin + openstack registered limit list --resource-name limit-update +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: Limit + name: limit-update + ref: limit +assertAll: + - celExpr: "limit.status.id != ''" diff --git a/internal/controllers/limit/tests/limit-update/00-dependency-resource.yaml b/internal/controllers/limit/tests/limit-update/00-dependency-resource.yaml new file mode 100644 index 000000000..b5a42b6b2 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/00-dependency-resource.yaml @@ -0,0 +1,41 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Service +metadata: + name: service-limit-update + labels: + test-case: limit-update +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + type: "service-type-limit-update" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Project +metadata: + name: project-limit-update + labels: + test-case: limit-update +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + domainRef: domain-limit-update +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Domain +metadata: + name: domain-limit-update + labels: + test-case: limit-update +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: {} diff --git a/internal/controllers/limit/tests/limit-update/00-minimal-resource.yaml b/internal/controllers/limit/tests/limit-update/00-minimal-resource.yaml new file mode 100644 index 000000000..c07a509fa --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/00-minimal-resource.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update + labels: + test-case: limit-update +spec: + cloudCredentialsRef: + cloudName: openstack-admin + secretName: openstack-clouds + managementPolicy: managed + resource: + description: some description + serviceRef: service-limit-update + projectRef: project-limit-update + resourceName: limit-update + resourceLimit: 50 diff --git a/internal/controllers/limit/tests/limit-update/00-secret.yaml b/internal/controllers/limit/tests/limit-update/00-secret.yaml new file mode 100644 index 000000000..11be38a4e --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/00-secret.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true + - command: kubectl create configmap common --from-file=../utils + namespaced: true diff --git a/internal/controllers/limit/tests/limit-update/00-setup-cleanup.yaml b/internal/controllers/limit/tests/limit-update/00-setup-cleanup.yaml new file mode 100644 index 000000000..a57098454 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/00-setup-cleanup.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: setup-teardown + labels: + app: setup-teardown +spec: + replicas: 1 + selector: + matchLabels: + app: setup-teardown + template: + metadata: + labels: + app: setup-teardown + spec: + containers: + - name: setup-teardown + image: ghcr.io/chenwng/orc-helper:latest + command: + - /orc-helper + volumeMounts: + - name: openstack-clouds + mountPath: /etc/openstack + readOnly: true + - name: common + mountPath: /app + readOnly: true + - name: log-volume + mountPath: /log + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: READINESS_FILE + value: /helper/ready + - name: OS_CLOUD + value: openstack-admin + - name: TEST_CASE + value: limit-update + - name: REGISTER_LIMITS + value: service-limit-update/limit-update/100 + - name: DOMAINS + value: domain-limit-update + readinessProbe: + exec: + command: + - test + - -f + - /helper/ready + initialDelaySeconds: 3 + periodSeconds: 1 + terminationGracePeriodSeconds: 60 + volumes: + - name: openstack-clouds + secret: + secretName: openstack-clouds + - name: common + configMap: + name: common + - name: log-volume + hostPath: + path: /log + type: DirectoryOrCreate diff --git a/internal/controllers/limit/tests/limit-update/01-assert.yaml b/internal/controllers/limit/tests/limit-update/01-assert.yaml new file mode 100644 index 000000000..c33e6938c --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/01-assert.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +status: + resource: + description: description updated + resourceLimit: 49 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/limit/tests/limit-update/01-updated-resource.yaml b/internal/controllers/limit/tests/limit-update/01-updated-resource.yaml new file mode 100644 index 000000000..930b4a5e9 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/01-updated-resource.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +spec: + resource: + description: description updated + resourceLimit: 49 diff --git a/internal/controllers/limit/tests/limit-update/02-assert.yaml b/internal/controllers/limit/tests/limit-update/02-assert.yaml new file mode 100644 index 000000000..0c38503dc --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/02-assert.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +status: + resource: + description: some description + resourceLimit: 50 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/limit/tests/limit-update/02-reverted-resource.yaml b/internal/controllers/limit/tests/limit-update/02-reverted-resource.yaml new file mode 100644 index 000000000..2c6c253ff --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/02-reverted-resource.yaml @@ -0,0 +1,7 @@ +# NOTE: kuttl only does patch updates, which means we can't delete a field. +# We have to use a kubectl apply command instead. +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl replace -f 00-minimal-resource.yaml + namespaced: true diff --git a/internal/controllers/limit/tests/limit-update/03-assert.yaml b/internal/controllers/limit/tests/limit-update/03-assert.yaml new file mode 100644 index 000000000..e3f113e44 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/03-assert.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +status: + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: InvalidConfiguration + message: "invalid configuration updating resource: limit cannot be updated with domainRef when projectRef has been used" diff --git a/internal/controllers/limit/tests/limit-update/03-updated-resource.yaml b/internal/controllers/limit/tests/limit-update/03-updated-resource.yaml new file mode 100644 index 000000000..41dbf09f9 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/03-updated-resource.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: Limit +metadata: + name: limit-update +spec: + resource: + projectRef: + domainRef: domain-limit-update diff --git a/internal/controllers/limit/tests/limit-update/README.md b/internal/controllers/limit/tests/limit-update/README.md new file mode 100644 index 000000000..5313e1ac6 --- /dev/null +++ b/internal/controllers/limit/tests/limit-update/README.md @@ -0,0 +1,17 @@ +# Update Limit + +## Step 00 + +Create a Limit using only mandatory fields. + +## Step 01 + +Update all mutable fields. + +## Step 02 + +Revert the resource to its original value and verify that the resulting object matches its state when first created. + +## Step 03 + +Replacing `projectRef` with `domainRef` should fail. diff --git a/internal/controllers/limit/tests/utils/hooks.go b/internal/controllers/limit/tests/utils/hooks.go new file mode 100644 index 000000000..0ce6b9e0d --- /dev/null +++ b/internal/controllers/limit/tests/utils/hooks.go @@ -0,0 +1,41 @@ +package main + +import ( + "context" + "time" +) + +var registeredLimitIds []string + +func onStart(ctx context.Context) (retErr error) { + c, err := newKeystoneClient(ctx) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*30) + defer cancel() + + if err := setupRegisteredLimits(ctx, c); err != nil { + return err + } + + return nil +} + +func onStop(ctx context.Context) error { + c, err := newKeystoneClient(ctx) + if err != nil { + return err + } + + cleanUpRegisteredLimits(ctx, c) + cleanUpDomains(ctx, c) + + return nil +} + +func init() { + onStartFunc = onStart + onStopFunc = onStop +} diff --git a/internal/controllers/limit/tests/utils/main.go b/internal/controllers/limit/tests/utils/main.go new file mode 100644 index 000000000..c1361461b --- /dev/null +++ b/internal/controllers/limit/tests/utils/main.go @@ -0,0 +1,256 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "os" + "os/signal" + "path" + "strings" + "syscall" + "time" + + "k8s.io/utils/ptr" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/domains" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/registeredlimits" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/services" + "github.com/gophercloud/utils/v2/openstack/clientconfig" +) + +var ( + onStartFunc func(context.Context) error + onStopFunc func(context.Context) error +) + +func main() { + logPath := os.Getenv("LOG_PATH") + if logPath == "" { + logPath = "/log" + } + + namespace := os.Getenv("POD_NAMESPACE") + if namespace == "" { + namespace = "default" + } + + podName := os.Getenv("POD_NAME") + if podName == "" { + podName = "default" + } + + testCase := os.Getenv("TEST_CASE") + if testCase == "" { + testCase = "unknown-case" + } + + logPath = path.Join(logPath, testCase, namespace) + + if err := os.MkdirAll(logPath, 0755); err != nil { + panic(err) + } + + f, err := os.OpenFile(path.Join(logPath, "log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + defer f.Close() //nolint:errcheck + + slog.SetDefault(slog.New(slog.NewTextHandler(io.MultiWriter(os.Stderr, f), nil)).With("podName", podName)) + + slog.Info("starting") + ctx, cancel := signal.NotifyContext(context.TODO(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + slog.Info("environment variables ", "env", os.Environ()) + + var failed bool + if onStartFunc != nil { + slog.Info("running onStart...") + if err := onStartFunc(ctx); err != nil { + slog.Error("executing onStart", "error", err) + failed = true + } + } + + if !failed { + slog.Info("onStart completed") + + readinessFile := os.Getenv("READINESS_FILE") + if readinessFile == "" { + slog.Error("READINESS_FILE not found") + os.Exit(1) + } + + if err := os.Mkdir(path.Dir(readinessFile), 0755); err != nil { + slog.Error("create path for readiness file", "readinessFile", readinessFile, "error", err) + os.Exit(1) + } + + if _, err := os.Create(readinessFile); err != nil { + slog.Error("create readiness file", "readinessFile", readinessFile, "error", err) + os.Exit(1) + } + } + + <-ctx.Done() + + slog.Info("signal received") + + ctx1, cancel1 := context.WithTimeout(context.TODO(), time.Second*30) + defer cancel1() + + if onStopFunc != nil { + slog.Info("running onStop...") + if err := onStopFunc(ctx1); err != nil { + slog.Error("executing onStop", "error", err) + os.Exit(1) + } + } + + slog.Info("onStop completed") + + slog.Info("exiting") +} + +func newKeystoneClient(ctx context.Context) (*gophercloud.ServiceClient, error) { + cloudName := os.Getenv("OS_CLOUD") + if cloudName == "" { + cloudName = "openstack-admin" + } + + opts := &clientconfig.ClientOpts{ + Cloud: cloudName, + } + + p, err := clientconfig.AuthenticatedClient(ctx, opts) + if err != nil { + return nil, fmt.Errorf("get authenticated client: %w", err) + } + + return openstack.NewIdentityV3(p, gophercloud.EndpointOpts{}) +} + +type errorDetail struct { + Title string `json:"title,omitempty"` + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +type errorDetailWrapper struct { + Error errorDetail `json:"error"` +} + +func extractErrorDetail(err error) (*errorDetail, bool) { + gcErr := &gophercloud.ErrUnexpectedResponseCode{} + if !errors.As(err, gcErr) { + return nil, false + } + detail := &errorDetailWrapper{} + if err := json.Unmarshal(gcErr.Body, detail); err != nil { + return nil, false + } + + return &detail.Error, true +} + +func retry(ctx context.Context, delay time.Duration, f func(ctx context.Context) (bool, error)) error { + for { + ok, err := f(ctx) + if err != nil { + return err + } + + if !ok { + return nil + } + + time.Sleep(delay) + } +} + +func deleteRegisteredLimit(ctx context.Context, c *gophercloud.ServiceClient, id string) error { + slog := slog.With("id", id) + return retry(ctx, time.Second, func(ctx context.Context) (bool, error) { + if err := registeredlimits.Delete(ctx, c, id).ExtractErr(); err != nil { + detail, ok := extractErrorDetail(err) + if !ok { + return false, err + } + + if detail.Code == 404 { + slog.Error("registered limit has been deleted") + return false, nil + } + if detail.Code == 403 && strings.Contains(detail.Message, "because there are project limits associated with it") { + slog.Info("there are project limits associated with it") + return true, nil + } + + return false, err + } + + return false, nil + }) +} + +func disableDomain(ctx context.Context, c *gophercloud.ServiceClient, domainName string) { + allPages, err := domains.List(c, domains.ListOpts{Name: domainName}).AllPages(ctx) + if err != nil { + slog.Error("find domain", "domainName", domainName, "error", err) + return + } + + dms, err := domains.ExtractDomains(allPages) + if err != nil { + slog.Error("extract domain result", "error", err) + return + } + + for _, d := range dms { + if d.Enabled { + slog := slog.With("domainName", d.Name, "domainId", d.ID) + if _, err := domains.Update(ctx, c, d.ID, domains.UpdateOpts{Enabled: ptr.To(false)}).Extract(); err != nil { + slog.Error("set domain to disabled", "error", err) + } else { + slog.Info("disabled domain") + } + } + } +} + +func getServiceId(ctx context.Context, c *gophercloud.ServiceClient, svcName string) (string, error) { + var svc *services.Service + + if err := retry(ctx, time.Second, func(ctx context.Context) (bool, error) { + allPages, err := services.List(c, services.ListOpts{Name: svcName}).AllPages(ctx) + if err != nil { + return false, fmt.Errorf("list service: %w", err) + } + svcs, err := services.ExtractServices(allPages) + if err != nil { + return false, fmt.Errorf("extract services: %w", err) + } + + switch len(svcs) { + case 0: + slog.Info("no service found", "serviceName", svcName) + return true, nil + case 1: + svc = &svcs[0] + return false, nil + default: + return false, fmt.Errorf("more than one service found") + } + }); err != nil { + return "", err + } + + return svc.ID, nil +} diff --git a/internal/controllers/limit/tests/utils/utils.go b/internal/controllers/limit/tests/utils/utils.go new file mode 100644 index 000000000..7206d8802 --- /dev/null +++ b/internal/controllers/limit/tests/utils/utils.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/registeredlimits" +) + +func createRegisteredLimit(ctx context.Context, c *gophercloud.ServiceClient, svcName, resourceName string, limit int) (string, error) { + svcId, err := getServiceId(ctx, c, svcName) + if err != nil { + return "", fmt.Errorf("get service id with name: %w", err) + } + + limits, err := registeredlimits.BatchCreate(ctx, c, registeredlimits.BatchCreateOpts{ + { + ServiceID: svcId, + ResourceName: resourceName, + DefaultLimit: limit, + }, + }).Extract() + + if err != nil { + return "", err + } + + if len(limits) != 1 { + return "", fmt.Errorf("unexpected creation response") + } + + slog.Info("registered limit created", "serviceName", svcName, "resourceName", resourceName, "limit", limit) + + return limits[0].ID, nil +} + +func setupRegisteredLimits(ctx context.Context, c *gophercloud.ServiceClient) error { + // Format of REGISTER_LIMITS: + // ServiceName/ResourceName/Limit,ServiceName/ResourceName/Limit... + limitEnv := os.Getenv("REGISTER_LIMITS") + if limitEnv == "" { + return nil + } + + for res := range strings.SplitSeq(limitEnv, ",") { + if res == "" { + continue + } + + splits := strings.Split(res, "/") + if len(splits) != 3 { + return fmt.Errorf("unexpect resource request %q", res) + } + + svcName, resName, limitStr := splits[0], splits[1], splits[2] + limit, err := strconv.Atoi(limitStr) + if err != nil { + return fmt.Errorf("invalid limit %s: %w", limitStr, err) + } + + id, err := createRegisteredLimit(ctx, c, svcName, resName, limit) + if err != nil { + return fmt.Errorf("create registered limit: %w", err) + } + + registeredLimitIds = append(registeredLimitIds, id) + } + + slog.Info("created registered limits", "ids", registeredLimitIds) + + return nil +} + +func cleanUpRegisteredLimits(ctx context.Context, c *gophercloud.ServiceClient) { + for _, id := range registeredLimitIds { + if err := deleteRegisteredLimit(ctx, c, id); err != nil { + slog.Error("delete registered limit", "error", err, "id", id) + continue + } + + slog.Info("deleted registered limit", "id", id) + } +} + +func cleanUpDomains(ctx context.Context, c *gophercloud.ServiceClient) { + domainEnv := os.Getenv("DOMAINS") + if domainEnv == "" { + return + } + + for d := range strings.SplitSeq(domainEnv, ",") { + disableDomain(ctx, c, d) + } +} diff --git a/internal/controllers/limit/zz_generated.adapter.go b/internal/controllers/limit/zz_generated.adapter.go new file mode 100644 index 000000000..2a06969f2 --- /dev/null +++ b/internal/controllers/limit/zz_generated.adapter.go @@ -0,0 +1,88 @@ +// 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 limit + +import ( + 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" +) + +// Fundamental types +type ( + orcObjectT = orcv1alpha1.Limit + orcObjectListT = orcv1alpha1.LimitList + resourceSpecT = orcv1alpha1.LimitResourceSpec + filterT = orcv1alpha1.LimitFilter +) + +// Derived types +type ( + orcObjectPT = *orcObjectT + adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT] + adapterT = limitAdapter +) + +type limitAdapter struct { + *orcv1alpha1.Limit +} + +var _ adapterI = &adapterT{} + +func (f adapterT) GetObject() orcObjectPT { + return f.Limit +} + +func (f adapterT) GetManagementPolicy() orcv1alpha1.ManagementPolicy { + return f.Spec.ManagementPolicy +} + +func (f adapterT) GetManagedOptions() *orcv1alpha1.ManagedOptions { + return f.Spec.ManagedOptions +} + +func (f adapterT) GetResyncPeriod() *metav1.Duration { + return f.Spec.ResyncPeriod +} + +func (f adapterT) GetLastSyncTime() *metav1.Time { + return f.Status.LastSyncTime +} + +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/limit/zz_generated.controller.go b/internal/controllers/limit/zz_generated.controller.go new file mode 100644 index 000000000..ff48940a6 --- /dev/null +++ b/internal/controllers/limit/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 limit + +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/limit.go b/internal/osclients/limit.go new file mode 100644 index 000000000..3aebe8eaa --- /dev/null +++ b/internal/osclients/limit.go @@ -0,0 +1,113 @@ +/* +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/limits" + "github.com/gophercloud/utils/v2/openstack/clientconfig" +) + +type LimitClient interface { + ListLimits(ctx context.Context, listOpts limits.ListOptsBuilder) iter.Seq2[*limits.Limit, error] + CreateLimit(ctx context.Context, opts limits.BatchCreateOptsBuilder) (*limits.Limit, error) + DeleteLimit(ctx context.Context, resourceID string) error + GetLimit(ctx context.Context, resourceID string) (*limits.Limit, error) + UpdateLimit(ctx context.Context, id string, opts limits.UpdateOptsBuilder) (*limits.Limit, error) +} + +type limitClient struct{ client *gophercloud.ServiceClient } + +// NewLimitClient returns a new OpenStack client. +func NewLimitClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (LimitClient, 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 limit service client: %v", err) + } + + return &limitClient{client}, nil +} + +func (c limitClient) ListLimits(ctx context.Context, listOpts limits.ListOptsBuilder) iter.Seq2[*limits.Limit, error] { + pager := limits.List(c.client, listOpts) + return func(yield func(*limits.Limit, error) bool) { + _ = pager.EachPage(ctx, yieldPage(limits.ExtractLimits, yield)) + } +} + +func (c limitClient) CreateLimit(ctx context.Context, opts limits.BatchCreateOptsBuilder) (*limits.Limit, error) { + limits, err := limits.BatchCreate(ctx, c.client, opts).Extract() + if err != nil { + return nil, fmt.Errorf("create limit: %w", err) + } + + if len(limits) != 1 { + return nil, fmt.Errorf("unexpected limit creation result %d", len(limits)) + } + + return &limits[0], nil +} + +func (c limitClient) DeleteLimit(ctx context.Context, resourceID string) error { + return limits.Delete(ctx, c.client, resourceID).ExtractErr() +} + +func (c limitClient) GetLimit(ctx context.Context, resourceID string) (*limits.Limit, error) { + return limits.Get(ctx, c.client, resourceID).Extract() +} + +func (c limitClient) UpdateLimit(ctx context.Context, id string, opts limits.UpdateOptsBuilder) (*limits.Limit, error) { + return limits.Update(ctx, c.client, id, opts).Extract() +} + +type limitErrorClient struct{ error } + +// NewLimitErrorClient returns a LimitClient in which every method returns the given error. +func NewLimitErrorClient(e error) LimitClient { + return limitErrorClient{e} +} + +func (e limitErrorClient) ListLimits(_ context.Context, _ limits.ListOptsBuilder) iter.Seq2[*limits.Limit, error] { + return func(yield func(*limits.Limit, error) bool) { + yield(nil, e.error) + } +} + +func (e limitErrorClient) CreateLimit(_ context.Context, _ limits.BatchCreateOptsBuilder) (*limits.Limit, error) { + return nil, e.error +} + +func (e limitErrorClient) DeleteLimit(_ context.Context, _ string) error { + return e.error +} + +func (e limitErrorClient) GetLimit(_ context.Context, _ string) (*limits.Limit, error) { + return nil, e.error +} + +func (e limitErrorClient) UpdateLimit(_ context.Context, _ string, _ limits.UpdateOptsBuilder) (*limits.Limit, error) { + return nil, e.error +} diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go index 466a548e6..7bafbb2a8 100644 --- a/internal/osclients/mock/doc.go +++ b/internal/osclients/mock/doc.go @@ -53,6 +53,9 @@ import ( //go:generate mockgen -package mock -destination=keypair.go -source=../keypair.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock KeyPairClient //go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt keypair.go > _keypair.go && mv _keypair.go keypair.go" +//go:generate mockgen -package mock -destination=limit.go -source=../limit.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock LimitClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt limit.go > _limit.go && mv _limit.go limit.go" + //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" diff --git a/internal/osclients/mock/limit.go b/internal/osclients/mock/limit.go new file mode 100644 index 000000000..bd74746ea --- /dev/null +++ b/internal/osclients/mock/limit.go @@ -0,0 +1,131 @@ +/* +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: ../limit.go +// +// Generated by this command: +// +// mockgen -package mock -destination=limit.go -source=../limit.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock LimitClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + iter "iter" + reflect "reflect" + + limits "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/limits" + gomock "go.uber.org/mock/gomock" +) + +// MockLimitClient is a mock of LimitClient interface. +type MockLimitClient struct { + ctrl *gomock.Controller + recorder *MockLimitClientMockRecorder + isgomock struct{} +} + +// MockLimitClientMockRecorder is the mock recorder for MockLimitClient. +type MockLimitClientMockRecorder struct { + mock *MockLimitClient +} + +// NewMockLimitClient creates a new mock instance. +func NewMockLimitClient(ctrl *gomock.Controller) *MockLimitClient { + mock := &MockLimitClient{ctrl: ctrl} + mock.recorder = &MockLimitClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLimitClient) EXPECT() *MockLimitClientMockRecorder { + return m.recorder +} + +// CreateLimit mocks base method. +func (m *MockLimitClient) CreateLimit(ctx context.Context, opts limits.BatchCreateOptsBuilder) (*limits.Limit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateLimit", ctx, opts) + ret0, _ := ret[0].(*limits.Limit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateLimit indicates an expected call of CreateLimit. +func (mr *MockLimitClientMockRecorder) CreateLimit(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLimit", reflect.TypeOf((*MockLimitClient)(nil).CreateLimit), ctx, opts) +} + +// DeleteLimit mocks base method. +func (m *MockLimitClient) DeleteLimit(ctx context.Context, resourceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteLimit", ctx, resourceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteLimit indicates an expected call of DeleteLimit. +func (mr *MockLimitClientMockRecorder) DeleteLimit(ctx, resourceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLimit", reflect.TypeOf((*MockLimitClient)(nil).DeleteLimit), ctx, resourceID) +} + +// GetLimit mocks base method. +func (m *MockLimitClient) GetLimit(ctx context.Context, resourceID string) (*limits.Limit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLimit", ctx, resourceID) + ret0, _ := ret[0].(*limits.Limit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLimit indicates an expected call of GetLimit. +func (mr *MockLimitClientMockRecorder) GetLimit(ctx, resourceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLimit", reflect.TypeOf((*MockLimitClient)(nil).GetLimit), ctx, resourceID) +} + +// ListLimits mocks base method. +func (m *MockLimitClient) ListLimits(ctx context.Context, listOpts limits.ListOptsBuilder) iter.Seq2[*limits.Limit, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLimits", ctx, listOpts) + ret0, _ := ret[0].(iter.Seq2[*limits.Limit, error]) + return ret0 +} + +// ListLimits indicates an expected call of ListLimits. +func (mr *MockLimitClientMockRecorder) ListLimits(ctx, listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLimits", reflect.TypeOf((*MockLimitClient)(nil).ListLimits), ctx, listOpts) +} + +// UpdateLimit mocks base method. +func (m *MockLimitClient) UpdateLimit(ctx context.Context, id string, opts limits.UpdateOptsBuilder) (*limits.Limit, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateLimit", ctx, id, opts) + ret0, _ := ret[0].(*limits.Limit) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateLimit indicates an expected call of UpdateLimit. +func (mr *MockLimitClientMockRecorder) UpdateLimit(ctx, id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateLimit", reflect.TypeOf((*MockLimitClient)(nil).UpdateLimit), ctx, id, opts) +} diff --git a/internal/scope/mock.go b/internal/scope/mock.go index e57e4da58..e7da1992d 100644 --- a/internal/scope/mock.go +++ b/internal/scope/mock.go @@ -51,6 +51,7 @@ type MockScopeFactory struct { VolumeClient *mock.MockVolumeClient VolumeTypeClient *mock.MockVolumeTypeClient ShareNetworkClient *mock.MockShareNetworkClient + LimitClient *mock.MockLimitClient clientScopeCreateError error } @@ -73,6 +74,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { sharenetworkClient := mock.NewMockShareNetworkClient(mockCtrl) volumeClient := mock.NewMockVolumeClient(mockCtrl) volumetypeClient := mock.NewMockVolumeTypeClient(mockCtrl) + limitClient := mock.NewMockLimitClient(mockCtrl) return &MockScopeFactory{ AddressScope: addressScope, @@ -92,6 +94,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { UserClient: userClient, VolumeClient: volumeClient, VolumeTypeClient: volumetypeClient, + LimitClient: limitClient, } } @@ -177,3 +180,7 @@ func (f *MockScopeFactory) NewApplicationCredentialClient() (osclients.Applicati func (f *MockScopeFactory) ExtractToken() (*tokens.Token, error) { return &tokens.Token{ExpiresAt: time.Now().Add(24 * time.Hour)}, nil } + +func (f *MockScopeFactory) NewLimitClient() (osclients.LimitClient, error) { + return f.LimitClient, nil +} diff --git a/internal/scope/provider.go b/internal/scope/provider.go index f1207bd6c..cdddb3b36 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -219,6 +219,10 @@ func (s *providerScope) ExtractToken() (*tokens.Token, error) { return tokens.Get(context.TODO(), client, s.providerClient.Token()).ExtractToken() } +func (s *providerScope) NewLimitClient() (clients.LimitClient, error) { + return clients.NewLimitClient(s.providerClient, s.providerClientOpts) +} + func NewProviderClient(cloud clientconfig.Cloud, caCert []byte, logger logr.Logger) (*gophercloud.ProviderClient, *clientconfig.ClientOpts, error) { clientOpts := new(clientconfig.ClientOpts) diff --git a/internal/scope/scope.go b/internal/scope/scope.go index 4f093fe9a..9c7a65d2a 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -66,6 +66,7 @@ type Scope interface { NewVolumeClient() (osclients.VolumeClient, error) NewVolumeTypeClient() (osclients.VolumeTypeClient, error) ExtractToken() (*tokens.Token, error) + NewLimitClient() (osclients.LimitClient, error) } // WithLogger extends Scope with a logger. diff --git a/kuttl-test.yaml b/kuttl-test.yaml index 90fc98dde..24280a8df 100644 --- a/kuttl-test.yaml +++ b/kuttl-test.yaml @@ -11,6 +11,7 @@ testDirs: - ./internal/controllers/group/tests/ - ./internal/controllers/image/tests/ - ./internal/controllers/keypair/tests/ +- ./internal/controllers/limit/tests/ - ./internal/controllers/network/tests/ - ./internal/controllers/port/tests/ - ./internal/controllers/project/tests/ diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limit.go b/pkg/clients/applyconfiguration/api/v1alpha1/limit.go new file mode 100644 index 000000000..4345aa970 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limit.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" +) + +// LimitApplyConfiguration represents a declarative configuration of the Limit type for use +// with apply. +type LimitApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *LimitSpecApplyConfiguration `json:"spec,omitempty"` + Status *LimitStatusApplyConfiguration `json:"status,omitempty"` +} + +// Limit constructs a declarative configuration of the Limit type for use with +// apply. +func Limit(name, namespace string) *LimitApplyConfiguration { + b := &LimitApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Limit") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b +} + +// ExtractLimit extracts the applied configuration owned by fieldManager from +// limit. If no managedFields are found in limit for fieldManager, a +// LimitApplyConfiguration 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. +// limit must be a unmodified Limit API object that was retrieved from the Kubernetes API. +// ExtractLimit 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 ExtractLimit(limit *apiv1alpha1.Limit, fieldManager string) (*LimitApplyConfiguration, error) { + return extractLimit(limit, fieldManager, "") +} + +// ExtractLimitStatus is the same as ExtractLimit except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractLimitStatus(limit *apiv1alpha1.Limit, fieldManager string) (*LimitApplyConfiguration, error) { + return extractLimit(limit, fieldManager, "status") +} + +func extractLimit(limit *apiv1alpha1.Limit, fieldManager string, subresource string) (*LimitApplyConfiguration, error) { + b := &LimitApplyConfiguration{} + err := managedfields.ExtractInto(limit, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Limit"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(limit.Name) + b.WithNamespace(limit.Namespace) + + b.WithKind("Limit") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b, nil +} +func (b LimitApplyConfiguration) 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 *LimitApplyConfiguration) WithKind(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithAPIVersion(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithName(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithGenerateName(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithNamespace(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithUID(value types.UID) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithResourceVersion(value string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithGeneration(value int64) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithCreationTimestamp(value metav1.Time) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithLabels(entries map[string]string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithAnnotations(entries map[string]string) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithFinalizers(values ...string) *LimitApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *LimitApplyConfiguration) 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 *LimitApplyConfiguration) WithSpec(value *LimitSpecApplyConfiguration) *LimitApplyConfiguration { + 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 *LimitApplyConfiguration) WithStatus(value *LimitStatusApplyConfiguration) *LimitApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *LimitApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *LimitApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *LimitApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *LimitApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitfilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitfilter.go new file mode 100644 index 000000000..8631b5024 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitfilter.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" +) + +// LimitFilterApplyConfiguration represents a declarative configuration of the LimitFilter type for use +// with apply. +type LimitFilterApplyConfiguration struct { + Description *string `json:"description,omitempty"` + ServiceRef *apiv1alpha1.KubernetesNameRef `json:"serviceRef,omitempty"` + ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"` + DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` + ResourceName *string `json:"resourceName,omitempty"` +} + +// LimitFilterApplyConfiguration constructs a declarative configuration of the LimitFilter type for use with +// apply. +func LimitFilter() *LimitFilterApplyConfiguration { + return &LimitFilterApplyConfiguration{} +} + +// WithDescription sets the Description 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 Description field is set to the value of the last call. +func (b *LimitFilterApplyConfiguration) WithDescription(value string) *LimitFilterApplyConfiguration { + b.Description = &value + return b +} + +// WithServiceRef sets the ServiceRef 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 ServiceRef field is set to the value of the last call. +func (b *LimitFilterApplyConfiguration) WithServiceRef(value apiv1alpha1.KubernetesNameRef) *LimitFilterApplyConfiguration { + b.ServiceRef = &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 *LimitFilterApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *LimitFilterApplyConfiguration { + 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 *LimitFilterApplyConfiguration) WithDomainRef(value apiv1alpha1.KubernetesNameRef) *LimitFilterApplyConfiguration { + b.DomainRef = &value + return b +} + +// WithResourceName sets the ResourceName 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 ResourceName field is set to the value of the last call. +func (b *LimitFilterApplyConfiguration) WithResourceName(value string) *LimitFilterApplyConfiguration { + b.ResourceName = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitimport.go new file mode 100644 index 000000000..e0e5de8d8 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitimport.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 + +// LimitImportApplyConfiguration represents a declarative configuration of the LimitImport type for use +// with apply. +type LimitImportApplyConfiguration struct { + ID *string `json:"id,omitempty"` + Filter *LimitFilterApplyConfiguration `json:"filter,omitempty"` +} + +// LimitImportApplyConfiguration constructs a declarative configuration of the LimitImport type for use with +// apply. +func LimitImport() *LimitImportApplyConfiguration { + return &LimitImportApplyConfiguration{} +} + +// 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 *LimitImportApplyConfiguration) WithID(value string) *LimitImportApplyConfiguration { + 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 *LimitImportApplyConfiguration) WithFilter(value *LimitFilterApplyConfiguration) *LimitImportApplyConfiguration { + b.Filter = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcespec.go new file mode 100644 index 000000000..86d2bf210 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcespec.go @@ -0,0 +1,88 @@ +/* +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" +) + +// LimitResourceSpecApplyConfiguration represents a declarative configuration of the LimitResourceSpec type for use +// with apply. +type LimitResourceSpecApplyConfiguration struct { + Description *string `json:"description,omitempty"` + ServiceRef *apiv1alpha1.KubernetesNameRef `json:"serviceRef,omitempty"` + ProjectRef *apiv1alpha1.KubernetesNameRef `json:"projectRef,omitempty"` + DomainRef *apiv1alpha1.KubernetesNameRef `json:"domainRef,omitempty"` + ResourceName *string `json:"resourceName,omitempty"` + ResourceLimit *int32 `json:"resourceLimit,omitempty"` +} + +// LimitResourceSpecApplyConfiguration constructs a declarative configuration of the LimitResourceSpec type for use with +// apply. +func LimitResourceSpec() *LimitResourceSpecApplyConfiguration { + return &LimitResourceSpecApplyConfiguration{} +} + +// WithDescription sets the Description 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 Description field is set to the value of the last call. +func (b *LimitResourceSpecApplyConfiguration) WithDescription(value string) *LimitResourceSpecApplyConfiguration { + b.Description = &value + return b +} + +// WithServiceRef sets the ServiceRef 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 ServiceRef field is set to the value of the last call. +func (b *LimitResourceSpecApplyConfiguration) WithServiceRef(value apiv1alpha1.KubernetesNameRef) *LimitResourceSpecApplyConfiguration { + b.ServiceRef = &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 *LimitResourceSpecApplyConfiguration) WithProjectRef(value apiv1alpha1.KubernetesNameRef) *LimitResourceSpecApplyConfiguration { + 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 *LimitResourceSpecApplyConfiguration) WithDomainRef(value apiv1alpha1.KubernetesNameRef) *LimitResourceSpecApplyConfiguration { + b.DomainRef = &value + return b +} + +// WithResourceName sets the ResourceName 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 ResourceName field is set to the value of the last call. +func (b *LimitResourceSpecApplyConfiguration) WithResourceName(value string) *LimitResourceSpecApplyConfiguration { + b.ResourceName = &value + return b +} + +// WithResourceLimit sets the ResourceLimit 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 ResourceLimit field is set to the value of the last call. +func (b *LimitResourceSpecApplyConfiguration) WithResourceLimit(value int32) *LimitResourceSpecApplyConfiguration { + b.ResourceLimit = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcestatus.go new file mode 100644 index 000000000..1283f66a0 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitresourcestatus.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. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// LimitResourceStatusApplyConfiguration represents a declarative configuration of the LimitResourceStatus type for use +// with apply. +type LimitResourceStatusApplyConfiguration struct { + Description *string `json:"description,omitempty"` + ServiceID *string `json:"serviceID,omitempty"` + ProjectID *string `json:"projectID,omitempty"` + DomainID *string `json:"domainID,omitempty"` + ResourceLimit *int32 `json:"resourceLimit,omitempty"` + ResourceName *string `json:"resourceName,omitempty"` +} + +// LimitResourceStatusApplyConfiguration constructs a declarative configuration of the LimitResourceStatus type for use with +// apply. +func LimitResourceStatus() *LimitResourceStatusApplyConfiguration { + return &LimitResourceStatusApplyConfiguration{} +} + +// WithDescription sets the Description 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 Description field is set to the value of the last call. +func (b *LimitResourceStatusApplyConfiguration) WithDescription(value string) *LimitResourceStatusApplyConfiguration { + b.Description = &value + return b +} + +// WithServiceID sets the ServiceID 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 ServiceID field is set to the value of the last call. +func (b *LimitResourceStatusApplyConfiguration) WithServiceID(value string) *LimitResourceStatusApplyConfiguration { + b.ServiceID = &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 *LimitResourceStatusApplyConfiguration) WithProjectID(value string) *LimitResourceStatusApplyConfiguration { + 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 *LimitResourceStatusApplyConfiguration) WithDomainID(value string) *LimitResourceStatusApplyConfiguration { + b.DomainID = &value + return b +} + +// WithResourceLimit sets the ResourceLimit 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 ResourceLimit field is set to the value of the last call. +func (b *LimitResourceStatusApplyConfiguration) WithResourceLimit(value int32) *LimitResourceStatusApplyConfiguration { + b.ResourceLimit = &value + return b +} + +// WithResourceName sets the ResourceName 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 ResourceName field is set to the value of the last call. +func (b *LimitResourceStatusApplyConfiguration) WithResourceName(value string) *LimitResourceStatusApplyConfiguration { + b.ResourceName = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitspec.go new file mode 100644 index 000000000..1ad557665 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitspec.go @@ -0,0 +1,89 @@ +/* +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" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// LimitSpecApplyConfiguration represents a declarative configuration of the LimitSpec type for use +// with apply. +type LimitSpecApplyConfiguration struct { + Import *LimitImportApplyConfiguration `json:"import,omitempty"` + Resource *LimitResourceSpecApplyConfiguration `json:"resource,omitempty"` + ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"` + ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"` + ResyncPeriod *v1.Duration `json:"resyncPeriod,omitempty"` + CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"` +} + +// LimitSpecApplyConfiguration constructs a declarative configuration of the LimitSpec type for use with +// apply. +func LimitSpec() *LimitSpecApplyConfiguration { + return &LimitSpecApplyConfiguration{} +} + +// 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 *LimitSpecApplyConfiguration) WithImport(value *LimitImportApplyConfiguration) *LimitSpecApplyConfiguration { + 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 *LimitSpecApplyConfiguration) WithResource(value *LimitResourceSpecApplyConfiguration) *LimitSpecApplyConfiguration { + 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 *LimitSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *LimitSpecApplyConfiguration { + 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 *LimitSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *LimitSpecApplyConfiguration { + b.ManagedOptions = value + return b +} + +// WithResyncPeriod sets the ResyncPeriod 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 ResyncPeriod field is set to the value of the last call. +func (b *LimitSpecApplyConfiguration) WithResyncPeriod(value v1.Duration) *LimitSpecApplyConfiguration { + b.ResyncPeriod = &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 *LimitSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *LimitSpecApplyConfiguration { + b.CloudCredentialsRef = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/limitstatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/limitstatus.go new file mode 100644 index 000000000..fbd824f76 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/limitstatus.go @@ -0,0 +1,76 @@ +/* +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// LimitStatusApplyConfiguration represents a declarative configuration of the LimitStatus type for use +// with apply. +type LimitStatusApplyConfiguration struct { + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + ID *string `json:"id,omitempty"` + Resource *LimitResourceStatusApplyConfiguration `json:"resource,omitempty"` + LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"` +} + +// LimitStatusApplyConfiguration constructs a declarative configuration of the LimitStatus type for use with +// apply. +func LimitStatus() *LimitStatusApplyConfiguration { + return &LimitStatusApplyConfiguration{} +} + +// 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 *LimitStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *LimitStatusApplyConfiguration { + 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 *LimitStatusApplyConfiguration) WithID(value string) *LimitStatusApplyConfiguration { + 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 *LimitStatusApplyConfiguration) WithResource(value *LimitResourceStatusApplyConfiguration) *LimitStatusApplyConfiguration { + b.Resource = value + return b +} + +// WithLastSyncTime sets the LastSyncTime 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 LastSyncTime field is set to the value of the last call. +func (b *LimitStatusApplyConfiguration) WithLastSyncTime(value metav1.Time) *LimitStatusApplyConfiguration { + b.LastSyncTime = &value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 9fc18f739..5e6dd638c 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -1505,6 +1505,139 @@ var schemaYAML = typed.YAMLObject(`types: - name: resource type: namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.KeyPairResourceStatus +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Limit + 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.LimitSpec + default: {} + - name: status + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitStatus + default: {} +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitFilter + map: + fields: + - name: description + type: + scalar: string + - name: domainRef + type: + scalar: string + - name: projectRef + type: + scalar: string + - name: resourceName + type: + scalar: string + - name: serviceRef + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitImport + map: + fields: + - name: filter + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitFilter + - name: id + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitResourceSpec + map: + fields: + - name: description + type: + scalar: string + - name: domainRef + type: + scalar: string + - name: projectRef + type: + scalar: string + - name: resourceLimit + type: + scalar: numeric + default: 0 + - name: resourceName + type: + scalar: string + - name: serviceRef + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitResourceStatus + map: + fields: + - name: description + type: + scalar: string + - name: domainID + type: + scalar: string + - name: projectID + type: + scalar: string + - name: resourceLimit + type: + scalar: numeric + - name: resourceName + type: + scalar: string + - name: serviceID + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitSpec + 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.LimitImport + - 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.LimitResourceSpec + - name: resyncPeriod + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Duration +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitStatus + 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: lastSyncTime + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time + - name: resource + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.LimitResourceStatus - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.ManagedOptions map: fields: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index 22e66aea0..54e2ab6bd 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -214,6 +214,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.KeyPairSpecApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("KeyPairStatus"): return &apiv1alpha1.KeyPairStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("Limit"): + return &apiv1alpha1.LimitApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitFilter"): + return &apiv1alpha1.LimitFilterApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitImport"): + return &apiv1alpha1.LimitImportApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitResourceSpec"): + return &apiv1alpha1.LimitResourceSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitResourceStatus"): + return &apiv1alpha1.LimitResourceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitSpec"): + return &apiv1alpha1.LimitSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("LimitStatus"): + return &apiv1alpha1.LimitStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ManagedOptions"): return &apiv1alpha1.ManagedOptionsApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Network"): 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 e39fc5dc9..fc41180e7 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go @@ -37,6 +37,7 @@ type OpenstackV1alpha1Interface interface { GroupsGetter ImagesGetter KeyPairsGetter + LimitsGetter NetworksGetter PortsGetter ProjectsGetter @@ -97,6 +98,10 @@ func (c *OpenstackV1alpha1Client) KeyPairs(namespace string) KeyPairInterface { return newKeyPairs(c, namespace) } +func (c *OpenstackV1alpha1Client) Limits(namespace string) LimitInterface { + return newLimits(c, namespace) +} + func (c *OpenstackV1alpha1Client) Networks(namespace string) NetworkInterface { return newNetworks(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 5015aed35..9da4134f9 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 @@ -64,6 +64,10 @@ func (c *FakeOpenstackV1alpha1) KeyPairs(namespace string) v1alpha1.KeyPairInter return newFakeKeyPairs(c, namespace) } +func (c *FakeOpenstackV1alpha1) Limits(namespace string) v1alpha1.LimitInterface { + return newFakeLimits(c, namespace) +} + func (c *FakeOpenstackV1alpha1) Networks(namespace string) v1alpha1.NetworkInterface { return newFakeNetworks(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_limit.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_limit.go new file mode 100644 index 000000000..40a26b7b8 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_limit.go @@ -0,0 +1,49 @@ +/* +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" +) + +// fakeLimits implements LimitInterface +type fakeLimits struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.Limit, *v1alpha1.LimitList, *apiv1alpha1.LimitApplyConfiguration] + Fake *FakeOpenstackV1alpha1 +} + +func newFakeLimits(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.LimitInterface { + return &fakeLimits{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.Limit, *v1alpha1.LimitList, *apiv1alpha1.LimitApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("limits"), + v1alpha1.SchemeGroupVersion.WithKind("Limit"), + func() *v1alpha1.Limit { return &v1alpha1.Limit{} }, + func() *v1alpha1.LimitList { return &v1alpha1.LimitList{} }, + func(dst, src *v1alpha1.LimitList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.LimitList) []*v1alpha1.Limit { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha1.LimitList, items []*v1alpha1.Limit) { 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 7c5d67d45..f73443bc8 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go @@ -36,6 +36,8 @@ type ImageExpansion interface{} type KeyPairExpansion interface{} +type LimitExpansion interface{} + type NetworkExpansion interface{} type PortExpansion interface{} diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/limit.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/limit.go new file mode 100644 index 000000000..f3e0141f9 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/limit.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" +) + +// LimitsGetter has a method to return a LimitInterface. +// A group's client should implement this interface. +type LimitsGetter interface { + Limits(namespace string) LimitInterface +} + +// LimitInterface has methods to work with Limit resources. +type LimitInterface interface { + Create(ctx context.Context, limit *apiv1alpha1.Limit, opts v1.CreateOptions) (*apiv1alpha1.Limit, error) + Update(ctx context.Context, limit *apiv1alpha1.Limit, opts v1.UpdateOptions) (*apiv1alpha1.Limit, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, limit *apiv1alpha1.Limit, opts v1.UpdateOptions) (*apiv1alpha1.Limit, 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.Limit, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.LimitList, 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.Limit, err error) + Apply(ctx context.Context, limit *applyconfigurationapiv1alpha1.LimitApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.Limit, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, limit *applyconfigurationapiv1alpha1.LimitApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.Limit, err error) + LimitExpansion +} + +// limits implements LimitInterface +type limits struct { + *gentype.ClientWithListAndApply[*apiv1alpha1.Limit, *apiv1alpha1.LimitList, *applyconfigurationapiv1alpha1.LimitApplyConfiguration] +} + +// newLimits returns a Limits +func newLimits(c *OpenstackV1alpha1Client, namespace string) *limits { + return &limits{ + gentype.NewClientWithListAndApply[*apiv1alpha1.Limit, *apiv1alpha1.LimitList, *applyconfigurationapiv1alpha1.LimitApplyConfiguration]( + "limits", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.Limit { return &apiv1alpha1.Limit{} }, + func() *apiv1alpha1.LimitList { return &apiv1alpha1.LimitList{} }, + ), + } +} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go index 69d2bdeaf..a4d6078f7 100644 --- a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go +++ b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go @@ -42,6 +42,8 @@ type Interface interface { Images() ImageInformer // KeyPairs returns a KeyPairInformer. KeyPairs() KeyPairInformer + // Limits returns a LimitInformer. + Limits() LimitInformer // Networks returns a NetworkInformer. Networks() NetworkInformer // Ports returns a PortInformer. @@ -134,6 +136,11 @@ func (v *version) KeyPairs() KeyPairInformer { return &keyPairInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// Limits returns a LimitInformer. +func (v *version) Limits() LimitInformer { + return &limitInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Networks returns a NetworkInformer. func (v *version) Networks() NetworkInformer { return &networkInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/limit.go b/pkg/clients/informers/externalversions/api/v1alpha1/limit.go new file mode 100644 index 000000000..04bc40bd9 --- /dev/null +++ b/pkg/clients/informers/externalversions/api/v1alpha1/limit.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" +) + +// LimitInformer provides access to a shared informer and lister for +// Limits. +type LimitInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.LimitLister +} + +type limitInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLimitInformer constructs a new informer for Limit 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 NewLimitInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLimitInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLimitInformer constructs a new informer for Limit 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 NewFilteredLimitInformer(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().Limits(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().Limits(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().Limits(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().Limits(namespace).Watch(ctx, options) + }, + }, + &v2apiv1alpha1.Limit{}, + resyncPeriod, + indexers, + ) +} + +func (f *limitInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLimitInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *limitInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&v2apiv1alpha1.Limit{}, f.defaultInformer) +} + +func (f *limitInformer) Lister() apiv1alpha1.LimitLister { + return apiv1alpha1.NewLimitLister(f.Informer().GetIndexer()) +} diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go index f58420886..22302c59d 100644 --- a/pkg/clients/informers/externalversions/generic.go +++ b/pkg/clients/informers/externalversions/generic.go @@ -71,6 +71,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Images().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("keypairs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().KeyPairs().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("limits"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Limits().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("networks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Networks().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("ports"): diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go index 76fec1603..d02f8305d 100644 --- a/pkg/clients/listers/api/v1alpha1/expansion_generated.go +++ b/pkg/clients/listers/api/v1alpha1/expansion_generated.go @@ -90,6 +90,14 @@ type KeyPairListerExpansion interface{} // KeyPairNamespaceLister. type KeyPairNamespaceListerExpansion interface{} +// LimitListerExpansion allows custom methods to be added to +// LimitLister. +type LimitListerExpansion interface{} + +// LimitNamespaceListerExpansion allows custom methods to be added to +// LimitNamespaceLister. +type LimitNamespaceListerExpansion interface{} + // NetworkListerExpansion allows custom methods to be added to // NetworkLister. type NetworkListerExpansion interface{} diff --git a/pkg/clients/listers/api/v1alpha1/limit.go b/pkg/clients/listers/api/v1alpha1/limit.go new file mode 100644 index 000000000..24a4fe911 --- /dev/null +++ b/pkg/clients/listers/api/v1alpha1/limit.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" +) + +// LimitLister helps list Limits. +// All objects returned here must be treated as read-only. +type LimitLister interface { + // List lists all Limits in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.Limit, err error) + // Limits returns an object that can list and get Limits. + Limits(namespace string) LimitNamespaceLister + LimitListerExpansion +} + +// limitLister implements the LimitLister interface. +type limitLister struct { + listers.ResourceIndexer[*apiv1alpha1.Limit] +} + +// NewLimitLister returns a new LimitLister. +func NewLimitLister(indexer cache.Indexer) LimitLister { + return &limitLister{listers.New[*apiv1alpha1.Limit](indexer, apiv1alpha1.Resource("limit"))} +} + +// Limits returns an object that can list and get Limits. +func (s *limitLister) Limits(namespace string) LimitNamespaceLister { + return limitNamespaceLister{listers.NewNamespaced[*apiv1alpha1.Limit](s.ResourceIndexer, namespace)} +} + +// LimitNamespaceLister helps list and get Limits. +// All objects returned here must be treated as read-only. +type LimitNamespaceLister interface { + // List lists all Limits in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.Limit, err error) + // Get retrieves the Limit from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.Limit, error) + LimitNamespaceListerExpansion +} + +// limitNamespaceLister implements the LimitNamespaceLister +// interface. +type limitNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.Limit] +} diff --git a/test/apivalidations/limit_test.go b/test/apivalidations/limit_test.go new file mode 100644 index 000000000..1b93f703e --- /dev/null +++ b/test/apivalidations/limit_test.go @@ -0,0 +1,193 @@ +/* +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" + "strings" + + . "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 ( + limitName = "limit" + limitID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" +) + +func limitStub(namespace *corev1.Namespace) *orcv1alpha1.Limit { + obj := &orcv1alpha1.Limit{} + obj.Name = limitName + obj.Namespace = namespace.Name + return obj +} + +func testLimitResource() *applyconfigv1alpha1.LimitResourceSpecApplyConfiguration { + return applyconfigv1alpha1.LimitResourceSpec(). + WithServiceRef("nova"). + WithResourceName("servers"). + WithResourceLimit(10) +} + +func testLimitResourceWithProject() *applyconfigv1alpha1.LimitResourceSpecApplyConfiguration { + return testLimitResource().WithProjectRef("demo") +} + +func baseLimitPatch(obj client.Object) *applyconfigv1alpha1.LimitApplyConfiguration { + return applyconfigv1alpha1.Limit(obj.GetName(), obj.GetNamespace()). + WithSpec(applyconfigv1alpha1.LimitSpec(). + WithCloudCredentialsRef(testCredentials())) +} + +func testLimitImport() *applyconfigv1alpha1.LimitImportApplyConfiguration { + return applyconfigv1alpha1.LimitImport().WithID(limitID) +} + +var _ = Describe("ORC Limit API validations", func() { + var namespace *corev1.Namespace + BeforeEach(func() { + namespace = createNamespace() + }) + + runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.LimitApplyConfiguration]{ + createObject: func(ns *corev1.Namespace) client.Object { return limitStub(ns) }, + basePatch: func(obj client.Object) *applyconfigv1alpha1.LimitApplyConfiguration { + return baseLimitPatch(obj) + }, + applyResource: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithResource(testLimitResourceWithProject()) + }, + applyImport: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithImport(testLimitImport()) + }, + applyEmptyImport: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.LimitImport()) + }, + applyEmptyFilter: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.LimitImport().WithFilter(applyconfigv1alpha1.LimitFilter())) + }, + applyValidFilter: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.LimitImport().WithFilter(applyconfigv1alpha1.LimitFilter().WithServiceRef("foo"))) + }, + applyManaged: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) + }, + applyUnmanaged: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + }, + applyManagedOptions: func(p *applyconfigv1alpha1.LimitApplyConfiguration) { + p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach)) + }, + getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy { + return obj.(*orcv1alpha1.Limit).Spec.ManagementPolicy + }, + getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete { + return obj.(*orcv1alpha1.Limit).Spec.ManagedOptions.OnDelete + }, + }) + + It("should reject a limit without required fields", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + patch.Spec.WithResource(applyconfigv1alpha1.LimitResourceSpec()) + Expect(applyObj(ctx, obj, patch)).NotTo(Succeed()) + }) + + It("should have immutable serviceRef", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + res := testLimitResourceWithProject() + patch.Spec.WithResource(res.WithServiceRef("service-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(res.WithServiceRef("service-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("serviceRef is immutable"))) + }) + + It("should have immutable projectRef", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + patch.Spec.WithResource(testLimitResource(). + WithProjectRef("project-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testLimitResource(). + WithProjectRef("project-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("projectRef is immutable"))) + }) + + It("should have immutable domainRef", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + patch.Spec.WithResource(testLimitResource(). + WithDomainRef("domain-a")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(testLimitResource(). + WithDomainRef("domain-b")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("domainRef is immutable"))) + }) + + It("should have immutable resourceName", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + res := testLimitResourceWithProject() + + patch.Spec.WithResource(res.WithResourceName("servers")) + Expect(applyObj(ctx, obj, patch)).To(Succeed()) + + patch.Spec.WithResource(res.WithResourceName("server_key_pairs")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring("resourceName is immutable"))) + }) + + It("should reject invalid resourceName", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + res := testLimitResourceWithProject() + + patch.Spec.WithResource(res.WithResourceName("invalid-name-\r\n")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring(`spec.resource.resourceName in body should match '^[\S]+$'`))) + + patch = baseLimitPatch(obj) + patch.Spec.WithResource(res.WithResourceName(strings.Repeat("a", 256))) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring(`spec.resource.resourceName: Too long: may not be longer than 255`))) + }) + + It("should reject invalid resourceLimit", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + patch.Spec.WithResource(testLimitResourceWithProject().WithResourceLimit(-2)) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring(`spec.resource.resourceLimit in body should be greater than or equal to -1`))) + }) + + It("should include either projectRef or domainRef but not both", func(ctx context.Context) { + obj := limitStub(namespace) + patch := baseLimitPatch(obj) + res := testLimitResource() + + patch.Spec.WithResource(res) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring(`either projectRef or domainRef must be specified`))) + + patch.Spec.WithResource(res.WithProjectRef("demo").WithDomainRef("default")) + Expect(applyObj(ctx, obj, patch)).To(MatchError(ContainSubstring(`projectRef and domainRef are mutually exclusive`))) + }) +}) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 3cfdb8ac9..7e0c52325 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -19,6 +19,7 @@ Package v1alpha1 contains API Schema definitions for the openstack v1alpha1 API - [Group](#group) - [Image](#image) - [KeyPair](#keypair) +- [Limit](#limit) - [Network](#network) - [Port](#port) - [Project](#project) @@ -516,6 +517,7 @@ _Appears in:_ - [GroupSpec](#groupspec) - [ImageSpec](#imagespec) - [KeyPairSpec](#keypairspec) +- [LimitSpec](#limitspec) - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec) @@ -2210,6 +2212,8 @@ _Appears in:_ - [GroupFilter](#groupfilter) - [GroupResourceSpec](#groupresourcespec) - [HostID](#hostid) +- [LimitFilter](#limitfilter) +- [LimitResourceSpec](#limitresourcespec) - [NetworkFilter](#networkfilter) - [NetworkResourceSpec](#networkresourcespec) - [PortFilter](#portfilter) @@ -2243,6 +2247,148 @@ _Appears in:_ +#### Limit + + + +Limit is the Schema for an ORC resource. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | | +| `kind` _string_ | `Limit` | | | +| `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` _[LimitSpec](#limitspec)_ | spec specifies the desired state of the resource. | | Required: \{\}
| +| `status` _[LimitStatus](#limitstatus)_ | status defines the observed state of the resource. | | Optional: \{\}
| + + +#### LimitFilter + + + +LimitFilter defines an existing resource by its properties + +_Validation:_ +- MinProperties: 1 + +_Appears in:_ +- [LimitImport](#limitimport) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `description` _string_ | description of the existing resource | | MaxLength: 255
MinLength: 1
Optional: \{\}
| +| `serviceRef` _[KubernetesNameRef](#kubernetesnameref)_ | serviceRef is a reference to the ORC Service which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef is a reference to the ORC Project which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with. | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `resourceName` _string_ | resourceName is the name of the resource this limit is associated with. | | MaxLength: 255
MinLength: 1
Pattern: `^[\S]+$`
Optional: \{\}
| + + +#### LimitImport + + + +LimitImport specifies an existing resource which will be imported instead of +creating a new one + +_Validation:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [LimitSpec](#limitspec) + +| 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` _[LimitFilter](#limitfilter)_ | 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: \{\}
| + + +#### LimitResourceSpec + + + +LimitResourceSpec contains the desired state of the resource. + + + +_Appears in:_ +- [LimitSpec](#limitspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| +| `serviceRef` _[KubernetesNameRef](#kubernetesnameref)_ | serviceRef is a reference to the ORC Service which this resource is associated with. | | MaxLength: 253
MinLength: 1
Required: \{\}
| +| `projectRef` _[KubernetesNameRef](#kubernetesnameref)_ | projectRef is a reference to the ORC Project which this resource is associated with.
Either Domain ID or Project ID must be provided.
https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `domainRef` _[KubernetesNameRef](#kubernetesnameref)_ | domainRef is a reference to the ORC Domain which this resource is associated with.
Either Domain ID or Project ID must be provided.
https://opendev.org/openstack/keystone/src/commit/30ef2ffa65a3486ef882f00538e20f2253c57d4c/keystone/limit/schema.py#L323-L340 | | MaxLength: 253
MinLength: 1
Optional: \{\}
| +| `resourceName` _string_ | resourceName is the name of the resource this limit is associated with. | | MaxLength: 255
MinLength: 1
Pattern: `^[\S]+$`
Required: \{\}
| +| `resourceLimit` _integer_ | resourceLimit is the override value of the limit. | | Minimum: -1
Required: \{\}
| + + +#### LimitResourceStatus + + + +LimitResourceStatus represents the observed state of the resource. + + + +_Appears in:_ +- [LimitStatus](#limitstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 1024
Optional: \{\}
| +| `serviceID` _string_ | serviceID is the ID of the Service to which the resource is associated. | | MaxLength: 1024
Optional: \{\}
| +| `projectID` _string_ | projectID is the ID of the Project to which the resource is associated. | | MaxLength: 1024
Optional: \{\}
| +| `domainID` _string_ | domainID is the ID of the Domain to which the resource is associated. | | MaxLength: 1024
Optional: \{\}
| +| `resourceLimit` _integer_ | resourceLimit is the override value of the limit. | | Optional: \{\}
| +| `resourceName` _string_ | resourceName is the name of the resource this limit is associated with. | | MaxLength: 1024
Optional: \{\}
| + + +#### LimitSpec + + + +LimitSpec defines the desired state of an ORC object. + + + +_Appears in:_ +- [Limit](#limit) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `import` _[LimitImport](#limitimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MaxProperties: 1
MinProperties: 1
Optional: \{\}
| +| `resource` _[LimitResourceSpec](#limitresourcespec)_ | 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: \{\}
| +| `resyncPeriod` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ | resyncPeriod defines how frequently the controller will re-reconcile
this resource even when no changes have been detected. This overrides
the global default resync period. The value must be a valid Go duration
string, e.g. "10m", "1h". Set to "0s" to disable periodic resync for
this resource. Very low values may cause excessive OpenStack API load. | | Optional: \{\}
| +| `cloudCredentialsRef` _[CloudCredentialsReference](#cloudcredentialsreference)_ | cloudCredentialsRef points to a secret containing OpenStack credentials | | Required: \{\}
| + + +#### LimitStatus + + + +LimitStatus defines the observed state of an ORC resource. + + + +_Appears in:_ +- [Limit](#limit) + +| 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` _[LimitResourceStatus](#limitresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| +| `lastSyncTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#time-v1-meta)_ | lastSyncTime is the timestamp of the last successful reconciliation
that fetched state from OpenStack. It is updated each time the
controller successfully reads the resource state from the OpenStack
API. | | Optional: \{\}
| + + #### MAC _Underlying type:_ _string_ @@ -2291,6 +2437,7 @@ _Appears in:_ - [GroupSpec](#groupspec) - [ImageSpec](#imagespec) - [KeyPairSpec](#keypairspec) +- [LimitSpec](#limitspec) - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec) @@ -2332,6 +2479,7 @@ _Appears in:_ - [GroupSpec](#groupspec) - [ImageSpec](#imagespec) - [KeyPairSpec](#keypairspec) +- [LimitSpec](#limitspec) - [NetworkSpec](#networkspec) - [PortSpec](#portspec) - [ProjectSpec](#projectspec)