diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 774cc5a29..8bc3eb172 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -44,6 +44,10 @@ jobs: 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/designate/designate.conf]] + [DEFAULT] + quota_zones = 100 + [[post-config|/etc/nova/nova.conf]] [filter_scheduler] enabled_filters = ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,ServerGroupAntiAffinityFilter,ServerGroupAffinityFilter,SameHostFilter,DifferentHostFilter,SimpleCIDRAffinityFilter,JsonFilter diff --git a/PROJECT b/PROJECT index 17983752f..9da612d2e 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: DNSRecordset + path: github.com/k-orc/openstack-resource-controller/api/v1alpha1 + version: v1alpha1 - api: crdVersion: v1 namespaced: true diff --git a/api/v1alpha1/dnsrecordset_types.go b/api/v1alpha1/dnsrecordset_types.go new file mode 100644 index 000000000..21d09c290 --- /dev/null +++ b/api/v1alpha1/dnsrecordset_types.go @@ -0,0 +1,138 @@ +/* +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 + +// DNSRecordsetResourceSpec specifies the desired state of the resource. +type DNSRecordsetResourceSpec 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="name must end with a period" + // +optional + Name *OpenStackName `json:"name,omitempty"` + + // type is the type of the recordset (e.g., A, AAAA, CNAME, MX, TXT, etc.). + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + // +kubebuilder:validation:MaxLength:=255 + // +required + Type string `json:"type"` + + // records are the DNS records of the recordset. + // +kubebuilder:validation:MinItems:=1 + // +kubebuilder:validation:MaxItems:=1024 + // +kubebuilder:validation:items:MaxLength:=1024 + // +listType=atomic + // +required + Records []string `json:"records,omitempty"` + + // ttl is the Time To Live for the recordset. + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=2147483647 + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // description is a human-readable description for the resource. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` + + // dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="dnsZoneRef is immutable" + // +required + DNSZoneRef KubernetesNameRef `json:"dnsZoneRef,omitempty"` +} + +// DNSRecordsetFilter defines an existing resource by its properties. +// +kubebuilder:validation:MinProperties:=1 +type DNSRecordsetFilter struct { + // dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="dnsZoneRef is immutable" + // +required + DNSZoneRef KubernetesNameRef `json:"dnsZoneRef,omitempty"` + + // name of the existing resource. + // +kubebuilder:validation:XValidation:rule="self.endsWith('.')",message="name must end with a period" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="name is immutable" + // +required + Name OpenStackName `json:"name"` //nolint:kubeapilinter // Name is required + + // type of the existing resource. + // +kubebuilder:validation:MaxLength:=255 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + // +required + Type string `json:"type"` + + // ttl of the existing resource. + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=2147483647 + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // description of the existing resource. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=255 + // +optional + Description *string `json:"description,omitempty"` +} + +// DNSRecordsetImport specifies an existing resource which will be imported instead of +// creating a new one. +// +kubebuilder:validation:MinProperties:=1 +// +kubebuilder:validation:MaxProperties:=1 +type DNSRecordsetImport struct { + // filter contains a resource query which is expected to return a single + // result. The controller will continue to retry if filter returns no + // results. If filter returns multiple results the controller will set an + // error state and will not continue to retry. + // +required + Filter *DNSRecordsetFilter `json:"filter,omitempty"` //nolint:kubeapilinter // Filter is a required pointer field because DNSRecordsetImport has no other fields, and required struct fields are represented as pointers across ORC. +} + +// DNSRecordsetResourceStatus represents the observed state of the resource. +type DNSRecordsetResourceStatus struct { + // name is a human-readable name for the resource. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Name string `json:"name,omitempty"` + + // type is the type of the recordset. + // +kubebuilder:validation:MaxLength=255 + // +optional + Type string `json:"type,omitempty"` + + // records are the DNS records of the recordset. + // +kubebuilder:validation:MaxItems:=1024 + // +kubebuilder:validation:items:MaxLength:=1024 + // +listType=atomic + // +optional + Records []string `json:"records,omitempty"` + + // ttl is the Time To Live for the recordset in seconds. + // +optional + TTL *int32 `json:"ttl,omitempty"` + + // description is a human-readable description for the resource. + // +kubebuilder:validation:MaxLength=1024 + // +optional + Description string `json:"description,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 cce104fd5..185309734 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -698,6 +698,233 @@ 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 *DNSRecordset) DeepCopyInto(out *DNSRecordset) { + *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 DNSRecordset. +func (in *DNSRecordset) DeepCopy() *DNSRecordset { + if in == nil { + return nil + } + out := new(DNSRecordset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordset) 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 *DNSRecordsetFilter) DeepCopyInto(out *DNSRecordsetFilter) { + *out = *in + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetFilter. +func (in *DNSRecordsetFilter) DeepCopy() *DNSRecordsetFilter { + if in == nil { + return nil + } + out := new(DNSRecordsetFilter) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordsetImport) DeepCopyInto(out *DNSRecordsetImport) { + *out = *in + if in.Filter != nil { + in, out := &in.Filter, &out.Filter + *out = new(DNSRecordsetFilter) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetImport. +func (in *DNSRecordsetImport) DeepCopy() *DNSRecordsetImport { + if in == nil { + return nil + } + out := new(DNSRecordsetImport) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordsetList) DeepCopyInto(out *DNSRecordsetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRecordset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetList. +func (in *DNSRecordsetList) DeepCopy() *DNSRecordsetList { + if in == nil { + return nil + } + out := new(DNSRecordsetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordsetList) 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 *DNSRecordsetResourceSpec) DeepCopyInto(out *DNSRecordsetResourceSpec) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(OpenStackName) + **out = **in + } + if in.Records != nil { + in, out := &in.Records, &out.Records + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetResourceSpec. +func (in *DNSRecordsetResourceSpec) DeepCopy() *DNSRecordsetResourceSpec { + if in == nil { + return nil + } + out := new(DNSRecordsetResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordsetResourceStatus) DeepCopyInto(out *DNSRecordsetResourceStatus) { + *out = *in + if in.Records != nil { + in, out := &in.Records, &out.Records + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetResourceStatus. +func (in *DNSRecordsetResourceStatus) DeepCopy() *DNSRecordsetResourceStatus { + if in == nil { + return nil + } + out := new(DNSRecordsetResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordsetSpec) DeepCopyInto(out *DNSRecordsetSpec) { + *out = *in + if in.Import != nil { + in, out := &in.Import, &out.Import + *out = new(DNSRecordsetImport) + (*in).DeepCopyInto(*out) + } + if in.Resource != nil { + in, out := &in.Resource, &out.Resource + *out = new(DNSRecordsetResourceSpec) + (*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 DNSRecordsetSpec. +func (in *DNSRecordsetSpec) DeepCopy() *DNSRecordsetSpec { + if in == nil { + return nil + } + out := new(DNSRecordsetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordsetStatus) DeepCopyInto(out *DNSRecordsetStatus) { + *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(DNSRecordsetResourceStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordsetStatus. +func (in *DNSRecordsetStatus) DeepCopy() *DNSRecordsetStatus { + if in == nil { + return nil + } + out := new(DNSRecordsetStatus) + in.DeepCopyInto(out) + 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 diff --git a/api/v1alpha1/zz_generated.dnsrecordset-resource.go b/api/v1alpha1/zz_generated.dnsrecordset-resource.go new file mode 100644 index 000000000..9afea3492 --- /dev/null +++ b/api/v1alpha1/zz_generated.dnsrecordset-resource.go @@ -0,0 +1,158 @@ +// 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" +) + +// DNSRecordsetSpec 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 DNSRecordsetSpec struct { + // import refers to an existing OpenStack resource which will be imported instead of + // creating a new one. + // +optional + Import *DNSRecordsetImport `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 *DNSRecordsetResourceSpec `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"` +} + +// DNSRecordsetStatus defines the observed state of an ORC resource. +type DNSRecordsetStatus 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 *DNSRecordsetResourceStatus `json:"resource,omitempty"` +} + +var _ ObjectWithConditions = &DNSRecordset{} + +func (i *DNSRecordset) 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" + +// DNSRecordset is the Schema for an ORC resource. +type DNSRecordset 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 DNSRecordsetSpec `json:"spec,omitzero"` + + // status defines the observed state of the resource. + // +optional + Status DNSRecordsetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// DNSRecordsetList contains a list of DNSRecordset. +type DNSRecordsetList struct { + metav1.TypeMeta `json:",inline"` + + // metadata contains the list metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // items contains a list of DNSRecordset. + // +required + Items []DNSRecordset `json:"items"` +} + +func (l *DNSRecordsetList) GetItems() []DNSRecordset { + return l.Items +} + +func init() { + SchemeBuilder.Register(&DNSRecordset{}, &DNSRecordsetList{}) +} + +func (i *DNSRecordset) GetCloudCredentialsRef() (*string, *CloudCredentialsReference) { + if i == nil { + return nil, nil + } + + return &i.Namespace, &i.Spec.CloudCredentialsRef +} + +var _ CloudCredentialsRefProvider = &DNSRecordset{} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 7ab89d68c..391136a74 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/dnsrecordset" "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" @@ -136,6 +137,7 @@ func main() { volumetype.New(scopeFactory), domain.New(scopeFactory), dnszone.New(scopeFactory), + dnsrecordset.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 cd7586118..f916256ad 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.DNSRecordset": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordset(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetFilter": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetFilter(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetImport": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetImport(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetList": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetList(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetResourceSpec": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetResourceSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetResourceStatus": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetResourceStatus(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetSpec": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetSpec(ref), + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetStatus": schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetStatus(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), @@ -1663,6 +1671,419 @@ func schema_openstack_resource_controller_v2_api_v1alpha1_CloudCredentialsRefere } } +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordset(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordset 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.DNSRecordsetSpec"), + }, + }, + "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.DNSRecordsetStatus"), + }, + }, + }, + Required: []string{"spec"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetFilter(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetFilter defines an existing resource by its properties.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "dnsZoneRef": { + SchemaProps: spec.SchemaProps{ + Description: "dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name of the existing resource.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type of the existing resource.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl of the existing resource.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description of the existing resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"dnsZoneRef", "name", "type"}, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetImport(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetImport specifies an existing resource which will be imported instead of creating a new one.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "filter": { + SchemaProps: spec.SchemaProps{ + Description: "filter contains a resource query which is expected to return a single result. The controller will continue to retry if filter returns no results. If filter returns multiple results the controller will set an error state and will not continue to retry.", + Ref: ref("github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetFilter"), + }, + }, + }, + Required: []string{"filter"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetFilter"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetList contains a list of DNSRecordset.", + 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 DNSRecordset.", + 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.DNSRecordset"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordset", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetResourceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetResourceSpec specifies 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: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is the type of the recordset (e.g., A, AAAA, CNAME, MX, TXT, etc.).", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "records": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "records are the DNS records of the recordset.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl is the Time To Live for the recordset.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "dnsZoneRef": { + SchemaProps: spec.SchemaProps{ + Description: "dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "records", "dnsZoneRef"}, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetResourceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetResourceStatus 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.", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is the type of the recordset.", + Type: []string{"string"}, + Format: "", + }, + }, + "records": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "records are the DNS records of the recordset.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ttl": { + SchemaProps: spec.SchemaProps{ + Description: "ttl is the Time To Live for the recordset in seconds.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human-readable description for the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "status is the status of the resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetSpec 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.DNSRecordsetImport"), + }, + }, + "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.DNSRecordsetResourceSpec"), + }, + }, + "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.DNSRecordsetImport", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetResourceSpec", "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.ManagedOptions"}, + } +} + +func schema_openstack_resource_controller_v2_api_v1alpha1_DNSRecordsetStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "DNSRecordsetStatus 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.DNSRecordsetResourceStatus"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1.DNSRecordsetResourceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.Condition"}, + } +} + func schema_openstack_resource_controller_v2_api_v1alpha1_DNSZone(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/cmd/resource-generator/data/adapter.template b/cmd/resource-generator/data/adapter.template index 62dafde50..6206b9476 100644 --- a/cmd/resource-generator/data/adapter.template +++ b/cmd/resource-generator/data/adapter.template @@ -66,7 +66,9 @@ func (f adapterT) GetImportID() *string { if f.Spec.Import == nil { return nil } -{{- if .HasCustomImport }} +{{- if .CustomImportHasNoID }} + return nil +{{- else if .HasCustomImport }} if f.Spec.Import.Name == nil { return nil } @@ -90,7 +92,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/cmd/resource-generator/main.go b/cmd/resource-generator/main.go index 224a237ec..541812a6d 100644 --- a/cmd/resource-generator/main.go +++ b/cmd/resource-generator/main.go @@ -71,9 +71,17 @@ type templateFields struct { // non-generated *_types.go file. When true, the Import type will not be generated. // Default is false (Import type is generated). HasCustomImport bool + // CustomImportHasNoID indicates that the custom import does not have an ID or Name field, + // and so GetImportID should return nil. + CustomImportHasNoID bool } var resources []templateFields = []templateFields{ + { + Name: "DNSRecordset", + HasCustomImport: true, + CustomImportHasNoID: true, + }, { Name: "DNSZone", }, diff --git a/config/crd/bases/openstack.k-orc.cloud_dnsrecordsets.yaml b/config/crd/bases/openstack.k-orc.cloud_dnsrecordsets.yaml new file mode 100644 index 000000000..2361f1e48 --- /dev/null +++ b/config/crd/bases/openstack.k-orc.cloud_dnsrecordsets.yaml @@ -0,0 +1,374 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: dnsrecordsets.openstack.k-orc.cloud +spec: + group: openstack.k-orc.cloud + names: + categories: + - openstack + kind: DNSRecordset + listKind: DNSRecordsetList + plural: dnsrecordsets + singular: dnsrecordset + 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: DNSRecordset 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 + dnsZoneRef: + description: dnsZoneRef is a reference to the ORC DNSZone + this recordset is associated with. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: dnsZoneRef is immutable + rule: self == oldSelf + 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('.') + - message: name is immutable + rule: self == oldSelf + ttl: + description: ttl of the existing resource. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + type: + description: type of the existing resource. + maxLength: 255 + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - dnsZoneRef + - name + - type + type: object + required: + - filter + 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 + dnsZoneRef: + description: dnsZoneRef is a reference to the ORC DNSZone this + recordset is associated with. + maxLength: 253 + minLength: 1 + type: string + x-kubernetes-validations: + - message: dnsZoneRef is immutable + rule: self == oldSelf + 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: name must end with a period + rule: self.endsWith('.') + records: + description: records are the DNS records of the recordset. + items: + maxLength: 1024 + type: string + maxItems: 1024 + minItems: 1 + type: array + x-kubernetes-list-type: atomic + ttl: + description: ttl is the Time To Live for the recordset. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + type: + description: type is the type of the recordset (e.g., A, AAAA, + CNAME, MX, TXT, etc.). + maxLength: 255 + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - dnsZoneRef + - records + - type + type: object + 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 + name: + description: name is a human-readable name for the resource. + maxLength: 1024 + type: string + records: + description: records are the DNS records of the recordset. + items: + maxLength: 1024 + type: string + maxItems: 1024 + type: array + x-kubernetes-list-type: atomic + status: + description: status is the status of the resource. + maxLength: 255 + type: string + ttl: + description: ttl is the Time To Live for the recordset in seconds. + format: int32 + type: integer + type: + description: type is the type of the recordset. + 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 bfea937aa..d7cef3cbd 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_dnsrecordsets.yaml - bases/openstack.k-orc.cloud_dnszones.yaml - bases/openstack.k-orc.cloud_domains.yaml - bases/openstack.k-orc.cloud_endpoints.yaml diff --git a/config/manifests/bases/orc.clusterserviceversion.yaml b/config/manifests/bases/orc.clusterserviceversion.yaml index 524dddb1b..1e6a0c40d 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: DNSRecordset is the Schema for an ORC resource. + displayName: DNSRecordset + kind: DNSRecordset + name: dnsrecordsets.openstack.k-orc.cloud + version: v1alpha1 - description: DNSZone is the Schema for an ORC resource. displayName: DNSZone kind: DNSZone diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f57caa7d3..29fa07914 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -19,6 +19,7 @@ rules: resources: - addressscopes - applicationcredentials + - dnsrecordsets - dnszones - domains - endpoints @@ -57,6 +58,7 @@ rules: resources: - addressscopes/status - applicationcredentials/status + - dnsrecordsets/status - dnszones/status - domains/status - endpoints/status diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 095b17151..fabe69747 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_dnsrecordset.yaml - openstack_v1alpha1_dnszone.yaml - openstack_v1alpha1_domain.yaml - openstack_v1alpha1_endpoint.yaml diff --git a/config/samples/openstack_v1alpha1_dnsrecordset.yaml b/config/samples/openstack_v1alpha1_dnsrecordset.yaml new file mode 100644 index 000000000..6fd019e52 --- /dev/null +++ b/config/samples/openstack_v1alpha1_dnsrecordset.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-sample +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + name: sample.example.com. + type: A + records: + - 1.2.3.4 + ttl: 3600 + description: Sample DNSRecordset + dnsZoneRef: dnszone-sample diff --git a/internal/controllers/addressscope/zz_generated.adapter.go b/internal/controllers/addressscope/zz_generated.adapter.go index 768861dbd..cb4291d97 100644 --- a/internal/controllers/addressscope/zz_generated.adapter.go +++ b/internal/controllers/addressscope/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/applicationcredential/zz_generated.adapter.go b/internal/controllers/applicationcredential/zz_generated.adapter.go index 55f0b5346..0651b7737 100644 --- a/internal/controllers/applicationcredential/zz_generated.adapter.go +++ b/internal/controllers/applicationcredential/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/dnsrecordset/actuator.go b/internal/controllers/dnsrecordset/actuator.go new file mode 100644 index 000000000..cd190ba47 --- /dev/null +++ b/internal/controllers/dnsrecordset/actuator.go @@ -0,0 +1,470 @@ +/* +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 dnsrecordset + +import ( + "context" + "fmt" + "iter" + "slices" + "strings" + + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/progress" + "github.com/k-orc/openstack-resource-controller/v2/internal/logging" + "github.com/k-orc/openstack-resource-controller/v2/internal/osclients" + "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency" + orcerrors "github.com/k-orc/openstack-resource-controller/v2/internal/util/errors" +) + +type ( + createResourceActuator = interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT] + deleteResourceActuator = interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT] + reconcileResourceActuator = interfaces.ReconcileResourceActuator[orcObjectPT, osResourceT] + resourceReconciler = interfaces.ResourceReconciler[orcObjectPT, osResourceT] + helperFactory = interfaces.ResourceHelperFactory[orcObjectPT, orcObjectT, resourceSpecT, filterT, osResourceT] +) + +type dnsRecordsetActuator struct { + osClient osclients.DNSRecordsetClient + k8sClient client.Client + zoneID string + zoneSuffix string + orcObject orcObjectPT + zoneGone bool +} + +var _ createResourceActuator = dnsRecordsetActuator{} +var _ deleteResourceActuator = dnsRecordsetActuator{} +var _ reconcileResourceActuator = dnsRecordsetActuator{} + +func (dnsRecordsetActuator) GetResourceID(osResource *osResourceT) string { + return osResource.ID +} + +func (actuator dnsRecordsetActuator) GetOSResourceByID(ctx context.Context, id string) (*osResourceT, progress.ReconcileStatus) { + if actuator.zoneGone { + return nil, nil + } + if actuator.zoneID == "" { + return nil, progress.WaitingOnObject("DNSZone", getDNSZoneRef(actuator.orcObject), progress.WaitingOnReady) + } + resource, err := actuator.osClient.GetRecordset(ctx, actuator.zoneID, id) + if err != nil { + return nil, progress.WrapError(err) + } + return resource, nil +} + +func (actuator dnsRecordsetActuator) ListOSResourcesForAdoption(ctx context.Context, orcObject orcObjectPT) (iter.Seq2[*osResourceT, error], bool) { + resourceSpec := orcObject.Spec.Resource + if resourceSpec == nil { + return nil, false + } + + if actuator.zoneID == "" { + return nil, false + } + + listOpts := recordsets.ListOpts{ + Name: getDNSRecordsetName(orcObject), + Type: resourceSpec.Type, + } + + recordsetsSeq := actuator.osClient.ListRecordsets(ctx, actuator.zoneID, listOpts) + + adoptionSeq := func(yield func(*osResourceT, error) bool) { + for f, err := range recordsetsSeq { + if err != nil { + yield(nil, err) + return + } + + if namesMatch(f.Name, getDNSRecordsetName(orcObject)) && strings.EqualFold(f.Type, resourceSpec.Type) { + matches := true + var mismatchMsg string + + if !recordsMatch(f.Records, getNormalizedRecords(resourceSpec.Type, resourceSpec.Records)) { + matches = false + mismatchMsg = fmt.Sprintf("records mismatch: OpenStack has %v, spec has %v", f.Records, getNormalizedRecords(resourceSpec.Type, resourceSpec.Records)) + } else if resourceSpec.TTL != nil && f.TTL != int(*resourceSpec.TTL) { + matches = false + mismatchMsg = fmt.Sprintf("TTL mismatch: OpenStack has %d, spec has %d", f.TTL, *resourceSpec.TTL) + } else if resourceSpec.Description != nil { + if f.Description != *resourceSpec.Description { + matches = false + mismatchMsg = fmt.Sprintf("description mismatch: OpenStack has %q, spec has %q", f.Description, *resourceSpec.Description) + } + } else if f.Description != "" { + matches = false + mismatchMsg = fmt.Sprintf("description mismatch: OpenStack has %q, spec has empty description (omitted)", f.Description) + } + + if !matches { + err := orcerrors.Terminal( + orcv1alpha1.ConditionReasonUnrecoverableError, + fmt.Sprintf("duplicate recordset found but properties mismatch: %s", mismatchMsg), + ) + yield(nil, err) + return + } + + if !yield(f, nil) { + return + } + } + } + } + + return adoptionSeq, true +} + +func (actuator dnsRecordsetActuator) ListOSResourcesForImport(ctx context.Context, orcObject orcObjectPT, filter filterT) (iter.Seq2[*osResourceT, error], progress.ReconcileStatus) { + if actuator.zoneID == "" { + return nil, progress.WaitingOnObject("DNSZone", getDNSZoneRef(orcObject), progress.WaitingOnReady) + } + + var filters []osclients.ResourceFilter[osResourceT] + + filters = append(filters, func(f *osResourceT) bool { return namesMatch(f.Name, string(filter.Name)) }) + filters = append(filters, func(f *osResourceT) bool { return strings.EqualFold(f.Type, filter.Type) }) + if filter.TTL != nil { + filters = append(filters, func(f *osResourceT) bool { return f.TTL == int(*filter.TTL) }) + } + if filter.Description != nil { + filters = append(filters, func(f *osResourceT) bool { return f.Description == *filter.Description }) + } + + listOpts := recordsets.ListOpts{ + Name: string(filter.Name), + Type: filter.Type, + } + + recordsetsSeq := actuator.osClient.ListRecordsets(ctx, actuator.zoneID, listOpts) + return osclients.Filter(recordsetsSeq, filters...), nil +} + +func (actuator dnsRecordsetActuator) CreateResource(ctx context.Context, obj orcObjectPT) (*osResourceT, progress.ReconcileStatus) { + resource := obj.Spec.Resource + + if resource == nil { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Creation requested, but spec.resource is not set")) + } + + if err := ValidateDNSRecordset(obj, actuator.zoneSuffix); err != nil { + return nil, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration: "+err.Error(), err)) + } + + if actuator.zoneID == "" { + return nil, progress.WaitingOnObject("DNSZone", getDNSZoneRef(obj), progress.WaitingOnReady) + } + + createOpts := recordsets.CreateOpts{ + Name: getDNSRecordsetName(obj), + Type: resource.Type, + Records: getNormalizedRecords(resource.Type, resource.Records), + Description: ptr.Deref(resource.Description, ""), + } + if resource.TTL != nil { + createOpts.TTL = int(*resource.TTL) + } + + osResource, err := actuator.osClient.CreateRecordset(ctx, actuator.zoneID, createOpts) + if err != nil { + if orcerrors.IsConflict(err) { + // Try to adopt the existing out-of-band resource if properties match + adoptionSeq, canAdopt := actuator.ListOSResourcesForAdoption(ctx, obj) + if canAdopt && adoptionSeq != nil { + for r, adoptErr := range adoptionSeq { + if adoptErr != nil { + return nil, progress.WrapError(adoptErr) + } + if r != nil { + return r, nil + } + } + } + // If we couldn't find a matching resource to adopt, treat the conflict as terminal + reason := orcv1alpha1.ConditionReasonUnrecoverableError + err = orcerrors.Terminal(reason, "duplicate recordset found and cannot be adopted: "+err.Error(), err) + } else if !orcerrors.IsRetryable(err) { + reason := orcv1alpha1.ConditionReasonInvalidConfiguration + err = orcerrors.Terminal(reason, "invalid configuration creating resource: "+err.Error(), err) + } + return nil, progress.WrapError(err) + } + + return osResource, nil +} + +func (actuator dnsRecordsetActuator) DeleteResource(ctx context.Context, _ orcObjectPT, resource *osResourceT) progress.ReconcileStatus { + if actuator.zoneGone { + return nil + } + if actuator.zoneID == "" { + return progress.WaitingOnObject("DNSZone", getDNSZoneRef(actuator.orcObject), progress.WaitingOnReady) + } + return progress.WrapError(actuator.osClient.DeleteRecordset(ctx, actuator.zoneID, resource.ID)) +} + +func (actuator dnsRecordsetActuator) GetResourceReconcilers(ctx context.Context, orcObject orcObjectPT, osResource *osResourceT, controller interfaces.ResourceController) ([]resourceReconciler, progress.ReconcileStatus) { + return []resourceReconciler{ + actuator.updateResource, + }, nil +} + +func (actuator dnsRecordsetActuator) updateResource(ctx context.Context, obj orcObjectPT, osResource *osResourceT) progress.ReconcileStatus { + log := ctrl.LoggerFrom(ctx) + resource := obj.Spec.Resource + if resource == nil { + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "Update requested, but spec.resource is not set")) + } + + if err := ValidateDNSRecordset(obj, actuator.zoneSuffix); err != nil { + return progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration: "+err.Error(), err)) + } + + if actuator.zoneID == "" { + return progress.WaitingOnObject("DNSZone", getDNSZoneRef(obj), progress.WaitingOnReady) + } + + updateOpts := recordsets.UpdateOpts{} + hasChanges := false + + // Check Description + desiredDesc := ptr.Deref(resource.Description, "") + if osResource.Description != desiredDesc { + updateOpts.Description = &desiredDesc + hasChanges = true + } + + // Check TTL + // Note: Omitting/removing the optional TTL from the spec indicates that the controller + // should not manage the TTL (keeping the existing value). This is the safest path as + // Designate does not support clearing TTL values entirely. + if resource.TTL != nil { + desiredTTL := int(*resource.TTL) + if osResource.TTL != desiredTTL { + updateOpts.TTL = &desiredTTL + hasChanges = true + } + } + + // Check Records + if !recordsMatch(osResource.Records, getNormalizedRecords(resource.Type, resource.Records)) { + updateOpts.Records = getNormalizedRecords(resource.Type, resource.Records) + hasChanges = true + } + + if !hasChanges { + log.V(logging.Verbose).Info("No changes") + return nil + } + + _, err := actuator.osClient.UpdateRecordset(ctx, actuator.zoneID, 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() +} + +type dnsRecordsetHelperFactory struct{} + +var _ helperFactory = dnsRecordsetHelperFactory{} + +func newActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController, validate bool) (dnsRecordsetActuator, progress.ReconcileStatus) { + log := ctrl.LoggerFrom(ctx) + + if !validate { + zoneRef := getDNSZoneRef(orcObject) + if zoneRef != "" { + var dnsZoneObj orcv1alpha1.DNSZone + err := controller.GetK8sClient().Get(ctx, types.NamespacedName{ + Namespace: orcObject.GetNamespace(), + Name: zoneRef, + }, &dnsZoneObj) + if err != nil { + if apierrors.IsNotFound(err) { + return dnsRecordsetActuator{zoneGone: true}, nil + } + return dnsRecordsetActuator{}, progress.WrapError(err) + } + if !dnsZoneObj.DeletionTimestamp.IsZero() { + return dnsRecordsetActuator{zoneGone: true}, nil + } + } + } + + _, reconcileStatus := credentialsDependency.GetDependencies(ctx, controller.GetK8sClient(), orcObject, func(*corev1.Secret) bool { return true }) + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { + return dnsRecordsetActuator{}, reconcileStatus + } + + clientScope, err := controller.GetScopeFactory().NewClientScopeFromObject(ctx, controller.GetK8sClient(), log, orcObject) + if err != nil { + return dnsRecordsetActuator{}, progress.WrapError(err) + } + osClient, err := clientScope.NewDNSRecordsetClient() + if err != nil { + return dnsRecordsetActuator{}, progress.WrapError(err) + } + + var dnsZone *orcv1alpha1.DNSZone + var dnsZoneRS progress.ReconcileStatus + + if orcObject.Spec.Resource != nil { + dnsZone, dnsZoneRS = dnsZoneDependency.GetDependency( + ctx, controller.GetK8sClient(), orcObject, + func(dep *orcv1alpha1.DNSZone) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil && *dep.Status.ID != "" + }, + ) + } else if orcObject.Spec.Import != nil && orcObject.Spec.Import.Filter != nil { + dnsZone, dnsZoneRS = dependency.FetchDependency( + ctx, controller.GetK8sClient(), orcObject.GetNamespace(), &orcObject.Spec.Import.Filter.DNSZoneRef, "DNSZone", + func(dep *orcv1alpha1.DNSZone) bool { + return orcv1alpha1.IsAvailable(dep) && dep.Status.ID != nil && *dep.Status.ID != "" + }, + ) + } + reconcileStatus = reconcileStatus.WithReconcileStatus(dnsZoneRS) + + if needsReschedule, _ := reconcileStatus.NeedsReschedule(); needsReschedule { + return dnsRecordsetActuator{}, reconcileStatus + } + + var zoneID string + if dnsZone != nil && dnsZone.Status.ID != nil { + zoneID = *dnsZone.Status.ID + } + + var zoneSuffix string + if dnsZone != nil { + if dnsZone.Status.Resource != nil && dnsZone.Status.Resource.Name != "" { + zoneSuffix = dnsZone.Status.Resource.Name + } else if dnsZone.Spec.Resource != nil && dnsZone.Spec.Resource.Name != nil { + zoneSuffix = string(*dnsZone.Spec.Resource.Name) + } else { + zoneSuffix = dnsZone.Name + } + } + + if validate && orcObject.Spec.Resource != nil { + if err := ValidateDNSRecordset(orcObject, zoneSuffix); err != nil { + return dnsRecordsetActuator{}, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonInvalidConfiguration, "invalid configuration: "+err.Error(), err)) + } + } + + return dnsRecordsetActuator{ + osClient: osClient, + k8sClient: controller.GetK8sClient(), + zoneID: zoneID, + zoneSuffix: zoneSuffix, + orcObject: orcObject, + }, nil +} + +func (dnsRecordsetHelperFactory) NewAPIObjectAdapter(obj orcObjectPT) interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT] { + return dnsrecordsetAdapter{obj} +} + +func (dnsRecordsetHelperFactory) NewCreateActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (interfaces.CreateResourceActuator[orcObjectPT, orcObjectT, filterT, osResourceT], progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller, true) +} + +func (dnsRecordsetHelperFactory) NewDeleteActuator(ctx context.Context, orcObject orcObjectPT, controller interfaces.ResourceController) (interfaces.DeleteResourceActuator[orcObjectPT, orcObjectT, osResourceT], progress.ReconcileStatus) { + return newActuator(ctx, orcObject, controller, false) +} + +func getDNSRecordsetName(orcObject orcObjectPT) string { + name := getResourceName(orcObject) + if name != "" && name[len(name)-1] != '.' { + return name + "." + } + return name +} + +func namesMatch(a, b string) bool { + return strings.EqualFold(a, b) +} + +func recordsMatch(a, b []string) bool { + if len(a) != len(b) { + return false + } + ac := make([]string, len(a)) + copy(ac, a) + bc := make([]string, len(b)) + copy(bc, b) + slices.Sort(ac) + slices.Sort(bc) + for i := range ac { + if ac[i] != bc[i] { + return false + } + } + return true +} + +func getDNSZoneRef(orcObject orcObjectPT) string { + if orcObject == nil { + return "" + } + if orcObject.Spec.Resource != nil { + return string(orcObject.Spec.Resource.DNSZoneRef) + } + if orcObject.Spec.Import != nil && orcObject.Spec.Import.Filter != nil { + return string(orcObject.Spec.Import.Filter.DNSZoneRef) + } + return "" +} + +func getNormalizedRecords(recordType string, records []string) []string { + if len(records) == 0 { + return nil + } + normalized := make([]string, len(records)) + copy(normalized, records) + if strings.ToUpper(recordType) == "TXT" { + for i, r := range normalized { + hasPrefix := strings.HasPrefix(r, `"`) + hasSuffix := strings.HasSuffix(r, `"`) + if !hasPrefix && !hasSuffix { + normalized[i] = `"` + r + `"` + } + } + } + return normalized +} diff --git a/internal/controllers/dnsrecordset/actuator_test.go b/internal/controllers/dnsrecordset/actuator_test.go new file mode 100644 index 000000000..42972f45e --- /dev/null +++ b/internal/controllers/dnsrecordset/actuator_test.go @@ -0,0 +1,540 @@ +/* +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 dnsrecordset + +import ( + "context" + "errors" + "iter" + "testing" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + "go.uber.org/mock/gomock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + 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" +) + +var ( + errTest = errors.New("test error") +) + +const ( + testRecordsetName = "www.example.com." + testZoneID = "test-zone-id" +) + +func mockListRecordsets(recordsetsList []recordsets.RecordSet) iter.Seq2[*recordsets.RecordSet, error] { + return func(yield func(*recordsets.RecordSet, error) bool) { + for i := range recordsetsList { + if !yield(&recordsetsList[i], nil) { + return + } + } + } +} + +func TestGetResourceID(t *testing.T) { + actuator := dnsRecordsetActuator{} + rs := &recordsets.RecordSet{ID: "test-rs-id"} + if got := actuator.GetResourceID(rs); got != "test-rs-id" { + t.Errorf("Expected test-rs-id, got %s", got) + } +} + +func TestGetOSResourceByID(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + DNSZoneRef: "test-zone", + }, + }, + } + + // Case 1: empty zoneID -> wait-on-parent + actuatorEmptyZone := dnsRecordsetActuator{zoneID: "", orcObject: orcObj} + _, status := actuatorEmptyZone.GetOSResourceByID(ctx, "any-id") + if status == nil { + t.Errorf("Expected wait status on empty zoneID, got nil") + } + + // Case 2: success + mockClient.EXPECT().GetRecordset(ctx, testZoneID, "found").Return(&recordsets.RecordSet{ID: "found", Name: testRecordsetName}, nil) + actuator := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObj} + 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 recordset with ID 'found', got %v", res) + } + + // Case 3: error + mockClient.EXPECT().GetRecordset(ctx, testZoneID, "notfound").Return(nil, errTest) + res, status = actuator.GetOSResourceByID(ctx, "notfound") + if status == nil { + t.Errorf("Expected error status, got nil") + } + if res != nil { + t.Errorf("Expected nil recordset, got %v", res) + } +} + +func TestListOSResourcesForAdoption(t *testing.T) { + ctx := context.Background() + + orcObj := &orcv1alpha1.DNSRecordset{ + ObjectMeta: metav1.ObjectMeta{ + Name: "www.example.com.", + }, + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: ptr.To[int32](300), + DNSZoneRef: "test-zone", + }, + }, + } + + // Case 1: no spec resource + orcObjNoSpec := &orcv1alpha1.DNSRecordset{} + actuator := dnsRecordsetActuator{} + _, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObjNoSpec) + if canAdopt { + t.Errorf("Expected canAdopt false with no spec resource") + } + + // Case 2: empty zoneID -> canAdopt is false + actuatorEmptyZone := dnsRecordsetActuator{zoneID: ""} + _, canAdopt = actuatorEmptyZone.ListOSResourcesForAdoption(ctx, orcObj) + if canAdopt { + t.Errorf("Expected canAdopt false with empty zoneID") + } + + // Case 3: property match succeeds + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + actuator = dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObj} + + listOpts := recordsets.ListOpts{ + Name: "www.example.com.", + Type: "A", + } + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 300}, + })) + + seq, canAdopt := actuator.ListOSResourcesForAdoption(ctx, orcObj) + if !canAdopt { + t.Errorf("Expected canAdopt true") + } + next, stop := iter.Pull2(seq) + defer stop() + f, err, ok := next() + if !ok || err != nil || f == nil || f.ID != "1" { + t.Errorf("Expected to fetch recordset with ID '1', got ok=%v, err=%v, f=%v", ok, err, f) + } + + // Case 4: property mismatch returns Terminal error + // records mismatch + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"8.8.8.8"}, TTL: 300}, + })) + seq, _ = actuator.ListOSResourcesForAdoption(ctx, orcObj) + next, stop = iter.Pull2(seq) + defer stop() + _, err, ok = next() + if !ok || err == nil { + t.Errorf("Expected mismatch error, got ok=%v, err=%v", ok, err) + } + var terminalErr *orcerrors.TerminalError + if !errors.As(err, &terminalErr) { + t.Errorf("Expected TerminalError, got %v", err) + } + + // Case 5: TTL mismatch returns Terminal error + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 600}, + })) + seq, _ = actuator.ListOSResourcesForAdoption(ctx, orcObj) + next, stop = iter.Pull2(seq) + defer stop() + _, err, ok = next() + if !ok || err == nil { + t.Errorf("Expected TTL mismatch error, got ok=%v, err=%v", ok, err) + } + + // Case 6: description mismatch returns Terminal error + orcObjWithDesc := orcObj.DeepCopy() + orcObjWithDesc.Spec.Resource.Description = ptr.To("testing description") + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 300, Description: "different desc"}, + })) + actuatorWithDesc := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObjWithDesc} + seq, _ = actuatorWithDesc.ListOSResourcesForAdoption(ctx, orcObjWithDesc) + next, stop = iter.Pull2(seq) + defer stop() + _, err, ok = next() + if !ok || err == nil { + t.Errorf("Expected description mismatch error, got ok=%v, err=%v", ok, err) + } + + // Case 7: adoption succeeds when both spec and OpenStack descriptions are empty/omitted + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 300, Description: ""}, + })) + seq, _ = actuator.ListOSResourcesForAdoption(ctx, orcObj) + next, stop = iter.Pull2(seq) + defer stop() + f, err, ok = next() + if !ok || err != nil || f == nil || f.ID != "1" { + t.Errorf("Expected to adopt recordset with empty description, got ok=%v, err=%v, f=%v", ok, err, f) + } + + // Case 8: adoption fails when spec description is omitted but OpenStack description is non-empty + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "1", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 300, Description: "non-empty-desc"}, + })) + seq, _ = actuator.ListOSResourcesForAdoption(ctx, orcObj) + next, stop = iter.Pull2(seq) + defer stop() + _, err, ok = next() + if !ok || err == nil { + t.Errorf("Expected description mismatch error when spec description is omitted but OpenStack description is non-empty, got ok=%v, err=%v", ok, err) + } + if !errors.As(err, &terminalErr) { + t.Errorf("Expected TerminalError, got %v", err) + } +} + +func TestCreateResource(t *testing.T) { + ctx := context.Background() + + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: ptr.To[int32](300), + DNSZoneRef: "test-zone", + }, + }, + } + + // Case 1: empty zoneID -> wait-on-parent + actuatorEmptyZone := dnsRecordsetActuator{zoneID: "", orcObject: orcObj} + _, status := actuatorEmptyZone.CreateResource(ctx, orcObj) + if status == nil { + t.Errorf("Expected wait status on empty zoneID, got nil") + } + + // Case 2: success + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + actuator := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObj} + + createOpts := recordsets.CreateOpts{ + Name: "www.example.com.", + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: 300, + } + mockClient.EXPECT().CreateRecordset(ctx, testZoneID, createOpts).Return(&recordsets.RecordSet{ID: "created-id", Name: "www.example.com."}, nil) + + res, status := actuator.CreateResource(ctx, orcObj) + if status != nil { + t.Errorf("Expected nil status, got %v", status) + } + if res == nil || res.ID != "created-id" { + t.Errorf("Expected created recordset, got %v", res) + } + + // Case 3: terminal error on create + mockClient.EXPECT().CreateRecordset(ctx, testZoneID, createOpts).Return(nil, errTest) + _, status = actuator.CreateResource(ctx, orcObj) + if status == nil { + t.Errorf("Expected error status on create failure, got nil") + } + + // Case 4: 409 Conflict error on create with properties mismatch (Terminal error) + errConflict := gophercloud.ErrUnexpectedResponseCode{Actual: 409} + mockClient.EXPECT().CreateRecordset(ctx, testZoneID, createOpts).Return(nil, errConflict) + listOpts := recordsets.ListOpts{ + Name: "www.example.com.", + Type: "A", + } + // Return mismatching recordset (e.g., TTL = 600 instead of 300) + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "existing-id", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 600}, + })) + _, status = actuator.CreateResource(ctx, orcObj) + if status == nil { + t.Fatalf("Expected error status on 409 Conflict, got nil") + } + err := status.GetError() + if err == nil { + t.Fatal("Expected status error to be non-nil") + } + var terminalErr *orcerrors.TerminalError + if !errors.As(err, &terminalErr) { + t.Errorf("Expected TerminalError for 409 Conflict, got %T", err) + } + if terminalErr.Reason != orcv1alpha1.ConditionReasonUnrecoverableError { + t.Errorf("Expected ConditionReasonUnrecoverableError, got %s", terminalErr.Reason) + } + + // Case 5: 409 Conflict error on create with properties match (Successful adoption) + mockClient.EXPECT().CreateRecordset(ctx, testZoneID, createOpts).Return(nil, errConflict) + // Return matching recordset (TTL = 300) + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "existing-id", Name: "www.example.com.", Type: "A", Records: []string{"1.2.3.4"}, TTL: 300}, + })) + res, status = actuator.CreateResource(ctx, orcObj) + if status != nil { + t.Errorf("Expected nil status on 409 Conflict with match, got %v", status) + } + if res == nil || res.ID != "existing-id" { + t.Errorf("Expected adopted recordset with ID 'existing-id', got %v", res) + } +} + +func TestDeleteResource(t *testing.T) { + ctx := context.Background() + + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + DNSZoneRef: "test-zone", + }, + }, + } + + // Case 1: empty zoneID -> wait-on-parent + actuatorEmptyZone := dnsRecordsetActuator{zoneID: "", orcObject: orcObj} + status := actuatorEmptyZone.DeleteResource(ctx, orcObj, &recordsets.RecordSet{ID: "any-id"}) + if status == nil { + t.Errorf("Expected wait status on empty zoneID, got nil") + } + + // Case 2: success + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + actuator := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObj} + + mockClient.EXPECT().DeleteRecordset(ctx, testZoneID, "del-id").Return(nil) + status = actuator.DeleteResource(ctx, orcObj, &recordsets.RecordSet{ID: "del-id"}) + if status != nil { + t.Errorf("Expected nil status, got %v", status) + } +} + +func TestCreateResourceValidation(t *testing.T) { + ctx := context.Background() + + // Invalid A record format should trigger validation failure + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"not-an-ip"}, + DNSZoneRef: "test-zone", + }, + }, + } + + actuator := dnsRecordsetActuator{zoneID: testZoneID, zoneSuffix: "example.com.", orcObject: orcObj} + _, status := actuator.CreateResource(ctx, orcObj) + if status == nil { + t.Fatal("Expected error status on validation failure, got nil") + } + + err := status.GetError() + if err == nil { + t.Fatal("Expected error to be set on status") + } + + var terminalErr *orcerrors.TerminalError + if !errors.As(err, &terminalErr) { + t.Errorf("Expected TerminalError, got %T", err) + } +} + +func TestUpdateResource(t *testing.T) { + ctx := context.Background() + + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: ptr.To[int32](300), + Description: ptr.To("new desc"), + DNSZoneRef: "test-zone", + }, + }, + } + + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + actuator := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, zoneSuffix: "example.com.", orcObject: orcObj} + + // Case 1: No changes -> return nil + osResourceNoChanges := &recordsets.RecordSet{ + ID: "rs-id", + Name: "www.example.com.", + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: 300, + Description: "new desc", + } + status := actuator.updateResource(ctx, orcObj, osResourceNoChanges) + if status != nil { + t.Errorf("Expected nil status for no changes, got %v", status) + } + + // Case 2: TTL and Description changed -> call UpdateRecordset + osResourceWithChanges := &recordsets.RecordSet{ + ID: "rs-id", + Name: "www.example.com.", + Type: "A", + Records: []string{"1.2.3.4"}, + TTL: 600, + Description: "old desc", + } + expectedDesc := "new desc" + expectedTTL := 300 + expectedOpts := recordsets.UpdateOpts{ + Description: &expectedDesc, + TTL: &expectedTTL, + } + + mockClient.EXPECT().UpdateRecordset(ctx, testZoneID, "rs-id", expectedOpts).Return(&recordsets.RecordSet{}, nil) + status = actuator.updateResource(ctx, orcObj, osResourceWithChanges) + if status == nil { + t.Fatal("Expected non-nil status for successful update, got nil") + } + messages := status.GetProgressMessages() + if len(messages) == 0 || messages[0] != "Resource status will be refreshed" { + t.Errorf("Expected progress status to indicate refresh, got %v", messages) + } + + // Case 3: validation fails during update + orcObjInvalid := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"invalid-ip"}, + DNSZoneRef: "test-zone", + }, + }, + } + status = actuator.updateResource(ctx, orcObjInvalid, osResourceNoChanges) + if status == nil { + t.Fatal("Expected non-nil status on validation failure, got nil") + } + err := status.GetError() + if err == nil { + t.Fatal("Expected error on validation failure") + } + var terminalErr *orcerrors.TerminalError + if !errors.As(err, &terminalErr) { + t.Errorf("Expected TerminalError, got %T", err) + } +} + +func TestListOSResourcesForImport(t *testing.T) { + ctx := context.Background() + mockctrl := gomock.NewController(t) + defer mockctrl.Finish() + mockClient := mock.NewMockDNSRecordsetClient(mockctrl) + + orcObj := &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Import: &orcv1alpha1.DNSRecordsetImport{ + Filter: &orcv1alpha1.DNSRecordsetFilter{ + DNSZoneRef: "test-zone", + Name: "www.example.com.", + Type: "A", + }, + }, + }, + } + + actuator := dnsRecordsetActuator{osClient: mockClient, zoneID: testZoneID, orcObject: orcObj} + + listOpts := recordsets.ListOpts{ + Name: "www.example.com.", + Type: "A", + } + mockClient.EXPECT().ListRecordsets(ctx, testZoneID, listOpts).Return(mockListRecordsets([]recordsets.RecordSet{ + {ID: "imported-id", Name: "www.example.com.", Type: "A"}, + })) + + filter := orcObj.Spec.Import.Filter + seq, status := actuator.ListOSResourcesForImport(ctx, orcObj, *filter) + if status != nil { + t.Fatalf("Expected nil status, got %v", status) + } + + next, stop := iter.Pull2(seq) + defer stop() + f, err, ok := next() + if !ok || err != nil || f == nil || f.ID != "imported-id" { + t.Errorf("Expected to fetch recordset with ID 'imported-id', got ok=%v, err=%v, f=%v", ok, err, f) + } +} + +func TestGetOSResourceByID_ZoneGone(t *testing.T) { + ctx := context.Background() + actuator := dnsRecordsetActuator{zoneGone: true} + res, status := actuator.GetOSResourceByID(ctx, "any-id") + if status != nil { + t.Errorf("Expected nil status when zoneGone is true, got %v", status) + } + if res != nil { + t.Errorf("Expected nil recordset when zoneGone is true, got %v", res) + } +} + +func TestDeleteResource_ZoneGone(t *testing.T) { + ctx := context.Background() + orcObj := &orcv1alpha1.DNSRecordset{} + actuator := dnsRecordsetActuator{zoneGone: true} + status := actuator.DeleteResource(ctx, orcObj, &recordsets.RecordSet{ID: "any-id"}) + if status != nil { + t.Errorf("Expected nil status when zoneGone is true, got %v", status) + } +} diff --git a/internal/controllers/dnsrecordset/controller.go b/internal/controllers/dnsrecordset/controller.go new file mode 100644 index 000000000..98f53e763 --- /dev/null +++ b/internal/controllers/dnsrecordset/controller.go @@ -0,0 +1,118 @@ +/* +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 dnsrecordset + +import ( + "context" + "errors" + + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/interfaces" + "github.com/k-orc/openstack-resource-controller/v2/internal/controllers/generic/reconciler" + "github.com/k-orc/openstack-resource-controller/v2/internal/scope" + "github.com/k-orc/openstack-resource-controller/v2/internal/util/credentials" + "github.com/k-orc/openstack-resource-controller/v2/internal/util/dependency" + orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" + "github.com/k-orc/openstack-resource-controller/v2/pkg/predicates" +) + +const controllerName = "dnsrecordset" + +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=dnsrecordsets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=openstack.k-orc.cloud,resources=dnsrecordsets/status,verbs=get;update;patch + +type dnsrecordsetReconcilerConstructor struct { + scopeFactory scope.Factory +} + +func New(scopeFactory scope.Factory) interfaces.Controller { + return dnsrecordsetReconcilerConstructor{scopeFactory: scopeFactory} +} + +func (dnsrecordsetReconcilerConstructor) GetName() string { + return controllerName +} + +var dnsZoneDependency = dependency.NewDeletionGuardDependency[*orcObjectListT, *orcv1alpha1.DNSZone]( + "spec.resource.dnsZoneRef", + func(obj orcObjectPT) []string { + resource := obj.Spec.Resource + if resource == nil { + return nil + } + return []string{string(resource.DNSZoneRef)} + }, + finalizer, externalObjectFieldOwner, +) + +var dnsZoneImportDependency = dependency.NewDependency[*orcObjectListT, *orcv1alpha1.DNSZone]( + "spec.import.filter.dnsZoneRef", + func(obj orcObjectPT) []string { + resource := obj.Spec.Import + if resource == nil || resource.Filter == nil { + return nil + } + return []string{string(resource.Filter.DNSZoneRef)} + }, +) + +// SetupWithManager sets up the controller with the Manager. +func (c dnsrecordsetReconcilerConstructor) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error { + log := ctrl.LoggerFrom(ctx) + k8sClient := mgr.GetClient() + + dnsZoneWatchEventHandler, err := dnsZoneDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + + dnsZoneImportWatchEventHandler, err := dnsZoneImportDependency.WatchEventHandler(log, k8sClient) + if err != nil { + return err + } + + builder := ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&orcv1alpha1.DNSRecordset{}). + Watches(&orcv1alpha1.DNSZone{}, dnsZoneWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.DNSZone{})), + ). + Watches(&orcv1alpha1.DNSZone{}, dnsZoneImportWatchEventHandler, + builder.WithPredicates(predicates.NewBecameAvailable(log, &orcv1alpha1.DNSZone{})), + ) + + if err := errors.Join( + dnsZoneDependency.AddToManager(ctx, mgr), + dnsZoneImportDependency.AddToManager(ctx, mgr), + credentialsDependency.AddToManager(ctx, mgr), + credentials.AddCredentialsWatch(log, k8sClient, builder, credentialsDependency), + ); err != nil { + return err + } + + r := reconciler.NewController(controllerName, k8sClient, c.scopeFactory, dnsRecordsetHelperFactory{}, dnsRecordsetStatusWriter{}) + return builder.Complete(&r) +} + +type objectApplyT = orcapplyconfigv1alpha1.DNSRecordsetApplyConfiguration +type statusApplyT = orcapplyconfigv1alpha1.DNSRecordsetStatusApplyConfiguration +type osResourceT = recordsets.RecordSet diff --git a/internal/controllers/dnsrecordset/status.go b/internal/controllers/dnsrecordset/status.go new file mode 100644 index 000000000..b9ba52f8a --- /dev/null +++ b/internal/controllers/dnsrecordset/status.go @@ -0,0 +1,97 @@ +/* +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 dnsrecordset + +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 ( + RecordsetStatusActive = "ACTIVE" + RecordsetStatusPending = "PENDING" + RecordsetStatusError = "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 dnsRecordsetStatusWriter struct{} + +var _ interfaces.ResourceStatusWriter[orcObjectPT, *osResourceT, *objectApplyT, *statusApplyT] = dnsRecordsetStatusWriter{} + +func (dnsRecordsetStatusWriter) GetApplyConfig(name, namespace string) *objectApplyT { + return orcapplyconfigv1alpha1.DNSRecordset(name, namespace) +} + +func (dnsRecordsetStatusWriter) ResourceAvailableStatus(orcObject orcObjectPT, 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 RecordsetStatusActive: + return metav1.ConditionTrue, nil + case RecordsetStatusPending: + return metav1.ConditionFalse, progress.WaitingOnOpenStack(progress.WaitingOnReady, externalUpdatePollingPeriod) + case RecordsetStatusError: + return metav1.ConditionFalse, progress.WrapError( + orcerrors.Terminal(orcv1alpha1.ConditionReasonUnrecoverableError, "OpenStack recordset is in ERROR status")) + default: + // Fallback for any other/unexpected status + return metav1.ConditionFalse, progress.WaitingOnOpenStack(progress.WaitingOnReady, externalUpdatePollingPeriod) + } +} + +func (dnsRecordsetStatusWriter) ApplyResourceStatus(log logr.Logger, osResource *osResourceT, statusApply *statusApplyT) { + resourceStatus := orcapplyconfigv1alpha1.DNSRecordsetResourceStatus(). + WithName(osResource.Name) + + if osResource.Type != "" { + resourceStatus.WithType(osResource.Type) + } + + if len(osResource.Records) > 0 { + resourceStatus.WithRecords(osResource.Records...) + } + + if osResource.TTL > 0 { + resourceStatus.WithTTL(int32(osResource.TTL)) + } + + if osResource.Description != "" { + resourceStatus.WithDescription(osResource.Description) + } + + if osResource.Status != "" { + resourceStatus.WithStatus(osResource.Status) + } + + statusApply.WithResource(resourceStatus) +} diff --git a/internal/controllers/dnsrecordset/status_test.go b/internal/controllers/dnsrecordset/status_test.go new file mode 100644 index 000000000..49b87a6f0 --- /dev/null +++ b/internal/controllers/dnsrecordset/status_test.go @@ -0,0 +1,204 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dnsrecordset + +import ( + "errors" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + 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" +) + +func TestResourceAvailableStatus(t *testing.T) { + writer := dnsRecordsetStatusWriter{} + + tests := []struct { + name string + orcObject *orcv1alpha1.DNSRecordset + osResource *recordsets.RecordSet + expectedStatus metav1.ConditionStatus + expectRequeue time.Duration + expectTerminal bool + }{ + { + name: "osResource is nil, Status.ID is nil", + orcObject: &orcv1alpha1.DNSRecordset{ + Status: orcv1alpha1.DNSRecordsetStatus{ + ID: nil, + }, + }, + osResource: nil, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "osResource is nil, Status.ID is set", + orcObject: &orcv1alpha1.DNSRecordset{ + Status: orcv1alpha1.DNSRecordsetStatus{ + ID: ptr.To("some-id"), + }, + }, + osResource: nil, + expectedStatus: metav1.ConditionUnknown, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "recordset is ACTIVE", + orcObject: &orcv1alpha1.DNSRecordset{}, + osResource: &recordsets.RecordSet{Status: "ACTIVE"}, + expectedStatus: metav1.ConditionTrue, + expectRequeue: 0, + expectTerminal: false, + }, + { + name: "recordset is PENDING", + orcObject: &orcv1alpha1.DNSRecordset{}, + osResource: &recordsets.RecordSet{Status: "PENDING"}, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 15 * time.Second, + expectTerminal: false, + }, + { + name: "recordset is ERROR", + orcObject: &orcv1alpha1.DNSRecordset{}, + osResource: &recordsets.RecordSet{Status: "ERROR"}, + expectedStatus: metav1.ConditionFalse, + expectRequeue: 0, + expectTerminal: true, + }, + { + name: "recordset has unknown status", + orcObject: &orcv1alpha1.DNSRecordset{}, + osResource: &recordsets.RecordSet{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 := dnsRecordsetStatusWriter{} + + osResource := &recordsets.RecordSet{ + Name: testRecordsetName, + Type: "A", + Records: []string{"192.0.2.1", "192.0.2.2"}, + TTL: 3600, + Description: "A test DNS recordset", + Status: "ACTIVE", + } + + statusApply := orcapplyconfigv1alpha1.DNSRecordsetStatus() + 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 != testRecordsetName { + t.Errorf("expected name %q, got %v", testRecordsetName, res.Name) + } + if res.Type == nil || *res.Type != "A" { + t.Errorf("expected type 'A', got %v", res.Type) + } + if len(res.Records) != 2 || res.Records[0] != "192.0.2.1" || res.Records[1] != "192.0.2.2" { + t.Errorf("expected records ['192.0.2.1', '192.0.2.2'], got %v", res.Records) + } + if res.TTL == nil || *res.TTL != 3600 { + t.Errorf("expected TTL 3600, got %v", res.TTL) + } + if res.Description == nil || *res.Description != "A test DNS recordset" { + t.Errorf("expected description 'A test DNS recordset', got %v", res.Description) + } + if res.Status == nil || *res.Status != "ACTIVE" { + t.Errorf("expected status 'ACTIVE', got %v", res.Status) + } +} + +func TestApplyResourceStatus_EmptyFields(t *testing.T) { + writer := dnsRecordsetStatusWriter{} + + osResource := &recordsets.RecordSet{ + Name: testRecordsetName, + } + + statusApply := orcapplyconfigv1alpha1.DNSRecordsetStatus() + 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 != testRecordsetName { + t.Errorf("expected name %q, got %v", testRecordsetName, res.Name) + } + if res.Type != nil { + t.Errorf("expected Type to be nil, got %v", res.Type) + } + if len(res.Records) != 0 { + t.Errorf("expected Records to be empty, got %v", res.Records) + } + if res.TTL != nil { + t.Errorf("expected TTL to be nil, got %v", res.TTL) + } + if res.Description != nil { + t.Errorf("expected Description to be nil, got %v", res.Description) + } + if res.Status != nil { + t.Errorf("expected Status to be nil, got %v", res.Status) + } +} diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-assert.yaml new file mode 100644 index 000000000..e98f5ec02 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-assert.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-create-full +status: + resource: + name: sample-record.create-full.example.com. + type: A + records: + - 1.2.3.4 + ttl: 3600 + description: DNSRecordset from "create full" test + 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: DNSRecordset + name: dnsrecordset-create-full + ref: dnsrecordset + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnsrecordset-create-full + ref: dNSZone +assertAll: + - celExpr: "dnsrecordset.status.id != ''" + - celExpr: "dnsrecordset.status.resource.ttl == 3600" + - celExpr: "dnsrecordset.status.resource.status == 'ACTIVE'" diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-create-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-create-resource.yaml new file mode 100644 index 000000000..0089acb2d --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-create-resource.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-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 + # TODO(scaffolding): Add the necessary fields to create the resource + resource: + name: create-full.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-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: sample-record.create-full.example.com. + type: A + records: + - 1.2.3.4 + ttl: 3600 + description: DNSRecordset from "create full" test + dnsZoneRef: dnsrecordset-create-full diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset/tests/dnsrecordset-create-full/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/README.md new file mode 100644 index 000000000..4f947998b --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-full/README.md @@ -0,0 +1,11 @@ +# Create a DNSRecordset with all the options + +## Step 00 + +Create a DNSRecordset 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/dnsrecordset/tests/dnsrecordset-create-minimal/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-assert.yaml new file mode 100644 index 000000000..6ab1e34f2 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-assert.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-create-minimal +status: + resource: + name: dnsrecordset-create-minimal.create-minimal.example.com. + type: A + records: + - 1.2.3.4 + 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: DNSRecordset + name: dnsrecordset-create-minimal + ref: dnsrecordset + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnsrecordset-create-minimal + ref: dNSZone +assertAll: + - celExpr: "dnsrecordset.status.id != ''" + - celExpr: "!has(dnsrecordset.status.resource.description)" + - celExpr: "!has(dnsrecordset.status.resource.ttl)" diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-create-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-create-resource.yaml new file mode 100644 index 000000000..e09170ada --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-create-resource.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-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): Add the necessary fields to create the resource + resource: + name: create-minimal.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-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: + dnsZoneRef: dnsrecordset-create-minimal + name: dnsrecordset-create-minimal.create-minimal.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset/tests/dnsrecordset-create-minimal/01-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/01-assert.yaml new file mode 100644 index 000000000..2c039c848 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset' in secret.metadata.finalizers" diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/01-delete-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/01-delete-secret.yaml new file mode 100644 index 000000000..1620791b9 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset/tests/dnsrecordset-create-minimal/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/README.md new file mode 100644 index 000000000..a97730a0b --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-create-minimal/README.md @@ -0,0 +1,15 @@ +# Create a DNSRecordset with the minimum options + +## Step 00 + +Create a minimal DNSRecordset, 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/dnsrecordset/tests/dnsrecordset-dependency/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-assert.yaml new file mode 100644 index 000000000..ee83e3351 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-secret +status: + conditions: + - type: Available + message: Waiting for Secret/dnsrecordset-dependency to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for Secret/dnsrecordset-dependency to be created + status: "True" + reason: Progressing +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-dnszone +status: + conditions: + - type: Available + message: Waiting for DNSZone/dnsrecordset-dependency-pending to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for DNSZone/dnsrecordset-dependency-pending to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-create-resources-missing-deps.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-create-resources-missing-deps.yaml new file mode 100644 index 000000000..4a2552c46 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-create-resources-missing-deps.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-dependency +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): Add the necessary fields to create the resource + resource: + name: dependency.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-dnszone +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + dnsZoneRef: dnsrecordset-dependency-pending + name: no-dnszone.dependency-pending.example.com. + type: A + records: + - 1.2.3.4 + +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-secret +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: dnsrecordset-dependency + managementPolicy: managed + # TODO(scaffolding): Add the necessary fields to create the resource + resource: + dnsZoneRef: dnsrecordset-dependency + name: no-secret.dependency.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/01-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/01-assert.yaml new file mode 100644 index 000000000..bf86511d9 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/01-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-secret +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: DNSRecordset +metadata: + name: dnsrecordset-dependency-no-dnszone +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/dnsrecordset/tests/dnsrecordset-dependency/01-create-dependencies.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/01-create-dependencies.yaml new file mode 100644 index 000000000..818fbe911 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/01-create-dependencies.yaml @@ -0,0 +1,21 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic dnsrecordset-dependency --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-dependency-pending +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): Add the necessary fields to create the resource + resource: + name: dependency-pending.example.com. + email: admin@example.com diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-assert.yaml new file mode 100644 index 000000000..57c115db1 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-assert.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnsrecordset-dependency + ref: dNSZone + - apiVersion: v1 + kind: Secret + name: dnsrecordset-dependency + ref: secret +assertAll: + - celExpr: "dNSZone.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/dnsrecordset' in dNSZone.metadata.finalizers" + - celExpr: "secret.metadata.deletionTimestamp != 0" + - celExpr: "'openstack.k-orc.cloud/dnsrecordset' in secret.metadata.finalizers" diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-delete-dependencies.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-delete-dependencies.yaml new file mode 100644 index 000000000..56825b66e --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/02-delete-dependencies.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We expect the deletion to hang due to the finalizer, so use --wait=false + - command: kubectl delete dnszone.openstack.k-orc.cloud dnsrecordset-dependency --wait=false + namespaced: true + - command: kubectl delete secret dnsrecordset-dependency --wait=false + namespaced: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-assert.yaml new file mode 100644 index 000000000..c7b6a9835 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-assert.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +# Dependencies that were prevented deletion before should now be gone +- script: "! kubectl get dnszone.openstack.k-orc.cloud dnsrecordset-dependency --namespace $NAMESPACE" + skipLogOutput: true +- script: "! kubectl get secret dnsrecordset-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-delete-resources.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-delete-resources.yaml new file mode 100644 index 000000000..3fe2d5214 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/03-delete-resources.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-dependency-no-secret +- apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-dependency-no-dnszone diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/README.md new file mode 100644 index 000000000..046812236 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-dependency/README.md @@ -0,0 +1,21 @@ +# Creation and deletion dependencies + +## Step 00 + +Create DNSRecordsets referencing non-existing resources. Each DNSRecordset is dependent on other non-existing resource. Verify that the DNSRecordsets are waiting for the needed resources to be created externally. + +## Step 01 + +Create the missing dependencies and verify all the DNSRecordsets are available. + +## Step 02 + +Delete all the dependencies and check that ORC prevents deletion since there is still a resource that depends on them. + +## Step 03 + +Delete the DNSRecordsets and validate that all resources are gone. + +## Reference + +https://k-orc.cloud/development/writing-tests/#dependency diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-assert.yaml new file mode 100644 index 000000000..e34bb52a0 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-assert.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency +status: + conditions: + - type: Available + message: |- + Waiting for DNSZone/dnsrecordset-import-dependency to be ready + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for DNSZone/dnsrecordset-import-dependency to be ready + status: "True" + reason: Progressing diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-import-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-import-resource.yaml new file mode 100644 index 000000000..f09bc179f --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-import-resource.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + name: import-dependency-external.example.com. +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + dnsZoneRef: dnsrecordset-import-dependency + name: dnsrecordset-import-dependency-external.import-dependency-external.example.com. + type: A diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-assert.yaml new file mode 100644 index 000000000..9291f7c8e --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-assert.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency-not-this-one +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency +status: + conditions: + - type: Available + message: |- + Waiting for DNSZone/dnsrecordset-import-dependency to be ready + status: "False" + reason: Progressing + - type: Progressing + message: |- + Waiting for DNSZone/dnsrecordset-import-dependency to be ready + status: "True" + reason: Progressing diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-create-trap-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-create-trap-resource.yaml new file mode 100644 index 000000000..bea2a3c69 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/01-create-trap-resource.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-import-dependency-not-this-one +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): Add the necessary fields to create the resource + resource: + name: import-dependency-not-this-one.example.com. + email: admin@example.com +--- +# This `dnsrecordset-import-dependency-not-this-one` should not be picked by the import filter +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency-not-this-one +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + dnsZoneRef: dnsrecordset-import-dependency-not-this-one + name: not-this-one.import-dependency-not-this-one.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/02-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/02-assert.yaml new file mode 100644 index 000000000..a1416d1c2 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/02-assert.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-import-dependency + ref: dnsrecordset1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-import-dependency-not-this-one + ref: dnsrecordset2 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSZone + name: dnsrecordset-import-dependency + ref: dNSZone +assertAll: + - celExpr: "dnsrecordset1.status.id != dnsrecordset2.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency +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/dnsrecordset/tests/dnsrecordset-import-dependency/02-create-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/02-create-resource.yaml new file mode 100644 index 000000000..72523c0e3 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/02-create-resource.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-import-dependency-external +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): Add the necessary fields to create the resource + resource: + name: import-dependency-external.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-dependency-external +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + dnsZoneRef: dnsrecordset-import-dependency-external + name: dnsrecordset-import-dependency-external.import-dependency-external.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-assert.yaml new file mode 100644 index 000000000..f80da5ec0 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: "! kubectl get dnszone.openstack.k-orc.cloud dnsrecordset-import-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-delete-import-dependencies.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-delete-import-dependencies.yaml new file mode 100644 index 000000000..27e63d6b5 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/03-delete-import-dependencies.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + # We should be able to delete the import dependencies + - command: kubectl delete dnszone.openstack.k-orc.cloud dnsrecordset-import-dependency + namespaced: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-assert.yaml new file mode 100644 index 000000000..7aa1edb75 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-assert.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: "! kubectl get dnsrecordset.openstack.k-orc.cloud dnsrecordset-import-dependency --namespace $NAMESPACE" + skipLogOutput: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-delete-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-delete-resource.yaml new file mode 100644 index 000000000..dd684bc09 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/04-delete-resource.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-import-dependency diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/README.md new file mode 100644 index 000000000..be9df253d --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import-dependency/README.md @@ -0,0 +1,29 @@ +# Check dependency handling for imported DNSRecordset + +## Step 00 + +Import a DNSRecordset that references other imported resources. The referenced imported resources have no matching resources yet. +Verify the DNSRecordset is waiting for the dependency to be ready. + +## Step 01 + +Create a DNSRecordset matching the import filter, except for referenced resources, and verify that it's not being imported. + +## Step 02 + +Create the referenced resources and a DNSRecordset matching the import filters. + +Verify that the observed status on the imported DNSRecordset corresponds to the spec of the created DNSRecordset. + +## Step 03 + +Delete the referenced resources and check that ORC does not prevent deletion. The OpenStack resources still exist because they +were imported resources and we only deleted the ORC representation of it. + +## Step 04 + +Delete the DNSRecordset and validate that all resources are gone. + +## Reference + +https://k-orc.cloud/development/writing-tests/#import-dependency diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-assert.yaml new file mode 100644 index 000000000..b418049bb --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-assert.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import +status: + conditions: + - type: Available + message: Waiting for DNSZone/dnsrecordset-import to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for DNSZone/dnsrecordset-import to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-import-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-import-resource.yaml new file mode 100644 index 000000000..ecef97ce0 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-import-resource.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import +spec: + cloudCredentialsRef: + cloudName: openstack + secretName: openstack-clouds + managementPolicy: unmanaged + import: + filter: + dnsZoneRef: dnsrecordset-import + name: dnsrecordset-import-external.import.example.com. + type: A + description: DNSRecordset dnsrecordset-import-external from "dnsrecordset-import" test diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/00-secret.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +commands: + - command: kubectl create secret generic openstack-clouds --from-file=clouds.yaml=${E2E_KUTTL_OSCLOUDS} ${E2E_KUTTL_CACERT_OPT} + namespaced: true diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-assert.yaml new file mode 100644 index 000000000..4275bf829 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-assert.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-external-not-this-one +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success + resource: + name: dnsrecordset-import-external-not-this-one.import-external-not-this-one.example.com. + type: A + records: + - 1.2.3.4 + description: DNSRecordset dnsrecordset-import-external from "dnsrecordset-import" test +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import +status: + conditions: + - type: Available + message: Waiting for DNSZone/dnsrecordset-import to be created + status: "False" + reason: Progressing + - type: Progressing + message: Waiting for DNSZone/dnsrecordset-import to be created + status: "True" + reason: Progressing diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-create-trap-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-create-trap-resource.yaml new file mode 100644 index 000000000..c9fd9bcad --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/01-create-trap-resource.yaml @@ -0,0 +1,36 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-import-external-not-this-one +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): Add the necessary fields to create the resource + resource: + name: import-external-not-this-one.example.com. + email: admin@example.com +--- +# This `dnsrecordset-import-external-not-this-one` resource serves two purposes: +# - ensure that we can successfully create another resource which name is a substring of it (i.e. it's not being adopted) +# - ensure that importing a resource which name is a substring of it will not pick this one. +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-external-not-this-one +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: DNSRecordset dnsrecordset-import-external from "dnsrecordset-import" test + dnsZoneRef: dnsrecordset-import-external-not-this-one + name: dnsrecordset-import-external-not-this-one.import-external-not-this-one.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-assert.yaml new file mode 100644 index 000000000..4f1414a8e --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-assert.yaml @@ -0,0 +1,35 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-import-external + ref: dnsrecordset1 + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-import-external-not-this-one + ref: dnsrecordset2 +assertAll: + - celExpr: "dnsrecordset1.status.id != dnsrecordset2.status.id" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import +status: + conditions: + - type: Available + message: OpenStack resource is available + status: "True" + reason: Success + - type: Progressing + message: OpenStack resource is up to date + status: "False" + reason: Success + resource: + name: dnsrecordset-import-external.import.example.com. + type: A + records: + - 1.2.3.4 + description: DNSRecordset dnsrecordset-import-external from "dnsrecordset-import" test diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-create-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-create-resource.yaml new file mode 100644 index 000000000..3b7f6085e --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/02-create-resource.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-import +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): Add the necessary fields to create the resource + resource: + name: import.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-import-external +spec: + cloudCredentialsRef: + # TODO(scaffolding): Use openstack-admin if the resource needs admin credentials to be created + cloudName: openstack + secretName: openstack-clouds + managementPolicy: managed + resource: + description: DNSRecordset dnsrecordset-import-external from "dnsrecordset-import" test + dnsZoneRef: dnsrecordset-import + name: dnsrecordset-import-external.import.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-import/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/README.md new file mode 100644 index 000000000..3c13dfabd --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-import/README.md @@ -0,0 +1,18 @@ +# Import DNSRecordset + +## Step 00 + +Import a dnsrecordset that matches all fields in the filter, and verify it is waiting for the external resource to be created. + +## Step 01 + +Create a dnsrecordset whose name is a superstring of the one specified in the import filter, otherwise matching the filter, and verify that it's not being imported. + +## Step 02 + +Create a dnsrecordset matching the filter and verify that the observed status on the imported dnsrecordset corresponds to the spec of the created dnsrecordset. +Also, confirm that it does not adopt any dnsrecordset whose name is a superstring of its own. + +## Reference + +https://k-orc.cloud/development/writing-tests/#import diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-assert.yaml new file mode 100644 index 000000000..f2c399390 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-assert.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-update + ref: dnsrecordset +assertAll: + - celExpr: "!has(dnsrecordset.status.resource.description)" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-update +status: + resource: + name: record.update.example.com. + type: A + records: + - 1.2.3.4 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-minimal-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-minimal-resource.yaml new file mode 100644 index 000000000..c9fda34cd --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-minimal-resource.yaml @@ -0,0 +1,34 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSZone +metadata: + name: dnsrecordset-update +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): Add the necessary fields to create the resource + resource: + name: update.example.com. + email: admin@example.com +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-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: + dnsZoneRef: dnsrecordset-update + name: record.update.example.com. + type: A + records: + - 1.2.3.4 diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-secret.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/00-secret.yaml new file mode 100644 index 000000000..045711ee7 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset/tests/dnsrecordset-update/01-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/01-assert.yaml new file mode 100644 index 000000000..31f30a41c --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/01-assert.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-update +status: + resource: + name: record.update.example.com. + type: A + records: + - 5.6.7.8 + ttl: 1800 + description: dnsrecordset-update-updated + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/01-updated-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/01-updated-resource.yaml new file mode 100644 index 000000000..92b2d649a --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/01-updated-resource.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-update +spec: + resource: + dnsZoneRef: dnsrecordset-update + name: record.update.example.com. + type: A + records: + - 5.6.7.8 + ttl: 1800 + description: dnsrecordset-update-updated diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/02-assert.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/02-assert.yaml new file mode 100644 index 000000000..c3b9f7d72 --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/02-assert.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +resourceRefs: + - apiVersion: openstack.k-orc.cloud/v1alpha1 + kind: DNSRecordset + name: dnsrecordset-update + ref: dnsrecordset +assertAll: + - celExpr: "!has(dnsrecordset.status.resource.description)" + - celExpr: "dnsrecordset.status.resource.ttl == 1800" +--- +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: dnsrecordset-update +status: + resource: + name: record.update.example.com. + type: A + records: + - 1.2.3.4 + ttl: 1800 + conditions: + - type: Available + status: "True" + reason: Success + - type: Progressing + status: "False" + reason: Success diff --git a/internal/controllers/dnsrecordset/tests/dnsrecordset-update/02-reverted-resource.yaml b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/02-reverted-resource.yaml new file mode 100644 index 000000000..2c6c253ff --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-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/dnsrecordset/tests/dnsrecordset-update/README.md b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/README.md new file mode 100644 index 000000000..f34bd0e3e --- /dev/null +++ b/internal/controllers/dnsrecordset/tests/dnsrecordset-update/README.md @@ -0,0 +1,17 @@ +# Update DNSRecordset + +## Step 00 + +Create a DNSRecordset using only mandatory fields. + +## Step 01 + +Update all mutable fields. + +## Step 02 + +Revert the resource to its original value and verify that records are reverted, while the TTL is preserved since removing TTL from the spec is not managed/reconciled by the actuator. + +## Reference + +https://k-orc.cloud/development/writing-tests/#update diff --git a/internal/controllers/dnsrecordset/validation.go b/internal/controllers/dnsrecordset/validation.go new file mode 100644 index 000000000..d0babe436 --- /dev/null +++ b/internal/controllers/dnsrecordset/validation.go @@ -0,0 +1,81 @@ +/* +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 dnsrecordset + +import ( + "errors" + "fmt" + "net" + "strings" +) + +// ValidateDNSRecordset implements format validation rules for DNSRecordset specifications +// to catch format issues before making OpenStack API calls. +func ValidateDNSRecordset(obj orcObjectPT, zoneSuffix string) error { + if obj == nil || obj.Spec.Resource == nil { + return nil + } + resource := obj.Spec.Resource + + // 1. Name suffix validation + recordsetName := getDNSRecordsetName(obj) + normRecordsetName := strings.ToLower(recordsetName) + normZoneSuffix := strings.ToLower(zoneSuffix) + if normZoneSuffix != "" && !strings.HasSuffix(normZoneSuffix, ".") { + normZoneSuffix += "." + } + + if normZoneSuffix != "" { + if normRecordsetName != normZoneSuffix && !strings.HasSuffix(normRecordsetName, "."+normZoneSuffix) { + return fmt.Errorf("recordset name %q does not end with the parent zone suffix %q", recordsetName, zoneSuffix) + } + } + + // 2. Records format validation per recordset type + recordType := strings.ToUpper(resource.Type) + if len(resource.Records) == 0 { + return errors.New("records are required") + } + + for _, r := range resource.Records { + switch recordType { + case "A": + ip := net.ParseIP(r) + if ip == nil || ip.To4() == nil { + return fmt.Errorf("invalid IPv4 address %q for A record", r) + } + case "AAAA": + ip := net.ParseIP(r) + if ip == nil || ip.To4() != nil { + return fmt.Errorf("invalid IPv6 address %q for AAAA record", r) + } + case "CNAME": + if !strings.HasSuffix(r, ".") { + return fmt.Errorf("invalid CNAME record %q: must end with a trailing dot", r) + } + case "TXT": + // Check for unbalanced quotes (syntax error) + hasPrefix := strings.HasPrefix(r, `"`) + hasSuffix := strings.HasSuffix(r, `"`) + if hasPrefix != hasSuffix { + return fmt.Errorf("invalid TXT record %q: mismatched/unbalanced quotes", r) + } + } + } + + return nil +} diff --git a/internal/controllers/dnsrecordset/validation_test.go b/internal/controllers/dnsrecordset/validation_test.go new file mode 100644 index 000000000..db1697d6e --- /dev/null +++ b/internal/controllers/dnsrecordset/validation_test.go @@ -0,0 +1,251 @@ +/* +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 dnsrecordset + +import ( + "testing" + + "k8s.io/utils/ptr" + + orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" +) + +func TestValidateDNSRecordset(t *testing.T) { + tests := []struct { + name string + obj *orcv1alpha1.DNSRecordset + zoneSuffix string + wantErr bool + wantRecords []string + }{ + { + name: "Valid A Record", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"192.168.1.1", "10.0.0.1"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + }, + { + name: "Invalid A Record format", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{"192.168.1.300"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Valid AAAA Record", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "AAAA", + Records: []string{"2001:db8::1"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + }, + { + name: "Invalid AAAA Record format", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "AAAA", + Records: []string{"1.2.3.4"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Valid CNAME Record", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "CNAME", + Records: []string{"target.example.com."}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + }, + { + name: "Invalid CNAME Record (missing trailing dot)", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "CNAME", + Records: []string{"target.example.com"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Valid TXT Record already quoted", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "TXT", + Records: []string{`"hello"`}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + wantRecords: []string{`"hello"`}, + }, + { + name: "Valid TXT Record needing quotes", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "TXT", + Records: []string{"hello"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + wantRecords: []string{`"hello"`}, + }, + { + name: "Invalid TXT Record (mismatched prefix quote)", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "TXT", + Records: []string{`"hello`}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Invalid TXT Record (mismatched suffix quote)", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "TXT", + Records: []string{`hello"`}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Invalid name suffix matching", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.otherdomain.com."), + Type: "A", + Records: []string{"192.168.1.1"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Suffix collision but not subdomain", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("notexample.com."), + Type: "A", + Records: []string{"192.168.1.1"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + { + name: "Exact match of zone suffix (Apex record)", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("example.com."), + Type: "A", + Records: []string{"192.168.1.1"}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: false, + }, + { + name: "Empty records list", + obj: &orcv1alpha1.DNSRecordset{ + Spec: orcv1alpha1.DNSRecordsetSpec{ + Resource: &orcv1alpha1.DNSRecordsetResourceSpec{ + Name: ptr.To[orcv1alpha1.OpenStackName]("www.example.com."), + Type: "A", + Records: []string{}, + }, + }, + }, + zoneSuffix: "example.com.", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateDNSRecordset(tt.obj, tt.zoneSuffix) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateDNSRecordset() error = %v, wantErr %v", err, tt.wantErr) + } + if err == nil && tt.wantRecords != nil { + normalized := getNormalizedRecords(tt.obj.Spec.Resource.Type, tt.obj.Spec.Resource.Records) + for i, r := range normalized { + if r != tt.wantRecords[i] { + t.Errorf("getNormalizedRecords() record = %q, want %q", r, tt.wantRecords[i]) + } + } + } + }) + } +} diff --git a/internal/controllers/dnsrecordset/zz_generated.adapter.go b/internal/controllers/dnsrecordset/zz_generated.adapter.go new file mode 100644 index 000000000..5f0731b45 --- /dev/null +++ b/internal/controllers/dnsrecordset/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 dnsrecordset + +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.DNSRecordset + orcObjectListT = orcv1alpha1.DNSRecordsetList + resourceSpecT = orcv1alpha1.DNSRecordsetResourceSpec + filterT = orcv1alpha1.DNSRecordsetFilter +) + +// Derived types +type ( + orcObjectPT = *orcObjectT + adapterI = interfaces.APIObjectAdapter[orcObjectPT, resourceSpecT, filterT] + adapterT = dnsrecordsetAdapter +) + +type dnsrecordsetAdapter struct { + *orcv1alpha1.DNSRecordset +} + +var _ adapterI = &adapterT{} + +func (f adapterT) GetObject() orcObjectPT { + return f.DNSRecordset +} + +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 nil +} + +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 != nil && orcObject.Spec.Resource.Name != nil { + return string(*orcObject.Spec.Resource.Name) + } + return orcObject.Name +} diff --git a/internal/controllers/dnsrecordset/zz_generated.controller.go b/internal/controllers/dnsrecordset/zz_generated.controller.go new file mode 100644 index 000000000..bc795d268 --- /dev/null +++ b/internal/controllers/dnsrecordset/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 dnsrecordset + +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/controllers/dnszone/zz_generated.adapter.go b/internal/controllers/dnszone/zz_generated.adapter.go index f87f188fd..b4296e4a8 100644 --- a/internal/controllers/dnszone/zz_generated.adapter.go +++ b/internal/controllers/dnszone/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/domain/zz_generated.adapter.go b/internal/controllers/domain/zz_generated.adapter.go index 6a386af72..12fdbd744 100644 --- a/internal/controllers/domain/zz_generated.adapter.go +++ b/internal/controllers/domain/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/flavor/zz_generated.adapter.go b/internal/controllers/flavor/zz_generated.adapter.go index 936fc6735..07f6fe7d3 100644 --- a/internal/controllers/flavor/zz_generated.adapter.go +++ b/internal/controllers/flavor/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/group/zz_generated.adapter.go b/internal/controllers/group/zz_generated.adapter.go index be06e584f..00cd5b8a1 100644 --- a/internal/controllers/group/zz_generated.adapter.go +++ b/internal/controllers/group/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/image/zz_generated.adapter.go b/internal/controllers/image/zz_generated.adapter.go index fe096571b..cdc136e08 100644 --- a/internal/controllers/image/zz_generated.adapter.go +++ b/internal/controllers/image/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/keypair/zz_generated.adapter.go b/internal/controllers/keypair/zz_generated.adapter.go index d3b72644c..3a1776252 100644 --- a/internal/controllers/keypair/zz_generated.adapter.go +++ b/internal/controllers/keypair/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/network/zz_generated.adapter.go b/internal/controllers/network/zz_generated.adapter.go index 771518735..6d628ad2d 100644 --- a/internal/controllers/network/zz_generated.adapter.go +++ b/internal/controllers/network/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/port/zz_generated.adapter.go b/internal/controllers/port/zz_generated.adapter.go index 1cafbd343..283f8ba13 100644 --- a/internal/controllers/port/zz_generated.adapter.go +++ b/internal/controllers/port/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/project/zz_generated.adapter.go b/internal/controllers/project/zz_generated.adapter.go index fea8a21c1..c20bc03e2 100644 --- a/internal/controllers/project/zz_generated.adapter.go +++ b/internal/controllers/project/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/role/zz_generated.adapter.go b/internal/controllers/role/zz_generated.adapter.go index 5587b85d4..e8cc7c75b 100644 --- a/internal/controllers/role/zz_generated.adapter.go +++ b/internal/controllers/role/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/router/zz_generated.adapter.go b/internal/controllers/router/zz_generated.adapter.go index ccab08587..4b0d1cb94 100644 --- a/internal/controllers/router/zz_generated.adapter.go +++ b/internal/controllers/router/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/securitygroup/zz_generated.adapter.go b/internal/controllers/securitygroup/zz_generated.adapter.go index 1b055740c..2afd18156 100644 --- a/internal/controllers/securitygroup/zz_generated.adapter.go +++ b/internal/controllers/securitygroup/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/server/zz_generated.adapter.go b/internal/controllers/server/zz_generated.adapter.go index 1b51cde39..9b5accc1f 100644 --- a/internal/controllers/server/zz_generated.adapter.go +++ b/internal/controllers/server/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/servergroup/zz_generated.adapter.go b/internal/controllers/servergroup/zz_generated.adapter.go index dc272f462..a419e8617 100644 --- a/internal/controllers/servergroup/zz_generated.adapter.go +++ b/internal/controllers/servergroup/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/service/zz_generated.adapter.go b/internal/controllers/service/zz_generated.adapter.go index f70ba04d9..dfd9e5acd 100644 --- a/internal/controllers/service/zz_generated.adapter.go +++ b/internal/controllers/service/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/sharenetwork/zz_generated.adapter.go b/internal/controllers/sharenetwork/zz_generated.adapter.go index b89627a5b..e7de963c3 100644 --- a/internal/controllers/sharenetwork/zz_generated.adapter.go +++ b/internal/controllers/sharenetwork/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/subnet/zz_generated.adapter.go b/internal/controllers/subnet/zz_generated.adapter.go index 34c84d5b8..aca49682b 100644 --- a/internal/controllers/subnet/zz_generated.adapter.go +++ b/internal/controllers/subnet/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/swiftcontainer/zz_generated.adapter.go b/internal/controllers/swiftcontainer/zz_generated.adapter.go index 9dcb12416..84dd533e2 100644 --- a/internal/controllers/swiftcontainer/zz_generated.adapter.go +++ b/internal/controllers/swiftcontainer/zz_generated.adapter.go @@ -85,7 +85,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/trunk/zz_generated.adapter.go b/internal/controllers/trunk/zz_generated.adapter.go index ef7e54457..b6cabc596 100644 --- a/internal/controllers/trunk/zz_generated.adapter.go +++ b/internal/controllers/trunk/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/user/zz_generated.adapter.go b/internal/controllers/user/zz_generated.adapter.go index 718a1ef46..ae7fcf8c6 100644 --- a/internal/controllers/user/zz_generated.adapter.go +++ b/internal/controllers/user/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/volume/zz_generated.adapter.go b/internal/controllers/volume/zz_generated.adapter.go index 956b64693..b483261e1 100644 --- a/internal/controllers/volume/zz_generated.adapter.go +++ b/internal/controllers/volume/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/controllers/volumetype/zz_generated.adapter.go b/internal/controllers/volumetype/zz_generated.adapter.go index 9f19fa751..e74d61fd7 100644 --- a/internal/controllers/volumetype/zz_generated.adapter.go +++ b/internal/controllers/volumetype/zz_generated.adapter.go @@ -81,7 +81,7 @@ func (f adapterT) GetImportFilter() *filterT { // 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 { + if orcObject.Spec.Resource != nil && orcObject.Spec.Resource.Name != nil { return string(*orcObject.Spec.Resource.Name) } return orcObject.Name diff --git a/internal/osclients/dnsrecordset.go b/internal/osclients/dnsrecordset.go new file mode 100644 index 000000000..bf3b0100c --- /dev/null +++ b/internal/osclients/dnsrecordset.go @@ -0,0 +1,104 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package osclients + +import ( + "context" + "fmt" + "iter" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + "github.com/gophercloud/utils/v2/openstack/clientconfig" +) + +type DNSRecordsetClient interface { + ListRecordsets(ctx context.Context, zoneID string, opts recordsets.ListOptsBuilder) iter.Seq2[*recordsets.RecordSet, error] + CreateRecordset(ctx context.Context, zoneID string, opts recordsets.CreateOptsBuilder) (*recordsets.RecordSet, error) + GetRecordset(ctx context.Context, zoneID string, recordsetID string) (*recordsets.RecordSet, error) + UpdateRecordset(ctx context.Context, zoneID string, recordsetID string, opts recordsets.UpdateOptsBuilder) (*recordsets.RecordSet, error) + DeleteRecordset(ctx context.Context, zoneID string, recordsetID string) error +} + +type dnsRecordsetClient struct{ client *gophercloud.ServiceClient } + +// NewDNSRecordsetClient returns a new OpenStack client. +func NewDNSRecordsetClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (DNSRecordsetClient, 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 dnsrecordset service client: %v", err) + } + + return &dnsRecordsetClient{client}, nil +} + +func (c dnsRecordsetClient) ListRecordsets(ctx context.Context, zoneID string, opts recordsets.ListOptsBuilder) iter.Seq2[*recordsets.RecordSet, error] { + pager := recordsets.ListByZone(c.client, zoneID, opts) + return func(yield func(*recordsets.RecordSet, error) bool) { + _ = pager.EachPage(ctx, yieldPage(recordsets.ExtractRecordSets, yield)) + } +} + +func (c dnsRecordsetClient) CreateRecordset(ctx context.Context, zoneID string, opts recordsets.CreateOptsBuilder) (*recordsets.RecordSet, error) { + return recordsets.Create(ctx, c.client, zoneID, opts).Extract() +} + +func (c dnsRecordsetClient) DeleteRecordset(ctx context.Context, zoneID string, recordsetID string) error { + return recordsets.Delete(ctx, c.client, zoneID, recordsetID).ExtractErr() +} + +func (c dnsRecordsetClient) GetRecordset(ctx context.Context, zoneID string, recordsetID string) (*recordsets.RecordSet, error) { + return recordsets.Get(ctx, c.client, zoneID, recordsetID).Extract() +} + +func (c dnsRecordsetClient) UpdateRecordset(ctx context.Context, zoneID string, recordsetID string, opts recordsets.UpdateOptsBuilder) (*recordsets.RecordSet, error) { + return recordsets.Update(ctx, c.client, zoneID, recordsetID, opts).Extract() +} + +type dnsRecordsetErrorClient struct{ error } + +// NewDNSRecordsetErrorClient returns a DNSRecordsetClient in which every method returns the given error. +func NewDNSRecordsetErrorClient(e error) DNSRecordsetClient { + return dnsRecordsetErrorClient{e} +} + +func (e dnsRecordsetErrorClient) ListRecordsets(_ context.Context, _ string, _ recordsets.ListOptsBuilder) iter.Seq2[*recordsets.RecordSet, error] { + return func(yield func(*recordsets.RecordSet, error) bool) { + yield(nil, e.error) + } +} + +func (e dnsRecordsetErrorClient) CreateRecordset(_ context.Context, _ string, _ recordsets.CreateOptsBuilder) (*recordsets.RecordSet, error) { + return nil, e.error +} + +func (e dnsRecordsetErrorClient) DeleteRecordset(_ context.Context, _ string, _ string) error { + return e.error +} + +func (e dnsRecordsetErrorClient) GetRecordset(_ context.Context, _ string, _ string) (*recordsets.RecordSet, error) { + return nil, e.error +} + +func (e dnsRecordsetErrorClient) UpdateRecordset(_ context.Context, _ string, _ string, _ recordsets.UpdateOptsBuilder) (*recordsets.RecordSet, error) { + return nil, e.error +} diff --git a/internal/osclients/mock/dnsrecordset.go b/internal/osclients/mock/dnsrecordset.go new file mode 100644 index 000000000..fcbef9cf4 --- /dev/null +++ b/internal/osclients/mock/dnsrecordset.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: ../dnsrecordset.go +// +// Generated by this command: +// +// mockgen -package mock -destination=dnsrecordset.go -source=../dnsrecordset.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock DNSRecordsetClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + iter "iter" + reflect "reflect" + + recordsets "github.com/gophercloud/gophercloud/v2/openstack/dns/v2/recordsets" + gomock "go.uber.org/mock/gomock" +) + +// MockDNSRecordsetClient is a mock of DNSRecordsetClient interface. +type MockDNSRecordsetClient struct { + ctrl *gomock.Controller + recorder *MockDNSRecordsetClientMockRecorder + isgomock struct{} +} + +// MockDNSRecordsetClientMockRecorder is the mock recorder for MockDNSRecordsetClient. +type MockDNSRecordsetClientMockRecorder struct { + mock *MockDNSRecordsetClient +} + +// NewMockDNSRecordsetClient creates a new mock instance. +func NewMockDNSRecordsetClient(ctrl *gomock.Controller) *MockDNSRecordsetClient { + mock := &MockDNSRecordsetClient{ctrl: ctrl} + mock.recorder = &MockDNSRecordsetClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDNSRecordsetClient) EXPECT() *MockDNSRecordsetClientMockRecorder { + return m.recorder +} + +// CreateRecordset mocks base method. +func (m *MockDNSRecordsetClient) CreateRecordset(ctx context.Context, zoneID string, opts recordsets.CreateOptsBuilder) (*recordsets.RecordSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRecordset", ctx, zoneID, opts) + ret0, _ := ret[0].(*recordsets.RecordSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRecordset indicates an expected call of CreateRecordset. +func (mr *MockDNSRecordsetClientMockRecorder) CreateRecordset(ctx, zoneID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRecordset", reflect.TypeOf((*MockDNSRecordsetClient)(nil).CreateRecordset), ctx, zoneID, opts) +} + +// DeleteRecordset mocks base method. +func (m *MockDNSRecordsetClient) DeleteRecordset(ctx context.Context, zoneID, recordsetID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRecordset", ctx, zoneID, recordsetID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteRecordset indicates an expected call of DeleteRecordset. +func (mr *MockDNSRecordsetClientMockRecorder) DeleteRecordset(ctx, zoneID, recordsetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRecordset", reflect.TypeOf((*MockDNSRecordsetClient)(nil).DeleteRecordset), ctx, zoneID, recordsetID) +} + +// GetRecordset mocks base method. +func (m *MockDNSRecordsetClient) GetRecordset(ctx context.Context, zoneID, recordsetID string) (*recordsets.RecordSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRecordset", ctx, zoneID, recordsetID) + ret0, _ := ret[0].(*recordsets.RecordSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRecordset indicates an expected call of GetRecordset. +func (mr *MockDNSRecordsetClientMockRecorder) GetRecordset(ctx, zoneID, recordsetID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecordset", reflect.TypeOf((*MockDNSRecordsetClient)(nil).GetRecordset), ctx, zoneID, recordsetID) +} + +// ListRecordsets mocks base method. +func (m *MockDNSRecordsetClient) ListRecordsets(ctx context.Context, zoneID string, opts recordsets.ListOptsBuilder) iter.Seq2[*recordsets.RecordSet, error] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRecordsets", ctx, zoneID, opts) + ret0, _ := ret[0].(iter.Seq2[*recordsets.RecordSet, error]) + return ret0 +} + +// ListRecordsets indicates an expected call of ListRecordsets. +func (mr *MockDNSRecordsetClientMockRecorder) ListRecordsets(ctx, zoneID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRecordsets", reflect.TypeOf((*MockDNSRecordsetClient)(nil).ListRecordsets), ctx, zoneID, opts) +} + +// UpdateRecordset mocks base method. +func (m *MockDNSRecordsetClient) UpdateRecordset(ctx context.Context, zoneID, recordsetID string, opts recordsets.UpdateOptsBuilder) (*recordsets.RecordSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRecordset", ctx, zoneID, recordsetID, opts) + ret0, _ := ret[0].(*recordsets.RecordSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRecordset indicates an expected call of UpdateRecordset. +func (mr *MockDNSRecordsetClientMockRecorder) UpdateRecordset(ctx, zoneID, recordsetID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRecordset", reflect.TypeOf((*MockDNSRecordsetClient)(nil).UpdateRecordset), ctx, zoneID, recordsetID, opts) +} diff --git a/internal/osclients/mock/doc.go b/internal/osclients/mock/doc.go index b4dccb31f..738ee55b8 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=dnsrecordset.go -source=../dnsrecordset.go github.com/k-orc/openstack-resource-controller/internal/osclients/mock DNSRecordsetClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate.go.txt dnsrecordset.go > _dnsrecordset.go && mv _dnsrecordset.go dnsrecordset.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" diff --git a/internal/scope/mock.go b/internal/scope/mock.go index 32ff9c74d..76e5a592c 100644 --- a/internal/scope/mock.go +++ b/internal/scope/mock.go @@ -38,6 +38,7 @@ type MockScopeFactory struct { ApplicationCredentialClient *mock.MockApplicationCredentialClient ComputeClient *mock.MockComputeClient DNSZoneClient *mock.MockDNSZoneClient + DNSRecordsetClient *mock.MockDNSRecordsetClient DomainClient *mock.MockDomainClient EndpointClient *mock.MockEndpointClient GroupClient *mock.MockGroupClient @@ -61,6 +62,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { applicationcredentialClient := mock.NewMockApplicationCredentialClient(mockCtrl) computeClient := mock.NewMockComputeClient(mockCtrl) dnszoneClient := mock.NewMockDNSZoneClient(mockCtrl) + dnsrecordsetClient := mock.NewMockDNSRecordsetClient(mockCtrl) domainClient := mock.NewMockDomainClient(mockCtrl) endpointClient := mock.NewMockEndpointClient(mockCtrl) groupClient := mock.NewMockGroupClient(mockCtrl) @@ -81,6 +83,7 @@ func NewMockScopeFactory(mockCtrl *gomock.Controller) *MockScopeFactory { ApplicationCredentialClient: applicationcredentialClient, ComputeClient: computeClient, DNSZoneClient: dnszoneClient, + DNSRecordsetClient: dnsrecordsetClient, DomainClient: domainClient, EndpointClient: endpointClient, GroupClient: groupClient, @@ -121,6 +124,10 @@ func (f *MockScopeFactory) NewDNSZoneClient() (osclients.DNSZoneClient, error) { return f.DNSZoneClient, nil } +func (f *MockScopeFactory) NewDNSRecordsetClient() (osclients.DNSRecordsetClient, error) { + return f.DNSRecordsetClient, 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 e5041bfce..c5ab7ad6f 100644 --- a/internal/scope/provider.go +++ b/internal/scope/provider.go @@ -153,6 +153,10 @@ func (s *providerScope) NewDNSZoneClient() (clients.DNSZoneClient, error) { return clients.NewDNSZoneClient(s.providerClient, s.providerClientOpts) } +func (s *providerScope) NewDNSRecordsetClient() (clients.DNSRecordsetClient, error) { + return clients.NewDNSRecordsetClient(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 22b4a2f07..12322028d 100644 --- a/internal/scope/scope.go +++ b/internal/scope/scope.go @@ -52,6 +52,7 @@ type Scope interface { NewApplicationCredentialClient() (osclients.ApplicationCredentialClient, error) NewComputeClient() (osclients.ComputeClient, error) NewDNSZoneClient() (osclients.DNSZoneClient, error) + NewDNSRecordsetClient() (osclients.DNSRecordsetClient, error) NewDomainClient() (osclients.DomainClient, error) NewEndpointClient() (osclients.EndpointClient, error) NewGroupClient() (osclients.GroupClient, error) diff --git a/kuttl-test.yaml b/kuttl-test.yaml index 6a8957411..97fd563d3 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/dnsrecordset/tests/ - ./internal/controllers/dnszone/tests/ - ./internal/controllers/domain/tests/ - ./internal/controllers/endpoint/tests/ diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordset.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordset.go new file mode 100644 index 000000000..65ceaf432 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordset.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" +) + +// DNSRecordsetApplyConfiguration represents a declarative configuration of the DNSRecordset type for use +// with apply. +type DNSRecordsetApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *DNSRecordsetSpecApplyConfiguration `json:"spec,omitempty"` + Status *DNSRecordsetStatusApplyConfiguration `json:"status,omitempty"` +} + +// DNSRecordset constructs a declarative configuration of the DNSRecordset type for use with +// apply. +func DNSRecordset(name, namespace string) *DNSRecordsetApplyConfiguration { + b := &DNSRecordsetApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("DNSRecordset") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b +} + +// ExtractDNSRecordset extracts the applied configuration owned by fieldManager from +// dNSRecordset. If no managedFields are found in dNSRecordset for fieldManager, a +// DNSRecordsetApplyConfiguration 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. +// dNSRecordset must be a unmodified DNSRecordset API object that was retrieved from the Kubernetes API. +// ExtractDNSRecordset 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 ExtractDNSRecordset(dNSRecordset *apiv1alpha1.DNSRecordset, fieldManager string) (*DNSRecordsetApplyConfiguration, error) { + return extractDNSRecordset(dNSRecordset, fieldManager, "") +} + +// ExtractDNSRecordsetStatus is the same as ExtractDNSRecordset except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractDNSRecordsetStatus(dNSRecordset *apiv1alpha1.DNSRecordset, fieldManager string) (*DNSRecordsetApplyConfiguration, error) { + return extractDNSRecordset(dNSRecordset, fieldManager, "status") +} + +func extractDNSRecordset(dNSRecordset *apiv1alpha1.DNSRecordset, fieldManager string, subresource string) (*DNSRecordsetApplyConfiguration, error) { + b := &DNSRecordsetApplyConfiguration{} + err := managedfields.ExtractInto(dNSRecordset, internal.Parser().Type("com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordset"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(dNSRecordset.Name) + b.WithNamespace(dNSRecordset.Namespace) + + b.WithKind("DNSRecordset") + b.WithAPIVersion("openstack.k-orc.cloud/v1alpha1") + return b, nil +} +func (b DNSRecordsetApplyConfiguration) 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 *DNSRecordsetApplyConfiguration) WithKind(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithAPIVersion(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithName(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithGenerateName(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithNamespace(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithUID(value types.UID) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithResourceVersion(value string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithGeneration(value int64) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithCreationTimestamp(value metav1.Time) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithLabels(entries map[string]string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithAnnotations(entries map[string]string) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithFinalizers(values ...string) *DNSRecordsetApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *DNSRecordsetApplyConfiguration) 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 *DNSRecordsetApplyConfiguration) WithSpec(value *DNSRecordsetSpecApplyConfiguration) *DNSRecordsetApplyConfiguration { + 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 *DNSRecordsetApplyConfiguration) WithStatus(value *DNSRecordsetStatusApplyConfiguration) *DNSRecordsetApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *DNSRecordsetApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *DNSRecordsetApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *DNSRecordsetApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *DNSRecordsetApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetfilter.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetfilter.go new file mode 100644 index 000000000..da524b90c --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetfilter.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" +) + +// DNSRecordsetFilterApplyConfiguration represents a declarative configuration of the DNSRecordsetFilter type for use +// with apply. +type DNSRecordsetFilterApplyConfiguration struct { + DNSZoneRef *apiv1alpha1.KubernetesNameRef `json:"dnsZoneRef,omitempty"` + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Description *string `json:"description,omitempty"` +} + +// DNSRecordsetFilterApplyConfiguration constructs a declarative configuration of the DNSRecordsetFilter type for use with +// apply. +func DNSRecordsetFilter() *DNSRecordsetFilterApplyConfiguration { + return &DNSRecordsetFilterApplyConfiguration{} +} + +// WithDNSZoneRef sets the DNSZoneRef 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 DNSZoneRef field is set to the value of the last call. +func (b *DNSRecordsetFilterApplyConfiguration) WithDNSZoneRef(value apiv1alpha1.KubernetesNameRef) *DNSRecordsetFilterApplyConfiguration { + b.DNSZoneRef = &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 *DNSRecordsetFilterApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *DNSRecordsetFilterApplyConfiguration { + b.Name = &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 *DNSRecordsetFilterApplyConfiguration) WithType(value string) *DNSRecordsetFilterApplyConfiguration { + b.Type = &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 *DNSRecordsetFilterApplyConfiguration) WithTTL(value int32) *DNSRecordsetFilterApplyConfiguration { + b.TTL = &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 *DNSRecordsetFilterApplyConfiguration) WithDescription(value string) *DNSRecordsetFilterApplyConfiguration { + b.Description = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetimport.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetimport.go new file mode 100644 index 000000000..686c97bdf --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetimport.go @@ -0,0 +1,39 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// DNSRecordsetImportApplyConfiguration represents a declarative configuration of the DNSRecordsetImport type for use +// with apply. +type DNSRecordsetImportApplyConfiguration struct { + Filter *DNSRecordsetFilterApplyConfiguration `json:"filter,omitempty"` +} + +// DNSRecordsetImportApplyConfiguration constructs a declarative configuration of the DNSRecordsetImport type for use with +// apply. +func DNSRecordsetImport() *DNSRecordsetImportApplyConfiguration { + return &DNSRecordsetImportApplyConfiguration{} +} + +// 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 *DNSRecordsetImportApplyConfiguration) WithFilter(value *DNSRecordsetFilterApplyConfiguration) *DNSRecordsetImportApplyConfiguration { + b.Filter = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcespec.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcespec.go new file mode 100644 index 000000000..c430a8908 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcespec.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" +) + +// DNSRecordsetResourceSpecApplyConfiguration represents a declarative configuration of the DNSRecordsetResourceSpec type for use +// with apply. +type DNSRecordsetResourceSpecApplyConfiguration struct { + Name *apiv1alpha1.OpenStackName `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Records []string `json:"records,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Description *string `json:"description,omitempty"` + DNSZoneRef *apiv1alpha1.KubernetesNameRef `json:"dnsZoneRef,omitempty"` +} + +// DNSRecordsetResourceSpecApplyConfiguration constructs a declarative configuration of the DNSRecordsetResourceSpec type for use with +// apply. +func DNSRecordsetResourceSpec() *DNSRecordsetResourceSpecApplyConfiguration { + return &DNSRecordsetResourceSpecApplyConfiguration{} +} + +// 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 *DNSRecordsetResourceSpecApplyConfiguration) WithName(value apiv1alpha1.OpenStackName) *DNSRecordsetResourceSpecApplyConfiguration { + b.Name = &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 *DNSRecordsetResourceSpecApplyConfiguration) WithType(value string) *DNSRecordsetResourceSpecApplyConfiguration { + b.Type = &value + return b +} + +// WithRecords adds the given value to the Records 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 Records field. +func (b *DNSRecordsetResourceSpecApplyConfiguration) WithRecords(values ...string) *DNSRecordsetResourceSpecApplyConfiguration { + for i := range values { + b.Records = append(b.Records, values[i]) + } + 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 *DNSRecordsetResourceSpecApplyConfiguration) WithTTL(value int32) *DNSRecordsetResourceSpecApplyConfiguration { + b.TTL = &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 *DNSRecordsetResourceSpecApplyConfiguration) WithDescription(value string) *DNSRecordsetResourceSpecApplyConfiguration { + b.Description = &value + return b +} + +// WithDNSZoneRef sets the DNSZoneRef 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 DNSZoneRef field is set to the value of the last call. +func (b *DNSRecordsetResourceSpecApplyConfiguration) WithDNSZoneRef(value apiv1alpha1.KubernetesNameRef) *DNSRecordsetResourceSpecApplyConfiguration { + b.DNSZoneRef = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcestatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcestatus.go new file mode 100644 index 000000000..b60a8b6e7 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetresourcestatus.go @@ -0,0 +1,86 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// DNSRecordsetResourceStatusApplyConfiguration represents a declarative configuration of the DNSRecordsetResourceStatus type for use +// with apply. +type DNSRecordsetResourceStatusApplyConfiguration struct { + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Records []string `json:"records,omitempty"` + TTL *int32 `json:"ttl,omitempty"` + Description *string `json:"description,omitempty"` + Status *string `json:"status,omitempty"` +} + +// DNSRecordsetResourceStatusApplyConfiguration constructs a declarative configuration of the DNSRecordsetResourceStatus type for use with +// apply. +func DNSRecordsetResourceStatus() *DNSRecordsetResourceStatusApplyConfiguration { + return &DNSRecordsetResourceStatusApplyConfiguration{} +} + +// 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 *DNSRecordsetResourceStatusApplyConfiguration) WithName(value string) *DNSRecordsetResourceStatusApplyConfiguration { + b.Name = &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 *DNSRecordsetResourceStatusApplyConfiguration) WithType(value string) *DNSRecordsetResourceStatusApplyConfiguration { + b.Type = &value + return b +} + +// WithRecords adds the given value to the Records 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 Records field. +func (b *DNSRecordsetResourceStatusApplyConfiguration) WithRecords(values ...string) *DNSRecordsetResourceStatusApplyConfiguration { + for i := range values { + b.Records = append(b.Records, values[i]) + } + 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 *DNSRecordsetResourceStatusApplyConfiguration) WithTTL(value int32) *DNSRecordsetResourceStatusApplyConfiguration { + b.TTL = &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 *DNSRecordsetResourceStatusApplyConfiguration) WithDescription(value string) *DNSRecordsetResourceStatusApplyConfiguration { + b.Description = &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 *DNSRecordsetResourceStatusApplyConfiguration) WithStatus(value string) *DNSRecordsetResourceStatusApplyConfiguration { + b.Status = &value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetspec.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetspec.go new file mode 100644 index 000000000..df1e54ee1 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetspec.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" +) + +// DNSRecordsetSpecApplyConfiguration represents a declarative configuration of the DNSRecordsetSpec type for use +// with apply. +type DNSRecordsetSpecApplyConfiguration struct { + Import *DNSRecordsetImportApplyConfiguration `json:"import,omitempty"` + Resource *DNSRecordsetResourceSpecApplyConfiguration `json:"resource,omitempty"` + ManagementPolicy *apiv1alpha1.ManagementPolicy `json:"managementPolicy,omitempty"` + ManagedOptions *ManagedOptionsApplyConfiguration `json:"managedOptions,omitempty"` + CloudCredentialsRef *CloudCredentialsReferenceApplyConfiguration `json:"cloudCredentialsRef,omitempty"` +} + +// DNSRecordsetSpecApplyConfiguration constructs a declarative configuration of the DNSRecordsetSpec type for use with +// apply. +func DNSRecordsetSpec() *DNSRecordsetSpecApplyConfiguration { + return &DNSRecordsetSpecApplyConfiguration{} +} + +// 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 *DNSRecordsetSpecApplyConfiguration) WithImport(value *DNSRecordsetImportApplyConfiguration) *DNSRecordsetSpecApplyConfiguration { + 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 *DNSRecordsetSpecApplyConfiguration) WithResource(value *DNSRecordsetResourceSpecApplyConfiguration) *DNSRecordsetSpecApplyConfiguration { + 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 *DNSRecordsetSpecApplyConfiguration) WithManagementPolicy(value apiv1alpha1.ManagementPolicy) *DNSRecordsetSpecApplyConfiguration { + 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 *DNSRecordsetSpecApplyConfiguration) WithManagedOptions(value *ManagedOptionsApplyConfiguration) *DNSRecordsetSpecApplyConfiguration { + 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 *DNSRecordsetSpecApplyConfiguration) WithCloudCredentialsRef(value *CloudCredentialsReferenceApplyConfiguration) *DNSRecordsetSpecApplyConfiguration { + b.CloudCredentialsRef = value + return b +} diff --git a/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetstatus.go b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetstatus.go new file mode 100644 index 000000000..e7f169d39 --- /dev/null +++ b/pkg/clients/applyconfiguration/api/v1alpha1/dnsrecordsetstatus.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" +) + +// DNSRecordsetStatusApplyConfiguration represents a declarative configuration of the DNSRecordsetStatus type for use +// with apply. +type DNSRecordsetStatusApplyConfiguration struct { + Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"` + ID *string `json:"id,omitempty"` + Resource *DNSRecordsetResourceStatusApplyConfiguration `json:"resource,omitempty"` +} + +// DNSRecordsetStatusApplyConfiguration constructs a declarative configuration of the DNSRecordsetStatus type for use with +// apply. +func DNSRecordsetStatus() *DNSRecordsetStatusApplyConfiguration { + return &DNSRecordsetStatusApplyConfiguration{} +} + +// 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 *DNSRecordsetStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *DNSRecordsetStatusApplyConfiguration { + 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 *DNSRecordsetStatusApplyConfiguration) WithID(value string) *DNSRecordsetStatusApplyConfiguration { + 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 *DNSRecordsetStatusApplyConfiguration) WithResource(value *DNSRecordsetResourceStatusApplyConfiguration) *DNSRecordsetStatusApplyConfiguration { + b.Resource = value + return b +} diff --git a/pkg/clients/applyconfiguration/internal/internal.go b/pkg/clients/applyconfiguration/internal/internal.go index 6b4bd1857..601d359f4 100644 --- a/pkg/clients/applyconfiguration/internal/internal.go +++ b/pkg/clients/applyconfiguration/internal/internal.go @@ -385,6 +385,138 @@ var schemaYAML = typed.YAMLObject(`types: - name: secretName type: scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordset + 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.DNSRecordsetSpec + default: {} + - name: status + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetStatus + default: {} +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetFilter + map: + fields: + - name: description + type: + scalar: string + - name: dnsZoneRef + type: + scalar: string + - name: name + type: + scalar: string + default: "" + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string + default: "" +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetImport + map: + fields: + - name: filter + type: + namedType: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetFilter +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetResourceSpec + map: + fields: + - name: description + type: + scalar: string + - name: dnsZoneRef + type: + scalar: string + - name: name + type: + scalar: string + - name: records + type: + list: + elementType: + scalar: string + elementRelationship: atomic + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string + default: "" +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetResourceStatus + map: + fields: + - name: description + type: + scalar: string + - name: name + type: + scalar: string + - name: records + type: + list: + elementType: + scalar: string + elementRelationship: atomic + - name: status + type: + scalar: string + - name: ttl + type: + scalar: numeric + - name: type + type: + scalar: string +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetSpec + 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.DNSRecordsetImport + - 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.DNSRecordsetResourceSpec +- name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSRecordsetStatus + 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.DNSRecordsetResourceStatus - name: com.github.k-orc.openstack-resource-controller.v2.api.v1alpha1.DNSZone map: fields: diff --git a/pkg/clients/applyconfiguration/utils.go b/pkg/clients/applyconfiguration/utils.go index 71d33f7a0..a4635e852 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("DNSRecordset"): + return &apiv1alpha1.DNSRecordsetApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetFilter"): + return &apiv1alpha1.DNSRecordsetFilterApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetImport"): + return &apiv1alpha1.DNSRecordsetImportApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetResourceSpec"): + return &apiv1alpha1.DNSRecordsetResourceSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetResourceStatus"): + return &apiv1alpha1.DNSRecordsetResourceStatusApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetSpec"): + return &apiv1alpha1.DNSRecordsetSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DNSRecordsetStatus"): + return &apiv1alpha1.DNSRecordsetStatusApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DNSZone"): return &apiv1alpha1.DNSZoneApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DNSZoneFilter"): 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 ba46fe881..15f17f1eb 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 + DNSRecordsetsGetter DNSZonesGetter DomainsGetter EndpointsGetter @@ -70,6 +71,10 @@ func (c *OpenstackV1alpha1Client) ApplicationCredentials(namespace string) Appli return newApplicationCredentials(c, namespace) } +func (c *OpenstackV1alpha1Client) DNSRecordsets(namespace string) DNSRecordsetInterface { + return newDNSRecordsets(c, namespace) +} + func (c *OpenstackV1alpha1Client) DNSZones(namespace string) DNSZoneInterface { return newDNSZones(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnsrecordset.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnsrecordset.go new file mode 100644 index 000000000..2be2db0d0 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/dnsrecordset.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" +) + +// DNSRecordsetsGetter has a method to return a DNSRecordsetInterface. +// A group's client should implement this interface. +type DNSRecordsetsGetter interface { + DNSRecordsets(namespace string) DNSRecordsetInterface +} + +// DNSRecordsetInterface has methods to work with DNSRecordset resources. +type DNSRecordsetInterface interface { + Create(ctx context.Context, dNSRecordset *apiv1alpha1.DNSRecordset, opts v1.CreateOptions) (*apiv1alpha1.DNSRecordset, error) + Update(ctx context.Context, dNSRecordset *apiv1alpha1.DNSRecordset, opts v1.UpdateOptions) (*apiv1alpha1.DNSRecordset, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, dNSRecordset *apiv1alpha1.DNSRecordset, opts v1.UpdateOptions) (*apiv1alpha1.DNSRecordset, 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.DNSRecordset, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.DNSRecordsetList, 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.DNSRecordset, err error) + Apply(ctx context.Context, dNSRecordset *applyconfigurationapiv1alpha1.DNSRecordsetApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.DNSRecordset, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, dNSRecordset *applyconfigurationapiv1alpha1.DNSRecordsetApplyConfiguration, opts v1.ApplyOptions) (result *apiv1alpha1.DNSRecordset, err error) + DNSRecordsetExpansion +} + +// dNSRecordsets implements DNSRecordsetInterface +type dNSRecordsets struct { + *gentype.ClientWithListAndApply[*apiv1alpha1.DNSRecordset, *apiv1alpha1.DNSRecordsetList, *applyconfigurationapiv1alpha1.DNSRecordsetApplyConfiguration] +} + +// newDNSRecordsets returns a DNSRecordsets +func newDNSRecordsets(c *OpenstackV1alpha1Client, namespace string) *dNSRecordsets { + return &dNSRecordsets{ + gentype.NewClientWithListAndApply[*apiv1alpha1.DNSRecordset, *apiv1alpha1.DNSRecordsetList, *applyconfigurationapiv1alpha1.DNSRecordsetApplyConfiguration]( + "dnsrecordsets", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.DNSRecordset { return &apiv1alpha1.DNSRecordset{} }, + func() *apiv1alpha1.DNSRecordsetList { return &apiv1alpha1.DNSRecordsetList{} }, + ), + } +} 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 f52e587bd..01279d60c 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) DNSRecordsets(namespace string) v1alpha1.DNSRecordsetInterface { + return newFakeDNSRecordsets(c, namespace) +} + func (c *FakeOpenstackV1alpha1) DNSZones(namespace string) v1alpha1.DNSZoneInterface { return newFakeDNSZones(c, namespace) } diff --git a/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnsrecordset.go b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnsrecordset.go new file mode 100644 index 000000000..776596bf9 --- /dev/null +++ b/pkg/clients/clientset/clientset/typed/api/v1alpha1/fake/fake_dnsrecordset.go @@ -0,0 +1,53 @@ +/* +Copyright The ORC Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1" + apiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1" + typedapiv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/clientset/clientset/typed/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeDNSRecordsets implements DNSRecordsetInterface +type fakeDNSRecordsets struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.DNSRecordset, *v1alpha1.DNSRecordsetList, *apiv1alpha1.DNSRecordsetApplyConfiguration] + Fake *FakeOpenstackV1alpha1 +} + +func newFakeDNSRecordsets(fake *FakeOpenstackV1alpha1, namespace string) typedapiv1alpha1.DNSRecordsetInterface { + return &fakeDNSRecordsets{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.DNSRecordset, *v1alpha1.DNSRecordsetList, *apiv1alpha1.DNSRecordsetApplyConfiguration]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("dnsrecordsets"), + v1alpha1.SchemeGroupVersion.WithKind("DNSRecordset"), + func() *v1alpha1.DNSRecordset { return &v1alpha1.DNSRecordset{} }, + func() *v1alpha1.DNSRecordsetList { return &v1alpha1.DNSRecordsetList{} }, + func(dst, src *v1alpha1.DNSRecordsetList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.DNSRecordsetList) []*v1alpha1.DNSRecordset { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.DNSRecordsetList, items []*v1alpha1.DNSRecordset) { + 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 1d5537f57..48c769dcb 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 DNSRecordsetExpansion interface{} + type DNSZoneExpansion interface{} type DomainExpansion interface{} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/dnsrecordset.go b/pkg/clients/informers/externalversions/api/v1alpha1/dnsrecordset.go new file mode 100644 index 000000000..678ece0d4 --- /dev/null +++ b/pkg/clients/informers/externalversions/api/v1alpha1/dnsrecordset.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" +) + +// DNSRecordsetInformer provides access to a shared informer and lister for +// DNSRecordsets. +type DNSRecordsetInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.DNSRecordsetLister +} + +type dNSRecordsetInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewDNSRecordsetInformer constructs a new informer for DNSRecordset 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 NewDNSRecordsetInformer(client clientset.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredDNSRecordsetInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredDNSRecordsetInformer constructs a new informer for DNSRecordset 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 NewFilteredDNSRecordsetInformer(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().DNSRecordsets(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSRecordsets(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSRecordsets(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OpenstackV1alpha1().DNSRecordsets(namespace).Watch(ctx, options) + }, + }, + &v2apiv1alpha1.DNSRecordset{}, + resyncPeriod, + indexers, + ) +} + +func (f *dNSRecordsetInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredDNSRecordsetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *dNSRecordsetInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&v2apiv1alpha1.DNSRecordset{}, f.defaultInformer) +} + +func (f *dNSRecordsetInformer) Lister() apiv1alpha1.DNSRecordsetLister { + return apiv1alpha1.NewDNSRecordsetLister(f.Informer().GetIndexer()) +} diff --git a/pkg/clients/informers/externalversions/api/v1alpha1/interface.go b/pkg/clients/informers/externalversions/api/v1alpha1/interface.go index 7cfe3b473..34615b535 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 + // DNSRecordsets returns a DNSRecordsetInformer. + DNSRecordsets() DNSRecordsetInformer // DNSZones returns a DNSZoneInformer. DNSZones() DNSZoneInformer // Domains returns a DomainInformer. @@ -101,6 +103,11 @@ func (v *version) ApplicationCredentials() ApplicationCredentialInformer { return &applicationCredentialInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// DNSRecordsets returns a DNSRecordsetInformer. +func (v *version) DNSRecordsets() DNSRecordsetInformer { + return &dNSRecordsetInformer{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} diff --git a/pkg/clients/informers/externalversions/generic.go b/pkg/clients/informers/externalversions/generic.go index 209ecf201..29f51fe74 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("dnsrecordsets"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().DNSRecordsets().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("dnszones"): return &genericInformer{resource: resource.GroupResource(), informer: f.Openstack().V1alpha1().DNSZones().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("domains"): diff --git a/pkg/clients/listers/api/v1alpha1/dnsrecordset.go b/pkg/clients/listers/api/v1alpha1/dnsrecordset.go new file mode 100644 index 000000000..d0cfdbd6a --- /dev/null +++ b/pkg/clients/listers/api/v1alpha1/dnsrecordset.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" +) + +// DNSRecordsetLister helps list DNSRecordsets. +// All objects returned here must be treated as read-only. +type DNSRecordsetLister interface { + // List lists all DNSRecordsets in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.DNSRecordset, err error) + // DNSRecordsets returns an object that can list and get DNSRecordsets. + DNSRecordsets(namespace string) DNSRecordsetNamespaceLister + DNSRecordsetListerExpansion +} + +// dNSRecordsetLister implements the DNSRecordsetLister interface. +type dNSRecordsetLister struct { + listers.ResourceIndexer[*apiv1alpha1.DNSRecordset] +} + +// NewDNSRecordsetLister returns a new DNSRecordsetLister. +func NewDNSRecordsetLister(indexer cache.Indexer) DNSRecordsetLister { + return &dNSRecordsetLister{listers.New[*apiv1alpha1.DNSRecordset](indexer, apiv1alpha1.Resource("dnsrecordset"))} +} + +// DNSRecordsets returns an object that can list and get DNSRecordsets. +func (s *dNSRecordsetLister) DNSRecordsets(namespace string) DNSRecordsetNamespaceLister { + return dNSRecordsetNamespaceLister{listers.NewNamespaced[*apiv1alpha1.DNSRecordset](s.ResourceIndexer, namespace)} +} + +// DNSRecordsetNamespaceLister helps list and get DNSRecordsets. +// All objects returned here must be treated as read-only. +type DNSRecordsetNamespaceLister interface { + // List lists all DNSRecordsets in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.DNSRecordset, err error) + // Get retrieves the DNSRecordset from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.DNSRecordset, error) + DNSRecordsetNamespaceListerExpansion +} + +// dNSRecordsetNamespaceLister implements the DNSRecordsetNamespaceLister +// interface. +type dNSRecordsetNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.DNSRecordset] +} diff --git a/pkg/clients/listers/api/v1alpha1/expansion_generated.go b/pkg/clients/listers/api/v1alpha1/expansion_generated.go index 9c5fd98af..6fe44ffec 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{} +// DNSRecordsetListerExpansion allows custom methods to be added to +// DNSRecordsetLister. +type DNSRecordsetListerExpansion interface{} + +// DNSRecordsetNamespaceListerExpansion allows custom methods to be added to +// DNSRecordsetNamespaceLister. +type DNSRecordsetNamespaceListerExpansion interface{} + // DNSZoneListerExpansion allows custom methods to be added to // DNSZoneLister. type DNSZoneListerExpansion interface{} diff --git a/test/apivalidations/dnsrecordset_test.go b/test/apivalidations/dnsrecordset_test.go new file mode 100644 index 000000000..63a045b45 --- /dev/null +++ b/test/apivalidations/dnsrecordset_test.go @@ -0,0 +1,227 @@ +/* +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 ( + dnsrecordsetName = "dnsrecordset" +) + +func dnsrecordsetStub(namespace *corev1.Namespace) *orcv1alpha1.DNSRecordset { + obj := &orcv1alpha1.DNSRecordset{} + obj.Name = dnsrecordsetName + obj.Namespace = namespace.Name + return obj +} + +func testDNSRecordsetResource() *applyconfigv1alpha1.DNSRecordsetResourceSpecApplyConfiguration { + return applyconfigv1alpha1.DNSRecordsetResourceSpec(). + WithType("A"). + WithRecords("192.0.2.1"). + WithDNSZoneRef("my-zone") +} + +func baseDNSRecordsetPatch(obj client.Object) *applyconfigv1alpha1.DNSRecordsetApplyConfiguration { + return applyconfigv1alpha1.DNSRecordset(obj.GetName(), obj.GetNamespace()). + WithSpec(applyconfigv1alpha1.DNSRecordsetSpec(). + WithCloudCredentialsRef(testCredentials())) +} + +func testDNSRecordsetImport() *applyconfigv1alpha1.DNSRecordsetImportApplyConfiguration { + return applyconfigv1alpha1.DNSRecordsetImport().WithFilter(applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("A")) +} + +var _ = Describe("ORC DNSRecordset API validations", func() { + var namespace *corev1.Namespace + BeforeEach(func() { + namespace = createNamespace() + }) + + runManagementPolicyTests(func() *corev1.Namespace { return namespace }, managementPolicyTestArgs[*applyconfigv1alpha1.DNSRecordsetApplyConfiguration]{ + createObject: func(ns *corev1.Namespace) client.Object { return dnsrecordsetStub(ns) }, + basePatch: func(obj client.Object) *applyconfigv1alpha1.DNSRecordsetApplyConfiguration { + return baseDNSRecordsetPatch(obj) + }, + applyResource: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithResource(testDNSRecordsetResource()) + }, + applyImport: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithImport(testDNSRecordsetImport()) + }, + applyEmptyImport: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport()) + }, + applyEmptyFilter: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter(applyconfigv1alpha1.DNSRecordsetFilter())) + }, + applyValidFilter: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter(applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("A"))) + }, + applyManaged: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyManaged) + }, + applyUnmanaged: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + }, + applyManagedOptions: func(p *applyconfigv1alpha1.DNSRecordsetApplyConfiguration) { + p.Spec.WithManagedOptions(applyconfigv1alpha1.ManagedOptions().WithOnDelete(orcv1alpha1.OnDeleteDetach)) + }, + getManagementPolicy: func(obj client.Object) orcv1alpha1.ManagementPolicy { + return obj.(*orcv1alpha1.DNSRecordset).Spec.ManagementPolicy + }, + getOnDelete: func(obj client.Object) orcv1alpha1.OnDelete { + return obj.(*orcv1alpha1.DNSRecordset).Spec.ManagedOptions.OnDelete + }, + }) + + It("should reject name not ending with a period", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithName("invalid-name")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("name must end with a period"))) + }) + + It("should accept name ending with a period", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithName("valid-name.")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + }) + + It("should reject TTL outside valid range", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithTTL(0)) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("spec.resource.ttl in body should be greater than or equal to 1"))) + }) + + It("should enforce name immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithName("original-name.")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithName("updated-name.")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("name is immutable"))) + }) + + It("should enforce type immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithType("A")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithType("AAAA")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("type is immutable"))) + }) + + It("should enforce dnsZoneRef immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithDNSZoneRef("original-zone")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithResource(testDNSRecordsetResource().WithDNSZoneRef("updated-zone")) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("dnsZoneRef is immutable"))) + }) + + It("should reject import filter missing name", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithDNSZoneRef("my-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("Required value"))) + }) + + It("should reject import filter missing type", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("Required value"))) + }) + + It("should enforce import filter name immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("bar.").WithDNSZoneRef("my-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("name is immutable"))) + }) + + It("should enforce import filter type immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("AAAA"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("type is immutable"))) + }) + + It("should enforce import filter dnsZoneRef immutability", func(ctx context.Context) { + dnsrecordset := dnsrecordsetStub(namespace) + patch := baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("my-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(Succeed()) + + patch = baseDNSRecordsetPatch(dnsrecordset) + patch.Spec.WithManagementPolicy(orcv1alpha1.ManagementPolicyUnmanaged) + patch.Spec.WithImport(applyconfigv1alpha1.DNSRecordsetImport().WithFilter( + applyconfigv1alpha1.DNSRecordsetFilter().WithName("foo.").WithDNSZoneRef("updated-zone").WithType("A"), + )) + Expect(applyObj(ctx, dnsrecordset, patch)).To(MatchError(ContainSubstring("dnsZoneRef is immutable"))) + }) +}) diff --git a/website/docs/crd-reference.md b/website/docs/crd-reference.md index f405ca141..22913846e 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) +- [DNSRecordset](#dnsrecordset) - [DNSZone](#dnszone) - [Domain](#domain) - [Endpoint](#endpoint) @@ -506,6 +507,7 @@ CloudCredentialsReference is a reference to a secret containing OpenStack creden _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSRecordsetSpec](#dnsrecordsetspec) - [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) @@ -553,6 +555,145 @@ _Appears in:_ +#### DNSRecordset + + + +DNSRecordset is the Schema for an ORC resource. + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `openstack.k-orc.cloud/v1alpha1` | | | +| `kind` _string_ | `DNSRecordset` | | | +| `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` _[DNSRecordsetSpec](#dnsrecordsetspec)_ | spec specifies the desired state of the resource. | | Required: \{\}
| +| `status` _[DNSRecordsetStatus](#dnsrecordsetstatus)_ | status defines the observed state of the resource. | | Optional: \{\}
| + + +#### DNSRecordsetFilter + + + +DNSRecordsetFilter defines an existing resource by its properties. + +_Validation:_ +- MinProperties: 1 + +_Appears in:_ +- [DNSRecordsetImport](#dnsrecordsetimport) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `dnsZoneRef` _[KubernetesNameRef](#kubernetesnameref)_ | dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with. | | MaxLength: 253
MinLength: 1
Required: \{\}
| +| `name` _[OpenStackName](#openstackname)_ | name of the existing resource. | | MaxLength: 255
MinLength: 1
Pattern: `^[^,]+$`
Required: \{\}
| +| `type` _string_ | type of the existing resource. | | MaxLength: 255
Required: \{\}
| +| `ttl` _integer_ | ttl of the existing resource. | | Maximum: 2.147483647e+09
Minimum: 1
Optional: \{\}
| +| `description` _string_ | description of the existing resource. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| + + +#### DNSRecordsetImport + + + +DNSRecordsetImport specifies an existing resource which will be imported instead of +creating a new one. + +_Validation:_ +- MaxProperties: 1 +- MinProperties: 1 + +_Appears in:_ +- [DNSRecordsetSpec](#dnsrecordsetspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `filter` _[DNSRecordsetFilter](#dnsrecordsetfilter)_ | 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
Required: \{\}
| + + +#### DNSRecordsetResourceSpec + + + +DNSRecordsetResourceSpec specifies the desired state of the resource. + + + +_Appears in:_ +- [DNSRecordsetSpec](#dnsrecordsetspec) + +| 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: \{\}
| +| `type` _string_ | type is the type of the recordset (e.g., A, AAAA, CNAME, MX, TXT, etc.). | | MaxLength: 255
Required: \{\}
| +| `records` _string array_ | records are the DNS records of the recordset. | | MaxItems: 1024
MinItems: 1
items:MaxLength: 1024
Required: \{\}
| +| `ttl` _integer_ | ttl is the Time To Live for the recordset. | | Maximum: 2.147483647e+09
Minimum: 1
Optional: \{\}
| +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 255
MinLength: 1
Optional: \{\}
| +| `dnsZoneRef` _[KubernetesNameRef](#kubernetesnameref)_ | dnsZoneRef is a reference to the ORC DNSZone this recordset is associated with. | | MaxLength: 253
MinLength: 1
Required: \{\}
| + + +#### DNSRecordsetResourceStatus + + + +DNSRecordsetResourceStatus represents the observed state of the resource. + + + +_Appears in:_ +- [DNSRecordsetStatus](#dnsrecordsetstatus) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | name is a human-readable name for the resource. | | MaxLength: 1024
Optional: \{\}
| +| `type` _string_ | type is the type of the recordset. | | MaxLength: 255
Optional: \{\}
| +| `records` _string array_ | records are the DNS records of the recordset. | | MaxItems: 1024
items:MaxLength: 1024
Optional: \{\}
| +| `ttl` _integer_ | ttl is the Time To Live for the recordset in seconds. | | Optional: \{\}
| +| `description` _string_ | description is a human-readable description for the resource. | | MaxLength: 1024
Optional: \{\}
| +| `status` _string_ | status is the status of the resource. | | MaxLength: 255
Optional: \{\}
| + + +#### DNSRecordsetSpec + + + +DNSRecordsetSpec defines the desired state of an ORC object. + + + +_Appears in:_ +- [DNSRecordset](#dnsrecordset) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `import` _[DNSRecordsetImport](#dnsrecordsetimport)_ | import refers to an existing OpenStack resource which will be imported instead of
creating a new one. | | MaxProperties: 1
MinProperties: 1
Optional: \{\}
| +| `resource` _[DNSRecordsetResourceSpec](#dnsrecordsetresourcespec)_ | 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: \{\}
| + + +#### DNSRecordsetStatus + + + +DNSRecordsetStatus defines the observed state of an ORC resource. + + + +_Appears in:_ +- [DNSRecordset](#dnsrecordset) + +| 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` _[DNSRecordsetResourceStatus](#dnsrecordsetresourcestatus)_ | resource contains the observed state of the OpenStack resource. | | Optional: \{\}
| + + #### DNSZone @@ -2348,6 +2489,8 @@ _Appears in:_ - [ApplicationCredentialAccessRule](#applicationcredentialaccessrule) - [ApplicationCredentialFilter](#applicationcredentialfilter) - [ApplicationCredentialResourceSpec](#applicationcredentialresourcespec) +- [DNSRecordsetFilter](#dnsrecordsetfilter) +- [DNSRecordsetResourceSpec](#dnsrecordsetresourcespec) - [EndpointFilter](#endpointfilter) - [EndpointResourceSpec](#endpointresourcespec) - [ExternalGateway](#externalgateway) @@ -2428,6 +2571,7 @@ _Appears in:_ _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSRecordsetSpec](#dnsrecordsetspec) - [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) @@ -2470,6 +2614,7 @@ _Validation:_ _Appears in:_ - [AddressScopeSpec](#addressscopespec) - [ApplicationCredentialSpec](#applicationcredentialspec) +- [DNSRecordsetSpec](#dnsrecordsetspec) - [DNSZoneSpec](#dnszonespec) - [DomainSpec](#domainspec) - [EndpointSpec](#endpointspec) @@ -2778,6 +2923,8 @@ _Appears in:_ - [AddressScopeResourceSpec](#addressscoperesourcespec) - [ApplicationCredentialFilter](#applicationcredentialfilter) - [ApplicationCredentialResourceSpec](#applicationcredentialresourcespec) +- [DNSRecordsetFilter](#dnsrecordsetfilter) +- [DNSRecordsetResourceSpec](#dnsrecordsetresourcespec) - [DNSZoneFilter](#dnszonefilter) - [DNSZoneResourceSpec](#dnszoneresourcespec) - [FlavorFilter](#flavorfilter) diff --git a/website/docs/user-guide/dnszone.md b/website/docs/user-guide/dnszone.md index 3db069735..af194b71a 100644 --- a/website/docs/user-guide/dnszone.md +++ b/website/docs/user-guide/dnszone.md @@ -1,6 +1,6 @@ -# DNS Zones (DNSZone) +# DNS (DNSZone & DNSRecordset) -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. +The `DNSZone` and `DNSRecordset` resources manage DNS zones and recordsets in OpenStack Designate. They allow you to declaratively create, update, and delete zones and recordsets, or import existing ones for read-only access. --- @@ -242,3 +242,209 @@ Designate is asynchronously processing the zone creation/update or communicating **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. + +--- + +# DNS Recordsets (DNSRecordset) + +The `DNSRecordset` resource manages DNS recordsets (such as `A`, `AAAA`, `CNAME`, `MX`, `TXT`, etc.) inside a `DNSZone` in OpenStack Designate. It allows you to declaratively create, update, and delete recordsets. + +--- + +## Core Concepts + +### Zone Reference +Each `DNSRecordset` must reference a parent `DNSZone` resource in Kubernetes via the `dnsZoneRef` field. +* **Reconciliation Block**: If the referenced `DNSZone` is not yet ready (its status is not `Available`), the `DNSRecordset` controller will wait and retry until the zone becomes ready. +* **Deletion Guard**: A deletion guard prevents the parent `DNSZone` from being deleted from Kubernetes while any `DNSRecordset` referencing it is active. + +### Domain Name Syntax & Suffix Rule +All DNS recordset names in OpenStack Designate and ORC **must end with a trailing period** (e.g., `www.example.com.`). This is enforced by Kubernetes API validation rules. +Additionally, the recordset's name **must end with the parent zone name as a suffix**. For example, if the parent `DNSZone` is `example.com.`, a valid recordset name is `www.example.com.`. + +--- + +## Management Policies + +Like all ORC resources, `DNSRecordset` supports two management policies: `managed` and `unmanaged`. + +### 1. Managed Recordset (Default) + +In the `managed` policy, ORC handles the entire lifecycle of the DNS recordset in OpenStack Designate. +* **Creation**: ORC creates the recordset if it does not exist under the referenced zone. +* **Update**: ORC synchronizes specifications (like records, TTL, and description) to Designate. (Note: `name` and `type` are immutable). +* **Deletion**: On deletion of the Kubernetes resource, the corresponding Designate recordset is deleted (unless `managedOptions.onDelete` is set to `detach`). + +#### Example: Managed A Recordset + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: sample-a-record + namespace: default +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: managed + resource: + # name specifies the name of the Recordset. Must end with a period and the parent zone suffix. + # Defaults to the ORC object name (with a trailing period) if not specified. + # Immutable after creation. + name: www.example.com. + + # type is the DNS record type (e.g., A, AAAA, CNAME, MX, TXT, SRV, SPF, NS, PTR, CAA). + # Immutable after creation. + type: A + + # records is the list of values for the recordset. + # Must specify at least 1 record. + records: + - 192.0.2.10 + - 192.0.2.11 + + # ttl is the Time To Live in seconds. + ttl: 3600 + + # description is a human-readable description. + description: "Managed A recordset example" + + # dnsZoneRef references the DNSZone resource in Kubernetes. + dnsZoneRef: primary-zone +``` + +### 2. Unmanaged Import Workflow + +In the `unmanaged` policy, ORC imports an existing DNS recordset 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 recordset 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 recordset. + +```yaml +apiVersion: openstack.k-orc.cloud/v1alpha1 +kind: DNSRecordset +metadata: + name: imported-record-by-id +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + id: "265c9e4f-0f5a-46e4-9f3f-fb8de25ae120" +``` + +#### Option B: Import by Filter +Use this option when you want to look up an existing recordset based on its properties. The filter must specify the `dnsZoneRef` and can additionally query by `name`, `type`, etc. 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: DNSRecordset +metadata: + name: imported-record-by-filter +spec: + cloudCredentialsRef: + secretName: openstack-clouds + cloudName: openstack + managementPolicy: unmanaged + import: + filter: + dnsZoneRef: primary-zone + name: www.example.com. + type: A +``` + +--- + +## Validation Rules & Immutability + +The `DNSRecordset` Custom Resource Definition (CRD) implements strict validation via Common Expression Language (CEL), OpenAPI schemas, and controller-level format checks: + +* **Name Validation**: + * Must end with a trailing period (`.`). + * Must have the parent DNS zone name as a suffix. + * Immutable. Once created, you cannot change the recordset name in the specification. +* **Type Validation**: + * Must be specified (e.g., `A`, `AAAA`, `CNAME`, `MX`, `TXT`, etc.). + * Immutable. Once created, you cannot change the type. +* **Records Validation**: + * Must specify at least `1` record (MinItems: 1) and at most `1024` (MaxItems: 1024). + * Each record's format is validated based on the type: + * **A**: Must be a valid IPv4 address. + * **AAAA**: Must be a valid IPv6 address. + * **CNAME**: Must be a valid domain name ending with a trailing period. + * **TXT**: If specified with quotes, the quotes must be balanced. If missing quotes entirely, the controller will automatically wrap it in double quotes. +* **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 `DNSRecordset`, inspect its status conditions: + +```bash +kubectl get dnsrecordset sample-a-record -o yaml +``` + +### 1. `Available` Condition +* `True`: The DNS recordset is successfully created and active in OpenStack Designate. +* `False`: The recordset is not ready for use. Common reasons include: + * `WaitingOnObject`: The referenced parent `DNSZone` is not yet available, so provisioning is blocked. + * `Progressing`: ORC is actively creating or updating the recordset in Designate. + * `UnrecoverableError`/`InvalidConfiguration`: Configuration or backend API error. + +### 2. `Progressing` Condition +* `True`: ORC is still performing operations or polling for updates (e.g., waiting for the zone or recordset creation). +* `False`: ORC has completed reconciliation, or a terminal error has been reached. + +--- + +## Troubleshooting + +Here are common issues you might encounter with `DNSRecordset` 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 "dnsrecordset.yaml": DNSRecordset.openstack.k-orc.cloud "my-record" is invalid: spec.resource.name: Invalid value: "my-record": name must end with a period +``` + +**Solution:** +Ensure that your `spec.resource.name` ends with a period (e.g., `www.example.com.`). Note that since Kubernetes resource names cannot end with a period, you must explicitly specify `spec.resource.name` with the trailing period. + +--- + +### 2. Recordset Name Does Not Match Parent Zone Suffix + +**Symptom:** +The resource reports `Available=False` and `Progressing=False` with a message: +``` +recordset name "www.wrong-zone.com." does not end with the parent zone suffix "example.com." +``` + +**Solution:** +Make sure the recordset name ends with the exact domain name of the parent `DNSZone` referenced by `dnsZoneRef`. + +--- + +### 3. Invalid Record Format for Recordset Type + +**Symptom:** +Reconciliation fails with errors such as: +``` +invalid IPv4 address "not-an-ip" for A record +``` + +**Solution:** +Ensure the values in `spec.resource.records` comply with the record type: +* For `A` records, specify valid IPv4 addresses. +* For `AAAA` records, specify valid IPv6 addresses. +* For `CNAME` records, ensure the target ends with a trailing period (e.g., `target.example.com.`). diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 1d382ff38..ca26425db 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -9,7 +9,7 @@ nav: - Quick Start: getting-started.md - User Guide: - Overview: user-guide/index.md - - DNS Zones: user-guide/dnszone.md + - DNS (Zones & Recordsets): user-guide/dnszone.md - CRD Reference: crd-reference.md - Troubleshooting: troubleshooting.md - Contributing: