diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index d19ccc71e..e30867b10 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -42,11 +42,18 @@ jobs: conf_overrides: | enable_plugin neutron https://github.com/openstack/neutron ${{ matrix.openstack_version }} enable_plugin manila https://github.com/openstack/manila ${{ matrix.openstack_version }} + enable_plugin designate https://github.com/openstack/designate ${{ matrix.openstack_version }} [[post-config|/etc/nova/nova.conf]] [filter_scheduler] enabled_filters = ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,ServerGroupAntiAffinityFilter,ServerGroupAffinityFilter,SameHostFilter,DifferentHostFilter,SimpleCIDRAffinityFilter,JsonFilter + - name: Verify Designate CLI + run: | + openstack zone list + env: + OS_CLOUD: devstack + - name: Deploy a Kind Cluster uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # tag=v1.14.0 with: diff --git a/PROJECT b/PROJECT index e5a188a8b..e05203898 100644 --- a/PROJECT +++ b/PROJECT @@ -24,6 +24,14 @@ resources: kind: ApplicationCredential path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: k-orc.cloud + group: openstack + kind: DNSZone + path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/dnszone_types.go b/api/v1alpha1/dnszone_types.go new file mode 100644 index 000000000..5140952d9 --- /dev/null +++ b/api/v1alpha1/dnszone_types.go @@ -0,0 +1,155 @@ +/* +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" +) + +// +kubebuilder:validation:Enum:=PRIMARY;SECONDARY +type DNSZoneType string + +const ( + DNSZoneTypePrimary DNSZoneType = "PRIMARY" + DNSZoneTypeSecondary DNSZoneType = "SECONDARY" +) + +// DNSZoneResourceSpec contains the desired state of the resource. +// +kubebuilder:validation:XValidation:rule="self.type == 'PRIMARY' ? (has(self.email) && self.email != \"\") : true",message="email is required for PRIMARY zones" +// +kubebuilder:validation:XValidation:rule="self.type == 'SECONDARY' ? (has(self.masters) && self.masters.size() > 0) : true",message="masters: required when type is SECONDARY" +// +kubebuilder:validation:XValidation:rule="self.type == 'PRIMARY' ? !has(self.masters) : true",message="masters: must not be specified when type is PRIMARY" +// +kubebuilder:validation:XValidation:rule="self.type == 'SECONDARY' ? !has(self.email) : true",message="email: must not be specified when type is SECONDARY" +type DNSZoneResourceSpec struct { + // name will be the name of the created resource. If not specified, the + // name of the ORC object will be used. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + // +kubebuilder:validation:XValidation:rule="self.endsWith('.')",message="zone name must end with a period" + // +optional + Name *OpenStackName `json:"name,omitempty"` + + // email is the email address of the administrator for the zone. + // +kubebuilder:validation:Format:=email + // +kubebuilder:validation:MaxLength:=255 + // +optional + Email *string `json:"email,omitempty"` + + // description is a human-readable description for the resource. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` + + // ttl is the Time To Live for the zone in seconds. + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=2147483647 + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // type is the type of the zone. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + // +kubebuilder:default:="PRIMARY" + // +optional + Type DNSZoneType `json:"type,omitempty"` + + // masters specifies zone masters if this is a secondary zone. + // +kubebuilder:validation:MaxItems:=32 + // +kubebuilder:validation:items:MaxLength:=255 + // +listType=atomic + // +optional + Masters []string `json:"masters,omitempty"` +} + +// DNSZoneFilter defines an existing resource by its properties +// +kubebuilder:validation:MinProperties:=1 +type DNSZoneFilter struct { + // name of the existing resource + // +kubebuilder:validation:XValidation:rule="self.endsWith('.')",message="name must end with a period" + // +optional + Name *OpenStackName `json:"name,omitempty"` + + // email of the existing resource + // +kubebuilder:validation:Format:=email + // +kubebuilder:validation:MaxLength:=255 + // +optional + Email *string `json:"email,omitempty"` + + // description of the existing resource + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` + + // ttl of the existing resource + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=2147483647 + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // type of the existing resource + // +optional + Type *DNSZoneType `json:"type,omitempty"` + + // masters of the existing resource + // +kubebuilder:validation:MaxItems:=32 + // +kubebuilder:validation:items:MaxLength:=255 + // +listType=atomic + // +optional + Masters []string `json:"masters,omitempty"` +} + +// DNSZoneResourceStatus represents the observed state of the resource. +type DNSZoneResourceStatus struct { + // name is a Human-readable name for the resource. Might not be unique. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Name string `json:"name,omitempty"` + + // email is the email contact of the zone. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Email string `json:"email,omitempty"` + + // description is a human-readable description for the resource. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Description string `json:"description,omitempty"` + + // ttl is the Time to Live for the zone in seconds. + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // type is the type of the zone. + // +kubebuilder:validation:MaxLength=255 + // +optional + Type string `json:"type,omitempty"` + + // masters specifies zone masters if this is a secondary zone. + // +kubebuilder:validation:MaxItems:=32 + // +kubebuilder:validation:items:MaxLength:=255 + // +listType=atomic + // +optional + Masters []string `json:"masters,omitempty"` + + // transferredAt is the last time an update was retrieved from the master servers. + // +optional + TransferredAt *metav1.Time `json:"transferredAt,omitempty"` + + // status is the status of the resource. + // +kubebuilder:validation:MaxLength=255 + // +optional + Status string `json:"status,omitempty"` +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2f4fe34aa..ef43428fa 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -698,6 +698,267 @@ func (in *CloudCredentialsReference) DeepCopy() *CloudCredentialsReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZone) DeepCopyInto(out *DNSZone) { + *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 DNSZone. +func (in *DNSZone) DeepCopy() *DNSZone { + if in == nil { + return nil + } + out := new(DNSZone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZone) 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 *DNSZoneFilter) DeepCopyInto(out *DNSZoneFilter) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(OpenStackName) + **out = **in + } + if in.Email != nil { + in, out := &in.Email, &out.Email + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(DNSZoneType) + **out = **in + } + if in.Masters != nil { + in, out := &in.Masters, &out.Masters + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneFilter. +func (in *DNSZoneFilter) DeepCopy() *DNSZoneFilter { + if in == nil { + return nil + } + out := new(DNSZoneFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneImport) DeepCopyInto(out *DNSZoneImport) { + *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(DNSZoneFilter) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneImport. +func (in *DNSZoneImport) DeepCopy() *DNSZoneImport { + if in == nil { + return nil + } + out := new(DNSZoneImport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneList) DeepCopyInto(out *DNSZoneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSZone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneList. +func (in *DNSZoneList) DeepCopy() *DNSZoneList { + if in == nil { + return nil + } + out := new(DNSZoneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSZoneList) 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 *DNSZoneResourceSpec) DeepCopyInto(out *DNSZoneResourceSpec) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(OpenStackName) + **out = **in + } + if in.Email != nil { + in, out := &in.Email, &out.Email + *out = new(string) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } + if in.Masters != nil { + in, out := &in.Masters, &out.Masters + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneResourceSpec. +func (in *DNSZoneResourceSpec) DeepCopy() *DNSZoneResourceSpec { + if in == nil { + return nil + } + out := new(DNSZoneResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneResourceStatus) DeepCopyInto(out *DNSZoneResourceStatus) { + *out = *in + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } + if in.Masters != nil { + in, out := &in.Masters, &out.Masters + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TransferredAt != nil { + in, out := &in.TransferredAt, &out.TransferredAt + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneResourceStatus. +func (in *DNSZoneResourceStatus) DeepCopy() *DNSZoneResourceStatus { + if in == nil { + return nil + } + out := new(DNSZoneResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneSpec) DeepCopyInto(out *DNSZoneSpec) { + *out = *in + if in.Import != nil { + in, out := &in.Import, &out.Import + *out = new(DNSZoneImport) + (*in).DeepCopyInto(*out) + } + if in.Resource != nil { + in, out := &in.Resource, &out.Resource + *out = new(DNSZoneResourceSpec) + (*in).DeepCopyInto(*out) + } + if in.ManagedOptions != nil { + in, out := &in.ManagedOptions, &out.ManagedOptions + *out = new(ManagedOptions) + **out = **in + } + out.CloudCredentialsRef = in.CloudCredentialsRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneSpec. +func (in *DNSZoneSpec) DeepCopy() *DNSZoneSpec { + if in == nil { + return nil + } + out := new(DNSZoneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSZoneStatus) DeepCopyInto(out *DNSZoneStatus) { + *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(DNSZoneResourceStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSZoneStatus. +func (in *DNSZoneStatus) DeepCopy() *DNSZoneStatus { + if in == nil { + return nil + } + out := new(DNSZoneStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Domain) DeepCopyInto(out *Domain) { *out = *in diff --git a/api/v1alpha1/zz_generated.dnszone-resource.go b/api/v1alpha1/zz_generated.dnszone-resource.go new file mode 100644 index 000000000..5a95cb4c6 --- /dev/null +++ b/api/v1alpha1/zz_generated.dnszone-resource.go @@ -0,0 +1,179 @@ +// Code generated by resource-generator. DO NOT EDIT. +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DNSZoneImport specifies an existing resource which will be imported instead of +// creating a new one +// +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:MaxProperties:=1 +type DNSZoneImport 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 *DNSZoneFilter `json:"filter,omitempty"` +} + +// DNSZoneSpec 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 DNSZoneSpec struct { + // import refers to an existing OpenStack resource which will be imported instead of + // creating a new one. + // +optional + Import *DNSZoneImport `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 *DNSZoneResourceSpec `json:"resource,omitempty"` + + // managementPolicy defines how ORC will treat the object. Valid values are + // `managed`: ORC will create, update, and delete the resource; `unmanaged`: + // ORC will import an existing resource, and will not apply updates to it or + // delete it. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="managementPolicy is immutable" + // +kubebuilder:default:=managed + // +optional + ManagementPolicy ManagementPolicy `json:"managementPolicy,omitempty"` + + // managedOptions specifies options which may be applied to managed objects. + // +optional + ManagedOptions *ManagedOptions `json:"managedOptions,omitempty"` + + // cloudCredentialsRef points to a secret containing OpenStack credentials + // +required + CloudCredentialsRef CloudCredentialsReference `json:"cloudCredentialsRef,omitzero"` +} + +// DNSZoneStatus defines the observed state of an ORC resource. +type DNSZoneStatus 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 *DNSZoneResourceStatus `json:"resource,omitempty"` +} + +var _ ObjectWithConditions = &DNSZone{} + +func (i *DNSZone) GetConditions() []metav1.Condition { + return i.Status.Conditions +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:resource:categories=openstack +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="Resource ID" +// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type=='Available')].status",description="Availability status of resource" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Progressing')].message",description="Message describing current progress status" + +// DNSZone is the Schema for an ORC resource. +type DNSZone 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 DNSZoneSpec `json:"spec,omitzero"` + + // status defines the observed state of the resource. + // +optional + Status DNSZoneStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DNSZoneList contains a list of DNSZone. +type DNSZoneList struct { + metav1.TypeMeta `json:",inline"` + + // metadata contains the list metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items contains a list of DNSZone. + // +required + Items []DNSZone `json:"items"` +} + +func (l *DNSZoneList) GetItems() []DNSZone { + return l.Items +} + +func init() { + SchemeBuilder.Register(&DNSZone{}, &DNSZoneList{}) +} + +func (i *DNSZone) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) { + if i == nil { + return nil, nil + } + + return &i.Namespace, &i.Spec.CloudCredentialsRef +} + +var _ CloudCredentialsRefProvider = &DNSZone{} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9addc552d..d7fd91cca 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -29,6 +29,7 @@ import ( "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/addressscope" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/applicationcredential" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/dnszone" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/domain" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/endpoint" "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/flavor" @@ -133,6 +134,7 @@ func main() { volume.New(scopeFactory), volumetype.New(scopeFactory), domain.New(scopeFactory), + dnszone.New(scopeFactory), service.New(scopeFactory), sharenetwork.New(scopeFactory), keypair.New(scopeFactory), diff --git a/cmd/models-schema/zz_generated.openapi.go b/cmd/models-schema/zz_generated.openapi.go index bbdaee5f0..db64d56a5 100644 --- a/cmd/models-schema/zz_generated.openapi.go +++ b/cmd/models-schema/zz_generated.openapi.go @@ -55,6 +55,14 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ApplicationCredentialSpec": schema_openstack_resource_controller_v2_api_v1alpha1_ApplicationCredentialSpec(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ApplicationCredentialStatus": schema_openstack_resource_controller_v2_api_v1alpha1_ApplicationCredentialStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference": schema_openstack_resource_controller_v2_api_v1alpha1_CloudCredentialsReference(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZone": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZone(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneFilter": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneFilter(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneImport": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneImport(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneList": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneList(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneResourceSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneResourceStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneSpec": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneStatus": schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneStatus(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.Domain": schema_openstack_resource_controller_v2_api_v1alpha1_Domain(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DomainFilter": schema_openstack_resource_controller_v2_api_v1alpha1_DomainFilter(ref), "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DomainImport": schema_openstack_resource_controller_v2_api_v1alpha1_DomainImport(ref), @@ -1645,6 +1653,455 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_CloudCredentialsRefere } } +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZone(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZone 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.DNSZoneSpec"), + }, + }, + "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.DNSZoneStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneFilter defines an existing resource by its properties", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name of the existing resource", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Description: "email of the existing resource", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description of the existing resource", + Type: []string{"string"}, + Format: "", + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl of the existing resource", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type of the existing resource", + Type: []string{"string"}, + Format: "", + }, + }, + "masters": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "masters of the existing resource", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneImport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneImport 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.DNSZoneFilter"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneFilter"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneList contains a list of DNSZone.", + 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 DNSZone.", + 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.DNSZone"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZone", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneResourceSpec contains the desired state of the resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name will be the name of the created resource. If not specified, the name of the ORC object will be used.", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Description: "email is the email address of the administrator for the zone.", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl is the Time To Live for the zone in seconds.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is the type of the zone.", + Type: []string{"string"}, + Format: "", + }, + }, + "masters": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "masters specifies zone masters if this is a secondary zone.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneResourceStatus represents the observed state of the resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is a Human-readable name for the resource. Might not be unique.", + Type: []string{"string"}, + Format: "", + }, + }, + "email": { + SchemaProps: spec.SchemaProps{ + Description: "email is the email contact of the zone.", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl is the Time to Live for the zone in seconds.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is the type of the zone.", + Type: []string{"string"}, + Format: "", + }, + }, + "masters": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "masters specifies zone masters if this is a secondary zone.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "transferredAt": { + SchemaProps: spec.SchemaProps{ + Description: "transferredAt is the last time an update was retrieved from the master servers.", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Time"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "status is the status of the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneSpec 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.DNSZoneImport"), + }, + }, + "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.DNSZoneResourceSpec"), + }, + }, + "managementPolicy": { + SchemaProps: spec.SchemaProps{ + Description: "managementPolicy defines how ORC will treat the object. Valid values are `managed`: ORC will create, update, and delete the resource; `unmanaged`: ORC will import an existing resource, and will not apply updates to it or delete it.", + Type: []string{"string"}, + Format: "", + }, + }, + "managedOptions": { + SchemaProps: spec.SchemaProps{ + Description: "managedOptions specifies options which may be applied to managed objects.", + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"), + }, + }, + "cloudCredentialsRef": { + SchemaProps: spec.SchemaProps{ + Description: "cloudCredentialsRef points to a secret containing OpenStack credentials", + Default: map[string]interface{}{}, + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference"), + }, + }, + }, + Required: []string{"cloudCredentialsRef"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.CloudCredentialsReference", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneResourceSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZoneStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSZoneStatus 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.DNSZoneResourceStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSZoneResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_Domain(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 609bf9084..bc56d4ce5 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -70,6 +70,9 @@ type templateFields struct { } var resources []templateFields = []templateFields{ + { + Name: "DNSZone", + }, { Name: "Domain", }, diff --git a/config/crd/bases/openstack.k-orc.cloud_dnszones.yaml b/config/crd/bases/openstack.k-orc.cloud_dnszones.yaml new file mode 100644 index 000000000..7cc51717c --- /dev/null +++ b/config/crd/bases/openstack.k-orc.cloud_dnszones.yaml @@ -0,0 +1,394 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: dnszones.openstack.k-orc.cloud +spec: + group: openstack.k-orc.cloud + names: + categories: + - openstack + kind: DNSZone + listKind: DNSZoneList + plural: dnszones + singular: dnszone + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Resource ID + jsonPath: .status.id + name: ID + type: string + - description: Availability status of resource + jsonPath: .status.conditions[?(@.type=='Available')].status + name: Available + type: string + - description: Message describing current progress status + jsonPath: .status.conditions[?(@.type=='Progressing')].message + name: Message + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: DNSZone 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 + email: + description: email of the existing resource + format: email + maxLength: 255 + type: string + masters: + description: masters of the existing resource + items: + maxLength: 255 + type: string + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + name: + description: name of the existing resource + maxLength: 255 + minLength: 1 + pattern: ^[^,]+$ + type: string + x-kubernetes-validations: + - message: name must end with a period + rule: self.endsWith('.') + ttl: + description: ttl of the existing resource + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + type: + description: type of the existing resource + enum: + - PRIMARY + - SECONDARY + 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 + email: + description: email is the email address of the administrator for + the zone. + format: email + maxLength: 255 + type: string + masters: + description: masters specifies zone masters if this is a secondary + zone. + items: + maxLength: 255 + type: string + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + name: + description: |- + name will be the name of the created resource. If not specified, the + name of the ORC object will be used. + maxLength: 255 + minLength: 1 + pattern: ^[^,]+$ + type: string + x-kubernetes-validations: + - message: name is immutable + rule: self == oldSelf + - message: zone name must end with a period + rule: self.endsWith('.') + ttl: + description: ttl is the Time To Live for the zone in seconds. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + type: + default: PRIMARY + description: type is the type of the zone. + enum: + - PRIMARY + - SECONDARY + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: email is required for PRIMARY zones + rule: 'self.type == ''PRIMARY'' ? (has(self.email) && self.email + != "") : true' + - message: 'masters: required when type is SECONDARY' + rule: 'self.type == ''SECONDARY'' ? (has(self.masters) && self.masters.size() + > 0) : true' + - message: 'masters: must not be specified when type is PRIMARY' + rule: 'self.type == ''PRIMARY'' ? !has(self.masters) : true' + - message: 'email: must not be specified when type is SECONDARY' + rule: 'self.type == ''SECONDARY'' ? !has(self.email) : true' + required: + - cloudCredentialsRef + type: object + x-kubernetes-validations: + - message: resource must be specified when policy is managed + rule: 'self.managementPolicy == ''managed'' ? has(self.resource) : true' + - message: import may not be specified when policy is managed + rule: 'self.managementPolicy == ''managed'' ? !has(self.__import__) + : true' + - message: resource may not be specified when policy is unmanaged + rule: 'self.managementPolicy == ''unmanaged'' ? !has(self.resource) + : true' + - message: import must be specified when policy is unmanaged + rule: 'self.managementPolicy == ''unmanaged'' ? has(self.__import__) + : true' + - message: managedOptions may only be provided when policy is managed + rule: 'has(self.managedOptions) ? self.managementPolicy == ''managed'' + : true' + status: + description: status defines the observed state of the resource. + properties: + conditions: + description: |- + conditions represents the observed status of the object. + Known .status.conditions.type are: "Available", "Progressing" + + Available represents the availability of the OpenStack resource. If it is + true then the resource is ready for use. + + Progressing indicates whether the controller is still attempting to + reconcile the current state of the OpenStack resource to the desired + state. Progressing will be False either because the desired state has + been achieved, or because some terminal error prevents it from ever being + achieved and the controller is no longer attempting to reconcile. If + Progressing is True, an observer waiting on the resource should continue + to wait. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 32 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: id is the unique identifier of the OpenStack resource. + maxLength: 1024 + type: string + resource: + description: resource contains the observed state of the OpenStack + resource. + properties: + description: + description: description is a human-readable description for the + resource. + maxLength: 1024 + type: string + email: + description: email is the email contact of the zone. + maxLength: 1024 + type: string + masters: + description: masters specifies zone masters if this is a secondary + zone. + items: + maxLength: 255 + type: string + maxItems: 32 + type: array + x-kubernetes-list-type: atomic + name: + description: name is a Human-readable name for the resource. Might + not be unique. + maxLength: 1024 + type: string + status: + description: status is the status of the resource. + maxLength: 255 + type: string + transferredAt: + description: transferredAt is the last time an update was retrieved + from the master servers. + format: date-time + type: string + ttl: + description: ttl is the Time to Live for the zone in seconds. + format: int32 + type: integer + type: + description: type is the type of the zone. + maxLength: 255 + 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 26b47f63f..e7ce8490a 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,7 @@ resources: - bases/openstack.k-orc.cloud_addressscopes.yaml - bases/openstack.k-orc.cloud_applicationcredentials.yaml +- bases/openstack.k-orc.cloud_dnszones.yaml - bases/openstack.k-orc.cloud_domains.yaml - bases/openstack.k-orc.cloud_endpoints.yaml - bases/openstack.k-orc.cloud_flavors.yaml diff --git a/config/manifests/bases/orc.clusterserviceversion.yaml b/config/manifests/bases/orc.clusterserviceversion.yaml index 89421fc32..1d945fdf9 100644 --- a/config/manifests/bases/orc.clusterserviceversion.yaml +++ b/config/manifests/bases/orc.clusterserviceversion.yaml @@ -29,6 +29,11 @@ spec: kind: ApplicationCredential name: applicationcredentials.openstack.k-orc.cloud version: v1alpha1 + - description: DNSZone is the Schema for an ORC resource. + displayName: DNSZone + kind: DNSZone + name: dnszones.openstack.k-orc.cloud + version: v1alpha1 - description: Domain is the Schema for an ORC resource. displayName: Domain kind: Domain diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4991cff67..62656f592 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -19,6 +19,7 @@ rules: resources: - addressscopes - applicationcredentials + - dnszones - domains - endpoints - flavors @@ -55,6 +56,7 @@ rules: resources: - addressscopes/status - applicationcredentials/status + - dnszones/status - domains/status - endpoints/status - flavors/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 8a50ba039..647dbc076 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -3,6 +3,7 @@ resources: - openstack_v1alpha1_addressscope.yaml - openstack_v1alpha1_applicationcredential.yaml +- openstack_v1alpha1_dnszone.yaml - openstack_v1alpha1_domain.yaml - openstack_v1alpha1_endpoint.yaml - openstack_v1alpha1_flavor.yaml diff --git a/config/samples/openstack_v1alpha1_dnszone.yaml b/config/samples/openstack_v1alpha1_dnszone.yaml new file mode 100644 index 000000000..16ac6b3cc --- /dev/null +++ b/config/samples/openstack_v1alpha1_dnszone.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-sample +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: sample.example.com. + email: admin@example.com + description: Sample DNSZone + # TODO(scaffolding): Add all fields the resource supports diff --git a/examples/dnszone/dnszone-import.yaml b/examples/dnszone/dnszone-import.yaml new file mode 100644 index 000000000..df38f2cab --- /dev/null +++ b/examples/dnszone/dnszone-import.yaml @@ -0,0 +1,20 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + # id specifies the UUID of an existing DNS Zone in OpenStack Designate to import. + # Note that when specifying an import by ID, the resource MUST already exist. + id: "12345678-1234-1234-1234-1234567890ab" + + # Alternatively, you can import an existing DNS Zone by matching properties using filter. + # The filter must match exactly one DNS Zone, otherwise the controller will enter an error state. + # filter: + # name: existing-zone.example.com. + # email: admin@example.com + # ttl: 3600 diff --git a/examples/dnszone/dnszone-primary.yaml b/examples/dnszone/dnszone-primary.yaml new file mode 100644 index 000000000..2a23addcd --- /dev/null +++ b/examples/dnszone/dnszone-primary.yaml @@ -0,0 +1,24 @@ +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-primary +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: managed + resource: + # name specifies the name of the DNS Zone. Must end with a period. + # Defaults to the ORC object name if not specified. + # Immutable after creation. + name: primary.example.com. + # email is the email address of the administrator for the zone. + # Required for PRIMARY zones. + email: admin@example.com + # description is a human-readable description for the DNS Zone. + description: "Complete managed primary DNS zone example" + # ttl is the Time To Live for the zone in seconds. + ttl: 3600 + # type specifies the type of the zone. Can be 'PRIMARY' or 'SECONDARY'. + # Immutable after creation. + type: PRIMARY diff --git a/hack/bundle.sh b/hack/bundle.sh index 7f189b410..54c6c8a93 100755 --- a/hack/bundle.sh +++ b/hack/bundle.sh @@ -2,7 +2,7 @@ REGISTRY=${REGISTRY:-quay.io/orc} IMAGE=${BASE_IMAGE:-openstack-resource-controller} -TAG=${BASE_IMAGE:-$(git describe --abbrev=0 --tags)} +TAG=${TAG:-$(git describe --abbrev=0 --tags 2>/dev/null || echo "v0.0.1")} IMG=${REGISTRY}/${IMAGE}:${TAG} # Update config/manifests/bases/orc.clusterserviceversion.yaml if needed diff --git a/internal/controllers/dnszone/actuator.go b/internal/controllers/dnszone/actuator.go new file mode 100644 index 000000000..d6fcb9761 --- /dev/null +++ b/internal/controllers/dnszone/actuator.go @@ -0,0 +1,348 @@ +/* +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 dnszone + +import ( + "context" + "iter" + + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/zones" + 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" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" +) + +// OpenStack resource types +type ( + osResourceT = zones.Zone + + 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 dnsZoneActuator struct { + osClient osclients.DNSZoneClient + k8sClient client.Client +} + +var _ createResourceActuator = dnsZoneActuator{} +var _ deleteResourceActuator = dnsZoneActuator{} + +func (dnsZoneActuator) GetResourceID(osResource *osResourceT) string { + return osResource.ID +} + +func (actuator dnsZoneActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { + resource, err := actuator.osClient.GetZone(ctx, id) + if err != nil { + return nil, progress.WrapError(err) + } + return resource, nil +} + +func (actuator dnsZoneActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) { + resourceSpec := orcObject.Spec.Resource + if resourceSpec == nil { + return nil, false + } + + var filters []osclients.ResourceFilter[osResourceT] + + if resourceSpec.Description != nil { + filters = append(filters, func(f *zones.Zone) bool { + return f.Description == *resourceSpec.Description + }) + } else { + filters = append(filters, func(f *zones.Zone) bool { + return f.Description == "" + }) + } + if resourceSpec.Email != nil { + filters = append(filters, func(f *zones.Zone) bool { + return f.Email == *resourceSpec.Email + }) + } else { + filters = append(filters, func(f *zones.Zone) bool { + return f.Email == "" + }) + } + if resourceSpec.TTL != nil { + filters = append(filters, func(f *zones.Zone) bool { + return f.TTL == int(*resourceSpec.TTL) + }) + } + filters = append(filters, func(f *zones.Zone) bool { + return f.Type == string(resourceSpec.Type) + }) + if len(resourceSpec.Masters) > 0 { + filters = append(filters, func(f *zones.Zone) bool { + if len(f.Masters) != len(resourceSpec.Masters) { + return false + } + for i, m := range f.Masters { + if m != resourceSpec.Masters[i] { + return false + } + } + return true + }) + } else { + filters = append(filters, func(f *zones.Zone) bool { + return len(f.Masters) == 0 + }) + } + + listOpts := zones.ListOpts{ + Name: getDNSZoneName(orcObject), + } + + return actuator.listOSResources(ctx, filters, listOpts), true +} + +func (actuator dnsZoneActuator) ListOSResourcesForImport(ctx context.Context, obj orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { + var filters []osclients.ResourceFilter[osResourceT] + + if filter.Name != nil { + filters = append(filters, func(f *zones.Zone) bool { return f.Name == string(*filter.Name) }) + } + if filter.Email != nil { + filters = append(filters, func(f *zones.Zone) bool { return f.Email == *filter.Email }) + } + if filter.Description != nil { + filters = append(filters, func(f *zones.Zone) bool { return f.Description == *filter.Description }) + } + if filter.TTL != nil { + filters = append(filters, func(f *zones.Zone) bool { return f.TTL == int(*filter.TTL) }) + } + if filter.Type != nil { + filters = append(filters, func(f *zones.Zone) bool { return f.Type == string(*filter.Type) }) + } + if len(filter.Masters) > 0 { + filters = append(filters, func(f *zones.Zone) bool { + if len(f.Masters) != len(filter.Masters) { + return false + } + for i, m := range f.Masters { + if m != filter.Masters[i] { + return false + } + } + return true + }) + } + + listOpts := zones.ListOpts{} + if filter.Name != nil { + listOpts.Name = string(*filter.Name) + } + + return actuator.listOSResources(ctx, filters, listOpts), nil +} + +func (actuator dnsZoneActuator) listOSResources(ctx context.Context, filters []osclients.ResourceFilter[osResourceT], listOpts zones.ListOptsBuilder) iter.Seq2[*zones.Zone, error] { + zones := actuator.osClient.ListZones(ctx, listOpts) + return osclients.Filter(zones, filters...) +} + +func (actuator dnsZoneActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) { + resource := obj.Spec.Resource + + if resource == nil { + // Should have been caught by API validation + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set")) + } + createOpts := zones.CreateOpts{ + Name: getDNSZoneName(obj), + Email: ptr.Deref(resource.Email, ""), + Description: ptr.Deref(resource.Description, ""), + Type: string(resource.Type), + Masters: resource.Masters, + } + if resource.TTL != nil { + createOpts.TTL = int(*resource.TTL) + } + + osResource, err := actuator.osClient.CreateZone(ctx, createOpts) + if err != nil { + if !orcerrors.IsRetryable(err) { + reason := orcv1alpha1.ConditionReasonInvalidConfiguration + if orcerrors.IsConflict(err) { + reason = orcv1alpha1.ConditionReasonUnrecoverableError + } + err = orcerrors.Terminal(reason, "invalid configuration creating resource: "+err.Error(), err) + } + return nil, progress.WrapError(err) + } + + return osResource, nil +} + +func (actuator dnsZoneActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus { + return progress.WrapError(actuator.osClient.DeleteZone(ctx, resource.ID)) +} + +func (actuator dnsZoneActuator) 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")) + } + + updateOpts := zones.UpdateOpts{} + + handleDescriptionUpdate(&updateOpts, resource, osResource) + handleEmailUpdate(&updateOpts, resource, osResource) + handleTTLUpdate(&updateOpts, resource, osResource) + handleMastersUpdate(&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.UpdateZone(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 zones.UpdateOpts) (bool, error) { + updateOptsMap, err := updateOpts.ToZoneUpdateMap() + if err != nil { + return false, err + } + + return len(updateOptsMap) > 0, nil +} + +func handleDescriptionUpdate(updateOpts *zones.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + description := ptr.Deref(resource.Description, "") + if osResource.Description != description { + updateOpts.Description = &description + } +} + +func handleEmailUpdate(updateOpts *zones.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + email := ptr.Deref(resource.Email, "") + if osResource.Email != email { + updateOpts.Email = email + } +} + +func handleMastersUpdate(updateOpts *zones.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + mastersMatch := true + if len(osResource.Masters) != len(resource.Masters) { + mastersMatch = false + } else { + for i, m := range osResource.Masters { + if m != resource.Masters[i] { + mastersMatch = false + break + } + } + } + if !mastersMatch { + updateOpts.Masters = resource.Masters + } +} + +func handleTTLUpdate(updateOpts *zones.UpdateOpts, resource *resourceSpecT, osResource *osResourceT) { + if resource.TTL != nil { + ttl := int(*resource.TTL) + if osResource.TTL != ttl { + updateOpts.TTL = ttl + } + } +} + +func (actuator dnsZoneActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { + return []resourceReconciler{ + actuator.updateResource, + }, nil +} + +type dnszoneHelperFactory struct{} + +var _ helperFactory = dnszoneHelperFactory{} + +func newActuator(ctx context.Context, orcObject *orcv1alpha1.DNSZone, controller interfaces.ResourceController) (dnsZoneActuator, 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 dnsZoneActuator{}, reconcileStatus + } + + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) + if err != nil { + return dnsZoneActuator{}, progress.WrapError(err) + } + osClient, err := clientScope.NewDNSZoneClient() + if err != nil { + return dnsZoneActuator{}, progress.WrapError(err) + } + + return dnsZoneActuator{ + osClient: osClient, + k8sClient: controller.GetK8sClient(), + }, nil +} + +func (dnszoneHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) adapterI { + return dnszoneAdapter{obj} +} + +func (dnszoneHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (createResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} + +func (dnszoneHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (deleteResourceActuator, progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller) +} + +func getDNSZoneName(orcObject orcObjectPT) string { + name := getResourceName(orcObject) + if name != "" && name[len(name)-1] != '.' { + return name + "." + } + return name +} diff --git a/internal/controllers/dnszone/actuator_test.go b/internal/controllers/dnszone/actuator_test.go new file mode 100644 index 000000000..a0d05b05b --- /dev/null +++ b/internal/controllers/dnszone/actuator_test.go @@ -0,0 +1,795 @@ +/* +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 dnszone + +import ( + "context" + "errors" + "iter" + "testing" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/zones" + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/v2/internal/osclients/mock" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" + "go.uber.org/mock/gomock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +var ( + errTest = errors.New("test error") +) + +const testZoneName = "example.com." + +func mockListZones(zonesList []zones.Zone) iter.Seq2[*zones.Zone, error] { + return func(yield func(*zones.Zone, error) bool) { + for i := range zonesList { + if !yield(&zonesList[i], nil) { + return + } + } + } +} + +type zoneResult struct { + zone *zones.Zone + err error +} + +func TestGetResourceID(t *testing.T) { + actuator := dnsZoneActuator{} + zone := &zones.Zone{ID: "test-zone-id"} + if got := actuator.GetResourceID(zone); got != "test-zone-id" { + t.Errorf("Expected test-zone-id, got %s", got) + } +} + +func TestGetOSResourceByID(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSZoneClient(mockctrl) + + mockClient.EXPECT().GetZone(ctx, "found").Return(&zones.Zone{ID: "found", Name: testZoneName}, nil) + mockClient.EXPECT().GetZone(ctx, "notfound").Return(nil, errTest) + + actuator := dnsZoneActuator{osClient: mockClient} + + // Case 1: success + res, status := actuator.GetOSResourceByID(ctx, "found") + if status != nil { + t.Errorf("Expected nil status, got %v", status) + } + if res == nil || res.ID != "found" { + t.Errorf("Expected zone with ID 'found', got %v", res) + } + + // Case 2: error + res, status = actuator.GetOSResourceByID(ctx, "notfound") + if status == nil { + t.Errorf("Expected error status, got nil") + } + if res != nil { + t.Errorf("Expected nil zone, got %v", res) + } +} + +func TestListOSResourcesForAdoption(t *testing.T) { + for _, tc := range [...]struct { + name string + resourceSpec orcv1alpha1.DNSZoneResourceSpec + zones []zones.Zone + expectCount int + expectIDs []string + }{ + { + name: "exact match", + resourceSpec: orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("admin@example.com"), + Description: ptr.To("desc"), + TTL: ptr.To[int32](3600), + Type: orcv1alpha1.DNSZoneTypePrimary, + }, + zones: []zones.Zone{ + {ID: "1", Name: testZoneName, Email: "admin@example.com", Description: "desc", TTL: 3600, Type: "PRIMARY"}, + {ID: "2", Name: testZoneName, Email: "other@example.com", Description: "desc", TTL: 3600, Type: "PRIMARY"}, + }, + expectCount: 1, + expectIDs: []string{"1"}, + }, + { + name: "no spec description, matches empty description", + resourceSpec: orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("admin@example.com"), + Type: orcv1alpha1.DNSZoneTypePrimary, + }, + zones: []zones.Zone{ + {ID: "1", Name: testZoneName, Email: "admin@example.com", Description: "", Type: "PRIMARY"}, + {ID: "2", Name: testZoneName, Email: "admin@example.com", Description: "some-desc", Type: "PRIMARY"}, + }, + expectCount: 1, + expectIDs: []string{"1"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSZoneClient(mockctrl) + + mockClient.EXPECT().ListZones(ctx, zones.ListOpts{Name: testZoneName}).Return(mockListZones(tc.zones)) + + actuator := dnsZoneActuator{osClient: mockClient} + + obj := &orcv1alpha1.DNSZone{ + ObjectMeta: metav1.ObjectMeta{ + Name: testZoneName, + }, + Spec: orcv1alpha1.DNSZoneSpec{ + Resource: &tc.resourceSpec, + }, + } + + iter, ok := actuator.ListOSResourcesForAdoption(ctx, obj) + if !ok { + t.Fatalf("Expected ok to be true") + } + + var results []zoneResult + for zone, err := range iter { + results = append(results, zoneResult{zone, err}) + } + + if len(results) != tc.expectCount { + t.Errorf("Expected %d results, got %d", tc.expectCount, len(results)) + } + + for i, id := range tc.expectIDs { + if i < len(results) && results[i].zone.ID != id { + t.Errorf("Expected ID %s, got %s", id, results[i].zone.ID) + } + } + }) + } +} + +func TestListOSResourcesForAdoption_NilSpec(t *testing.T) { + ctx := context.Background() + actuator := dnsZoneActuator{} + _, ok := actuator.ListOSResourcesForAdoption(ctx, &orcv1alpha1.DNSZone{}) + if ok { + t.Errorf("Expected ok to be false with nil spec") + } +} + +func TestListOSResourcesForImport(t *testing.T) { + for _, tc := range [...]struct { + name string + filter orcv1alpha1.DNSZoneFilter + zones []zones.Zone + expectCount int + expectIDs []string + expectedOpts zones.ListOpts + }{ + { + name: "match name and email", + filter: orcv1alpha1.DNSZoneFilter{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("admin@example.com"), + }, + zones: []zones.Zone{ + {ID: "1", Name: testZoneName, Email: "admin@example.com"}, + {ID: "2", Name: testZoneName, Email: "other@example.com"}, + }, + expectCount: 1, + expectIDs: []string{"1"}, + expectedOpts: zones.ListOpts{Name: testZoneName}, + }, + { + name: "match TTL and Type", + filter: orcv1alpha1.DNSZoneFilter{ + TTL: ptr.To[int32](1800), + Type: ptr.To(orcv1alpha1.DNSZoneTypePrimary), + }, + zones: []zones.Zone{ + {ID: "1", Name: testZoneName, TTL: 1800, Type: "PRIMARY"}, + {ID: "2", Name: testZoneName, TTL: 3600, Type: "PRIMARY"}, + {ID: "3", Name: testZoneName, TTL: 1800, Type: "SECONDARY"}, + }, + expectCount: 1, + expectIDs: []string{"1"}, + expectedOpts: zones.ListOpts{}, + }, + { + name: "match description", + filter: orcv1alpha1.DNSZoneFilter{ + Description: ptr.To("special zone"), + }, + zones: []zones.Zone{ + {ID: "1", Name: testZoneName, Description: "special zone"}, + {ID: "2", Name: testZoneName, Description: "other zone"}, + }, + expectCount: 1, + expectIDs: []string{"1"}, + expectedOpts: zones.ListOpts{}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSZoneClient(mockctrl) + + mockClient.EXPECT().ListZones(ctx, tc.expectedOpts).Return(mockListZones(tc.zones)) + + actuator := dnsZoneActuator{osClient: mockClient} + + iter, status := actuator.ListOSResourcesForImport(ctx, &orcv1alpha1.DNSZone{}, tc.filter) + if status != nil { + t.Fatalf("Expected nil status, got %v", status) + } + + var results []zoneResult + for zone, err := range iter { + results = append(results, zoneResult{zone, err}) + } + + if len(results) != tc.expectCount { + t.Errorf("Expected %d results, got %d", tc.expectCount, len(results)) + } + + for i, id := range tc.expectIDs { + if i < len(results) && results[i].zone.ID != id { + t.Errorf("Expected ID %s, got %s", id, results[i].zone.ID) + } + } + }) + } +} + +func TestCreateResource(t *testing.T) { + ctx := context.Background() + + obj := &orcv1alpha1.DNSZone{ + Spec: orcv1alpha1.DNSZoneSpec{ + Resource: &orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("admin@example.com"), + Description: ptr.To("desc"), + TTL: ptr.To[int32](3600), + Type: orcv1alpha1.DNSZoneTypePrimary, + }, + }, + } + + expectedCreateOpts := zones.CreateOpts{ + Name: testZoneName, + Email: "admin@example.com", + Description: "desc", + Type: "PRIMARY", + TTL: 3600, + } + + // Case 1: Success + { + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + mockClient.EXPECT().CreateZone(ctx, expectedCreateOpts).Return(&zones.Zone{ + ID: "created-id", + Name: testZoneName, + Email: "admin@example.com", + Description: "desc", + TTL: 3600, + Type: "PRIMARY", + }, nil) + + actuator := dnsZoneActuator{osClient: mockClient} + res, status := actuator.CreateResource(ctx, obj) + if status != nil { + t.Fatalf("Expected nil status, got %v", status) + } + if res.ID != "created-id" || res.Name != testZoneName || res.Email != "admin@example.com" || res.Description != "desc" || res.TTL != 3600 || res.Type != "PRIMARY" { + t.Errorf("Created resource does not match: %v", res) + } + mockctrl.Finish() + } + + // Case 2: Conflict (already exists) + { + conflictErr := gophercloud.ErrUnexpectedResponseCode{ + URL: "http://designate/zones", + Method: "POST", + Expected: []int{201}, + Actual: 409, + Body: []byte(`{"message": "Zone already exists"}`), + } + + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + mockClient.EXPECT().CreateZone(ctx, expectedCreateOpts).Return(nil, conflictErr) + + actuatorConflict := dnsZoneActuator{osClient: mockClient} + _, status := actuatorConflict.CreateResource(ctx, obj) + if status == nil { + t.Fatalf("Expected non-nil status on conflict") + } + needsReschedule, err := status.NeedsReschedule() + if !needsReschedule { + t.Errorf("Expected needsReschedule on error") + } + if err == nil { + t.Errorf("Expected error from status, got nil") + } + if !orcerrors.IsConflict(err) { + t.Errorf("Expected conflict error, got %v", err) + } + if orcerrors.IsRetryable(err) { + t.Errorf("Expected conflict error to be terminal (not retryable)") + } + var terminalError *orcerrors.TerminalError + if !errors.As(err, &terminalError) { + t.Errorf("Expected error to contain a *TerminalError, got %T", err) + } else if terminalError.Reason != string(orcv1alpha1.ConditionReasonUnrecoverableError) { + t.Errorf("Expected TerminalError reason %s, got %s", orcv1alpha1.ConditionReasonUnrecoverableError, terminalError.Reason) + } + mockctrl.Finish() + } + + // Case 3: Other errors (transient API errors) + { + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + mockClient.EXPECT().CreateZone(ctx, expectedCreateOpts).Return(nil, errTest) + + actuatorError := dnsZoneActuator{osClient: mockClient} + _, status := actuatorError.CreateResource(ctx, obj) + if status == nil { + t.Fatalf("Expected non-nil status on generic API error") + } + needsReschedule, err := status.NeedsReschedule() + if !needsReschedule { + t.Errorf("Expected needsReschedule to be true") + } + if err == nil { + t.Errorf("Expected error from status, got nil") + } + if !errors.Is(err, errTest) { + t.Errorf("Expected error %v, got %v", errTest, err) + } + mockctrl.Finish() + } +} + +func TestCreateResource_NilSpec(t *testing.T) { + ctx := context.Background() + actuator := dnsZoneActuator{} + _, status := actuator.CreateResource(ctx, &orcv1alpha1.DNSZone{}) + if status == nil { + t.Fatalf("Expected status to be non-nil when resource is nil") + } + err := status.GetError() + if err == nil { + t.Fatalf("Expected error when resource is nil") + } + var terminalError *orcerrors.TerminalError + if !errors.As(err, &terminalError) { + t.Errorf("Expected error to be a terminal error, got %T", err) + } +} + +func TestDeleteResource(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSZoneClient(mockctrl) + + mockClient.EXPECT().DeleteZone(ctx, "delete-me").Return(nil) + + actuator := dnsZoneActuator{osClient: mockClient} + zone := &zones.Zone{ID: "delete-me"} + + status := actuator.DeleteResource(ctx, &orcv1alpha1.DNSZone{}, zone) + if status != nil { + t.Errorf("Expected nil status, got %v", status) + } +} + +func TestUpdateResource(t *testing.T) { + ctx := context.Background() + + obj := &orcv1alpha1.DNSZone{ + Spec: orcv1alpha1.DNSZoneSpec{ + Resource: &orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("new-admin@example.com"), + Description: ptr.To("new-desc"), + TTL: ptr.To[int32](7200), + Type: orcv1alpha1.DNSZoneTypePrimary, + }, + }, + } + osResource := &zones.Zone{ + ID: "zone-id", + Name: testZoneName, + Email: "admin@example.com", + Description: "desc", + TTL: 3600, + Type: "PRIMARY", + } + + expectedUpdateOpts := zones.UpdateOpts{ + Email: "new-admin@example.com", + Description: ptr.To("new-desc"), + TTL: 7200, + } + + // Case 1: Progress (change requires update) + { + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + mockClient.EXPECT().UpdateZone(ctx, "zone-id", expectedUpdateOpts).Return(&zones.Zone{ID: "zone-id"}, nil) + + actuator := dnsZoneActuator{osClient: mockClient} + status := actuator.updateResource(ctx, obj, osResource) + if status == nil { + t.Fatalf("Expected progress status, got nil") + } + needsReschedule, err := status.NeedsReschedule() + if !needsReschedule { + t.Errorf("Expected needsReschedule to be true") + } + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + mockctrl.Finish() + } + + // Case 2: No change (no update call should be made) + { + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + // Expect no call to UpdateZone + + actuator := dnsZoneActuator{osClient: mockClient} + unchangedObj := &orcv1alpha1.DNSZone{ + Spec: orcv1alpha1.DNSZoneSpec{ + Resource: &orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + Email: ptr.To("admin@example.com"), + Description: ptr.To("desc"), + TTL: ptr.To[int32](3600), + Type: orcv1alpha1.DNSZoneTypePrimary, + }, + }, + } + status := actuator.updateResource(ctx, unchangedObj, osResource) + if status != nil { + t.Errorf("Expected nil status when no update needed, got %v", status) + } + mockctrl.Finish() + } + + // Case 3: Error during update (transient error) + { + mockctrl := gomock.NewController(t) + mockClient := mock.NewMockDNSZoneClient(mockctrl) + mockClient.EXPECT().UpdateZone(ctx, "zone-id", expectedUpdateOpts).Return(nil, errTest) + + actuator := dnsZoneActuator{osClient: mockClient} + status := actuator.updateResource(ctx, obj, osResource) + if status == nil { + t.Fatalf("Expected progress status on error, got nil") + } + needsReschedule, err := status.NeedsReschedule() + if !needsReschedule { + t.Errorf("Expected needsReschedule to be true") + } + if !errors.Is(err, errTest) { + t.Errorf("Expected error %v, got %v", errTest, err) + } + mockctrl.Finish() + } +} + +func TestUpdateResource_NilSpec(t *testing.T) { + ctx := context.Background() + actuator := dnsZoneActuator{} + status := actuator.updateResource(ctx, &orcv1alpha1.DNSZone{}, &zones.Zone{}) + if status == nil { + t.Fatalf("Expected status to be non-nil when resource is nil") + } + err := status.GetError() + if err == nil { + t.Fatalf("Expected error when resource is nil") + } + var terminalError *orcerrors.TerminalError + if !errors.As(err, &terminalError) { + t.Errorf("Expected error to be a terminal error, got %T", err) + } +} + +func TestNeedsUpdate(t *testing.T) { + testCases := []struct { + name string + updateOpts zones.UpdateOpts + expectChange bool + }{ + { + name: "Empty base opts", + updateOpts: zones.UpdateOpts{}, + expectChange: false, + }, + { + name: "Updated opts", + updateOpts: zones.UpdateOpts{Description: ptr.To("updated")}, + 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.DNSZoneResourceSpec{Description: tt.newValue} + osResource := &osResourceT{Description: tt.existingValue} + + updateOpts := zones.UpdateOpts{} + handleDescriptionUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + + } +} + +func TestHandleEmailUpdate(t *testing.T) { + testCases := []struct { + name string + newValue string + existingValue string + expectChange bool + }{ + {name: "Identical", newValue: "admin@example.com", existingValue: "admin@example.com", expectChange: false}, + {name: "Different", newValue: "new-admin@example.com", existingValue: "admin@example.com", expectChange: true}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + resource := &orcv1alpha1.DNSZoneResourceSpec{Email: ptr.To(tt.newValue)} + osResource := &osResourceT{Email: tt.existingValue} + + updateOpts := zones.UpdateOpts{} + handleEmailUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + } +} + +func TestHandleTTLUpdate(t *testing.T) { + testCases := []struct { + name string + newValue *int32 + existingValue int + expectChange bool + }{ + {name: "Identical", newValue: ptr.To[int32](3600), existingValue: 3600, expectChange: false}, + {name: "Different", newValue: ptr.To[int32](1800), existingValue: 3600, expectChange: true}, + {name: "Nil value", newValue: nil, existingValue: 3600, expectChange: false}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + resource := &orcv1alpha1.DNSZoneResourceSpec{TTL: tt.newValue} + osResource := &osResourceT{TTL: tt.existingValue} + + updateOpts := zones.UpdateOpts{} + handleTTLUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + } +} + +func TestHandleMastersUpdate(t *testing.T) { + testCases := []struct { + name string + newValue []string + existingValue []string + expectChange bool + }{ + {name: "Identical", newValue: []string{"1.2.3.4"}, existingValue: []string{"1.2.3.4"}, expectChange: false}, + {name: "Different length", newValue: []string{"1.2.3.4", "5.6.7.8"}, existingValue: []string{"1.2.3.4"}, expectChange: true}, + {name: "Different value", newValue: []string{"1.2.3.4"}, existingValue: []string{"5.6.7.8"}, expectChange: true}, + {name: "Both empty", newValue: nil, existingValue: nil, expectChange: false}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + resource := &orcv1alpha1.DNSZoneResourceSpec{Masters: tt.newValue} + osResource := &osResourceT{Masters: tt.existingValue} + + updateOpts := zones.UpdateOpts{} + handleMastersUpdate(&updateOpts, resource, osResource) + + got, _ := needsUpdate(updateOpts) + if got != tt.expectChange { + t.Errorf("Expected change: %v, got: %v", tt.expectChange, got) + } + }) + } +} + +func TestGetResourceReconcilers(t *testing.T) { + actuator := dnsZoneActuator{} + reconcilers, status := actuator.GetResourceReconcilers(context.Background(), &orcv1alpha1.DNSZone{}, &zones.Zone{}, nil) + if status != nil { + t.Errorf("Expected nil status, got %v", status) + } + if len(reconcilers) != 1 { + t.Errorf("Expected 1 reconciler, got %d", len(reconcilers)) + } +} + +func TestHelperFactory_NewAPIObjectAdapter(t *testing.T) { + factory := dnszoneHelperFactory{} + obj := &orcv1alpha1.DNSZone{ + Spec: orcv1alpha1.DNSZoneSpec{ + ManagementPolicy: orcv1alpha1.ManagementPolicyManaged, + ManagedOptions: &orcv1alpha1.ManagedOptions{ + OnDelete: orcv1alpha1.OnDeleteDelete, + }, + Resource: &orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + }, + Import: &orcv1alpha1.DNSZoneImport{ + ID: ptr.To("imported-id"), + Filter: &orcv1alpha1.DNSZoneFilter{ + Name: ptr.To[orcv1alpha1.OpenStackName](testZoneName), + }, + }, + }, + Status: orcv1alpha1.DNSZoneStatus{ + ID: ptr.To("status-id"), + }, + } + adapter := factory.NewAPIObjectAdapter(obj) + if adapter.GetObject() != obj { + t.Errorf("Expected GetObject to return the original object") + } + if adapter.GetManagementPolicy() != orcv1alpha1.ManagementPolicyManaged { + t.Errorf("Expected GetManagementPolicy to match") + } + if adapter.GetManagedOptions().OnDelete != orcv1alpha1.OnDeleteDelete { + t.Errorf("Expected GetManagedOptions to match") + } + if *adapter.GetStatusID() != "status-id" { + t.Errorf("Expected GetStatusID to return 'status-id'") + } + if adapter.GetResourceSpec().Name == nil || string(*adapter.GetResourceSpec().Name) != testZoneName { + t.Errorf("Expected GetResourceSpec Name to match") + } + if *adapter.GetImportID() != "imported-id" { + t.Errorf("Expected GetImportID to return 'imported-id'") + } + if adapter.GetImportFilter().Name == nil || string(*adapter.GetImportFilter().Name) != testZoneName { + t.Errorf("Expected GetImportFilter Name to match") + } +} + +func TestHelperFactory_NewAPIObjectAdapter_NilImport(t *testing.T) { + factory := dnszoneHelperFactory{} + obj := &orcv1alpha1.DNSZone{ + Spec: orcv1alpha1.DNSZoneSpec{}, + } + adapter := factory.NewAPIObjectAdapter(obj) + if adapter.GetImportID() != nil { + t.Errorf("Expected GetImportID to be nil") + } + if adapter.GetImportFilter() != nil { + t.Errorf("Expected GetImportFilter to be nil") + } +} + +func TestGetDNSZoneName(t *testing.T) { + testCases := []struct { + name string + specName *string + objName string + expectedName string + }{ + { + name: "spec name ends with dot", + specName: ptr.To("example.com."), + objName: "my-dnszone", + expectedName: "example.com.", + }, + { + name: "spec name has no dot", + specName: ptr.To("example.com"), + objName: "my-dnszone", + expectedName: "example.com.", + }, + { + name: "fallback to obj name with dot", + specName: nil, + objName: "example.org.", + expectedName: "example.org.", + }, + { + name: "fallback to obj name without dot", + specName: nil, + objName: "example.org", + expectedName: "example.org.", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + obj := &orcv1alpha1.DNSZone{} + obj.Name = tt.objName + if tt.specName != nil { + obj.Spec.Resource = &orcv1alpha1.DNSZoneResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName](orcv1alpha1.OpenStackName(*tt.specName)), + } + } else { + obj.Spec.Resource = &orcv1alpha1.DNSZoneResourceSpec{} + } + + got := getDNSZoneName(obj) + if got != tt.expectedName { + t.Errorf("Expected name %q, got %q", tt.expectedName, got) + } + }) + } +} diff --git a/internal/controllers/dnszone/controller.go b/internal/controllers/dnszone/controller.go new file mode 100644 index 000000000..1e7466748 --- /dev/null +++ b/internal/controllers/dnszone/controller.go @@ -0,0 +1,68 @@ +/* +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 dnszone + +import ( + "context" + "errors" + + ctrl "sigs.k8s.io/controller-runtime" + "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" +) + +const controllerName = "dnszone" + +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=dnszones,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=dnszones/status,verbs=get;update;patch + +type dnszoneReconcilerConstructor struct { + scopeFactory scope.Factory +} + +func New(scopeFactory scope.Factory) interfaces.Controller { + return dnszoneReconcilerConstructor{scopeFactory: scopeFactory} +} + +func (dnszoneReconcilerConstructor) GetName() string { + return controllerName +} + +// SetupWithManager sets up the controller with the Manager. +func (c dnszoneReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) + + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&orcv1alpha1.DNSZone{}) + + if err := errors.Join( + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, mgr.GetClient(), builder, credentialsDependency), + ); err != nil { + return err + } + + r := reconciler.NewController(controllerName, mgr.GetClient(), c.scopeFactory, dnszoneHelperFactory{}, dnsZoneStatusWriter{}) + return builder.Complete(&r) +} diff --git a/internal/controllers/dnszone/status.go b/internal/controllers/dnszone/status.go new file mode 100644 index 000000000..fd5f38798 --- /dev/null +++ b/internal/controllers/dnszone/status.go @@ -0,0 +1,108 @@ +/* +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 dnszone + +import ( + "time" + + "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" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" + orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" +) + +const ( + ZoneStatusActive = "ACTIVE" + ZoneStatusPending = "PENDING" + ZoneStatusError = "ERROR" + + // The time to wait before reconciling again when we are expecting OpenStack to finish some task and update status. + externalUpdatePollingPeriod = 15 * time.Second +) + +type dnsZoneStatusWriter struct{} + +type objectApplyT = orcapplyconfigv1alpha1.DNSZoneApplyConfiguration +type statusApplyT = orcapplyconfigv1alpha1.DNSZoneStatusApplyConfiguration + +var _ interfaces.ResourceStatusWriter[*orcv1alpha1.DNSZone, *osResourceT, *objectApplyT, *statusApplyT] = dnsZoneStatusWriter{} + +func (dnsZoneStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT { + return orcapplyconfigv1alpha1.DNSZone(name, namespace) +} + +func (dnsZoneStatusWriter) ResourceAvailableStatus(orcObject *orcv1alpha1.DNSZone, osResource *osResourceT) (metav1.ConditionStatus, progress.ReconcileStatus) { + if osResource == nil { + if orcObject.Status.ID == nil { + return metav1.ConditionFalse, nil + } else { + return metav1.ConditionUnknown, nil + } + } + + switch osResource.Status { + case ZoneStatusActive: + return metav1.ConditionTrue, nil + case ZoneStatusPending: + return metav1.ConditionFalse, progress.WaitingOnOpenStack(progress.WaitingOnReady, externalUpdatePollingPeriod) + case ZoneStatusError: + return metav1.ConditionFalse, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "OpenStack zone is in ERROR status")) + default: + // Fallback for any other/unexpected status + return metav1.ConditionFalse, progress.WaitingOnOpenStack(progress.WaitingOnReady, externalUpdatePollingPeriod) + } +} + +func (dnsZoneStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { + resourceStatus := orcapplyconfigv1alpha1.DNSZoneResourceStatus(). + WithName(osResource.Name) + + if osResource.Email != "" { + resourceStatus.WithEmail(osResource.Email) + } + + if osResource.Description != "" { + resourceStatus.WithDescription(osResource.Description) + } + + if osResource.TTL > 0 { + resourceStatus.WithTTL(int32(osResource.TTL)) + } + + if osResource.Type != "" { + resourceStatus.WithType(osResource.Type) + } + + if len(osResource.Masters) > 0 { + resourceStatus.WithMasters(osResource.Masters...) + } + + if !osResource.TransferredAt.IsZero() { + resourceStatus.WithTransferredAt(metav1.NewTime(osResource.TransferredAt)) + } + + if osResource.Status != "" { + resourceStatus.WithStatus(osResource.Status) + } + + statusApply.WithResource(resourceStatus) +} diff --git a/internal/controllers/dnszone/status_test.go b/internal/controllers/dnszone/status_test.go new file mode 100644 index 000000000..b87042589 --- /dev/null +++ b/internal/controllers/dnszone/status_test.go @@ -0,0 +1,226 @@ +/* +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 dnszone + +import ( + "errors" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/zones" + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" + orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" +) + +func TestResourceAvailableStatus(t *testing.T) { + writer := dnsZoneStatusWriter{} + + tests := []struct { + name string + orcObject *orcv1alpha1.DNSZone + osResource *zones.Zone + expectedStatus metav1.ConditionStatus + expectRequeue time.Duration + expectTerminal bool + }{ + { + name: "osResource is nil, Status.ID is nil", + orcObject: &orcv1alpha1.DNSZone{ + Status: orcv1alpha1.DNSZoneStatus{ + ID: nil, + }, + }, + osResource: nil, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "osResource is nil, Status.ID is set", + orcObject: &orcv1alpha1.DNSZone{ + Status: orcv1alpha1.DNSZoneStatus{ + ID: ptr.To("some-id"), + }, + }, + osResource: nil, + expectedStatus: metav1.ConditionUnknown, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "zone is ACTIVE", + orcObject: &orcv1alpha1.DNSZone{}, + osResource: &zones.Zone{Status: "ACTIVE"}, + expectedStatus: metav1.ConditionTrue, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "zone is PENDING", + orcObject: &orcv1alpha1.DNSZone{}, + osResource: &zones.Zone{Status: "PENDING"}, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 15 * time.Second, + expectTerminal: false, + }, + { + name: "zone is ERROR", + orcObject: &orcv1alpha1.DNSZone{}, + osResource: &zones.Zone{Status: "ERROR"}, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 0, + expectTerminal: true, + }, + { + name: "zone has unknown status", + orcObject: &orcv1alpha1.DNSZone{}, + osResource: &zones.Zone{Status: "UNKNOWN_STATUS"}, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 15 * time.Second, + expectTerminal: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, rs := writer.ResourceAvailableStatus(tt.orcObject, tt.osResource) + if status != tt.expectedStatus { + t.Errorf("expected status %v, got %v", tt.expectedStatus, status) + } + + if rs == nil { + if tt.expectRequeue != 0 || tt.expectTerminal { + t.Errorf("expected non-nil ReconcileStatus") + } + return + } + + if rs.GetRequeue() != tt.expectRequeue { + t.Errorf("expected requeue %v, got %v", tt.expectRequeue, rs.GetRequeue()) + } + + err := rs.GetError() + var terminalError *orcerrors.TerminalError + hasTerminal := errors.As(err, &terminalError) + if hasTerminal != tt.expectTerminal { + t.Errorf("expected terminal error %v, got %v (err: %v)", tt.expectTerminal, hasTerminal, err) + } + }) + } +} + +func TestApplyResourceStatus(t *testing.T) { + writer := dnsZoneStatusWriter{} + + now := time.Now().UTC() + osResource := &zones.Zone{ + Name: testZoneName, + Email: "admin@example.com", + Description: "A test DNS zone", + TTL: 3600, + Type: "SECONDARY", + Status: "ACTIVE", + Masters: []string{"192.0.2.1", "192.0.2.2"}, + TransferredAt: now, + } + + statusApply := orcapplyconfigv1alpha1.DNSZoneStatus() + writer.ApplyResourceStatus(logr.Discard(), osResource, statusApply) + + if statusApply.Resource == nil { + t.Fatal("expected Resource in apply configuration to be non-nil") + } + + res := statusApply.Resource + if res.Name == nil || *res.Name != testZoneName { + t.Errorf("expected name 'example.com.', got %v", res.Name) + } + if res.Email == nil || *res.Email != "admin@example.com" { + t.Errorf("expected email 'admin@example.com', got %v", res.Email) + } + if res.Description == nil || *res.Description != "A test DNS zone" { + t.Errorf("expected description 'A test DNS zone', got %v", res.Description) + } + if res.TTL == nil || *res.TTL != 3600 { + t.Errorf("expected TTL 3600, got %v", res.TTL) + } + if res.Type == nil || *res.Type != "SECONDARY" { + t.Errorf("expected type 'SECONDARY', got %v", res.Type) + } + if len(res.Masters) != 2 || res.Masters[0] != "192.0.2.1" || res.Masters[1] != "192.0.2.2" { + t.Errorf("expected masters ['192.0.2.1', '192.0.2.2'], got %v", res.Masters) + } + if res.TransferredAt == nil || !res.TransferredAt.Time.Equal(now) { + t.Errorf("expected transferredAt %v, got %v", now, res.TransferredAt) + } + if res.Status == nil || *res.Status != "ACTIVE" { + t.Errorf("expected status 'ACTIVE', got %v", res.Status) + } +} + +func TestApplyResourceStatus_EmptyFields(t *testing.T) { + writer := dnsZoneStatusWriter{} + + osResource := &zones.Zone{ + Name: testZoneName, + } + + statusApply := orcapplyconfigv1alpha1.DNSZoneStatus() + writer.ApplyResourceStatus(logr.Discard(), osResource, statusApply) + + if statusApply.Resource == nil { + t.Fatal("expected Resource in apply configuration to be non-nil") + } + + res := statusApply.Resource + if res.Name == nil || *res.Name != testZoneName { + t.Errorf("expected name 'example.com.', got %v", res.Name) + } + if res.Email != nil { + t.Errorf("expected Email to be nil, got %v", res.Email) + } + if res.Description != nil { + t.Errorf("expected Description to be nil, got %v", res.Description) + } + if res.TTL != nil { + t.Errorf("expected TTL to be nil, got %v", res.TTL) + } + if res.Type != nil { + t.Errorf("expected Type to be nil, got %v", res.Type) + } + if res.Status != nil { + t.Errorf("expected Status to be nil, got %v", res.Status) + } +} + +func TestGetApplyConfig(t *testing.T) { + writer := dnsZoneStatusWriter{} + config := writer.GetApplyConfig("test-name", "test-namespace") + if config == nil { + t.Fatal("expected GetApplyConfig to return non-nil config") + } + if config.Name == nil || *config.Name != "test-name" { + t.Errorf("expected Name to be 'test-name', got %v", config.Name) + } + if config.Namespace == nil || *config.Namespace != "test-namespace" { + t.Errorf("expected Namespace to be 'test-namespace', got %v", config.Namespace) + } +} diff --git a/internal/controllers/dnszone/tests/dnszone-create-full/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-create-full/00-assert.yaml new file mode 100644 index 000000000..70910081e --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-full/00-assert.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-create-full +status: + resource: + name: create-full.example.com. + description: DNSZone from "create full" test + email: admin@example.com + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-create-full + ref: dnszone +assertAll: + - celExpr: "dnszone.status.id != ''" + # TODO(scaffolding): Add more checks diff --git a/internal/controllers/dnszone/tests/dnszone-create-full/00-create-resource.yaml b/internal/controllers/dnszone/tests/dnszone-create-full/00-create-resource.yaml new file mode 100644 index 000000000..f8cd69b58 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-full/00-create-resource.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-create-full +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: create-full.example.com. + description: DNSZone from "create full" test + email: admin@example.com diff --git a/internal/controllers/dnszone/tests/dnszone-create-full/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-create-full/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-full/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/dnszone/tests/dnszone-create-full/README.md b/internal/controllers/dnszone/tests/dnszone-create-full/README.md new file mode 100644 index 000000000..91060d485 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-full/README.md @@ -0,0 +1,11 @@ +# Create a DNSZone with all the options + +## Step 00 + +Create a DNSZone using all available fields, and verify that the observed state corresponds to the spec. + +Also validate that the OpenStack resource uses the name from the spec when it is specified. + +## Reference + +https://k-orc.cloud/development/writing-tests/#create-full diff --git a/internal/controllers/dnszone/tests/dnszone-create-minimal/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-create-minimal/00-assert.yaml new file mode 100644 index 000000000..1bc0a5d47 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/00-assert.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-create-minimal +status: + resource: + name: create-minimal.example.com. + # TODO(scaffolding): Add all fields the resource supports + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-create-minimal + ref: dnszone +assertAll: + - celExpr: "dnszone.status.id != ''" + # TODO(scaffolding): Add more checks diff --git a/internal/controllers/dnszone/tests/dnszone-create-minimal/00-create-resource.yaml b/internal/controllers/dnszone/tests/dnszone-create-minimal/00-create-resource.yaml new file mode 100644 index 000000000..2a9634a77 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/00-create-resource.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-create-minimal +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + # TODO(scaffolding): Only add the mandatory fields. It's possible the resource + # doesn't have mandatory fields, in that case, leave it empty. + resource: + name: create-minimal.example.com. + email: admin@example.com diff --git a/internal/controllers/dnszone/tests/dnszone-create-minimal/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-create-minimal/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/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/dnszone/tests/dnszone-create-minimal/01-assert.yaml b/internal/controllers/dnszone/tests/dnszone-create-minimal/01-assert.yaml new file mode 100644 index 000000000..b331a9533 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/01-assert.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: v1 + kind: Secret + name: openstack-clouds + ref: secret +assertAll: + - celExpr: "secret.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/dnszone' in secret.metadata.finalizers" diff --git a/internal/controllers/dnszone/tests/dnszone-create-minimal/01-delete-secret.yaml b/internal/controllers/dnszone/tests/dnszone-create-minimal/01-delete-secret.yaml new file mode 100644 index 000000000..1620791b9 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/01-delete-secret.yaml @@ -0,0 +1,7 @@ +--- +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 diff --git a/internal/controllers/dnszone/tests/dnszone-create-minimal/README.md b/internal/controllers/dnszone/tests/dnszone-create-minimal/README.md new file mode 100644 index 000000000..36a3ec679 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-create-minimal/README.md @@ -0,0 +1,15 @@ +# Create a DNSZone with the minimum options + +## Step 00 + +Create a minimal DNSZone, that sets only the required fields, and verify that the observed state corresponds to the spec. + +Also validate that the OpenStack resource uses the name of the ORC object when no name is explicitly specified. + +## Step 01 + +Try deleting the secret and ensure that it is not deleted thanks to the finalizer. + +## Reference + +https://k-orc.cloud/development/writing-tests/#create-minimal diff --git a/internal/controllers/dnszone/tests/dnszone-errors/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-errors/00-assert.yaml new file mode 100644 index 000000000..e8253a315 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-errors/00-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-nonexistent +status: + conditions: + - type: Available + message: "referenced resource does not exist in OpenStack" + status: "False" + reason: UnrecoverableError + - type: Progressing + message: "referenced resource does not exist in OpenStack" + status: "False" + reason: UnrecoverableError diff --git a/internal/controllers/dnszone/tests/dnszone-errors/00-create-resource.yaml b/internal/controllers/dnszone/tests/dnszone-errors/00-create-resource.yaml new file mode 100644 index 000000000..be5e924ae --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-errors/00-create-resource.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-nonexistent +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + id: "00000000-0000-0000-0000-000000000000" diff --git a/internal/controllers/dnszone/tests/dnszone-errors/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-errors/00-secret.yaml new file mode 100644 index 000000000..f0fb63e85 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-errors/00-secret.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnszone/tests/dnszone-errors/README.md b/internal/controllers/dnszone/tests/dnszone-errors/README.md new file mode 100644 index 000000000..b81e95893 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-errors/README.md @@ -0,0 +1,13 @@ +# DNSZone Errors (Terminal Error with Non-Existent Zone ID) + +## Description + +Verify that importing a non-existent zone ID produces a terminal error condition in status. + +## Steps + +### Step 00 + +- Create the credentials secret `openstack-clouds`. +- Apply a `DNSZone` CR with `spec.managementPolicy="unmanaged"` and `spec.import.id` pointing to a non-existent zone ID. +- Assert that the `DNSZone` resource transitions to a terminal error state (`UnrecoverableError` reason and `"referenced resource does not exist in OpenStack"` message). diff --git a/internal/controllers/dnszone/tests/dnszone-import-error/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-import-error/00-assert.yaml new file mode 100644 index 000000000..5ab3b422b --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/00-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-error-external-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: DNSZone +metadata: + name: dnszone-import-error-external-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 diff --git a/internal/controllers/dnszone/tests/dnszone-import-error/00-create-resources.yaml b/internal/controllers/dnszone/tests/dnszone-import-error/00-create-resources.yaml new file mode 100644 index 000000000..c50a64ff3 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/00-create-resources.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-error-external-1 +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: import-error-1.example.com. + description: DNSZone from "import error" test + email: admin@example.com + # TODO(scaffolding): add any required field +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-error-external-2 +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: import-error-2.example.com. + description: DNSZone from "import error" test + email: admin@example.com + # TODO(scaffolding): add any required field diff --git a/internal/controllers/dnszone/tests/dnszone-import-error/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-import-error/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnszone/tests/dnszone-import-error/01-assert.yaml b/internal/controllers/dnszone/tests/dnszone-import-error/01-assert.yaml new file mode 100644 index 000000000..132a15ec4 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/01-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-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/dnszone/tests/dnszone-import-error/01-import-resource.yaml b/internal/controllers/dnszone/tests/dnszone-import-error/01-import-resource.yaml new file mode 100644 index 000000000..9ec3bc0b1 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/01-import-resource.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-error +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + description: DNSZone from "import error" test diff --git a/internal/controllers/dnszone/tests/dnszone-import-error/README.md b/internal/controllers/dnszone/tests/dnszone-import-error/README.md new file mode 100644 index 000000000..79ab3bde1 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import-error/README.md @@ -0,0 +1,13 @@ +# Import DNSZone with more than one matching resources + +## Step 00 + +Create two DNSZones with identical specs. + +## Step 01 + +Ensure that an imported DNSZone with a filter matching the resources returns an error. + +## Reference + +https://k-orc.cloud/development/writing-tests/#import-error diff --git a/internal/controllers/dnszone/tests/dnszone-import/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-import/00-secret.yaml new file mode 100644 index 000000000..ef9f500de --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import/00-secret.yaml @@ -0,0 +1,39 @@ +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 + - script: | + set -xe + export OS_CLIENT_CONFIG_FILE=${E2E_KUTTL_OSCLOUDS} + export OS_CLOUD=openstack + + # Clean up any pre-existing zone from a previous run + openstack zone delete kuttl-import.example.com. || true + + # Pre-create the zone in Designate + openstack zone create --email admin@example.com --ttl 3600 --description "KUTTL Import test zone" kuttl-import.example.com. + + # Get the zone ID + ZONE_ID=$(openstack zone show kuttl-import.example.com. -f value -c id) + if [ -z "$ZONE_ID" ]; then + echo "Failed to get zone ID" + exit 1 + fi + + # Generate the unmanaged DNSZone CR manifest with spec.import.id + cat < 01-import-resource.yaml + apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + metadata: + name: dnszone-import-unmanaged + spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + id: ${ZONE_ID} + EOF + + kubectl -n "${NAMESPACE}" apply -f 01-import-resource.yaml diff --git a/internal/controllers/dnszone/tests/dnszone-import/01-assert.yaml b/internal/controllers/dnszone/tests/dnszone-import/01-assert.yaml new file mode 100644 index 000000000..4831874a8 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import/01-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-import-unmanaged +status: + resource: + name: kuttl-import.example.com. + email: admin@example.com + description: "KUTTL Import test zone" + ttl: 3600 + type: PRIMARY + status: ACTIVE + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-import-unmanaged + ref: dnszone +assertAll: + - celExpr: "dnszone.status.id != ''" diff --git a/internal/controllers/dnszone/tests/dnszone-import/02-assert.yaml b/internal/controllers/dnszone/tests/dnszone-import/02-assert.yaml new file mode 100644 index 000000000..682e42890 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import/02-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: + - script: "! kubectl get dnszone.openstack.k-orc.cloud dnszone-import-unmanaged --namespace $NAMESPACE" + skipLogOutput: true + - script: | + export OS_CLIENT_CONFIG_FILE=${E2E_KUTTL_OSCLOUDS} + export OS_CLOUD=openstack + + # Assert that the zone still exists in Designate + openstack zone show kuttl-import.example.com. + + # Clean up (delete the zone from Designate so we don't leak resources) + openstack zone delete kuttl-import.example.com. diff --git a/internal/controllers/dnszone/tests/dnszone-import/02-delete-resource.yaml b/internal/controllers/dnszone/tests/dnszone-import/02-delete-resource.yaml new file mode 100644 index 000000000..54a8c9372 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import/02-delete-resource.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-import-unmanaged diff --git a/internal/controllers/dnszone/tests/dnszone-import/README.md b/internal/controllers/dnszone/tests/dnszone-import/README.md new file mode 100644 index 000000000..0a28eaffb --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-import/README.md @@ -0,0 +1,18 @@ +# Import DNSZone (Unmanaged ID Scenario) + +## Step 00 + +- Create the credentials secret `openstack-clouds`. +- Pre-create a DNS zone in Designate using the `openstack` CLI. +- Dynamically generate `01-import-resource.yaml` with the ID of the pre-created zone. + +## Step 01 + +- Apply the unmanaged `DNSZone` CR with `spec.import.id` pointing to the pre-created zone. +- Assert that ORC imports the zone successfully and populates status correctly. + +## Step 02 + +- Delete the `DNSZone` CR. +- Verify that the `DNSZone` CR is deleted from Kubernetes, but the actual zone still exists in Designate. +- Clean up the pre-created zone in Designate. diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-assert.yaml new file mode 100644 index 000000000..ff65cb8d8 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-lifecycle-primary +status: + resource: + name: lifecycle-primary.example.com. + email: admin@example.com + description: "Primary DNS Zone lifecycle test" + ttl: 3600 + type: PRIMARY + status: ACTIVE + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-lifecycle-primary + ref: dnszone +assertAll: + - celExpr: "dnszone.status.id != ''" diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-create-resource.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-create-resource.yaml new file mode 100644 index 000000000..fe7451b9f --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-create-resource.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-lifecycle-primary +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: lifecycle-primary.example.com. + email: admin@example.com + description: "Primary DNS Zone lifecycle test" + ttl: 3600 + type: PRIMARY diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-secret.yaml new file mode 100644 index 000000000..f0fb63e85 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/00-secret.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-assert.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-assert.yaml new file mode 100644 index 000000000..d84d347c6 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-lifecycle-primary +status: + resource: + name: lifecycle-primary.example.com. + email: newadmin@example.com + description: "Primary DNS Zone lifecycle test - Updated" + ttl: 7200 + type: PRIMARY + status: ACTIVE + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-lifecycle-primary + ref: dnszone +assertAll: + - celExpr: "dnszone.status.id != ''" diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-update-resource.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-update-resource.yaml new file mode 100644 index 000000000..4cc51483d --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/01-update-resource.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-lifecycle-primary +spec: + resource: + email: newadmin@example.com + description: "Primary DNS Zone lifecycle test - Updated" + ttl: 7200 diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-assert.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-assert.yaml new file mode 100644 index 000000000..e14f1a146 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: "! kubectl get dnszone.openstack.k-orc.cloud dnszone-lifecycle-primary --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-delete-resource.yaml b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-delete-resource.yaml new file mode 100644 index 000000000..6d1b68ebf --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/02-delete-resource.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-lifecycle-primary diff --git a/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/README.md b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/README.md new file mode 100644 index 000000000..5ceb333b7 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-lifecycle-primary/README.md @@ -0,0 +1,31 @@ +# Primary DNSZone Lifecycle E2E Test + +This test verifies the end-to-end lifecycle of a primary managed `DNSZone` (create, assert status, update, delete). + +## Step 00 + +Creates a primary managed `DNSZone` CR with: +- `name`: `lifecycle-primary.example.com.` +- `email`: `admin@example.com` +- `description`: `"Primary DNS Zone lifecycle test"` +- `ttl`: `3600` +- `type`: `PRIMARY` + +And verifies that: +- It transition to status `ACTIVE` +- It generates `status.id` +- It populates the observed fields in `status.resource` matching the real Designate values +- Both `Available` and `Progressing` conditions are successfully reconciled + +## Step 01 + +Modifies the mutable fields on the created `DNSZone` CR: +- `email`: `newadmin@example.com` +- `description`: `"Primary DNS Zone lifecycle test - Updated"` +- `ttl`: `7200` + +And asserts that the updates successfully propagate to Designate and the CR's `status.resource` is updated accordingly. + +## Step 02 + +Deletes the `DNSZone` CR and verifies that the clean deletion is triggered, the finalizers execute, and the zone is completely removed. diff --git a/internal/controllers/dnszone/tests/dnszone-update/00-assert.yaml b/internal/controllers/dnszone/tests/dnszone-update/00-assert.yaml new file mode 100644 index 000000000..7f9cda245 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/00-assert.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-update + ref: dnszone +assertAll: + - celExpr: "!has(dnszone.status.resource.description)" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-update +status: + resource: + name: update-updated.example.com. + email: admin@example.com + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnszone/tests/dnszone-update/00-minimal-resource.yaml b/internal/controllers/dnszone/tests/dnszone-update/00-minimal-resource.yaml new file mode 100644 index 000000000..aa3c3fb19 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/00-minimal-resource.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-update +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created or updated + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + # TODO(scaffolding): Only add the mandatory fields. It's possible the resource + # doesn't have mandatory fields, in that case, leave it empty. + resource: + name: update-updated.example.com. + email: admin@example.com diff --git a/internal/controllers/dnszone/tests/dnszone-update/00-secret.yaml b/internal/controllers/dnszone/tests/dnszone-update/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/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/dnszone/tests/dnszone-update/01-assert.yaml b/internal/controllers/dnszone/tests/dnszone-update/01-assert.yaml new file mode 100644 index 000000000..c0e87fbb6 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/01-assert.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-update +status: + resource: + name: update-updated.example.com. + description: dnszone-update-updated + email: admin@example.com + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnszone/tests/dnszone-update/01-updated-resource.yaml b/internal/controllers/dnszone/tests/dnszone-update/01-updated-resource.yaml new file mode 100644 index 000000000..f99e41f29 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/01-updated-resource.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-update +spec: + resource: + name: update-updated.example.com. + email: admin@example.com + description: dnszone-update-updated diff --git a/internal/controllers/dnszone/tests/dnszone-update/02-assert.yaml b/internal/controllers/dnszone/tests/dnszone-update/02-assert.yaml new file mode 100644 index 000000000..7f9cda245 --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/02-assert.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnszone-update + ref: dnszone +assertAll: + - celExpr: "!has(dnszone.status.resource.description)" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnszone-update +status: + resource: + name: update-updated.example.com. + email: admin@example.com + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnszone/tests/dnszone-update/02-reverted-resource.yaml b/internal/controllers/dnszone/tests/dnszone-update/02-reverted-resource.yaml new file mode 100644 index 000000000..2c6c253ff --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-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/dnszone/tests/dnszone-update/README.md b/internal/controllers/dnszone/tests/dnszone-update/README.md new file mode 100644 index 000000000..3c579deed --- /dev/null +++ b/internal/controllers/dnszone/tests/dnszone-update/README.md @@ -0,0 +1,17 @@ +# Update DNSZone + +## Step 00 + +Create a DNSZone 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. + +## Reference + +https://k-orc.cloud/development/writing-tests/#update diff --git a/internal/controllers/dnszone/zz_generated.adapter.go b/internal/controllers/dnszone/zz_generated.adapter.go new file mode 100644 index 000000000..f87f188fd --- /dev/null +++ b/internal/controllers/dnszone/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 dnszone + +import ( + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces" +) + +// Fundamental types +type ( + orcObjectT = orcv1alpha1.DNSZone + orcObjectListT = orcv1alpha1.DNSZoneList + resourceSpecT = orcv1alpha1.DNSZoneResourceSpec + filterT = orcv1alpha1.DNSZoneFilter +) + +// Derived types +type ( + orcObjectPT = *orcObjectT + adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT] + adapterT = dnszoneAdapter +) + +type dnszoneAdapter struct { + *orcv1alpha1.DNSZone +} + +var _ adapterI = &adapterT{} + +func (f adapterT) GetObject() orcObjectPT { + return f.DNSZone +} + +func (f adapterT) GetManagementPolicy() orcv1alpha1.ManagementPolicy { + return f.Spec.ManagementPolicy +} + +func (f adapterT) GetManagedOptions() *orcv1alpha1.ManagedOptions { + return f.Spec.ManagedOptions +} + +func (f adapterT) GetStatusID() *string { + return f.Status.ID +} + +func (f adapterT) GetResourceSpec() *resourceSpecT { + return f.Spec.Resource +} + +func (f adapterT) GetImportID() *string { + if f.Spec.Import == nil { + return nil + } + return f.Spec.Import.ID +} + +func (f adapterT) GetImportFilter() *filterT { + if f.Spec.Import == nil { + return nil + } + return f.Spec.Import.Filter +} + +// getResourceName returns the name of the OpenStack resource we should use. +// This method is not implemented as part of APIObjectAdapter as it is intended +// to be used by resource actuators, which don't use the adapter. +func getResourceName(orcObject orcObjectPT) string { + if orcObject.Spec.Resource.Name != nil { + return string(*orcObject.Spec.Resource.Name) + } + return orcObject.Name +} diff --git a/internal/controllers/dnszone/zz_generated.controller.go b/internal/controllers/dnszone/zz_generated.controller.go new file mode 100644 index 000000000..6c3a0ef3e --- /dev/null +++ b/internal/controllers/dnszone/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 dnszone + +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/dnszone.go b/internal/osclients/dnszone.go new file mode 100644 index 000000000..818ebf13b --- /dev/null +++ b/internal/osclients/dnszone.go @@ -0,0 +1,105 @@ +/* +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/dns/v2/zones" + "github.com/gophercloud/utils/v2/openstack/clientconfig" +) + +type DNSZoneClient interface { + ListZones(ctx context.Context, listOpts zones.ListOptsBuilder) iter.Seq2[*zones.Zone, error] + CreateZone(ctx context.Context, opts zones.CreateOptsBuilder) (*zones.Zone, error) + DeleteZone(ctx context.Context, resourceID string) error + GetZone(ctx context.Context, resourceID string) (*zones.Zone, error) + UpdateZone(ctx context.Context, id string, opts zones.UpdateOptsBuilder) (*zones.Zone, error) +} + +type dnsZoneClient struct{ client *gophercloud.ServiceClient } + +// NewDNSZoneClient returns a new OpenStack client. +func NewDNSZoneClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (DNSZoneClient, error) { + client, err := openstack.NewDNSV2(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + + if err != nil { + return nil, fmt.Errorf("failed to create dnszone service client: %v", err) + } + + return &dnsZoneClient{client}, nil +} + +func (c dnsZoneClient) ListZones(ctx context.Context, listOpts zones.ListOptsBuilder) iter.Seq2[*zones.Zone, error] { + pager := zones.List(c.client, listOpts) + return func(yield func(*zones.Zone, error) bool) { + _ = pager.EachPage(ctx, yieldPage(zones.ExtractZones, yield)) + } +} + +func (c dnsZoneClient) CreateZone(ctx context.Context, opts zones.CreateOptsBuilder) (*zones.Zone, error) { + return zones.Create(ctx, c.client, opts).Extract() +} + +func (c dnsZoneClient) DeleteZone(ctx context.Context, resourceID string) error { + _, err := zones.Delete(ctx, c.client, resourceID).Extract() + return err +} + +func (c dnsZoneClient) GetZone(ctx context.Context, resourceID string) (*zones.Zone, error) { + return zones.Get(ctx, c.client, resourceID).Extract() +} + +func (c dnsZoneClient) UpdateZone(ctx context.Context, id string, opts zones.UpdateOptsBuilder) (*zones.Zone, error) { + return zones.Update(ctx, c.client, id, opts).Extract() +} + +type dnsZoneErrorClient struct{ error } + +// NewDNSZoneErrorClient returns a DNSZoneClient in which every method returns the given error. +func NewDNSZoneErrorClient(e error) DNSZoneClient { + return dnsZoneErrorClient{e} +} + +func (e dnsZoneErrorClient) ListZones(_ context.Context, _ zones.ListOptsBuilder) iter.Seq2[*zones.Zone, error] { + return func(yield func(*zones.Zone, error) bool) { + yield(nil, e.error) + } +} + +func (e dnsZoneErrorClient) CreateZone(_ context.Context, _ zones.CreateOptsBuilder) (*zones.Zone, error) { + return nil, e.error +} + +func (e dnsZoneErrorClient) DeleteZone(_ context.Context, _ string) error { + return e.error +} + +func (e dnsZoneErrorClient) GetZone(_ context.Context, _ string) (*zones.Zone, error) { + return nil, e.error +} + +func (e dnsZoneErrorClient) UpdateZone(_ context.Context, _ string, _ zones.UpdateOptsBuilder) (*zones.Zone, error) { + return nil, e.error +} diff --git a/internal/osclients/dnszone_test.go b/internal/osclients/dnszone_test.go new file mode 100644 index 000000000..3b5ebde81 --- /dev/null +++ b/internal/osclients/dnszone_test.go @@ -0,0 +1,72 @@ +/* +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_test + +import ( + "context" + "errors" + "testing" + + "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" +) + +// TestDNSZoneErrorClient verifies that the error client returns the +// configured error for every method. +func TestDNSZoneErrorClient(t *testing.T) { + testErr := errors.New("test configured error") + client := osclients.NewDNSZoneErrorClient(testErr) + ctx := context.Background() + + t.Run("ListZones", func(t *testing.T) { + var gotErr error + for _, err := range client.ListZones(ctx, nil) { + gotErr = err + break + } + if !errors.Is(gotErr, testErr) { + t.Errorf("ListZones: expected %v, got %v", testErr, gotErr) + } + }) + + t.Run("CreateZone", func(t *testing.T) { + _, err := client.CreateZone(ctx, nil) + if !errors.Is(err, testErr) { + t.Errorf("CreateZone: expected %v, got %v", testErr, err) + } + }) + + t.Run("DeleteZone", func(t *testing.T) { + err := client.DeleteZone(ctx, "id") + if !errors.Is(err, testErr) { + t.Errorf("DeleteZone: expected %v, got %v", testErr, err) + } + }) + + t.Run("GetZone", func(t *testing.T) { + _, err := client.GetZone(ctx, "id") + if !errors.Is(err, testErr) { + t.Errorf("GetZone: expected %v, got %v", testErr, err) + } + }) + + t.Run("UpdateZone", func(t *testing.T) { + _, err := client.UpdateZone(ctx, "id", nil) + if !errors.Is(err, testErr) { + t.Errorf("UpdateZone: expected %v, got %v", testErr, err) + } + }) +} diff --git a/internal/osclients/mock/dnszone.go b/internal/osclients/mock/dnszone.go new file mode 100644 index 000000000..3cb4be26b --- /dev/null +++ b/internal/osclients/mock/dnszone.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: ../dnszone.go +// +// Generated by this command: +// +// mockgen -package mock -destination=dnszone.go -source=../dnszone.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock DNSZoneClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + iter "iter" + reflect "reflect" + + zones "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/zones" + gomock "go.uber.org/mock/gomock" +) + +// MockDNSZoneClient is a mock of DNSZoneClient interface. +type MockDNSZoneClient struct { + ctrl *gomock.Controller + recorder *MockDNSZoneClientMockRecorder + isgomock struct{} +} + +// MockDNSZoneClientMockRecorder is the mock recorder for MockDNSZoneClient. +type MockDNSZoneClientMockRecorder struct { + mock *MockDNSZoneClient +} + +// NewMockDNSZoneClient creates a new mock instance. +func NewMockDNSZoneClient(ctrl *gomock.Controller) *MockDNSZoneClient { + mock := &MockDNSZoneClient{ctrl: ctrl} + mock.recorder = &MockDNSZoneClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDNSZoneClient) EXPECT() *MockDNSZoneClientMockRecorder { + return m.recorder +} + +// CreateZone mocks base method. +func (m *MockDNSZoneClient) CreateZone(ctx context.Context, opts zones.CreateOptsBuilder) (*zones.Zone, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateZone", ctx, opts) + ret0, _ := ret[0].(*zones.Zone) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateZone indicates an expected call of CreateZone. +func (mr *MockDNSZoneClientMockRecorder) CreateZone(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateZone", reflect.TypeOf((*MockDNSZoneClient)(nil).CreateZone), ctx, opts) +} + +// DeleteZone mocks base method. +func (m *MockDNSZoneClient) DeleteZone(ctx context.Context, resourceID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteZone", ctx, resourceID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteZone indicates an expected call of DeleteZone. +func (mr *MockDNSZoneClientMockRecorder) DeleteZone(ctx, resourceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZone", reflect.TypeOf((*MockDNSZoneClient)(nil).DeleteZone), ctx, resourceID) +} + +// GetZone mocks base method. +func (m *MockDNSZoneClient) GetZone(ctx context.Context, resourceID string) (*zones.Zone, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetZone", ctx, resourceID) + ret0, _ := ret[0].(*zones.Zone) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetZone indicates an expected call of GetZone. +func (mr *MockDNSZoneClientMockRecorder) GetZone(ctx, resourceID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetZone", reflect.TypeOf((*MockDNSZoneClient)(nil).GetZone), ctx, resourceID) +} + +// ListZones mocks base method. +func (m *MockDNSZoneClient) ListZones(ctx context.Context, listOpts zones.ListOptsBuilder) iter.Seq2[*zones.Zone, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListZones", ctx, listOpts) + ret0, _ := ret[0].(iter.Seq2[*zones.Zone, error]) + return ret0 +} + +// ListZones indicates an expected call of ListZones. +func (mr *MockDNSZoneClientMockRecorder) ListZones(ctx, listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListZones", reflect.TypeOf((*MockDNSZoneClient)(nil).ListZones), ctx, listOpts) +} + +// UpdateZone mocks base method. +func (m *MockDNSZoneClient) UpdateZone(ctx context.Context, id string, opts zones.UpdateOptsBuilder) (*zones.Zone, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateZone", ctx, id, opts) + ret0, _ := ret[0].(*zones.Zone) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateZone indicates an expected call of UpdateZone. +func (mr *MockDNSZoneClientMockRecorder) UpdateZone(ctx, id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateZone", reflect.TypeOf((*MockDNSZoneClient)(nil).UpdateZone), ctx, id, opts) +} diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go index 5ee7aa5da..0d188b934 100644 --- a/internal/osclients/mock/doc.go +++ b/internal/osclients/mock/doc.go @@ -41,6 +41,9 @@ import ( //go:generate mockgen -package mock -destination=applicationcredential.go -source=../applicationcredential.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock ApplicationCredentialClient //go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt applicationcredential.go > _applicationcredential.go && mv _applicationcredential.go applicationcredential.go" +//go:generate mockgen -package mock -destination=dnszone.go -source=../dnszone.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock DNSZoneClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt dnszone.go > _dnszone.go && mv _dnszone.go dnszone.go" + //go:generate mockgen -package mock -destination=domain.go -source=../domain.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock DomainClient //go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt domain.go > _domain.go && mv _domain.go domain.go" diff --git a/internal/scope/mock.go b/internal/scope/mock.go index 8ea474b64..7fb38d159 100644 --- a/internal/scope/mock.go +++ b/internal/scope/mock.go @@ -37,6 +37,7 @@ type MockScopeFactory struct { AddressScope *mock.MockAddressScopeClient ApplicationCredentialClient *mock.MockApplicationCredentialClient ComputeClient *mock.MockComputeClient + DNSZoneClient *mock.MockDNSZoneClient DomainClient *mock.MockDomainClient EndpointClient *mock.MockEndpointClient GroupClient *mock.MockGroupClient @@ -58,6 +59,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { addressScope := mock.NewMockAddressScopeClient(mockCtrl) applicationcredentialClient := mock.NewMockApplicationCredentialClient(mockCtrl) computeClient := mock.NewMockComputeClient(mockCtrl) + dnszoneClient := mock.NewMockDNSZoneClient(mockCtrl) domainClient := mock.NewMockDomainClient(mockCtrl) endpointClient := mock.NewMockEndpointClient(mockCtrl) groupClient := mock.NewMockGroupClient(mockCtrl) @@ -76,6 +78,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { AddressScope: addressScope, ApplicationCredentialClient: applicationcredentialClient, ComputeClient: computeClient, + DNSZoneClient: dnszoneClient, DomainClient: domainClient, EndpointClient: endpointClient, GroupClient: groupClient, @@ -111,6 +114,10 @@ func (f *MockScopeFactory) NewComputeClient() (osclients.ComputeClient, error) { return f.ComputeClient, nil } +func (f *MockScopeFactory) NewDNSZoneClient() (osclients.DNSZoneClient, error) { + return f.DNSZoneClient, nil +} + func (f *MockScopeFactory) NewImageClient() (osclients.ImageClient, error) { return f.ImageClient, nil } diff --git a/internal/scope/provider.go b/internal/scope/provider.go index 1606e18a1..d7804f37a 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -149,6 +149,10 @@ func (s *providerScope) NewComputeClient() (clients.ComputeClient, error) { return clients.NewComputeClient(s.providerClient, s.providerClientOpts) } +func (s *providerScope) NewDNSZoneClient() (clients.DNSZoneClient, error) { + return clients.NewDNSZoneClient(s.providerClient, s.providerClientOpts) +} + func (s *providerScope) NewNetworkClient() (clients.NetworkClient, error) { return clients.NewNetworkClient(s.providerClient, s.providerClientOpts) } diff --git a/internal/scope/scope.go b/internal/scope/scope.go index 0b02b79bd..dd5adfc88 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -51,6 +51,7 @@ type Scope interface { NewAddressScopeClient() (osclients.AddressScopeClient, error) NewApplicationCredentialClient() (osclients.ApplicationCredentialClient, error) NewComputeClient() (osclients.ComputeClient, error) + NewDNSZoneClient() (osclients.DNSZoneClient, error) NewDomainClient() (osclients.DomainClient, error) NewEndpointClient() (osclients.EndpointClient, error) NewGroupClient() (osclients.GroupClient, error) diff --git a/kuttl-test.yaml b/kuttl-test.yaml index 71fc135ed..2fe5c8dba 100644 --- a/kuttl-test.yaml +++ b/kuttl-test.yaml @@ -4,6 +4,7 @@ kind: TestSuite testDirs: - ./internal/controllers/addressscope/tests/ - ./internal/controllers/applicationcredential/tests/ +- ./internal/controllers/dnszone/tests/ - ./internal/controllers/domain/tests/ - ./internal/controllers/endpoint/tests/ - ./internal/controllers/flavor/tests/ diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszone.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszone.go new file mode 100644 index 000000000..054151edb --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszone.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" +) + +// DNSZoneApplyConfiguration represents a declarative configuration of the DNSZone type for use +// with apply. +type DNSZoneApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *DNSZoneSpecApplyConfiguration `json:"spec,omitempty"` + Status *DNSZoneStatusApplyConfiguration `json:"status,omitempty"` +} + +// DNSZone constructs a declarative configuration of the DNSZone type for use with +// apply. +func DNSZone(name, namespace string) *DNSZoneApplyConfiguration { + b := &DNSZoneApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("DNSZone") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b +} + +// ExtractDNSZone extracts the applied configuration owned by fieldManager from +// dNSZone. If no managedFields are found in dNSZone for fieldManager, a +// DNSZoneApplyConfiguration 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. +// dNSZone must be a unmodified DNSZone API object that was retrieved from the Kubernetes API. +// ExtractDNSZone 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 ExtractDNSZone(dNSZone *apiv1alpha1.DNSZone, fieldManager string) (*DNSZoneApplyConfiguration, error) { + return extractDNSZone(dNSZone, fieldManager, "") +} + +// ExtractDNSZoneStatus is the same as ExtractDNSZone except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractDNSZoneStatus(dNSZone *apiv1alpha1.DNSZone, fieldManager string) (*DNSZoneApplyConfiguration, error) { + return extractDNSZone(dNSZone, fieldManager, "status") +} + +func extractDNSZone(dNSZone *apiv1alpha1.DNSZone, fieldManager string, subresource string) (*DNSZoneApplyConfiguration, error) { + b := &DNSZoneApplyConfiguration{} + err := managedfields.ExtractInto(dNSZone, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZone"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(dNSZone.Name) + b.WithNamespace(dNSZone.Namespace) + + b.WithKind("DNSZone") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b, nil +} +func (b DNSZoneApplyConfiguration) 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 *DNSZoneApplyConfiguration) WithKind(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithAPIVersion(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithName(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithGenerateName(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithNamespace(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithUID(value types.UID) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithResourceVersion(value string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithGeneration(value int64) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithCreationTimestamp(value metav1.Time) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithLabels(entries map[string]string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithAnnotations(entries map[string]string) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithFinalizers(values ...string) *DNSZoneApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *DNSZoneApplyConfiguration) 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 *DNSZoneApplyConfiguration) WithSpec(value *DNSZoneSpecApplyConfiguration) *DNSZoneApplyConfiguration { + 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 *DNSZoneApplyConfiguration) WithStatus(value *DNSZoneStatusApplyConfiguration) *DNSZoneApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *DNSZoneApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *DNSZoneApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *DNSZoneApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *DNSZoneApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszonefilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonefilter.go new file mode 100644 index 000000000..5338d44f2 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonefilter.go @@ -0,0 +1,90 @@ +/* +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" +) + +// DNSZoneFilterApplyConfiguration represents a declarative configuration of the DNSZoneFilter type for use +// with apply. +type DNSZoneFilterApplyConfiguration struct { + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Description *string `json:"description,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Type *apiv1alpha1.DNSZoneType `json:"type,omitempty"` + Masters []string `json:"masters,omitempty"` +} + +// DNSZoneFilterApplyConfiguration constructs a declarative configuration of the DNSZoneFilter type for use with +// apply. +func DNSZoneFilter() *DNSZoneFilterApplyConfiguration { + return &DNSZoneFilterApplyConfiguration{} +} + +// 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 *DNSZoneFilterApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *DNSZoneFilterApplyConfiguration { + b.Name = &value + return b +} + +// WithEmail sets the Email 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 Email field is set to the value of the last call. +func (b *DNSZoneFilterApplyConfiguration) WithEmail(value string) *DNSZoneFilterApplyConfiguration { + b.Email = &value + return b +} + +// 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 *DNSZoneFilterApplyConfiguration) WithDescription(value string) *DNSZoneFilterApplyConfiguration { + b.Description = &value + return b +} + +// WithTTL sets the TTL 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 TTL field is set to the value of the last call. +func (b *DNSZoneFilterApplyConfiguration) WithTTL(value int32) *DNSZoneFilterApplyConfiguration { + b.TTL = &value + return b +} + +// WithType sets the Type 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 Type field is set to the value of the last call. +func (b *DNSZoneFilterApplyConfiguration) WithType(value apiv1alpha1.DNSZoneType) *DNSZoneFilterApplyConfiguration { + b.Type = &value + return b +} + +// WithMasters adds the given value to the Masters 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 Masters field. +func (b *DNSZoneFilterApplyConfiguration) WithMasters(values ...string) *DNSZoneFilterApplyConfiguration { + for i := range values { + b.Masters = append(b.Masters, values[i]) + } + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneimport.go new file mode 100644 index 000000000..a59cdf254 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneimport.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 + +// DNSZoneImportApplyConfiguration represents a declarative configuration of the DNSZoneImport type for use +// with apply. +type DNSZoneImportApplyConfiguration struct { + ID *string `json:"id,omitempty"` + Filter *DNSZoneFilterApplyConfiguration `json:"filter,omitempty"` +} + +// DNSZoneImportApplyConfiguration constructs a declarative configuration of the DNSZoneImport type for use with +// apply. +func DNSZoneImport() *DNSZoneImportApplyConfiguration { + return &DNSZoneImportApplyConfiguration{} +} + +// 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 *DNSZoneImportApplyConfiguration) WithID(value string) *DNSZoneImportApplyConfiguration { + 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 *DNSZoneImportApplyConfiguration) WithFilter(value *DNSZoneFilterApplyConfiguration) *DNSZoneImportApplyConfiguration { + b.Filter = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcespec.go new file mode 100644 index 000000000..9a7b92539 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcespec.go @@ -0,0 +1,90 @@ +/* +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" +) + +// DNSZoneResourceSpecApplyConfiguration represents a declarative configuration of the DNSZoneResourceSpec type for use +// with apply. +type DNSZoneResourceSpecApplyConfiguration struct { + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Description *string `json:"description,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Type *apiv1alpha1.DNSZoneType `json:"type,omitempty"` + Masters []string `json:"masters,omitempty"` +} + +// DNSZoneResourceSpecApplyConfiguration constructs a declarative configuration of the DNSZoneResourceSpec type for use with +// apply. +func DNSZoneResourceSpec() *DNSZoneResourceSpecApplyConfiguration { + return &DNSZoneResourceSpecApplyConfiguration{} +} + +// 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 *DNSZoneResourceSpecApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *DNSZoneResourceSpecApplyConfiguration { + b.Name = &value + return b +} + +// WithEmail sets the Email 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 Email field is set to the value of the last call. +func (b *DNSZoneResourceSpecApplyConfiguration) WithEmail(value string) *DNSZoneResourceSpecApplyConfiguration { + b.Email = &value + return b +} + +// 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 *DNSZoneResourceSpecApplyConfiguration) WithDescription(value string) *DNSZoneResourceSpecApplyConfiguration { + b.Description = &value + return b +} + +// WithTTL sets the TTL 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 TTL field is set to the value of the last call. +func (b *DNSZoneResourceSpecApplyConfiguration) WithTTL(value int32) *DNSZoneResourceSpecApplyConfiguration { + b.TTL = &value + return b +} + +// WithType sets the Type 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 Type field is set to the value of the last call. +func (b *DNSZoneResourceSpecApplyConfiguration) WithType(value apiv1alpha1.DNSZoneType) *DNSZoneResourceSpecApplyConfiguration { + b.Type = &value + return b +} + +// WithMasters adds the given value to the Masters 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 Masters field. +func (b *DNSZoneResourceSpecApplyConfiguration) WithMasters(values ...string) *DNSZoneResourceSpecApplyConfiguration { + for i := range values { + b.Masters = append(b.Masters, values[i]) + } + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcestatus.go new file mode 100644 index 000000000..5e62238d8 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszoneresourcestatus.go @@ -0,0 +1,108 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DNSZoneResourceStatusApplyConfiguration represents a declarative configuration of the DNSZoneResourceStatus type for use +// with apply. +type DNSZoneResourceStatusApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Email *string `json:"email,omitempty"` + Description *string `json:"description,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Type *string `json:"type,omitempty"` + Masters []string `json:"masters,omitempty"` + TransferredAt *v1.Time `json:"transferredAt,omitempty"` + Status *string `json:"status,omitempty"` +} + +// DNSZoneResourceStatusApplyConfiguration constructs a declarative configuration of the DNSZoneResourceStatus type for use with +// apply. +func DNSZoneResourceStatus() *DNSZoneResourceStatusApplyConfiguration { + return &DNSZoneResourceStatusApplyConfiguration{} +} + +// 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 *DNSZoneResourceStatusApplyConfiguration) WithName(value string) *DNSZoneResourceStatusApplyConfiguration { + b.Name = &value + return b +} + +// WithEmail sets the Email 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 Email field is set to the value of the last call. +func (b *DNSZoneResourceStatusApplyConfiguration) WithEmail(value string) *DNSZoneResourceStatusApplyConfiguration { + b.Email = &value + return b +} + +// 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 *DNSZoneResourceStatusApplyConfiguration) WithDescription(value string) *DNSZoneResourceStatusApplyConfiguration { + b.Description = &value + return b +} + +// WithTTL sets the TTL 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 TTL field is set to the value of the last call. +func (b *DNSZoneResourceStatusApplyConfiguration) WithTTL(value int32) *DNSZoneResourceStatusApplyConfiguration { + b.TTL = &value + return b +} + +// WithType sets the Type 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 Type field is set to the value of the last call. +func (b *DNSZoneResourceStatusApplyConfiguration) WithType(value string) *DNSZoneResourceStatusApplyConfiguration { + b.Type = &value + return b +} + +// WithMasters adds the given value to the Masters 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 Masters field. +func (b *DNSZoneResourceStatusApplyConfiguration) WithMasters(values ...string) *DNSZoneResourceStatusApplyConfiguration { + for i := range values { + b.Masters = append(b.Masters, values[i]) + } + return b +} + +// WithTransferredAt sets the TransferredAt 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 TransferredAt field is set to the value of the last call. +func (b *DNSZoneResourceStatusApplyConfiguration) WithTransferredAt(value v1.Time) *DNSZoneResourceStatusApplyConfiguration { + b.TransferredAt = &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 *DNSZoneResourceStatusApplyConfiguration) WithStatus(value string) *DNSZoneResourceStatusApplyConfiguration { + b.Status = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszonespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonespec.go new file mode 100644 index 000000000..cc826a5fe --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonespec.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" +) + +// DNSZoneSpecApplyConfiguration represents a declarative configuration of the DNSZoneSpec type for use +// with apply. +type DNSZoneSpecApplyConfiguration struct { + Import *DNSZoneImportApplyConfiguration `json:"import,omitempty"` + Resource *DNSZoneResourceSpecApplyConfiguration `json:"resource,omitempty"` + ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"` + ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"` + CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"` +} + +// DNSZoneSpecApplyConfiguration constructs a declarative configuration of the DNSZoneSpec type for use with +// apply. +func DNSZoneSpec() *DNSZoneSpecApplyConfiguration { + return &DNSZoneSpecApplyConfiguration{} +} + +// 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 *DNSZoneSpecApplyConfiguration) WithImport(value *DNSZoneImportApplyConfiguration) *DNSZoneSpecApplyConfiguration { + 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 *DNSZoneSpecApplyConfiguration) WithResource(value *DNSZoneResourceSpecApplyConfiguration) *DNSZoneSpecApplyConfiguration { + 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 *DNSZoneSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *DNSZoneSpecApplyConfiguration { + 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 *DNSZoneSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *DNSZoneSpecApplyConfiguration { + b.ManagedOptions = value + return b +} + +// WithCloudCredentialsRef sets the CloudCredentialsRef field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CloudCredentialsRef field is set to the value of the last call. +func (b *DNSZoneSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *DNSZoneSpecApplyConfiguration { + b.CloudCredentialsRef = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnszonestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonestatus.go new file mode 100644 index 000000000..94325f4eb --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnszonestatus.go @@ -0,0 +1,66 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// DNSZoneStatusApplyConfiguration represents a declarative configuration of the DNSZoneStatus type for use +// with apply. +type DNSZoneStatusApplyConfiguration struct { + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + ID *string `json:"id,omitempty"` + Resource *DNSZoneResourceStatusApplyConfiguration `json:"resource,omitempty"` +} + +// DNSZoneStatusApplyConfiguration constructs a declarative configuration of the DNSZoneStatus type for use with +// apply. +func DNSZoneStatus() *DNSZoneStatusApplyConfiguration { + return &DNSZoneStatusApplyConfiguration{} +} + +// 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 *DNSZoneStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *DNSZoneStatusApplyConfiguration { + 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 *DNSZoneStatusApplyConfiguration) WithID(value string) *DNSZoneStatusApplyConfiguration { + 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 *DNSZoneStatusApplyConfiguration) WithResource(value *DNSZoneResourceStatusApplyConfiguration) *DNSZoneStatusApplyConfiguration { + b.Resource = value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index abfc36fe8..52d023db0 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -385,6 +385,150 @@ var schemaYAML = typed.YAMLObject(`types: - name: secretName type: scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZone + 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.DNSZoneSpec + default: {} + - name: status + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneStatus + default: {} +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneFilter + map: + fields: + - name: description + type: + scalar: string + - name: email + type: + scalar: string + - name: masters + type: + list: + elementType: + scalar: string + elementRelationship: atomic + - name: name + type: + scalar: string + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneImport + map: + fields: + - name: filter + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneFilter + - name: id + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneResourceSpec + map: + fields: + - name: description + type: + scalar: string + - name: email + type: + scalar: string + - name: masters + type: + list: + elementType: + scalar: string + elementRelationship: atomic + - name: name + type: + scalar: string + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneResourceStatus + map: + fields: + - name: description + type: + scalar: string + - name: email + type: + scalar: string + - name: masters + type: + list: + elementType: + scalar: string + elementRelationship: atomic + - name: name + type: + scalar: string + - name: status + type: + scalar: string + - name: transferredAt + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneSpec + 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.DNSZoneImport + - 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.DNSZoneResourceSpec +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneStatus + map: + fields: + - name: conditions + type: + list: + elementType: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Condition + elementRelationship: associative + keys: + - type + - name: id + type: + scalar: string + - name: resource + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZoneResourceStatus - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.Domain map: fields: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index 1e5ffabc0..f6ce297b1 100644 --- a/pkg/clients/applyconfiguration/utils.go +++ b/pkg/clients/applyconfiguration/utils.go @@ -78,6 +78,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1alpha1.ApplicationCredentialStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("CloudCredentialsReference"): return &apiv1alpha1.CloudCredentialsReferenceApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZone"): + return &apiv1alpha1.DNSZoneApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneFilter"): + return &apiv1alpha1.DNSZoneFilterApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneImport"): + return &apiv1alpha1.DNSZoneImportApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneResourceSpec"): + return &apiv1alpha1.DNSZoneResourceSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneResourceStatus"): + return &apiv1alpha1.DNSZoneResourceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneSpec"): + return &apiv1alpha1.DNSZoneSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneStatus"): + return &apiv1alpha1.DNSZoneStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("Domain"): return &apiv1alpha1.DomainApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DomainFilter"): 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 d5c517b1e..00b3d3c51 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/api_client.go @@ -30,6 +30,7 @@ type OpenstackV1alpha1Interface interface { RESTClient() rest.Interface AddressScopesGetter ApplicationCredentialsGetter + DNSZonesGetter DomainsGetter EndpointsGetter FlavorsGetter @@ -68,6 +69,10 @@ func (c *OpenstackV1alpha1Client) ApplicationCredentials(namespace string) Appli return newApplicationCredentials(c, namespace) } +func (c *OpenstackV1alpha1Client) DNSZones(namespace string) DNSZoneInterface { + return newDNSZones(c, namespace) +} + func (c *OpenstackV1alpha1Client) Domains(namespace string) DomainInterface { return newDomains(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnszone.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnszone.go new file mode 100644 index 000000000..5aad6d083 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnszone.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" +) + +// DNSZonesGetter has a method to return a DNSZoneInterface. +// A group's client should implement this interface. +type DNSZonesGetter interface { + DNSZones(namespace string) DNSZoneInterface +} + +// DNSZoneInterface has methods to work with DNSZone resources. +type DNSZoneInterface interface { + Create(ctx context.Context, dNSZone *apiv1alpha1.DNSZone, opts v1.CreateOptions) (*apiv1alpha1.DNSZone, error) + Update(ctx context.Context, dNSZone *apiv1alpha1.DNSZone, opts v1.UpdateOptions) (*apiv1alpha1.DNSZone, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, dNSZone *apiv1alpha1.DNSZone, opts v1.UpdateOptions) (*apiv1alpha1.DNSZone, 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.DNSZone, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.DNSZoneList, 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.DNSZone, err error) + Apply(ctx context.Context, dNSZone *applyconfigurationapiv1alpha1.DNSZoneApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.DNSZone, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, dNSZone *applyconfigurationapiv1alpha1.DNSZoneApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.DNSZone, err error) + DNSZoneExpansion +} + +// dNSZones implements DNSZoneInterface +type dNSZones struct { + *gentype.ClientWithListAndApply[*apiv1alpha1.DNSZone, *apiv1alpha1.DNSZoneList, *applyconfigurationapiv1alpha1.DNSZoneApplyConfiguration] +} + +// newDNSZones returns a DNSZones +func newDNSZones(c *OpenstackV1alpha1Client, namespace string) *dNSZones { + return &dNSZones{ + gentype.NewClientWithListAndApply[*apiv1alpha1.DNSZone, *apiv1alpha1.DNSZoneList, *applyconfigurationapiv1alpha1.DNSZoneApplyConfiguration]( + "dnszones", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.DNSZone { return &apiv1alpha1.DNSZone{} }, + func() *apiv1alpha1.DNSZoneList { return &apiv1alpha1.DNSZoneList{} }, + ), + } +} 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 f5dcb5da4..867363956 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 @@ -36,6 +36,10 @@ func (c *FakeOpenstackV1alpha1) ApplicationCredentials(namespace string) v1alpha return newFakeApplicationCredentials(c, namespace) } +func (c *FakeOpenstackV1alpha1) DNSZones(namespace string) v1alpha1.DNSZoneInterface { + return newFakeDNSZones(c, namespace) +} + func (c *FakeOpenstackV1alpha1) Domains(namespace string) v1alpha1.DomainInterface { return newFakeDomains(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnszone.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnszone.go new file mode 100644 index 000000000..3b9b97db6 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnszone.go @@ -0,0 +1,51 @@ +/* +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" +) + +// fakeDNSZones implements DNSZoneInterface +type fakeDNSZones struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.DNSZone, *v1alpha1.DNSZoneList, *apiv1alpha1.DNSZoneApplyConfiguration] + Fake *FakeOpenstackV1alpha1 +} + +func newFakeDNSZones(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.DNSZoneInterface { + return &fakeDNSZones{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.DNSZone, *v1alpha1.DNSZoneList, *apiv1alpha1.DNSZoneApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("dnszones"), + v1alpha1.SchemeGroupVersion.WithKind("DNSZone"), + func() *v1alpha1.DNSZone { return &v1alpha1.DNSZone{} }, + func() *v1alpha1.DNSZoneList { return &v1alpha1.DNSZoneList{} }, + func(dst, src *v1alpha1.DNSZoneList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.DNSZoneList) []*v1alpha1.DNSZone { return gentype.ToPointerSlice(list.Items) }, + func(list *v1alpha1.DNSZoneList, items []*v1alpha1.DNSZone) { + 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 e13858a9c..f13c24f37 100644 --- a/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/generated_expansion.go @@ -22,6 +22,8 @@ type AddressScopeExpansion interface{} type ApplicationCredentialExpansion interface{} +type DNSZoneExpansion interface{} + type DomainExpansion interface{} type EndpointExpansion interface{} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/dnszone.go b/pkg/clients/informers/externalversions/api/v1alpha1/dnszone.go new file mode 100644 index 000000000..db9fa8e78 --- /dev/null +++ b/pkg/clients/informers/externalversions/api/v1alpha1/dnszone.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" +) + +// DNSZoneInformer provides access to a shared informer and lister for +// DNSZones. +type DNSZoneInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.DNSZoneLister +} + +type dNSZoneInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewDNSZoneInformer constructs a new informer for DNSZone 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 NewDNSZoneInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredDNSZoneInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredDNSZoneInformer constructs a new informer for DNSZone 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 NewFilteredDNSZoneInformer(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().DNSZones(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSZones(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSZones(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSZones(namespace).Watch(ctx, options) + }, + }, + &v2apiv1alpha1.DNSZone{}, + resyncPeriod, + indexers, + ) +} + +func (f *dNSZoneInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredDNSZoneInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *dNSZoneInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&v2apiv1alpha1.DNSZone{}, f.defaultInformer) +} + +func (f *dNSZoneInformer) Lister() apiv1alpha1.DNSZoneLister { + return apiv1alpha1.NewDNSZoneLister(f.Informer().GetIndexer()) +} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go index 2e9f92392..d37eb2b4c 100644 --- a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go +++ b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go @@ -28,6 +28,8 @@ type Interface interface { AddressScopes() AddressScopeInformer // ApplicationCredentials returns a ApplicationCredentialInformer. ApplicationCredentials() ApplicationCredentialInformer + // DNSZones returns a DNSZoneInformer. + DNSZones() DNSZoneInformer // Domains returns a DomainInformer. Domains() DomainInformer // Endpoints returns a EndpointInformer. @@ -97,6 +99,11 @@ func (v *version) ApplicationCredentials() ApplicationCredentialInformer { return &applicationCredentialInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// DNSZones returns a DNSZoneInformer. +func (v *version) DNSZones() DNSZoneInformer { + return &dNSZoneInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // Domains returns a DomainInformer. func (v *version) Domains() DomainInformer { return &domainInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go index fb3637f1e..2b6d9f43f 100644 --- a/pkg/clients/informers/externalversions/generic.go +++ b/pkg/clients/informers/externalversions/generic.go @@ -57,6 +57,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().AddressScopes().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("applicationcredentials"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().ApplicationCredentials().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("dnszones"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().DNSZones().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("domains"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().Domains().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("endpoints"): diff --git a/pkg/clients/listers/api/v1alpha1/dnszone.go b/pkg/clients/listers/api/v1alpha1/dnszone.go new file mode 100644 index 000000000..b77bd935a --- /dev/null +++ b/pkg/clients/listers/api/v1alpha1/dnszone.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" +) + +// DNSZoneLister helps list DNSZones. +// All objects returned here must be treated as read-only. +type DNSZoneLister interface { + // List lists all DNSZones in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.DNSZone, err error) + // DNSZones returns an object that can list and get DNSZones. + DNSZones(namespace string) DNSZoneNamespaceLister + DNSZoneListerExpansion +} + +// dNSZoneLister implements the DNSZoneLister interface. +type dNSZoneLister struct { + listers.ResourceIndexer[*apiv1alpha1.DNSZone] +} + +// NewDNSZoneLister returns a new DNSZoneLister. +func NewDNSZoneLister(indexer cache.Indexer) DNSZoneLister { + return &dNSZoneLister{listers.New[*apiv1alpha1.DNSZone](indexer, apiv1alpha1.Resource("dnszone"))} +} + +// DNSZones returns an object that can list and get DNSZones. +func (s *dNSZoneLister) DNSZones(namespace string) DNSZoneNamespaceLister { + return dNSZoneNamespaceLister{listers.NewNamespaced[*apiv1alpha1.DNSZone](s.ResourceIndexer, namespace)} +} + +// DNSZoneNamespaceLister helps list and get DNSZones. +// All objects returned here must be treated as read-only. +type DNSZoneNamespaceLister interface { + // List lists all DNSZones in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.DNSZone, err error) + // Get retrieves the DNSZone from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.DNSZone, error) + DNSZoneNamespaceListerExpansion +} + +// dNSZoneNamespaceLister implements the DNSZoneNamespaceLister +// interface. +type dNSZoneNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.DNSZone] +} diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go index 4d7043581..fad52d8a0 100644 --- a/pkg/clients/listers/api/v1alpha1/expansion_generated.go +++ b/pkg/clients/listers/api/v1alpha1/expansion_generated.go @@ -34,6 +34,14 @@ type ApplicationCredentialListerExpansion interface{} // ApplicationCredentialNamespaceLister. type ApplicationCredentialNamespaceListerExpansion interface{} +// DNSZoneListerExpansion allows custom methods to be added to +// DNSZoneLister. +type DNSZoneListerExpansion interface{} + +// DNSZoneNamespaceListerExpansion allows custom methods to be added to +// DNSZoneNamespaceLister. +type DNSZoneNamespaceListerExpansion interface{} + // DomainListerExpansion allows custom methods to be added to // DomainLister. type DomainListerExpansion interface{} diff --git a/test/apivalidations/dnszone_test.go b/test/apivalidations/dnszone_test.go new file mode 100644 index 000000000..be05a5d84 --- /dev/null +++ b/test/apivalidations/dnszone_test.go @@ -0,0 +1,278 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apivalidations + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + applyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" +) + +const ( + dnszoneName = "dnszone" + dnszoneID = "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" +) + +func dnszoneStub(namespace *corev1.Namespace) *orcv1alpha1.DNSZone { + obj := &orcv1alpha1.DNSZone{} + obj.Name = dnszoneName + obj.Namespace = namespace.Name + return obj +} + +func testDNSZoneResource() *applyconfigv1alpha1.DNSZoneResourceSpecApplyConfiguration { + return applyconfigv1alpha1.DNSZoneResourceSpec().WithEmail("admin@example.com") +} + +func baseDNSZonePatch(obj client.Object) *applyconfigv1alpha1.DNSZoneApplyConfiguration { + return applyconfigv1alpha1.DNSZone(obj.GetName(), obj.GetNamespace()). + WithSpec(applyconfigv1alpha1.DNSZoneSpec(). + WithCloudCredentialsRef(testCredentials())) +} + +func testDNSZoneImport() *applyconfigv1alpha1.DNSZoneImportApplyConfiguration { + return applyconfigv1alpha1.DNSZoneImport().WithID(dnszoneID) +} + +var _ = Describe("ORC DNSZone API validations", func() { + var namespace *corev1.Namespace + BeforeEach(func() { + namespace = createNamespace() + }) + + runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.DNSZoneApplyConfiguration]{ + createObject: func(ns *corev1.Namespace) client.Object { return dnszoneStub(ns) }, + basePatch: func(obj client.Object) *applyconfigv1alpha1.DNSZoneApplyConfiguration { + return baseDNSZonePatch(obj) + }, + applyResource: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithResource(testDNSZoneResource()) + }, + applyImport: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithImport(testDNSZoneImport()) + }, + applyEmptyImport: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSZoneImport()) + }, + applyEmptyFilter: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSZoneImport().WithFilter(applyconfigv1alpha1.DNSZoneFilter())) + }, + applyValidFilter: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSZoneImport().WithFilter(applyconfigv1alpha1.DNSZoneFilter().WithName("foo."))) + }, + applyManaged: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) + }, + applyUnmanaged: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + }, + applyManagedOptions: func(p *applyconfigv1alpha1.DNSZoneApplyConfiguration) { + p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach)) + }, + getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy { + return obj.(*orcv1alpha1.DNSZone).Spec.ManagementPolicy + }, + getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete { + return obj.(*orcv1alpha1.DNSZone).Spec.ManagedOptions.OnDelete + }, + }) + + // TODO(scaffolding): Add more resource-specific validation tests. + // Some common things to test: + // - Immutability of fields with `self == oldSelf` validation + // - Enum validation (valid and invalid values) + // - Numeric range validation (min/max bounds) + // - Tag uniqueness (if the resource has tags with listType=set) + // - Format validation (CIDR, UUID, etc.) + // - Cross-field validation rules + It("should reject a dnszone without required fields (email)", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec()) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("email is required for PRIMARY zones"))) + }) + + It("should reject invalid type enum value", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithType(orcv1alpha1.DNSZoneType("INVALID"))) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("Unsupported value"))) + }) + + DescribeTable("should permit valid type enum values", + func(ctx context.Context, ztype orcv1alpha1.DNSZoneType) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithType(ztype)) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + }, + Entry("PRIMARY", orcv1alpha1.DNSZoneTypePrimary), + ) + + It("should reject SECONDARY type without masters", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithType(orcv1alpha1.DNSZoneTypeSecondary)) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("masters: required when type is SECONDARY"))) + }) + + It("should reject SECONDARY type with email specified", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithType(orcv1alpha1.DNSZoneTypeSecondary). + WithEmail("admin@example.com"). + WithMasters("1.2.3.4")) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("email: must not be specified when type is SECONDARY"))) + }) + + It("should permit SECONDARY type with masters", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithType(orcv1alpha1.DNSZoneTypeSecondary). + WithMasters("1.2.3.4")) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + }) + + It("should reject PRIMARY type with masters specified", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithType(orcv1alpha1.DNSZoneTypePrimary). + WithEmail("admin@example.com"). + WithMasters("1.2.3.4")) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("masters: must not be specified when type is PRIMARY"))) + }) + + It("should reject invalid email formats", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("invalid-email")) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("spec.resource.email"))) + }) + + It("should have immutable name", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithName(orcv1alpha1.OpenStackName("example.com."))) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithName(orcv1alpha1.OpenStackName("different.com."))) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("name is immutable"))) + }) + + It("should have immutable type", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithType(orcv1alpha1.DNSZoneTypePrimary)) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithType(orcv1alpha1.DNSZoneTypeSecondary). + WithMasters("1.2.3.4")) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("type is immutable"))) + }) + + It("should accept a valid DNSZone manifest", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithName("example.com."). + WithEmail("admin@example.com"). + WithTTL(3600). + WithType(orcv1alpha1.DNSZoneTypePrimary)) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + }) + + It("should reject invalid TTL values", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithTTL(0)) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("should be greater than or equal to 1"))) + }) + + It("should reject TTL values greater than 2147483647", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := map[string]interface{}{ + "apiVersion": "openstack.k-orc.cloud/v1alpha1", + "kind": "DNSZone", + "metadata": map[string]interface{}{ + "name": dnszone.Name, + "namespace": dnszone.Namespace, + }, + "spec": map[string]interface{}{ + "cloudCredentialsRef": map[string]interface{}{ + "secretName": "openstack-credentials", + "cloudName": "openstack", + }, + "resource": map[string]interface{}{ + "email": "admin@example.com", + "ttl": 2147483648, + }, + }, + } + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("should be less than or equal to 2147483647"))) + }) + + It("should permit valid TTL values", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithTTL(300)) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + }) + + It("should reject Name if it does not end with a period", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithName(orcv1alpha1.OpenStackName("example.com"))) + Expect(applyObj(ctx, dnszone, patch)).To(MatchError(ContainSubstring("name must end with a period"))) + }) + + It("should permit Name ending with a period", func(ctx context.Context) { + dnszone := dnszoneStub(namespace) + patch := baseDNSZonePatch(dnszone) + patch.Spec.WithResource(applyconfigv1alpha1.DNSZoneResourceSpec(). + WithEmail("admin@example.com"). + WithName(orcv1alpha1.OpenStackName("example.com."))) + Expect(applyObj(ctx, dnszone, patch)).To(Succeed()) + }) +}) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index 1ccbb7352..66a66ff36 100644 --- a/website/docs/crd-reference.md +++ b/website/docs/crd-reference.md @@ -12,6 +12,7 @@ Package v1alpha1 contains API Schema definitions for the openstack v1alpha1 API ### Resource Types - [AddressScope](#addressscope) - [ApplicationCredential](#applicationcredential) +- [DNSZone](#dnszone) - [Domain](#domain) - [Endpoint](#endpoint) - [Flavor](#flavor) @@ -504,6 +505,7 @@ CloudCredentialsReference is a reference to a secret containing OpenStack creden _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) - [FlavorSpec](#flavorspec) @@ -549,6 +551,168 @@ _Appears in:_ +#### DNSZone + + + +DNSZone is the Schema for an ORC resource. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | | +| `kind` _string_ | `DNSZone` | | | +| `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` _[DNSZoneSpec](#dnszonespec)_ | spec specifies the desired state of the resource. | | Required: \{\}
| +| `status` _[DNSZoneStatus](#dnszonestatus)_ | status defines the observed state of the resource. | | Optional: \{\}
| + + +#### DNSZoneFilter + + + +DNSZoneFilter defines an existing resource by its properties + +_Validation:_ +- MinProperties: 1 + +_Appears in:_ +- [DNSZoneImport](#dnszoneimport) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _[OpenStackName](#openstackname)_ | name of the existing resource | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
Optional: \{\}
| +| `email` _string_ | email of the existing resource | | Format: email
MaxLength: 255
Optional: \{\}
| +| `description` _string_ | description of the existing resource | | MaxLength: 255
MinLength: 1
Optional: \{\}
| +| `ttl` _integer_ | ttl of the existing resource | | Maximum: 2.147483647e+09
Minimum: 1
Optional: \{\}
| +| `type` _[DNSZoneType](#dnszonetype)_ | type of the existing resource | | Enum: [PRIMARY SECONDARY]
Optional: \{\}
| +| `masters` _string array_ | masters of the existing resource | | MaxItems: 32
items:MaxLength: 255
Optional: \{\}
| + + +#### DNSZoneImport + + + +DNSZoneImport specifies an existing resource which will be imported instead of +creating a new one + +_Validation:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [DNSZoneSpec](#dnszonespec) + +| 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` _[DNSZoneFilter](#dnszonefilter)_ | 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: \{\}
| + + +#### DNSZoneResourceSpec + + + +DNSZoneResourceSpec contains the desired state of the resource. + + + +_Appears in:_ +- [DNSZoneSpec](#dnszonespec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _[OpenStackName](#openstackname)_ | name will be the name of the created resource. If not specified, the
name of the ORC object will be used. | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
Optional: \{\}
| +| `email` _string_ | email is the email address of the administrator for the zone. | | Format: email
MaxLength: 255
Optional: \{\}
| +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| +| `ttl` _integer_ | ttl is the Time To Live for the zone in seconds. | | Maximum: 2.147483647e+09
Minimum: 1
Optional: \{\}
| +| `type` _[DNSZoneType](#dnszonetype)_ | type is the type of the zone. | PRIMARY | Enum: [PRIMARY SECONDARY]
Optional: \{\}
| +| `masters` _string array_ | masters specifies zone masters if this is a secondary zone. | | MaxItems: 32
items:MaxLength: 255
Optional: \{\}
| + + +#### DNSZoneResourceStatus + + + +DNSZoneResourceStatus represents the observed state of the resource. + + + +_Appears in:_ +- [DNSZoneStatus](#dnszonestatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is a Human-readable name for the resource. Might not be unique. | | MaxLength: 1024
Optional: \{\}
| +| `email` _string_ | email is the email contact of the zone. | | MaxLength: 1024
Optional: \{\}
| +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 1024
Optional: \{\}
| +| `ttl` _integer_ | ttl is the Time to Live for the zone in seconds. | | Optional: \{\}
| +| `type` _string_ | type is the type of the zone. | | MaxLength: 255
Optional: \{\}
| +| `masters` _string array_ | masters specifies zone masters if this is a secondary zone. | | MaxItems: 32
items:MaxLength: 255
Optional: \{\}
| +| `transferredAt` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#time-v1-meta)_ | transferredAt is the last time an update was retrieved from the master servers. | | Optional: \{\}
| +| `status` _string_ | status is the status of the resource. | | MaxLength: 255
Optional: \{\}
| + + +#### DNSZoneSpec + + + +DNSZoneSpec defines the desired state of an ORC object. + + + +_Appears in:_ +- [DNSZone](#dnszone) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `import` _[DNSZoneImport](#dnszoneimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MaxProperties: 1
MinProperties: 1
Optional: \{\}
| +| `resource` _[DNSZoneResourceSpec](#dnszoneresourcespec)_ | resource specifies the desired state of the resource.
resource may not be specified if the management policy is `unmanaged`.
resource must be specified if the management policy is `managed`. | | Optional: \{\}
| +| `managementPolicy` _[ManagementPolicy](#managementpolicy)_ | managementPolicy defines how ORC will treat the object. Valid values are
`managed`: ORC will create, update, and delete the resource; `unmanaged`:
ORC will import an existing resource, and will not apply updates to it or
delete it. | managed | Enum: [managed unmanaged]
Optional: \{\}
| +| `managedOptions` _[ManagedOptions](#managedoptions)_ | managedOptions specifies options which may be applied to managed objects. | | Optional: \{\}
| +| `cloudCredentialsRef` _[CloudCredentialsReference](#cloudcredentialsreference)_ | cloudCredentialsRef points to a secret containing OpenStack credentials | | Required: \{\}
| + + +#### DNSZoneStatus + + + +DNSZoneStatus defines the observed state of an ORC resource. + + + +_Appears in:_ +- [DNSZone](#dnszone) + +| 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` _[DNSZoneResourceStatus](#dnszoneresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| + + +#### DNSZoneType + +_Underlying type:_ _string_ + + + +_Validation:_ +- Enum: [PRIMARY SECONDARY] + +_Appears in:_ +- [DNSZoneFilter](#dnszonefilter) +- [DNSZoneResourceSpec](#dnszoneresourcespec) + +| Field | Description | +| --- | --- | +| `PRIMARY` | | +| `SECONDARY` | | + + #### Domain @@ -2262,6 +2426,7 @@ _Appears in:_ _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) - [FlavorSpec](#flavorspec) @@ -2302,6 +2467,7 @@ _Validation:_ _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) - [FlavorSpec](#flavorspec) @@ -2608,6 +2774,8 @@ _Appears in:_ - [AddressScopeResourceSpec](#addressscoperesourcespec) - [ApplicationCredentialFilter](#applicationcredentialfilter) - [ApplicationCredentialResourceSpec](#applicationcredentialresourcespec) +- [DNSZoneFilter](#dnszonefilter) +- [DNSZoneResourceSpec](#dnszoneresourcespec) - [FlavorFilter](#flavorfilter) - [FlavorResourceSpec](#flavorresourcespec) - [ImageFilter](#imagefilter) diff --git a/website/docs/user-guide/dnszone.md b/website/docs/user-guide/dnszone.md new file mode 100644 index 000000000..3db069735 --- /dev/null +++ b/website/docs/user-guide/dnszone.md @@ -0,0 +1,244 @@ +# DNS Zones (DNSZone) + +The `DNSZone` resource manages DNS zones in OpenStack Designate. It allows you to declaratively create, update, and delete primary and secondary DNS zones, or import existing zones for read-only access. + +--- + +## Core Concepts + +In OpenStack Designate, a DNS zone holds DNS records (such as A, AAAA, MX, TXT, etc.). ORC supports both **PRIMARY** and **SECONDARY** zones: +* **PRIMARY** zones are master zones where DNS records are managed directly within OpenStack Designate. +* **SECONDARY** zones are read-only copies of zones that automatically perform zone transfers from external master DNS servers. + +### Domain Name Syntax +All DNS zone names in OpenStack Designate and ORC **must end with a trailing period** (e.g., `example.com.`). This is enforced by Kubernetes API validation rules. + +--- + +## Management Policies + +Like all ORC resources, `DNSZone` supports two management policies: `managed` and `unmanaged`. + +### 1. Managed Zone (Default) + +In the `managed` policy, ORC handles the entire lifecycle of the DNS zone in OpenStack Designate. +* **Creation**: ORC creates the zone if it does not exist. +* **Update**: ORC synchronizes specifications (like description, email, TTL, and masters) to Designate. (Note: `name` and `type` are immutable). +* **Deletion**: On deletion of the Kubernetes resource, the corresponding Designate zone is deleted (unless `managedOptions.onDelete` is set to `detach`). + +#### Option A: Managed Primary Zone + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: primary-zone +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: managed + resource: + # name specifies the name of the DNS Zone. Must end with a period. + # Defaults to the ORC object name if not specified. + # Immutable after creation. + name: primary.example.com. + + # email is the email address of the administrator for the zone. + # Required for PRIMARY zones. Must be omitted for SECONDARY zones. + email: admin@example.com + + # description is a human-readable description for the DNS Zone. + description: "Complete managed primary DNS zone example" + + # ttl is the Time To Live for the zone in seconds. + ttl: 3600 + + # type specifies the type of the zone. Can be 'PRIMARY' or 'SECONDARY'. + # Immutable after creation. + type: PRIMARY +``` + +#### Option B: Managed Secondary Zone + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: secondary-zone +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: managed + resource: + # name specifies the name of the DNS Zone. Must end with a period. + name: secondary.example.com. + + # type specifies the type of the zone. + type: SECONDARY + + # masters specifies zone masters from which zone transfers are performed. + # Required for SECONDARY zones. Must be omitted for PRIMARY zones. + masters: + - 192.0.2.1 + - 192.0.2.2 + + description: "Complete managed secondary DNS zone example" + ttl: 3600 +``` + +### 2. Unmanaged Import Workflow + +In the `unmanaged` policy, ORC imports an existing DNS zone from OpenStack Designate. ORC will **never** modify or delete the OpenStack resource; it is imported for read-only status propagation. + +You can import an existing zone using either its **ID (UUID)** or by matching its **properties (filters)**. + +#### Option A: Import by ID +Use this option when you know the exact UUID of the existing zone. + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: imported-zone-by-id +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + id: "12345678-1234-1234-1234-1234567890ab" +``` + +#### Option B: Import by Filter +Use this option when you want to look up an existing zone based on its properties. The filter **must resolve to exactly one resource** in OpenStack Designate, otherwise ORC will enter an error state. + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: imported-zone-by-filter +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + filter: + name: existing-zone.example.com. + email: admin@example.com + ttl: 3600 +``` + +--- + +## Validation Rules & Immutability + +The `DNSZone` Custom Resource Definition (CRD) implements strict validation via Common Expression Language (CEL) and OpenAPI schemas: + +* **Name Validation**: + * Must end with a trailing period (`.`). + * Immutable. Once created, you cannot change the zone name in the specification. + * Defaults to the ORC object name (with a trailing period appended by the user) if not explicitly set. +* **Type Validation**: + * Allowed values are `PRIMARY` and `SECONDARY`. + * Immutable. Once created, you cannot change the zone type. +* **Email Validation**: + * Required when `type` is `PRIMARY`. + * Must be omitted (not specified) when `type` is `SECONDARY`. + * Must be a valid email format. + * Maximum length of `255` characters. +* **Masters Validation**: + * Required when `type` is `SECONDARY` (must specify at least one master IP address). + * Must be omitted (not specified) when `type` is `PRIMARY`. + * Maximum of `32` items, each with a maximum length of `255` characters. +* **TTL Validation**: + * Must be an integer between `1` and `2147483647` (inclusive). +* **Description Validation**: + * Length must be between `1` and `255` characters. + +--- + +## Status Conditions + +To check the reconciliation and readiness of your `DNSZone`, inspect its status conditions: + +```bash +kubectl get dnszone primary-zone -o yaml +``` + +### 1. `Available` Condition +* `True`: The DNS zone is created and active (`ACTIVE` status in Designate). +* `False`: The DNS zone is not ready for use. Common reasons include: + * `Progressing`: The zone is currently in a `PENDING` state in Designate. ORC is actively polling until it transitions to `ACTIVE`. + * `UnrecoverableError`: The zone is in an `ERROR` state in Designate, or the import configuration points to a non-existent ID or filter. + * `InvalidConfiguration`: The configuration is invalid or there was an authentication/client issue. + +### 2. `Progressing` Condition +* `True`: ORC is still performing operations or polling for updates (e.g., waiting for the zone to become `ACTIVE` from a `PENDING` state). +* `False`: ORC has completed reconciliation. The spec matches the observed status or a terminal error has been reached. + +--- + +## Troubleshooting + +Here are common issues you might encounter with `DNSZone` resources and how to solve them: + +### 1. Validation Failures on Creation + +**Symptom:** +When applying the YAML, you get an API rejection error: +``` +Error from server (BadRequest): error when creating "dnszone.yaml": DNSZone.openstack.k-orc.cloud "my-zone" is invalid: spec.resource.name: Invalid value: "my-zone": name must end with a period +``` + +**Solution:** +Ensure that your `spec.resource.name` ends with a period (e.g., `my-zone.example.com.`). If you omit `spec.resource.name`, the controller will default to the Kubernetes object name, but the name must still be valid as a DNS Zone name (ending in a `.`). Therefore, if you omit `spec.resource.name`, the Kubernetes object's name itself is used, but because Kubernetes resource names cannot end with a period, you must explicitly specify `spec.resource.name` with the trailing period. + +--- + +### 2. Zone Creation Hangs or Enters Terminal Error (409 Conflict) + +**Symptom:** +The resource reports `Available=False` and `Progressing=False` with a message containing: +``` +Conflicting zone already exists in OpenStack Designate +``` + +**Cause:** +A DNS Zone with the same domain name already exists in the target OpenStack tenant. Designate does not allow duplicate zone names. + +**Solution:** +* If you want to take over and manage this existing zone, you should change your `managementPolicy` to `unmanaged` and use the `import` mechanism. +* If you want to create a new managed zone, choose a unique domain name. + +--- + +### 3. Unmanaged Import Fails to Find Zone + +**Symptom:** +The imported `DNSZone` is stuck in an error state with the message: +``` +referenced resource does not exist in OpenStack +``` + +**Solution:** +* Verify that the ID in `spec.import.id` is correct and matches an existing zone in Designate. +* If importing by `spec.import.filter`, verify that the filter matches **exactly one** zone. If it matches zero or multiple zones, the import will fail. Use the OpenStack CLI to verify: + ```bash + openstack zone list --name existing-zone.example.com. + ``` + +--- + +### 4. Zone stuck in PENDING status + +**Symptom:** +`Available` is `False`, `Progressing` is `True`, and the message is `Waiting for resource to become available in OpenStack`. + +**Cause:** +Designate is asynchronously processing the zone creation/update or communicating with backend DNS nameservers. + +**Solution:** +This is normal behavior. ORC is actively polling OpenStack. Wait a few moments for Designate to transition the zone status to `ACTIVE`. If it remains in this state for a prolonged period, check the Designate API service status and your OpenStack logs. diff --git a/website/mkdocs.yml b/website/mkdocs.yml index e71fbe9e9..1d382ff38 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -7,7 +7,9 @@ nav: - Getting Started: - Installation: installation.md - Quick Start: getting-started.md - - User Guide: user-guide/index.md + - User Guide: + - Overview: user-guide/index.md + - DNS Zones: user-guide/dnszone.md - CRD Reference: crd-reference.md - Troubleshooting: troubleshooting.md - Contributing: