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