From 044942d84b8616e53a8d5095649e08f8446b0c61 Mon Sep 17 00:00:00 2001 From: Michael Nairn Date: Fri, 2 Feb 2024 11:55:54 +0000 Subject: [PATCH] Copy MGC DNS apis, controllers and packages Copy all DNS apis and logic from MGC (DNSRecord, ManagedZone and DNSHealthCheck). MGC Version: https://github.com/Kuadrant/multicluster-gateway-controller/tree/d2e05a088a57bfb1ecb7c486d8fd6c48de439a4f Add openshift-goimports make targets and hack/verify-imports.sh script. --- Dockerfile | 2 +- Makefile | 11 + api/v1alpha1/dnshealthcheckprobe_types.go | 42 +- api/v1alpha1/dnsrecord_types.go | 141 ++++- api/v1alpha1/health_types.go | 56 ++ api/v1alpha1/managedzone_types.go | 65 ++- api/v1alpha1/shared_types.go | 28 + api/v1alpha1/zz_generated.deepcopy.go | 303 +++++++++- ...nt-dns-operator.clusterserviceversion.yaml | 4 +- .../kuadrant.io_dnshealthcheckprobes.yaml | 57 +- bundle/manifests/kuadrant.io_dnsrecords.yaml | 199 ++++++- .../manifests/kuadrant.io_managedzones.yaml | 151 ++++- cmd/main.go | 58 +- .../kuadrant.io_dnshealthcheckprobes.yaml | 57 +- config/crd/bases/kuadrant.io_dnsrecords.yaml | 199 ++++++- .../crd/bases/kuadrant.io_managedzones.yaml | 151 ++++- config/rbac/role.yaml | 2 + go.mod | 99 ++-- go.sum | 284 +++++---- hack/verify-imports.sh | 18 + internal/common/conditions/conditions.go | 6 + internal/common/slice/slice.go | 84 +++ .../dnshealthcheckprobe_controller.go | 303 ++++++++-- internal/controller/dnsrecord_controller.go | 189 +++++- internal/controller/managedzone_controller.go | 319 +++++++++- internal/health/metrics.go | 61 ++ internal/health/monitor.go | 96 ++++ internal/health/notifier.go | 67 +++ internal/health/probeQueuer.go | 92 +++ internal/health/queuedProbeWorker.go | 217 +++++++ internal/provider/aws/aws.go | 328 +++++++++++ internal/provider/aws/client.go | 118 ++++ internal/provider/aws/metrics.go | 105 ++++ internal/provider/factory.go | 85 +++ internal/provider/fake/factory.go | 18 + internal/provider/fake/provider.go | 31 + internal/provider/google/google.go | 503 ++++++++++++++++ internal/provider/google/google_test.go | 544 ++++++++++++++++++ internal/provider/iso3166.go | 272 +++++++++ internal/provider/provider.go | 47 ++ internal/provider/provider_test.go | 35 ++ 41 files changed, 5151 insertions(+), 296 deletions(-) create mode 100644 api/v1alpha1/health_types.go create mode 100644 api/v1alpha1/shared_types.go create mode 100755 hack/verify-imports.sh create mode 100644 internal/common/conditions/conditions.go create mode 100644 internal/common/slice/slice.go create mode 100644 internal/health/metrics.go create mode 100644 internal/health/monitor.go create mode 100644 internal/health/notifier.go create mode 100644 internal/health/probeQueuer.go create mode 100644 internal/health/queuedProbeWorker.go create mode 100644 internal/provider/aws/aws.go create mode 100644 internal/provider/aws/client.go create mode 100644 internal/provider/aws/metrics.go create mode 100644 internal/provider/factory.go create mode 100644 internal/provider/fake/factory.go create mode 100644 internal/provider/fake/provider.go create mode 100644 internal/provider/google/google.go create mode 100644 internal/provider/google/google_test.go create mode 100644 internal/provider/iso3166.go create mode 100644 internal/provider/provider.go create mode 100644 internal/provider/provider_test.go diff --git a/Dockerfile b/Dockerfile index aa74435b..36e5cd88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command diff --git a/Makefile b/Makefile index 297dad25..88f4b4db 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,10 @@ fmt: ## Run go fmt against code. vet: ## Run go vet against code. go vet ./... +.PHONY: imports +imports: openshift-goimports ## Run openshift goimports against code. + $(OPENSHIFT_GOIMPORTS) -m github.com/kuadrant/kuadrant-dns-operator -i github.com/kuadrant/kuadrant-operator + .PHONY: test test: manifests generate fmt vet envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out @@ -188,10 +192,12 @@ KUBECTL ?= kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest +OPENSHIFT_GOIMPORTS ?= $(LOCALBIN)/openshift-goimports ## Tool Versions KUSTOMIZE_VERSION ?= v5.0.1 CONTROLLER_TOOLS_VERSION ?= v0.12.0 +OPENSHIFT_GOIMPORTS_VERSION ?= c70783e636f2213cac683f6865d88c5edace3157 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. @@ -230,6 +236,11 @@ OPERATOR_SDK = $(shell which operator-sdk) endif endif +.PHONY: openshift-goimports +openshift-goimports: $(OPENSHIFT_GOIMPORTS) ## Download openshift-goimports locally if necessary +$(OPENSHIFT_GOIMPORTS): + GOBIN=$(LOCALBIN) go install github.com/openshift-eng/openshift-goimports@$(OPENSHIFT_GOIMPORTS_VERSION) + .PHONY: bundle bundle: manifests kustomize operator-sdk ## Generate bundle manifests and metadata, then validate generated files. $(OPERATOR_SDK) generate kustomize manifests -q diff --git a/api/v1alpha1/dnshealthcheckprobe_types.go b/api/v1alpha1/dnshealthcheckprobe_types.go index d935af1c..4ad14631 100644 --- a/api/v1alpha1/dnshealthcheckprobe_types.go +++ b/api/v1alpha1/dnshealthcheckprobe_types.go @@ -20,26 +20,44 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // DNSHealthCheckProbeSpec defines the desired state of DNSHealthCheckProbe type DNSHealthCheckProbeSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + Port int `json:"port,omitempty"` + Host string `json:"host,omitempty"` + Address string `json:"address,omitempty"` + Path string `json:"path,omitempty"` + Protocol HealthProtocol `json:"protocol,omitempty"` + Interval metav1.Duration `json:"interval,omitempty"` + AdditionalHeadersRef *AdditionalHeadersRef `json:"additionalHeadersRef,omitempty"` + FailureThreshold *int `json:"failureThreshold,omitempty"` + ExpectedResponses []int `json:"expectedResponses,omitempty"` + AllowInsecureCertificate bool `json:"allowInsecureCertificate,omitempty"` +} - // Foo is an example field of DNSHealthCheckProbe. Edit dnshealthcheckprobe_types.go to remove/update - Foo string `json:"foo,omitempty"` +type AdditionalHeadersRef struct { + Name string `json:"name"` +} + +type AdditionalHeaders []AdditionalHeader + +type AdditionalHeader struct { + Name string `json:"name"` + Value string `json:"value"` } // DNSHealthCheckProbeStatus defines the observed state of DNSHealthCheckProbe type DNSHealthCheckProbeStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + LastCheckedAt metav1.Time `json:"lastCheckedAt"` + ConsecutiveFailures int `json:"consecutiveFailures,omitempty"` + Reason string `json:"reason,omitempty"` + Status int `json:"status,omitempty"` + Healthy *bool `json:"healthy"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Healthy",type="boolean",JSONPath=".status.healthy",description="DNSHealthCheckProbe healthy." +//+kubebuilder:printcolumn:name="Last Checked",type="date",JSONPath=".status.lastCheckedAt",description="Last checked at." // DNSHealthCheckProbe is the Schema for the dnshealthcheckprobes API type DNSHealthCheckProbe struct { @@ -59,6 +77,12 @@ type DNSHealthCheckProbeList struct { Items []DNSHealthCheckProbe `json:"items"` } +func (p *DNSHealthCheckProbe) Default() { + if p.Spec.Protocol == "" { + p.Spec.Protocol = HttpProtocol + } +} + func init() { SchemeBuilder.Register(&DNSHealthCheckProbe{}, &DNSHealthCheckProbeList{}) } diff --git a/api/v1alpha1/dnsrecord_types.go b/api/v1alpha1/dnsrecord_types.go index 79ae143e..0164e41f 100644 --- a/api/v1alpha1/dnsrecord_types.go +++ b/api/v1alpha1/dnsrecord_types.go @@ -17,29 +17,135 @@ limitations under the License. package v1alpha1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers +type ProviderSpecificProperty struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + +// Targets is a representation of a list of targets for an endpoint. +type Targets []string + +// TTL is a structure defining the TTL of a DNS record +type TTL int64 + +// Labels store metadata related to the endpoint +// it is then stored in a persistent storage via serialization +type Labels map[string]string + +// ProviderSpecific holds configuration which is specific to individual DNS providers +type ProviderSpecific []ProviderSpecificProperty + +// Endpoint is a high-level way of a connection between a service and an IP +type Endpoint struct { + // The hostname of the DNS record + DNSName string `json:"dnsName,omitempty"` + // The targets the DNS record points to + Targets Targets `json:"targets,omitempty"` + // RecordType type of record, e.g. CNAME, A, SRV, TXT etc + RecordType string `json:"recordType,omitempty"` + // Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') + SetIdentifier string `json:"setIdentifier,omitempty"` + // TTL for the record + RecordTTL TTL `json:"recordTTL,omitempty"` + // Labels stores labels defined for the Endpoint + // +optional + Labels Labels `json:"labels,omitempty"` + // ProviderSpecific stores provider specific config + // +optional + ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"` +} + +// SetID returns an id that should be unique across a set of endpoints +func (e *Endpoint) SetID() string { + return e.DNSName + e.SetIdentifier +} + +// WithSetIdentifier applies the given set identifier to the endpoint. +func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint { + e.SetIdentifier = setIdentifier + return e +} + +// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists. +func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) { + for _, providerSpecific := range e.ProviderSpecific { + if providerSpecific.Name == key { + return providerSpecific, true + } + } + return ProviderSpecificProperty{}, false +} + +// SetProviderSpecific sets a provider specific key/value pair. +func (e *Endpoint) SetProviderSpecific(name, value string) { + if e.ProviderSpecific == nil { + e.ProviderSpecific = ProviderSpecific{} + } + + for i, pair := range e.ProviderSpecific { + if pair.Name == name { + e.ProviderSpecific[i].Value = value + return + } + } + + e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{ + Name: name, + Value: value, + }) +} + +func (e *Endpoint) String() string { + return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific) +} // DNSRecordSpec defines the desired state of DNSRecord type DNSRecordSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of DNSRecord. Edit dnsrecord_types.go to remove/update - Foo string `json:"foo,omitempty"` + // +kubebuilder:validation:Required + // +required + ManagedZoneRef *ManagedZoneReference `json:"managedZone,omitempty"` + // +kubebuilder:validation:MinItems=1 + // +optional + Endpoints []*Endpoint `json:"endpoints,omitempty"` } // DNSRecordStatus defines the observed state of DNSRecord type DNSRecordStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + + // conditions are any conditions associated with the record in the managed zone. + // + // If publishing the record fails, the "Failed" condition will be set with a + // reason and message describing the cause of the failure. + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // observedGeneration is the most recently observed generation of the + // DNSRecord. When the DNSRecord is updated, the controller updates the + // corresponding record in each managed zone. If an update for a + // particular zone fails, that failure is recorded in the status + // condition for the zone so that the controller can determine that it + // needs to retry the update for that specific zone. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // endpoints are the last endpoints that were successfully published by the provider + // + // Provides a simple mechanism to store the current provider records in order to + // delete any that are no longer present in DNSRecordSpec.Endpoints + // + // Note: This will not be required if/when we switch to using external-dns since when + // running with a "sync" policy it will clean up unused records automatically. + Endpoints []*Endpoint `json:"endpoints,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="DNSRecord ready." // DNSRecord is the Schema for the dnsrecords API type DNSRecord struct { @@ -59,6 +165,23 @@ type DNSRecordList struct { Items []DNSRecord `json:"items"` } +// DNSRecordType is a DNS resource record type. +// +kubebuilder:validation:Enum=CNAME;A +type DNSRecordType string + +const ( + // CNAMERecordType is an RFC 1035 CNAME record. + CNAMERecordType DNSRecordType = "CNAME" + + // ARecordType is an RFC 1035 A record. + ARecordType DNSRecordType = "A" + + // NSRecordType is a name server record. + NSRecordType DNSRecordType = "NS" + + DefaultGeo string = "default" +) + func init() { SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{}) } diff --git a/api/v1alpha1/health_types.go b/api/v1alpha1/health_types.go new file mode 100644 index 00000000..0ffbd080 --- /dev/null +++ b/api/v1alpha1/health_types.go @@ -0,0 +1,56 @@ +/* +Copyright 2024. + +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 "strings" + +// HealthProtocol represents the protocol to use when making a health check request +type HealthProtocol string + +const ( + HttpProtocol HealthProtocol = "HTTP" + HttpsProtocol HealthProtocol = "HTTPS" +) + +func NewHealthProtocol(p string) HealthProtocol { + switch strings.ToUpper(p) { + case "HTTPS": + return HttpsProtocol + case "HTTP": + return HttpProtocol + } + return HttpProtocol +} + +func (p HealthProtocol) ToScheme() string { + switch p { + case HttpProtocol: + return "http" + case HttpsProtocol: + return "https" + default: + return "http" + } +} + +func (p HealthProtocol) IsHttp() bool { + return p == HttpProtocol +} + +func (p HealthProtocol) IsHttps() bool { + return p == HttpsProtocol +} diff --git a/api/v1alpha1/managedzone_types.go b/api/v1alpha1/managedzone_types.go index d02adf61..bee8f66d 100644 --- a/api/v1alpha1/managedzone_types.go +++ b/api/v1alpha1/managedzone_types.go @@ -20,26 +20,60 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +// ManagedZoneReference holds a reference to a ManagedZone +type ManagedZoneReference struct { + // `name` is the name of the managed zone. + // Required + Name string `json:"name"` +} // ManagedZoneSpec defines the desired state of ManagedZone type ManagedZoneSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of ManagedZone. Edit managedzone_types.go to remove/update - Foo string `json:"foo,omitempty"` + // ID is the provider assigned id of this zone (i.e. route53.HostedZone.ID). + // +optional + ID string `json:"id,omitempty"` + //Domain name of this ManagedZone + // +kubebuilder:validation:Pattern=`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$` + DomainName string `json:"domainName"` + //Description for this ManagedZone + Description string `json:"description"` + // Reference to another managed zone that this managed zone belongs to. + // +optional + ParentManagedZone *ManagedZoneReference `json:"parentManagedZone,omitempty"` + // +required + SecretRef ProviderRef `json:"dnsProviderSecretRef"` } -// ManagedZoneStatus defines the observed state of ManagedZone +// ManagedZoneStatus defines the observed state of a Zone type ManagedZoneStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // List of status conditions to indicate the status of a ManagedZone. + // Known condition types are `Ready`. + // +listType=map + // +listMapKey=type + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // observedGeneration is the most recently observed generation of the + // ManagedZone. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // The ID assigned by this provider for this zone (i.e. route53.HostedZone.ID) + ID string `json:"id,omitempty"` + + // The number of records in the provider zone + RecordCount int64 `json:"recordCount,omitempty"` + + // The NameServers assigned by the provider for this zone (i.e. route53.DelegationSet.NameServers) + NameServers []*string `json:"nameServers,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Domain Name",type="string",JSONPath=".spec.domainName",description="Domain of this Managed Zone" +//+kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id",description="The ID assigned by this provider for this zone ." +//+kubebuilder:printcolumn:name="Record Count",type="string",JSONPath=".status.recordCount",description="Number of records in the provider zone." +//+kubebuilder:printcolumn:name="NameServers",type="string",JSONPath=".status.nameServers",description="The NameServers assigned by the provider for this zone." +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Managed Zone ready." // ManagedZone is the Schema for the managedzones API type ManagedZone struct { @@ -50,6 +84,10 @@ type ManagedZone struct { Status ManagedZoneStatus `json:"status,omitempty"` } +func (mz *ManagedZone) GetProviderRef() ProviderRef { + return mz.Spec.SecretRef +} + //+kubebuilder:object:root=true // ManagedZoneList contains a list of ManagedZone @@ -59,6 +97,13 @@ type ManagedZoneList struct { Items []ManagedZone `json:"items"` } +type ManagedHost struct { + Subdomain string + Host string + ManagedZone *ManagedZone + DnsRecord *DNSRecord +} + func init() { SchemeBuilder.Register(&ManagedZone{}, &ManagedZoneList{}) } diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go new file mode 100644 index 00000000..8ef1791b --- /dev/null +++ b/api/v1alpha1/shared_types.go @@ -0,0 +1,28 @@ +/* +Copyright 2024. + +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 + +type ProviderRef struct { + //+required + Name string `json:"name"` +} + +// +kubebuilder:object:generate=false +type ProviderAccessor interface { + GetNamespace() string + GetProviderRef() ProviderRef +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 913245ba..bb4e529b 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,16 +22,66 @@ limitations under the License. package v1alpha1 import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalHeader) DeepCopyInto(out *AdditionalHeader) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalHeader. +func (in *AdditionalHeader) DeepCopy() *AdditionalHeader { + if in == nil { + return nil + } + out := new(AdditionalHeader) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in AdditionalHeaders) DeepCopyInto(out *AdditionalHeaders) { + { + in := &in + *out = make(AdditionalHeaders, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalHeaders. +func (in AdditionalHeaders) DeepCopy() AdditionalHeaders { + if in == nil { + return nil + } + out := new(AdditionalHeaders) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalHeadersRef) DeepCopyInto(out *AdditionalHeadersRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalHeadersRef. +func (in *AdditionalHeadersRef) DeepCopy() *AdditionalHeadersRef { + if in == nil { + return nil + } + out := new(AdditionalHeadersRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSHealthCheckProbe) DeepCopyInto(out *DNSHealthCheckProbe) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSHealthCheckProbe. @@ -87,6 +137,22 @@ func (in *DNSHealthCheckProbeList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSHealthCheckProbeSpec) DeepCopyInto(out *DNSHealthCheckProbeSpec) { *out = *in + out.Interval = in.Interval + if in.AdditionalHeadersRef != nil { + in, out := &in.AdditionalHeadersRef, &out.AdditionalHeadersRef + *out = new(AdditionalHeadersRef) + **out = **in + } + if in.FailureThreshold != nil { + in, out := &in.FailureThreshold, &out.FailureThreshold + *out = new(int) + **out = **in + } + if in.ExpectedResponses != nil { + in, out := &in.ExpectedResponses, &out.ExpectedResponses + *out = make([]int, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSHealthCheckProbeSpec. @@ -102,6 +168,12 @@ func (in *DNSHealthCheckProbeSpec) DeepCopy() *DNSHealthCheckProbeSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSHealthCheckProbeStatus) DeepCopyInto(out *DNSHealthCheckProbeStatus) { *out = *in + in.LastCheckedAt.DeepCopyInto(&out.LastCheckedAt) + if in.Healthy != nil { + in, out := &in.Healthy, &out.Healthy + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSHealthCheckProbeStatus. @@ -119,8 +191,8 @@ func (in *DNSRecord) DeepCopyInto(out *DNSRecord) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecord. @@ -176,6 +248,22 @@ func (in *DNSRecordList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordSpec) DeepCopyInto(out *DNSRecordSpec) { *out = *in + if in.ManagedZoneRef != nil { + in, out := &in.ManagedZoneRef, &out.ManagedZoneRef + *out = new(ManagedZoneReference) + **out = **in + } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]*Endpoint, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Endpoint) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSpec. @@ -191,6 +279,24 @@ func (in *DNSRecordSpec) DeepCopy() *DNSRecordSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNSRecordStatus) DeepCopyInto(out *DNSRecordStatus) { *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.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = make([]*Endpoint, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Endpoint) + (*in).DeepCopyInto(*out) + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordStatus. @@ -203,13 +309,91 @@ func (in *DNSRecordStatus) DeepCopy() *DNSRecordStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Endpoint) DeepCopyInto(out *Endpoint) { + *out = *in + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make(Targets, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(Labels, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ProviderSpecific != nil { + in, out := &in.ProviderSpecific, &out.ProviderSpecific + *out = make(ProviderSpecific, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. +func (in *Endpoint) DeepCopy() *Endpoint { + if in == nil { + return nil + } + out := new(Endpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Labels) DeepCopyInto(out *Labels) { + { + in := &in + *out = make(Labels, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Labels. +func (in Labels) DeepCopy() Labels { + if in == nil { + return nil + } + out := new(Labels) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedHost) DeepCopyInto(out *ManagedHost) { + *out = *in + if in.ManagedZone != nil { + in, out := &in.ManagedZone, &out.ManagedZone + *out = new(ManagedZone) + (*in).DeepCopyInto(*out) + } + if in.DnsRecord != nil { + in, out := &in.DnsRecord, &out.DnsRecord + *out = new(DNSRecord) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedHost. +func (in *ManagedHost) DeepCopy() *ManagedHost { + if in == nil { + return nil + } + out := new(ManagedHost) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedZone) DeepCopyInto(out *ManagedZone) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZone. @@ -262,9 +446,30 @@ func (in *ManagedZoneList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedZoneReference) DeepCopyInto(out *ManagedZoneReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneReference. +func (in *ManagedZoneReference) DeepCopy() *ManagedZoneReference { + if in == nil { + return nil + } + out := new(ManagedZoneReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedZoneSpec) DeepCopyInto(out *ManagedZoneSpec) { *out = *in + if in.ParentManagedZone != nil { + in, out := &in.ParentManagedZone, &out.ParentManagedZone + *out = new(ManagedZoneReference) + **out = **in + } + out.SecretRef = in.SecretRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneSpec. @@ -280,6 +485,24 @@ func (in *ManagedZoneSpec) DeepCopy() *ManagedZoneSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagedZoneStatus) DeepCopyInto(out *ManagedZoneStatus) { *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.NameServers != nil { + in, out := &in.NameServers, &out.NameServers + *out = make([]*string, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(string) + **out = **in + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedZoneStatus. @@ -291,3 +514,71 @@ func (in *ManagedZoneStatus) DeepCopy() *ManagedZoneStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderRef) DeepCopyInto(out *ProviderRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderRef. +func (in *ProviderRef) DeepCopy() *ProviderRef { + if in == nil { + return nil + } + out := new(ProviderRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ProviderSpecific) DeepCopyInto(out *ProviderSpecific) { + { + in := &in + *out = make(ProviderSpecific, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpecific. +func (in ProviderSpecific) DeepCopy() ProviderSpecific { + if in == nil { + return nil + } + out := new(ProviderSpecific) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpecificProperty) DeepCopyInto(out *ProviderSpecificProperty) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpecificProperty. +func (in *ProviderSpecificProperty) DeepCopy() *ProviderSpecificProperty { + if in == nil { + return nil + } + out := new(ProviderSpecificProperty) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Targets) DeepCopyInto(out *Targets) { + { + in := &in + *out = make(Targets, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Targets. +func (in Targets) DeepCopy() Targets { + if in == nil { + return nil + } + out := new(Targets) + in.DeepCopyInto(out) + return *out +} diff --git a/bundle/manifests/kuadrant-dns-operator.clusterserviceversion.yaml b/bundle/manifests/kuadrant-dns-operator.clusterserviceversion.yaml index 4e607468..224f272c 100644 --- a/bundle/manifests/kuadrant-dns-operator.clusterserviceversion.yaml +++ b/bundle/manifests/kuadrant-dns-operator.clusterserviceversion.yaml @@ -51,7 +51,7 @@ metadata: } ] capabilities: Basic Install - createdAt: "2024-01-31T14:13:25Z" + createdAt: "2024-02-01T15:06:38Z" operators.operatorframework.io/builder: operator-sdk-v1.33.0 operators.operatorframework.io/project_layout: go.kubebuilder.io/v4 name: kuadrant-dns-operator.v0.0.1 @@ -102,6 +102,8 @@ spec: resources: - dnshealthcheckprobes/finalizers verbs: + - get + - patch - update - apiGroups: - kuadrant.io diff --git a/bundle/manifests/kuadrant.io_dnshealthcheckprobes.yaml b/bundle/manifests/kuadrant.io_dnshealthcheckprobes.yaml index d4c45838..3b732a22 100644 --- a/bundle/manifests/kuadrant.io_dnshealthcheckprobes.yaml +++ b/bundle/manifests/kuadrant.io_dnshealthcheckprobes.yaml @@ -14,7 +14,16 @@ spec: singular: dnshealthcheckprobe scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: DNSHealthCheckProbe healthy. + jsonPath: .status.healthy + name: Healthy + type: boolean + - description: Last checked at. + jsonPath: .status.lastCheckedAt + name: Last Checked + type: date + name: v1alpha1 schema: openAPIV3Schema: description: DNSHealthCheckProbe is the Schema for the dnshealthcheckprobes @@ -35,13 +44,53 @@ spec: spec: description: DNSHealthCheckProbeSpec defines the desired state of DNSHealthCheckProbe properties: - foo: - description: Foo is an example field of DNSHealthCheckProbe. Edit - dnshealthcheckprobe_types.go to remove/update + additionalHeadersRef: + properties: + name: + type: string + required: + - name + type: object + address: + type: string + allowInsecureCertificate: + type: boolean + expectedResponses: + items: + type: integer + type: array + failureThreshold: + type: integer + host: + type: string + interval: + type: string + path: + type: string + port: + type: integer + protocol: + description: HealthProtocol represents the protocol to use when making + a health check request type: string type: object status: description: DNSHealthCheckProbeStatus defines the observed state of DNSHealthCheckProbe + properties: + consecutiveFailures: + type: integer + healthy: + type: boolean + lastCheckedAt: + format: date-time + type: string + reason: + type: string + status: + type: integer + required: + - healthy + - lastCheckedAt type: object type: object served: true diff --git a/bundle/manifests/kuadrant.io_dnsrecords.yaml b/bundle/manifests/kuadrant.io_dnsrecords.yaml index 9b995617..b635ce62 100644 --- a/bundle/manifests/kuadrant.io_dnsrecords.yaml +++ b/bundle/manifests/kuadrant.io_dnsrecords.yaml @@ -14,7 +14,12 @@ spec: singular: dnsrecord scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: DNSRecord ready. + jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha1 schema: openAPIV3Schema: description: DNSRecord is the Schema for the dnsrecords API @@ -34,13 +39,197 @@ spec: spec: description: DNSRecordSpec defines the desired state of DNSRecord properties: - foo: - description: Foo is an example field of DNSRecord. Edit dnsrecord_types.go - to remove/update - type: string + endpoints: + items: + description: Endpoint is a high-level way of a connection between + a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value + of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, + TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with + the same name and type (e.g. Route53 records with routing + policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + minItems: 1 + type: array + managedZone: + description: ManagedZoneReference holds a reference to a ManagedZone + properties: + name: + description: '`name` is the name of the managed zone. Required' + type: string + required: + - name + type: object type: object status: description: DNSRecordStatus defines the observed state of DNSRecord + properties: + conditions: + description: "conditions are any conditions associated with the record + in the managed zone. \n If publishing the record fails, the \"Failed\" + condition will be set with a reason and message describing the cause + of the failure." + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array + endpoints: + description: "endpoints are the last endpoints that were successfully + published by the provider \n Provides a simple mechanism to store + the current provider records in order to delete any that are no + longer present in DNSRecordSpec.Endpoints \n Note: This will not + be required if/when we switch to using external-dns since when running + with a \"sync\" policy it will clean up unused records automatically." + items: + description: Endpoint is a high-level way of a connection between + a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value + of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, + TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with + the same name and type (e.g. Route53 records with routing + policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + type: array + observedGeneration: + description: observedGeneration is the most recently observed generation + of the DNSRecord. When the DNSRecord is updated, the controller + updates the corresponding record in each managed zone. If an update + for a particular zone fails, that failure is recorded in the status + condition for the zone so that the controller can determine that + it needs to retry the update for that specific zone. + format: int64 + type: integer type: object type: object served: true diff --git a/bundle/manifests/kuadrant.io_managedzones.yaml b/bundle/manifests/kuadrant.io_managedzones.yaml index 72e4e8e6..53424fe3 100644 --- a/bundle/manifests/kuadrant.io_managedzones.yaml +++ b/bundle/manifests/kuadrant.io_managedzones.yaml @@ -14,7 +14,28 @@ spec: singular: managedzone scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Domain of this Managed Zone + jsonPath: .spec.domainName + name: Domain Name + type: string + - description: The ID assigned by this provider for this zone . + jsonPath: .status.id + name: ID + type: string + - description: Number of records in the provider zone. + jsonPath: .status.recordCount + name: Record Count + type: string + - description: The NameServers assigned by the provider for this zone. + jsonPath: .status.nameServers + name: NameServers + type: string + - description: Managed Zone ready. + jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha1 schema: openAPIV3Schema: description: ManagedZone is the Schema for the managedzones API @@ -34,13 +55,133 @@ spec: spec: description: ManagedZoneSpec defines the desired state of ManagedZone properties: - foo: - description: Foo is an example field of ManagedZone. Edit managedzone_types.go - to remove/update + description: + description: Description for this ManagedZone type: string + dnsProviderSecretRef: + properties: + name: + type: string + required: + - name + type: object + domainName: + description: Domain name of this ManagedZone + pattern: ^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$ + type: string + id: + description: ID is the provider assigned id of this zone (i.e. route53.HostedZone.ID). + type: string + parentManagedZone: + description: Reference to another managed zone that this managed zone + belongs to. + properties: + name: + description: '`name` is the name of the managed zone. Required' + type: string + required: + - name + type: object + required: + - description + - dnsProviderSecretRef + - domainName type: object status: - description: ManagedZoneStatus defines the observed state of ManagedZone + description: ManagedZoneStatus defines the observed state of a Zone + properties: + conditions: + description: List of status conditions to indicate the status of a + ManagedZone. Known condition types are `Ready`. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: The ID assigned by this provider for this zone (i.e. + route53.HostedZone.ID) + type: string + nameServers: + description: The NameServers assigned by the provider for this zone + (i.e. route53.DelegationSet.NameServers) + items: + type: string + type: array + observedGeneration: + description: observedGeneration is the most recently observed generation + of the ManagedZone. + format: int64 + type: integer + recordCount: + description: The number of records in the provider zone + format: int64 + type: integer type: object type: object served: true diff --git a/cmd/main.go b/cmd/main.go index 71424a61..88ac4de4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -19,20 +19,24 @@ package main import ( "flag" "os" - - // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) - // to ensure that exec-entrypoint and run can make use of them. - _ "k8s.io/client-go/plugin/pkg/client/auth" + "time" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" kuadrantiov1alpha1 "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" "github.com/kuadrant/kuadrant-dns-operator/internal/controller" + "github.com/kuadrant/kuadrant-dns-operator/internal/health" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" + _ "github.com/kuadrant/kuadrant-dns-operator/internal/provider/aws" + _ "github.com/kuadrant/kuadrant-dns-operator/internal/provider/google" //+kubebuilder:scaffold:imports ) @@ -67,45 +71,53 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, - MetricsBindAddress: metricsAddr, - Port: 9443, + Metrics: metricsserver.Options{BindAddress: metricsAddr}, + WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "a3f98d6c.kuadrant.io", - // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily - // when the Manager ends. This requires the binary to immediately end when the - // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait - // LeaseDuration time first. - // - // In the default scaffold provided, the program ends immediately after - // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups - // after the manager stops then its usage might be unsafe. - // LeaderElectionReleaseOnCancel: true, }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } + providerFactory := provider.NewFactory(mgr.GetClient()) + + healthMonitor := health.NewMonitor() + healthCheckQueue := health.NewRequestQueue(time.Second * 5) + + if err := mgr.Add(healthMonitor); err != nil { + setupLog.Error(err, "unable to start health monitor") + os.Exit(1) + } + + if err := mgr.Add(healthCheckQueue); err != nil { + setupLog.Error(err, "unable to start health check queue") + os.Exit(1) + } + if err = (&controller.ManagedZoneReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ProviderFactory: providerFactory, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ManagedZone") os.Exit(1) } if err = (&controller.DNSRecordReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + ProviderFactory: providerFactory, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DNSRecord") os.Exit(1) } if err = (&controller.DNSHealthCheckProbeReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + HealthMonitor: healthMonitor, + Queue: healthCheckQueue, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DNSHealthCheckProbe") os.Exit(1) diff --git a/config/crd/bases/kuadrant.io_dnshealthcheckprobes.yaml b/config/crd/bases/kuadrant.io_dnshealthcheckprobes.yaml index 07a051f3..d98bd1ac 100644 --- a/config/crd/bases/kuadrant.io_dnshealthcheckprobes.yaml +++ b/config/crd/bases/kuadrant.io_dnshealthcheckprobes.yaml @@ -14,7 +14,16 @@ spec: singular: dnshealthcheckprobe scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: DNSHealthCheckProbe healthy. + jsonPath: .status.healthy + name: Healthy + type: boolean + - description: Last checked at. + jsonPath: .status.lastCheckedAt + name: Last Checked + type: date + name: v1alpha1 schema: openAPIV3Schema: description: DNSHealthCheckProbe is the Schema for the dnshealthcheckprobes @@ -35,13 +44,53 @@ spec: spec: description: DNSHealthCheckProbeSpec defines the desired state of DNSHealthCheckProbe properties: - foo: - description: Foo is an example field of DNSHealthCheckProbe. Edit - dnshealthcheckprobe_types.go to remove/update + additionalHeadersRef: + properties: + name: + type: string + required: + - name + type: object + address: + type: string + allowInsecureCertificate: + type: boolean + expectedResponses: + items: + type: integer + type: array + failureThreshold: + type: integer + host: + type: string + interval: + type: string + path: + type: string + port: + type: integer + protocol: + description: HealthProtocol represents the protocol to use when making + a health check request type: string type: object status: description: DNSHealthCheckProbeStatus defines the observed state of DNSHealthCheckProbe + properties: + consecutiveFailures: + type: integer + healthy: + type: boolean + lastCheckedAt: + format: date-time + type: string + reason: + type: string + status: + type: integer + required: + - healthy + - lastCheckedAt type: object type: object served: true diff --git a/config/crd/bases/kuadrant.io_dnsrecords.yaml b/config/crd/bases/kuadrant.io_dnsrecords.yaml index 44a7310d..ef73c0d3 100644 --- a/config/crd/bases/kuadrant.io_dnsrecords.yaml +++ b/config/crd/bases/kuadrant.io_dnsrecords.yaml @@ -14,7 +14,12 @@ spec: singular: dnsrecord scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: DNSRecord ready. + jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha1 schema: openAPIV3Schema: description: DNSRecord is the Schema for the dnsrecords API @@ -34,13 +39,197 @@ spec: spec: description: DNSRecordSpec defines the desired state of DNSRecord properties: - foo: - description: Foo is an example field of DNSRecord. Edit dnsrecord_types.go - to remove/update - type: string + endpoints: + items: + description: Endpoint is a high-level way of a connection between + a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value + of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, + TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with + the same name and type (e.g. Route53 records with routing + policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + minItems: 1 + type: array + managedZone: + description: ManagedZoneReference holds a reference to a ManagedZone + properties: + name: + description: '`name` is the name of the managed zone. Required' + type: string + required: + - name + type: object type: object status: description: DNSRecordStatus defines the observed state of DNSRecord + properties: + conditions: + description: "conditions are any conditions associated with the record + in the managed zone. \n If publishing the record fails, the \"Failed\" + condition will be set with a reason and message describing the cause + of the failure." + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array + endpoints: + description: "endpoints are the last endpoints that were successfully + published by the provider \n Provides a simple mechanism to store + the current provider records in order to delete any that are no + longer present in DNSRecordSpec.Endpoints \n Note: This will not + be required if/when we switch to using external-dns since when running + with a \"sync\" policy it will clean up unused records automatically." + items: + description: Endpoint is a high-level way of a connection between + a service and an IP + properties: + dnsName: + description: The hostname of the DNS record + type: string + labels: + additionalProperties: + type: string + description: Labels stores labels defined for the Endpoint + type: object + providerSpecific: + description: ProviderSpecific stores provider specific config + items: + description: ProviderSpecificProperty holds the name and value + of a configuration which is specific to individual DNS providers + properties: + name: + type: string + value: + type: string + type: object + type: array + recordTTL: + description: TTL for the record + format: int64 + type: integer + recordType: + description: RecordType type of record, e.g. CNAME, A, SRV, + TXT etc + type: string + setIdentifier: + description: Identifier to distinguish multiple records with + the same name and type (e.g. Route53 records with routing + policies other than 'simple') + type: string + targets: + description: The targets the DNS record points to + items: + type: string + type: array + type: object + type: array + observedGeneration: + description: observedGeneration is the most recently observed generation + of the DNSRecord. When the DNSRecord is updated, the controller + updates the corresponding record in each managed zone. If an update + for a particular zone fails, that failure is recorded in the status + condition for the zone so that the controller can determine that + it needs to retry the update for that specific zone. + format: int64 + type: integer type: object type: object served: true diff --git a/config/crd/bases/kuadrant.io_managedzones.yaml b/config/crd/bases/kuadrant.io_managedzones.yaml index 1cedaba1..b4ce0a53 100644 --- a/config/crd/bases/kuadrant.io_managedzones.yaml +++ b/config/crd/bases/kuadrant.io_managedzones.yaml @@ -14,7 +14,28 @@ spec: singular: managedzone scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - description: Domain of this Managed Zone + jsonPath: .spec.domainName + name: Domain Name + type: string + - description: The ID assigned by this provider for this zone . + jsonPath: .status.id + name: ID + type: string + - description: Number of records in the provider zone. + jsonPath: .status.recordCount + name: Record Count + type: string + - description: The NameServers assigned by the provider for this zone. + jsonPath: .status.nameServers + name: NameServers + type: string + - description: Managed Zone ready. + jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + name: v1alpha1 schema: openAPIV3Schema: description: ManagedZone is the Schema for the managedzones API @@ -34,13 +55,133 @@ spec: spec: description: ManagedZoneSpec defines the desired state of ManagedZone properties: - foo: - description: Foo is an example field of ManagedZone. Edit managedzone_types.go - to remove/update + description: + description: Description for this ManagedZone type: string + dnsProviderSecretRef: + properties: + name: + type: string + required: + - name + type: object + domainName: + description: Domain name of this ManagedZone + pattern: ^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$ + type: string + id: + description: ID is the provider assigned id of this zone (i.e. route53.HostedZone.ID). + type: string + parentManagedZone: + description: Reference to another managed zone that this managed zone + belongs to. + properties: + name: + description: '`name` is the name of the managed zone. Required' + type: string + required: + - name + type: object + required: + - description + - dnsProviderSecretRef + - domainName type: object status: - description: ManagedZoneStatus defines the observed state of ManagedZone + description: ManagedZoneStatus defines the observed state of a Zone + properties: + conditions: + description: List of status conditions to indicate the status of a + ManagedZone. Known condition types are `Ready`. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + 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. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + 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 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: The ID assigned by this provider for this zone (i.e. + route53.HostedZone.ID) + type: string + nameServers: + description: The NameServers assigned by the provider for this zone + (i.e. route53.DelegationSet.NameServers) + items: + type: string + type: array + observedGeneration: + description: observedGeneration is the most recently observed generation + of the ManagedZone. + format: int64 + type: integer + recordCount: + description: The number of records in the provider zone + format: int64 + type: integer type: object type: object served: true diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 28964198..a33b8ae2 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -21,6 +21,8 @@ rules: resources: - dnshealthcheckprobes/finalizers verbs: + - get + - patch - update - apiGroups: - kuadrant.io diff --git a/go.mod b/go.mod index 099139c4..59cb109b 100644 --- a/go.mod +++ b/go.mod @@ -3,71 +3,84 @@ module github.com/kuadrant/kuadrant-dns-operator go 1.21 require ( - github.com/onsi/ginkgo/v2 v2.9.5 - github.com/onsi/gomega v1.27.7 - k8s.io/apimachinery v0.27.2 - k8s.io/client-go v0.27.2 - sigs.k8s.io/controller-runtime v0.15.0 + github.com/aws/aws-sdk-go v1.44.175 + github.com/go-logr/logr v1.2.4 + github.com/onsi/ginkgo/v2 v2.11.0 + github.com/onsi/gomega v1.27.10 + github.com/prometheus/client_golang v1.17.0 + google.golang.org/api v0.126.0 + k8s.io/api v0.28.3 + k8s.io/apimachinery v0.28.3 + k8s.io/client-go v0.28.3 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b + sigs.k8s.io/controller-runtime v0.16.3 + sigs.k8s.io/gateway-api v1.0.0 ) require ( + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.1 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/gofuzz v1.1.0 // indirect - github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93 // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/google/uuid v1.3.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.11.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.15.1 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.1 // indirect - gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + golang.org/x/tools v0.14.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/grpc v1.55.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.27.2 // indirect - k8s.io/apiextensions-apiserver v0.27.2 // indirect - k8s.io/component-base v0.27.2 // indirect - k8s.io/klog/v2 v2.90.1 // indirect - k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect - k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + k8s.io/apiextensions-apiserver v0.28.3 // indirect + k8s.io/component-base v0.28.3 // indirect + k8s.io/klog/v2 v2.100.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 55b096fc..aabe1a2a 100644 --- a/go.sum +++ b/go.sum @@ -1,83 +1,116 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aws/aws-sdk-go v1.44.175 h1:c0NzHHnPXV5kJoTUFQxFN5cUPpX1SxO635XnwL5/oIY= +github.com/aws/aws-sdk-go v1.44.175/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= -github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93 h1:D5iJJZKAi0rU4e/5E58BkrnN+xeCDjAIqcm1GGxAGSI= +github.com/google/pprof v0.0.0-20221212185716-aee1124e3a93/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -85,7 +118,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -95,8 +127,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -104,29 +136,29 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= -github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= -github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -136,24 +168,36 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -161,23 +205,31 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -185,25 +237,37 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -215,75 +279,93 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= -gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo= -k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4= -k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo= -k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ= -k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= -k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= -k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE= -k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ= -k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo= -k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo= -k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= -k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= -k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= -k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= -sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= +k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= +k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= +k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= +k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= +k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= +k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= +k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/gateway-api v1.0.0 h1:iPTStSv41+d9p0xFydll6d7f7MOBGuqXM6p2/zVYMAs= +sigs.k8s.io/gateway-api v1.0.0/go.mod h1:4cUgr0Lnp5FZ0Cdq8FdRwCvpiWws7LVhLHGIudLlf4c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/verify-imports.sh b/hack/verify-imports.sh new file mode 100755 index 00000000..31517bbe --- /dev/null +++ b/hack/verify-imports.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +make -C "$( dirname "${BASH_SOURCE[0]}")/../" imports +if ! git diff --quiet --exit-code ; then + cat << EOF +ERROR: This check enforces that import statements are ordered correctly. +ERROR: The import statements are out of order. Run the following command +ERROR: to regenerate the statements: +ERROR: $ make imports +ERROR: The following differences were found: +EOF + git diff + exit 1 +fi diff --git a/internal/common/conditions/conditions.go b/internal/common/conditions/conditions.go new file mode 100644 index 00000000..2a0b11d2 --- /dev/null +++ b/internal/common/conditions/conditions.go @@ -0,0 +1,6 @@ +package conditions + +type ConditionType string +type ConditionReason string + +const ConditionTypeReady ConditionType = "Ready" diff --git a/internal/common/slice/slice.go b/internal/common/slice/slice.go new file mode 100644 index 00000000..4e10ba0c --- /dev/null +++ b/internal/common/slice/slice.go @@ -0,0 +1,84 @@ +package slice + +// RemoveString returns a newly created []string that contains all items from slice that +// are not equal to s. +func RemoveString(slice []string, s string) []string { + newSlice := make([]string, 0) + for _, item := range slice { + if item == s { + continue + } + newSlice = append(newSlice, item) + } + if len(newSlice) == 0 { + // Sanitize for unit tests so we don't need to distinguish empty array + // and nil. + newSlice = nil + } + return newSlice +} + +// ContainsString checks if a given slice of strings contains the provided string. +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func Contains[T any](slice []T, predicate func(T) bool) bool { + _, ok := Find(slice, predicate) + return ok +} + +// Find checks if an element in slice satisfies the given predicate, and returns +// it. If no element is found returns false +func Find[T any](slice []T, predicate func(T) bool) (element T, ok bool) { + for _, elem := range slice { + if predicate(elem) { + element = elem + ok = true + return + } + } + + return +} + +func Filter[T any](slice []T, predicate func(T) bool) []T { + result := []T{} + for _, elem := range slice { + if predicate(elem) { + result = append(result, elem) + } + } + + return result +} + +func Map[T, R any](slice []T, f func(T) R) []R { + result := make([]R, len(slice)) + + for i, elem := range slice { + result[i] = f(elem) + } + + return result +} + +func MapErr[T, R any](slice []T, f func(T) (R, error)) ([]R, error) { + result := make([]R, len(slice)) + + for i, elem := range slice { + mapped, err := f(elem) + if err != nil { + return nil, err + } + + result[i] = mapped + } + + return result, nil +} diff --git a/internal/controller/dnshealthcheckprobe_controller.go b/internal/controller/dnshealthcheckprobe_controller.go index e74324ee..9ea24cc9 100644 --- a/internal/controller/dnshealthcheckprobe_controller.go +++ b/internal/controller/dnshealthcheckprobe_controller.go @@ -1,62 +1,289 @@ -/* -Copyright 2024. - -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 controller import ( "context" + "fmt" + "strings" + "github.com/go-logr/logr" + + v1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/common/slice" + "github.com/kuadrant/kuadrant-dns-operator/internal/health" +) - kuadrantiov1alpha1 "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +//ToDO This should not require sigs.k8s.io/gateway-api/apis/v1 + +const ( + DNSHealthCheckProbeFinalizer = "kuadrant.io/dns-health-check-probe" +) + +var ( + ErrInvalidHeader = fmt.Errorf("invalid header format") ) -// DNSHealthCheckProbeReconciler reconciles a DNSHealthCheckProbe object type DNSHealthCheckProbeReconciler struct { client.Client - Scheme *runtime.Scheme -} - -//+kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the DNSHealthCheckProbe object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile + Scheme *runtime.Scheme + HealthMonitor *health.Monitor + Queue *health.QueuedProbeWorker +} + +// +kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kuadrant.io,resources=dnshealthcheckprobes/finalizers,verbs=get;update;patch + func (r *DNSHealthCheckProbeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + logger := log.FromContext(ctx) + + previous := &v1alpha1.DNSHealthCheckProbe{} + err := r.Client.Get(ctx, req.NamespacedName, previous) + if err != nil { + if err := client.IgnoreNotFound(err); err != nil { + return ctrl.Result{}, nil + } else { + return ctrl.Result{}, err + } + } + + logger.V(3).Info("DNSHealthCheckProbeReconciler Reconcile", "dnsHealthCheckProbe", previous) + + probeObj := previous.DeepCopy() + + if probeObj.DeletionTimestamp != nil && !probeObj.DeletionTimestamp.IsZero() { + logger.Info("deleting probe", "probe", probeObj) + + r.deleteProbe(probeObj) + controllerutil.RemoveFinalizer(probeObj, DNSHealthCheckProbeFinalizer) + + if err := r.Update(ctx, probeObj); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(probeObj, DNSHealthCheckProbeFinalizer) { + controllerutil.AddFinalizer(probeObj, DNSHealthCheckProbeFinalizer) + if err := r.Update(ctx, probeObj); err != nil { + return ctrl.Result{}, err + } + } + + // Set the interval + interval := probeObj.Spec.Interval.Duration + + // Set the protocol: default to HTTP is not defined + protocol := probeObj.Spec.Protocol + if protocol == "" { + protocol = v1alpha1.HttpProtocol + } + + protocol = v1alpha1.NewHealthProtocol(string(probeObj.Spec.Protocol)) + + probeId := probeId(probeObj) + + additionalHeaders, err := getAdditionalHeaders(ctx, r.Client, probeObj) + + if err != nil { + f := false + logger.V(1).Info( + "error getting additional headers for probe", + "secret name", probeObj.Spec.AdditionalHeadersRef.Name, + "error", err) + //update probe status + probeObj.Status.Healthy = &f + probeObj.Status.LastCheckedAt = metav1.Now() + updateErr := r.Client.Status().Update(ctx, probeObj) + if updateErr != nil { + logger.V(1).Info("error updating probe status", "error", updateErr) + } + return ctrl.Result{}, err + } + + if r.HealthMonitor.HasProbe(probeId) { + r.HealthMonitor.UpdateProbe(probeId, func(p *health.ProbeQueuer) { + p.Interval = interval + p.Host = probeObj.Spec.Host + p.IPAddress = probeObj.Spec.Address + p.Path = probeObj.Spec.Path + p.Port = probeObj.Spec.Port + p.Protocol = protocol + p.AdditionalHeaders = additionalHeaders + p.ExpectedResponses = probeObj.Spec.ExpectedResponses + p.AllowInsecureCertificate = probeObj.Spec.AllowInsecureCertificate + }) + } else { + notifier, err := r.newProbeNotifierFor(ctx, logger, previous) + if err != nil { + return ctrl.Result{}, err + } - // TODO(user): your logic here + r.HealthMonitor.AddProbeQueuer(&health.ProbeQueuer{ + ID: probeId, + Interval: interval, + Host: probeObj.Spec.Host, + Path: probeObj.Spec.Path, + Port: probeObj.Spec.Port, + Protocol: protocol, + IPAddress: probeObj.Spec.Address, + AdditionalHeaders: additionalHeaders, + ExpectedResponses: probeObj.Spec.ExpectedResponses, + AllowInsecureCertificate: probeObj.Spec.AllowInsecureCertificate, + Notifier: notifier, + Queue: r.Queue, + }) + } return ctrl.Result{}, nil } -// SetupWithManager sets up the controller with the Manager. +// SetupWithManager sets up the controller with the manager func (r *DNSHealthCheckProbeReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantiov1alpha1.DNSHealthCheckProbe{}). + For(&v1alpha1.DNSHealthCheckProbe{}). Complete(r) } + +func (r *DNSHealthCheckProbeReconciler) deleteProbe(probeObj *v1alpha1.DNSHealthCheckProbe) { + r.HealthMonitor.RemoveProbe(probeId(probeObj)) +} + +func probeId(probeObj *v1alpha1.DNSHealthCheckProbe) string { + return fmt.Sprintf("%s/%s", probeObj.Namespace, probeObj.Name) +} + +func getAdditionalHeaders(ctx context.Context, clt client.Client, probeObj *v1alpha1.DNSHealthCheckProbe) (v1alpha1.AdditionalHeaders, error) { + additionalHeaders := v1alpha1.AdditionalHeaders{} + + if probeObj.Spec.AdditionalHeadersRef != nil { + secretKey := client.ObjectKey{Name: probeObj.Spec.AdditionalHeadersRef.Name, Namespace: probeObj.Namespace} + additionalHeadersSecret := &v1.Secret{} + if err := clt.Get(ctx, secretKey, additionalHeadersSecret); client.IgnoreNotFound(err) != nil { + return additionalHeaders, fmt.Errorf("error retrieving additional headers secret %v/%v: %w", secretKey.Namespace, secretKey.Name, err) + } else if err != nil { + probeError := fmt.Errorf("error retrieving additional headers secret %v/%v: %w", secretKey.Namespace, secretKey.Name, err) + probeObj.Status.ConsecutiveFailures = 0 + probeObj.Status.Reason = fmt.Sprintf("additional headers secret '%v' not found", secretKey.Name) + return additionalHeaders, probeError + } + for k, v := range additionalHeadersSecret.Data { + if strings.ContainsAny(strings.TrimSpace(k), " \t") { + probeObj.Status.ConsecutiveFailures = 0 + probeObj.Status.Reason = "invalid header found: " + k + return nil, fmt.Errorf("invalid header, must not contain whitespace '%v': %w", k, ErrInvalidHeader) + } + additionalHeaders = append(additionalHeaders, v1alpha1.AdditionalHeader{ + Name: strings.TrimSpace(k), + Value: string(v), + }) + } + } + return additionalHeaders, nil +} + +func (r *DNSHealthCheckProbeReconciler) getGatewayFor(ctx context.Context, probe *v1alpha1.DNSHealthCheckProbe) (*gatewayapiv1.Gateway, bool, error) { + if probe.Labels == nil { + return nil, false, nil + } + + name, nameOk := probe.Labels["kuadrant.io/gateway"] + namespace, namespaceOk := probe.Labels["kuadrant.io/gateway-namespace"] + + if !nameOk || !namespaceOk { + return nil, false, nil + } + + objKey := client.ObjectKey{ + Name: name, + Namespace: namespace, + } + + gw := &gatewayapiv1.Gateway{} + if err := r.Client.Get(ctx, objKey, gw); err != nil { + return nil, false, err + } + + return gw, true, nil +} + +func (r *DNSHealthCheckProbeReconciler) newProbeNotifierFor(ctx context.Context, logger logr.Logger, probe *v1alpha1.DNSHealthCheckProbe) (health.ProbeNotifier, error) { + // Base notifier to update the probe CR + notifier := health.NewStatusUpdateProbeNotifier(r.Client, probe) + + // Try to find the associated Gateway, if not fount, return the base + // notifier + gateway, ok, err := r.getGatewayFor(ctx, probe) + if err != nil { + return nil, err + } + if !ok { + logger.V(3).Info("no gateway associated to probe. Creating status update notifier") + return notifier, nil + } + + // Try to find the associated DNSRecord, if not found, return the base + // notifier + dnsRecord, ok, err := getDNSRecord(ctx, r.Client, probe) + if err != nil { + return nil, err + } + if !ok { + logger.V(3).Info("no DNSRecord associated to probe. Creating status update notifier") + return notifier, nil + } + + // Find the listener in the Gateway that matches the DNSRecord + listener, ok := slice.Find(gateway.Spec.Listeners, func(listener gatewayapiv1.Listener) bool { + dnsRecordName := fmt.Sprintf("%s-%s", gateway.Name, listener.Name) + return dnsRecord.Name == dnsRecordName + }) + if !ok { + return notifier, nil + } + + logger.V(3).Info("creating instrumented probe notifier for probe") + + // Wrap the base notifier with the instrumented one that updates metrics + return health.NewInstrumentedProbeNotifier( + gateway.Name, gateway.Namespace, string(listener.Name), + notifier, + ), nil +} + +func getDNSRecord(ctx context.Context, apiClient client.Client, obj metav1.Object) (*v1alpha1.DNSRecord, bool, error) { + if obj.GetAnnotations() == nil { + return nil, false, nil + } + + name, nameOk := obj.GetAnnotations()["dnsrecord-name"] + ns, nsOk := obj.GetAnnotations()["dnsrecord-namespace"] + + if !nameOk || !nsOk { + return nil, false, nil + } + + dnsRecord := &v1alpha1.DNSRecord{} + if err := apiClient.Get(ctx, client.ObjectKey{ + Name: name, + Namespace: ns, + }, dnsRecord); err != nil { + if k8serrors.IsNotFound(err) { + return nil, false, nil + } + + return nil, false, err + } + + return dnsRecord, true, nil +} diff --git a/internal/controller/dnsrecord_controller.go b/internal/controller/dnsrecord_controller.go index cb25b8aa..0b8bee88 100644 --- a/internal/controller/dnsrecord_controller.go +++ b/internal/controller/dnsrecord_controller.go @@ -18,45 +18,206 @@ package controller import ( "context" + "fmt" + "strings" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/clock" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - kuadrantiov1alpha1 "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/common/conditions" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" ) +const ( + DNSRecordFinalizer = "kuadrant.io/dns-record" +) + +var Clock clock.Clock = clock.RealClock{} + // DNSRecordReconciler reconciles a DNSRecord object type DNSRecordReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + ProviderFactory provider.Factory } //+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords/status,verbs=get;update;patch //+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords/finalizers,verbs=update -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the DNSRecord object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) - // TODO(user): your logic here + previous := &v1alpha1.DNSRecord{} + err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, previous) + if err != nil { + if err := client.IgnoreNotFound(err); err == nil { + return ctrl.Result{}, nil + } else { + return ctrl.Result{}, err + } + } + dnsRecord := previous.DeepCopy() + + log.Log.V(3).Info("DNSRecordReconciler Reconcile", "dnsRecord", dnsRecord) + + if dnsRecord.DeletionTimestamp != nil && !dnsRecord.DeletionTimestamp.IsZero() { + if err := r.deleteRecord(ctx, dnsRecord); err != nil { + log.Log.Error(err, "Failed to delete DNSRecord", "record", dnsRecord) + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(dnsRecord, DNSRecordFinalizer) + + err = r.Update(ctx, dnsRecord) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(dnsRecord, DNSRecordFinalizer) { + controllerutil.AddFinalizer(dnsRecord, DNSRecordFinalizer) + err = r.Update(ctx, dnsRecord) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } - return ctrl.Result{}, nil + var reason, message string + status := metav1.ConditionTrue + reason = "ProviderSuccess" + message = "Provider ensured the managed zone" + + // Publish the record + err = r.publishRecord(ctx, dnsRecord) + if err != nil { + status = metav1.ConditionFalse + reason = "ProviderError" + message = fmt.Sprintf("The DNS provider failed to ensure the record: %v", provider.SanitizeError(err)) + } else { + dnsRecord.Status.ObservedGeneration = dnsRecord.Generation + dnsRecord.Status.Endpoints = dnsRecord.Spec.Endpoints + } + setDNSRecordCondition(dnsRecord, string(conditions.ConditionTypeReady), status, reason, message) + + if !equality.Semantic.DeepEqual(previous.Status, dnsRecord.Status) { + updateErr := r.Status().Update(ctx, dnsRecord) + if updateErr != nil { + // Ignore conflicts, resource might just be outdated. + if apierrors.IsConflict(updateErr) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, updateErr + } + } + + return ctrl.Result{}, err } // SetupWithManager sets up the controller with the Manager. func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantiov1alpha1.DNSRecord{}). + For(&v1alpha1.DNSRecord{}). Complete(r) } + +// deleteRecord deletes record(s) in the DNSPRovider(i.e. route53) configured by the ManagedZone assigned to this +// DNSRecord (dnsRecord.Status.ParentManagedZone). +func (r *DNSRecordReconciler) deleteRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord) error { + managedZone := &v1alpha1.ManagedZone{ + ObjectMeta: metav1.ObjectMeta{ + Name: dnsRecord.Spec.ManagedZoneRef.Name, + Namespace: dnsRecord.Namespace, + }, + } + err := r.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone, &client.GetOptions{}) + if err != nil { + // If the Managed Zone isn't found, just continue + return client.IgnoreNotFound(err) + } + managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") + + if !managedZoneReady { + return fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) + } + + dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone) + if err != nil { + return err + } + + err = dnsProvider.Delete(dnsRecord, managedZone) + if err != nil { + if strings.Contains(err.Error(), "was not found") || strings.Contains(err.Error(), "notFound") { + log.Log.Info("Record not found in managed zone, continuing", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + return nil + } else if strings.Contains(err.Error(), "no endpoints") { + log.Log.Info("DNS record had no endpoint, continuing", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + return nil + } + return err + } + log.Log.Info("Deleted DNSRecord in manage zone", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + + return nil +} + +// publishRecord publishes record(s) to the DNSPRovider(i.e. route53) configured by the ManagedZone assigned to this +// DNSRecord (dnsRecord.Status.ParentManagedZone). +func (r *DNSRecordReconciler) publishRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord) error { + + managedZone := &v1alpha1.ManagedZone{ + ObjectMeta: metav1.ObjectMeta{ + Name: dnsRecord.Spec.ManagedZoneRef.Name, + Namespace: dnsRecord.Namespace, + }, + } + err := r.Get(ctx, client.ObjectKeyFromObject(managedZone), managedZone, &client.GetOptions{}) + if err != nil { + return err + } + managedZoneReady := meta.IsStatusConditionTrue(managedZone.Status.Conditions, "Ready") + + if !managedZoneReady { + return fmt.Errorf("the managed zone is not in a ready state : %s", managedZone.Name) + } + + if dnsRecord.Generation == dnsRecord.Status.ObservedGeneration { + log.Log.V(3).Info("Skipping managed zone to which the DNS dnsRecord is already published", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + return nil + } + dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone) + if err != nil { + return err + } + + err = dnsProvider.Ensure(dnsRecord, managedZone) + if err != nil { + return err + } + log.Log.Info("Published DNSRecord to manage zone", "dnsRecord", dnsRecord.Name, "managedZone", managedZone.Name) + + return nil +} + +// setDNSRecordCondition adds or updates a given condition in the DNSRecord status.. +func setDNSRecordCondition(dnsRecord *v1alpha1.DNSRecord, conditionType string, status metav1.ConditionStatus, reason, message string) { + cond := metav1.Condition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + ObservedGeneration: dnsRecord.Generation, + } + meta.SetStatusCondition(&dnsRecord.Status.Conditions, cond) +} diff --git a/internal/controller/managedzone_controller.go b/internal/controller/managedzone_controller.go index ed2e243f..17d7cf0d 100644 --- a/internal/controller/managedzone_controller.go +++ b/internal/controller/managedzone_controller.go @@ -18,45 +18,338 @@ package controller import ( "context" + "fmt" + "strings" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" - kuadrantiov1alpha1 "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/common/conditions" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +const ( + ManagedZoneFinalizer = "kuadrant.io/managed-zone" ) // ManagedZoneReconciler reconciles a ManagedZone object type ManagedZoneReconciler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + ProviderFactory provider.Factory } //+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones/status,verbs=get;update;patch //+kubebuilder:rbac:groups=kuadrant.io,resources=managedzones/finalizers,verbs=update -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the ManagedZone object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.0/pkg/reconcile func (r *ManagedZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) + previous := &v1alpha1.ManagedZone{} + err := r.Client.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, previous) + if err != nil { + if err := client.IgnoreNotFound(err); err == nil { + return ctrl.Result{}, nil + } else { + return ctrl.Result{}, err + } + } + managedZone := previous.DeepCopy() + + log.Log.V(3).Info("ManagedZoneReconciler Reconcile", "managedZone", managedZone) + + if managedZone.DeletionTimestamp != nil && !managedZone.DeletionTimestamp.IsZero() { + if err := r.deleteParentZoneNSRecord(ctx, managedZone); err != nil { + log.Log.Error(err, "Failed to delete parent Zone NS Record", "managedZone", managedZone) + return ctrl.Result{}, err + } + if err := r.deleteManagedZone(ctx, managedZone); err != nil { + log.Log.Error(err, "Failed to delete ManagedZone", "managedZone", managedZone) + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(managedZone, ManagedZoneFinalizer) + + err = r.Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + if !controllerutil.ContainsFinalizer(managedZone, ManagedZoneFinalizer) { + + controllerutil.AddFinalizer(managedZone, ManagedZoneFinalizer) + + err = r.setParentZoneOwner(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + + err = r.Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + } + + var reason, message string + status := metav1.ConditionTrue + reason = "ProviderSuccess" + message = "Provider ensured the managed zone" + + // Publish the managed zone + err = r.publishManagedZone(ctx, managedZone) + if err != nil { + status = metav1.ConditionFalse + reason = "ProviderError" + message = fmt.Sprintf("The DNS provider failed to ensure the managed zone: %v", err) + + err = r.Status().Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + } - // TODO(user): your logic here + // Create the parent zone NS record + err = r.createParentZoneNSRecord(ctx, managedZone) + if err != nil { + status = metav1.ConditionFalse + reason = "ParentZoneNSRecordError" + message = fmt.Sprintf("Failed to create the NS record in the parent managed zone: %v", err) + err = r.Status().Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + } + + // Check the parent zone NS record status + err = r.parentZoneNSRecordReady(ctx, managedZone) + if err != nil { + status = metav1.ConditionFalse + reason = "ParentZoneNSRecordNotReady" + message = fmt.Sprintf("NS Record ready status check failed: %v", err) + + err = r.Status().Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + } + + managedZone.Status.ObservedGeneration = managedZone.Generation + setManagedZoneCondition(managedZone, string(conditions.ConditionTypeReady), status, reason, message) + err = r.Status().Update(ctx, managedZone) + if err != nil { + return ctrl.Result{}, err + } + log.Log.Info("Reconciled ManagedZone", "managedZone", managedZone.Name) return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *ManagedZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&kuadrantiov1alpha1.ManagedZone{}). + For(&v1alpha1.ManagedZone{}). + Owns(&v1alpha1.ManagedZone{}). Complete(r) } + +func (r *ManagedZoneReconciler) publishManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + + dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone) + if err != nil { + return err + } + mzResp, err := dnsProvider.EnsureManagedZone(managedZone) + if err != nil { + return err + } + + managedZone.Status.ID = mzResp.ID + managedZone.Status.RecordCount = mzResp.RecordCount + managedZone.Status.NameServers = mzResp.NameServers + + return nil +} + +func (r *ManagedZoneReconciler) deleteManagedZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + if managedZone.Spec.ID != "" { + log.Log.Info("Skipping deletion of managed zone with provider ID specified in spec", "managedZone", managedZone.Name) + return nil + } + + dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone) + if err != nil { + var reason, message string + status := metav1.ConditionFalse + reason = "ProviderError" + message = fmt.Sprintf("The DNS provider creation failed: %v", err) + managedZone.Status.ObservedGeneration = managedZone.Generation + setManagedZoneCondition(managedZone, string(conditions.ConditionTypeReady), status, reason, message) + return err + } + err = dnsProvider.DeleteManagedZone(managedZone) + if err != nil { + if strings.Contains(err.Error(), "was not found") || strings.Contains(err.Error(), "notFound") { + log.Log.Info("ManagedZone was not found, continuing", "managedZone", managedZone.Name) + return nil + } + return err + } + log.Log.Info("Deleted ManagedZone", "managedZone", managedZone.Name) + + return nil +} + +func (r *ManagedZoneReconciler) getParentZone(ctx context.Context, managedZone *v1alpha1.ManagedZone) (*v1alpha1.ManagedZone, error) { + if managedZone.Spec.ParentManagedZone == nil { + return nil, nil + } + parentZone := &v1alpha1.ManagedZone{} + err := r.Client.Get(ctx, client.ObjectKey{Namespace: managedZone.Namespace, Name: managedZone.Spec.ParentManagedZone.Name}, parentZone) + if err != nil { + return parentZone, err + } + return parentZone, nil +} + +func (r *ManagedZoneReconciler) setParentZoneOwner(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + parentZone, err := r.getParentZone(ctx, managedZone) + if err != nil { + return err + } + if parentZone == nil { + return nil + } + + err = controllerutil.SetControllerReference(parentZone, managedZone, r.Scheme) + if err != nil { + return err + } + + return err +} + +func (r *ManagedZoneReconciler) createParentZoneNSRecord(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + parentZone, err := r.getParentZone(ctx, managedZone) + if err != nil { + return err + } + if parentZone == nil { + return nil + } + + recordName := managedZone.Spec.DomainName + //Ensure NS record is created in parent managed zone if one is set + recordTargets := make([]string, len(managedZone.Status.NameServers)) + for index := range managedZone.Status.NameServers { + recordTargets[index] = *managedZone.Status.NameServers[index] + } + recordType := string(v1alpha1.NSRecordType) + + nsRecord := &v1alpha1.DNSRecord{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: recordName, + Namespace: parentZone.Namespace, + }, + Spec: v1alpha1.DNSRecordSpec{ + ManagedZoneRef: &v1alpha1.ManagedZoneReference{ + Name: parentZone.Name, + }, + Endpoints: []*v1alpha1.Endpoint{ + { + DNSName: recordName, + Targets: recordTargets, + RecordType: recordType, + RecordTTL: 172800, + }, + }, + }, + } + err = controllerutil.SetControllerReference(parentZone, nsRecord, r.Scheme) + if err != nil { + return err + } + err = r.Client.Create(ctx, nsRecord, &client.CreateOptions{}) + if err != nil && !k8serrors.IsAlreadyExists(err) { + return err + } + + return nil +} + +func (r *ManagedZoneReconciler) deleteParentZoneNSRecord(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + parentZone, err := r.getParentZone(ctx, managedZone) + if err := client.IgnoreNotFound(err); err != nil { + return err + } + if parentZone == nil { + return nil + } + + recordName := managedZone.Spec.DomainName + + nsRecord := &v1alpha1.DNSRecord{} + err = r.Client.Get(ctx, client.ObjectKey{Namespace: parentZone.Namespace, Name: recordName}, nsRecord) + if err != nil { + if err := client.IgnoreNotFound(err); err == nil { + return nil + } else { + return err + } + } + + err = r.Client.Delete(ctx, nsRecord, &client.DeleteOptions{}) + if err != nil { + return err + } + + return nil +} + +func (r *ManagedZoneReconciler) parentZoneNSRecordReady(ctx context.Context, managedZone *v1alpha1.ManagedZone) error { + parentZone, err := r.getParentZone(ctx, managedZone) + if err := client.IgnoreNotFound(err); err != nil { + return err + } + if parentZone == nil { + return nil + } + + recordName := managedZone.Spec.DomainName + + nsRecord := &v1alpha1.DNSRecord{} + err = r.Client.Get(ctx, client.ObjectKey{Namespace: parentZone.Namespace, Name: recordName}, nsRecord) + if err != nil { + if err := client.IgnoreNotFound(err); err == nil { + return nil + } else { + return err + } + } + + nsRecordReady := meta.IsStatusConditionTrue(nsRecord.Status.Conditions, string(conditions.ConditionTypeReady)) + if !nsRecordReady { + return fmt.Errorf("the ns record is not in a ready state : %s", nsRecord.Name) + } + return nil +} + +// setManagedZoneCondition adds or updates a given condition in the ManagedZone status. +func setManagedZoneCondition(managedZone *v1alpha1.ManagedZone, conditionType string, status metav1.ConditionStatus, reason, message string) { + cond := metav1.Condition{ + Type: conditionType, + Status: status, + Reason: reason, + Message: message, + ObservedGeneration: managedZone.Generation, + } + meta.SetStatusCondition(&managedZone.Status.Conditions, cond) +} diff --git a/internal/health/metrics.go b/internal/health/metrics.go new file mode 100644 index 00000000..3a91092c --- /dev/null +++ b/internal/health/metrics.go @@ -0,0 +1,61 @@ +package health + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +var ( + healthCheckAttempts = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mgc_dns_health_check_attempts_total", + Help: "MGC DNS Health Check Probe total number of attempts", + }, + []string{"gateway_name", "gateway_namespace", "listener"}, + ) + + healthCheckFailures = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mgc_dns_health_check_failures_total", + Help: "MGC DNS Health Check Probe total number of failures", + }, + []string{"gateway_name", "gateway_namespace", "listener"}, + ) +) + +func init() { + metrics.Registry.MustRegister( + healthCheckAttempts, + healthCheckFailures, + ) +} + +// InstrumentedProbeNotifier wraps a notifier by incrementing the failure counter +// when the result is unhealthy +type InstrumentedProbeNotifier struct { + gatewayName, gatewayNamespace, listener string + notifier ProbeNotifier +} + +func NewInstrumentedProbeNotifier(gatewayName, gatewayNamespace, listener string, notifier ProbeNotifier) *InstrumentedProbeNotifier { + return &InstrumentedProbeNotifier{ + gatewayName: gatewayName, + gatewayNamespace: gatewayNamespace, + listener: listener, + notifier: notifier, + } +} + +func (n *InstrumentedProbeNotifier) Notify(ctx context.Context, result ProbeResult) (NotificationResult, error) { + healthCheckAttempts.WithLabelValues(n.gatewayName, n.gatewayNamespace, n.listener).Inc() + if !result.Healthy { + healthCheckFailures.WithLabelValues(n.gatewayName, n.gatewayNamespace, n.listener).Inc() + } + + return n.notifier.Notify(ctx, result) +} + +var _ ProbeNotifier = &InstrumentedProbeNotifier{} diff --git a/internal/health/monitor.go b/internal/health/monitor.go new file mode 100644 index 00000000..f76aa362 --- /dev/null +++ b/internal/health/monitor.go @@ -0,0 +1,96 @@ +package health + +import ( + "context" + "sync" + + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type Monitor struct { + ProbeQueuers []*ProbeQueuer + + mux sync.Mutex +} + +func NewMonitor() *Monitor { + return &Monitor{ + ProbeQueuers: make([]*ProbeQueuer, 0), + } +} + +func (m *Monitor) Start(ctx context.Context) error { + logger := log.FromContext(ctx) + logger.V(3).Info("Starting health check monitor") + + <-ctx.Done() + m.mux.Lock() + defer m.mux.Unlock() + + logger.Info("Stopping health check monitor") + + for _, probeQueuer := range m.ProbeQueuers { + probeQueuer.Stop() + } + + return nil +} + +var _ manager.Runnable = &Monitor{} + +func (m *Monitor) HasProbe(id string) bool { + m.mux.Lock() + defer m.mux.Unlock() + + for _, probeQueuer := range m.ProbeQueuers { + if probeQueuer.ID == id { + return true + } + } + + return false +} + +func (m *Monitor) UpdateProbe(id string, update func(*ProbeQueuer)) { + m.mux.Lock() + defer m.mux.Unlock() + + for _, probeQueuer := range m.ProbeQueuers { + if probeQueuer.ID == id { + update(probeQueuer) + } + } +} + +func (m *Monitor) AddProbeQueuer(probeQueuer *ProbeQueuer) bool { + m.mux.Lock() + defer m.mux.Unlock() + + for _, existingProbe := range m.ProbeQueuers { + if probeQueuer.ID == existingProbe.ID { + return false + } + } + + m.ProbeQueuers = append(m.ProbeQueuers, probeQueuer) + probeQueuer.Start() + return true +} + +func (m *Monitor) RemoveProbe(id string) { + m.mux.Lock() + defer m.mux.Unlock() + + updatedProbes := []*ProbeQueuer{} + + for _, probeQueuer := range m.ProbeQueuers { + if probeQueuer.ID == id { + probeQueuer.Stop() + } else { + updatedProbes = append(updatedProbes, probeQueuer) + } + } + + m.ProbeQueuers = updatedProbes +} diff --git a/internal/health/notifier.go b/internal/health/notifier.go new file mode 100644 index 00000000..bddb8816 --- /dev/null +++ b/internal/health/notifier.go @@ -0,0 +1,67 @@ +package health + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +) + +type StatusUpdateProbeNotifier struct { + apiClient client.Client + probeObjKey client.ObjectKey +} + +var _ ProbeNotifier = StatusUpdateProbeNotifier{} + +func NewStatusUpdateProbeNotifier(apiClient client.Client, forObj *v1alpha1.DNSHealthCheckProbe) StatusUpdateProbeNotifier { + return StatusUpdateProbeNotifier{ + apiClient: apiClient, + probeObjKey: client.ObjectKeyFromObject(forObj), + } +} + +func (n StatusUpdateProbeNotifier) Notify(ctx context.Context, result ProbeResult) (NotificationResult, error) { + probeObj := &v1alpha1.DNSHealthCheckProbe{} + if err := n.apiClient.Get(ctx, n.probeObjKey, probeObj); err != nil { + return NotificationResult{}, err + } + + // Increase the number of consecutive failures if it failed previously + if !result.Healthy { + probeHealthy := true + if probeObj.Status.Healthy != nil { + probeHealthy = *probeObj.Status.Healthy + } + if probeHealthy { + probeObj.Status.ConsecutiveFailures = 1 + } else { + probeObj.Status.ConsecutiveFailures++ + } + } else { + probeObj.Status.ConsecutiveFailures = 0 + } + + probeObj.Status.LastCheckedAt = metav1.NewTime(result.CheckedAt) + if probeObj.Status.Healthy == nil { + probeObj.Status.Healthy = aws.Bool(true) + } + probeObj.Status.Healthy = &result.Healthy + probeObj.Status.Reason = result.Reason + probeObj.Status.Status = result.Status + + if err := n.apiClient.Status().Update(ctx, probeObj); err != nil { + if errors.IsConflict(err) { + return NotificationResult{Requeue: true}, nil + } + + return NotificationResult{}, err + } + + return NotificationResult{}, nil +} diff --git a/internal/health/probeQueuer.go b/internal/health/probeQueuer.go new file mode 100644 index 00000000..4e447152 --- /dev/null +++ b/internal/health/probeQueuer.go @@ -0,0 +1,92 @@ +package health + +import ( + "context" + "time" + + "github.com/go-logr/logr" + + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +) + +type ProbeQueuer struct { + ID string + + Interval time.Duration + Protocol v1alpha1.HealthProtocol + Path string + IPAddress string + Host string + Port int + AdditionalHeaders v1alpha1.AdditionalHeaders + ExpectedResponses []int + AllowInsecureCertificate bool + + Notifier ProbeNotifier + Queue *QueuedProbeWorker + + cancel context.CancelFunc + started bool + logger logr.Logger +} + +type ProbeResult struct { + CheckedAt time.Time + Reason string + Status int + Healthy bool +} + +type ProbeNotifier interface { + Notify(ctx context.Context, result ProbeResult) (NotificationResult, error) +} + +type NotificationResult struct { + Requeue bool +} + +func (p *ProbeQueuer) Start() { + if p.started { + return + } + + ctx, cancel := context.WithCancel(context.Background()) + p.cancel = cancel + p.logger = log.FromContext(ctx) + + p.logger.V(3).Info("Starting probe queuer", "id", p.ID) + + go func() { + for { + select { + case <-time.After(p.Interval): + p.Queue.EnqueueCheck(HealthRequest{ + Host: p.Host, + Path: p.Path, + Protocol: p.Protocol, + Address: p.IPAddress, + Port: p.Port, + AdditionalHeaders: p.AdditionalHeaders, + ExpectedResponses: p.ExpectedResponses, + Notifier: p.Notifier, + AllowInsecureCertificate: p.AllowInsecureCertificate, + }) + case <-ctx.Done(): + return + } + } + }() + + p.started = true +} + +func (p *ProbeQueuer) Stop() { + if !p.started { + return + } + + p.logger.V(3).Info("stopping probe", "id", p.ID) + p.cancel() +} diff --git a/internal/health/queuedProbeWorker.go b/internal/health/queuedProbeWorker.go new file mode 100644 index 00000000..ea061fb6 --- /dev/null +++ b/internal/health/queuedProbeWorker.go @@ -0,0 +1,217 @@ +package health + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "runtime" + "sync" + "time" + + "github.com/go-logr/logr" + + utilnet "k8s.io/apimachinery/pkg/util/net" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +) + +// QueuedProbeWorker funnels incoming health check requests from health probes, +// processing them one at a time and spacing them by a specified duration +type QueuedProbeWorker struct { + Throttle time.Duration + + requests []HealthRequest + logger logr.Logger + + mux sync.Mutex +} + +func NewRequestQueue(throttle time.Duration) *QueuedProbeWorker { + return &QueuedProbeWorker{ + Throttle: throttle, + requests: make([]HealthRequest, 0), + } +} + +type HealthRequest struct { + Host, Path, Address string + Protocol v1alpha1.HealthProtocol + Port int + AdditionalHeaders v1alpha1.AdditionalHeaders + ExpectedResponses []int + AllowInsecureCertificate bool + Notifier ProbeNotifier +} + +func (q *QueuedProbeWorker) EnqueueCheck(req HealthRequest) { + q.mux.Lock() + defer q.mux.Unlock() + + q.logger.V(3).Info("enqueueing health check", "request", req) + q.requests = append(q.requests, req) +} + +// deqeue takes the next element of the queue and returns it. It blocks +// if the queue is empty, and returns false if the context is cancelled +func (q *QueuedProbeWorker) dequeue(ctx context.Context) (HealthRequest, bool) { + reqChn := make(chan HealthRequest) + + go func() { + for { + select { + case <-ctx.Done(): + close(reqChn) + return + default: + } + + q.mux.Lock() + if len(q.requests) == 0 { + q.mux.Unlock() + runtime.Gosched() + continue + } + + req := q.requests[0] + q.requests = q.requests[1:] + q.mux.Unlock() + + reqChn <- req + return + } + + }() + + req, ok := <-reqChn + return req, ok +} + +func (q *QueuedProbeWorker) Start(ctx context.Context) error { + q.logger = log.FromContext(ctx) + q.logger.V(3).Info("Starting health check queue") + defer q.logger.Info("Stopping health check queue") + + for { + select { + case <-ctx.Done(): + if ctx.Err() != context.Canceled { + return ctx.Err() + } + return nil + case <-time.After(q.Throttle): + q.logger.V(3).Info("dequeing health check") + req, ok := q.dequeue(ctx) + if !ok { + return nil + } + + q.process(ctx, req) + } + } +} + +func (q *QueuedProbeWorker) process(ctx context.Context, req HealthRequest) { + go func() { + result := q.performRequest(ctx, req) + notificationResult, err := req.Notifier.Notify(ctx, result) + if err != nil { + q.logger.Error(err, "failed to notify health check result") + } + + if notificationResult.Requeue { + q.EnqueueCheck(req) + } + }() +} + +func (q *QueuedProbeWorker) performRequest(ctx context.Context, req HealthRequest) ProbeResult { + q.logger.V(3).Info("performing health check", "request", req) + + probeClient := &http.Client{ + Transport: TransportWithDNSResponse(map[string]string{req.Host: req.Address}), + } + + if req.AllowInsecureCertificate { + probeClient.Transport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + // Default port to 80 + port := 80 + if req.Port != 0 { + port = req.Port + } + + // Build the http request + httpReq, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s://%s:%d%s", req.Protocol.ToScheme(), req.Host, port, req.Path), nil) + if err != nil { + return ProbeResult{CheckedAt: time.Now(), Healthy: false, Reason: err.Error()} + } + + // add any user-defined additional headers + for _, h := range req.AdditionalHeaders { + httpReq.Header.Add(h.Name, h.Value) + } + + // Send the request + res, err := probeClient.Do(httpReq) + if utilnet.IsConnectionReset(err) { + res = &http.Response{StatusCode: 104} + } else if err != nil { + return ProbeResult{CheckedAt: time.Now(), Healthy: false, Reason: fmt.Sprintf("error: %s, response: %+v", err.Error(), res)} + } + + // Create the result based on the response + if req.ExpectedResponses == nil { + req.ExpectedResponses = []int{200, 201} + } + healthy := true + reason := "" + + if !checkResponse(res.StatusCode, req.ExpectedResponses) { + healthy = false + reason = fmt.Sprintf("Status code: %d", res.StatusCode) + } + + return ProbeResult{ + CheckedAt: time.Now(), + Healthy: healthy, + Status: res.StatusCode, + Reason: reason, + } +} + +func checkResponse(response int, expected []int) bool { + for _, i := range expected { + if response == i { + return true + } + } + return false +} + +// TransportWithDNSResponse creates a new transport which overrides hostnames. +func TransportWithDNSResponse(overrides map[string]string) http.RoundTripper { + transport := http.DefaultTransport.(*http.Transport).Clone() + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + + transport.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + newHost, ok := overrides[host] + if !ok { + return dialer.DialContext(ctx, network, address) + } + overrideAddress := net.JoinHostPort(newHost, port) + return dialer.DialContext(ctx, network, overrideAddress) + } + + return transport +} diff --git a/internal/provider/aws/aws.go b/internal/provider/aws/aws.go new file mode 100644 index 00000000..6012dd04 --- /dev/null +++ b/internal/provider/aws/aws.go @@ -0,0 +1,328 @@ +/* +Copyright 2024. + +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 aws + +import ( + "context" + "fmt" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/route53" + "github.com/go-logr/logr" + + v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +const ( + ProviderSpecificRegion = "aws/region" + ProviderSpecificFailover = "aws/failover" + ProviderSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code" + ProviderSpecificMultiValueAnswer = "aws/multi-value-answer" + ProviderSpecificHealthCheckID = "aws/health-check-id" +) + +type Route53DNSProvider struct { + client *InstrumentedRoute53 + logger logr.Logger + ctx context.Context +} + +var _ provider.Provider = &Route53DNSProvider{} + +func NewProviderFromSecret(ctx context.Context, s *v1.Secret) (provider.Provider, error) { + + config := aws.NewConfig() + sessionOpts := session.Options{ + Config: *config, + } + if string(s.Data["AWS_ACCESS_KEY_ID"]) == "" || string(s.Data["AWS_SECRET_ACCESS_KEY"]) == "" { + return nil, fmt.Errorf("AWS Provider credentials is empty") + } + + sessionOpts.Config.Credentials = credentials.NewStaticCredentials(string(s.Data["AWS_ACCESS_KEY_ID"]), string(s.Data["AWS_SECRET_ACCESS_KEY"]), "") + sessionOpts.SharedConfigState = session.SharedConfigDisable + sess, err := session.NewSessionWithOptions(sessionOpts) + if err != nil { + return nil, fmt.Errorf("unable to create aws session: %s", err) + } + if string(s.Data["REGION"]) != "" { + sess.Config.WithRegion(string(s.Data["REGION"])) + } + + p := &Route53DNSProvider{ + client: &InstrumentedRoute53{route53.New(sess, config)}, + logger: log.Log.WithName("aws-route53").WithValues("region", config.Region), + ctx: ctx, + } + + if err := validateServiceEndpoints(p); err != nil { + return nil, fmt.Errorf("failed to validate AWS provider service endpoints: %v", err) + } + + return p, nil +} + +type action string + +const ( + upsertAction action = "UPSERT" + deleteAction action = "DELETE" +) + +func (p *Route53DNSProvider) Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return p.change(record, managedZone, upsertAction) +} + +func (p *Route53DNSProvider) Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return p.change(record, managedZone, deleteAction) +} + +func (p *Route53DNSProvider) EnsureManagedZone(zone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + var zoneID string + if zone.Spec.ID != "" { + zoneID = zone.Spec.ID + } else { + zoneID = zone.Status.ID + } + + var managedZoneOutput provider.ManagedZoneOutput + + if zoneID != "" { + getResp, err := p.client.GetHostedZone(&route53.GetHostedZoneInput{ + Id: &zoneID, + }) + if err != nil { + log.Log.Error(err, "failed to get hosted zone") + return managedZoneOutput, err + } + + _, err = p.client.UpdateHostedZoneComment(&route53.UpdateHostedZoneCommentInput{ + Comment: &zone.Spec.Description, + Id: &zoneID, + }) + if err != nil { + log.Log.Error(err, "failed to update hosted zone comment") + } + + managedZoneOutput.ID = *getResp.HostedZone.Id + managedZoneOutput.RecordCount = *getResp.HostedZone.ResourceRecordSetCount + managedZoneOutput.NameServers = getResp.DelegationSet.NameServers + + return managedZoneOutput, nil + } + + //ToDo callerRef must be unique, but this can cause duplicates if the status can't be written back during a + //reconciliation that successfully created a new hosted zone i.e. the object has been modified; please apply your + //changes to the latest version and try again + callerRef := time.Now().Format("20060102150405") + // Create the hosted zone + createResp, err := p.client.CreateHostedZone(&route53.CreateHostedZoneInput{ + CallerReference: &callerRef, + Name: &zone.Spec.DomainName, + HostedZoneConfig: &route53.HostedZoneConfig{ + Comment: &zone.Spec.Description, + PrivateZone: aws.Bool(false), + }, + }) + if err != nil { + log.Log.Error(err, "failed to create hosted zone") + return managedZoneOutput, err + } + managedZoneOutput.ID = *createResp.HostedZone.Id + managedZoneOutput.RecordCount = *createResp.HostedZone.ResourceRecordSetCount + managedZoneOutput.NameServers = createResp.DelegationSet.NameServers + return managedZoneOutput, nil +} + +func (p *Route53DNSProvider) DeleteManagedZone(zone *v1alpha1.ManagedZone) error { + _, err := p.client.DeleteHostedZone(&route53.DeleteHostedZoneInput{ + Id: &zone.Status.ID, + }) + if err != nil { + log.Log.Error(err, "failed to delete hosted zone") + return err + } + return nil +} + +func (p *Route53DNSProvider) change(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone, action action) error { + // Configure records. + if len(record.Spec.Endpoints) == 0 { + return nil + } + err := p.updateRecord(record, managedZone.Status.ID, string(action)) + if err != nil { + return fmt.Errorf("failed to update record in route53 hosted zone %s: %v", managedZone.Status.ID, err) + } + switch action { + case upsertAction: + p.logger.Info("Upserted DNS record", "record", record.Spec, "hostedZoneID", managedZone.Status.ID) + case deleteAction: + p.logger.Info("Deleted DNS record", "record", record.Spec, "hostedZoneID", managedZone.Status.ID) + } + return nil +} + +func (p *Route53DNSProvider) updateRecord(record *v1alpha1.DNSRecord, zoneID, action string) error { + + if len(record.Spec.Endpoints) == 0 { + return fmt.Errorf("no endpoints") + } + + input := route53.ChangeResourceRecordSetsInput{HostedZoneId: aws.String(zoneID)} + + expectedEndpointsMap := make(map[string]struct{}) + var changes []*route53.Change + for _, endpoint := range record.Spec.Endpoints { + expectedEndpointsMap[endpoint.SetID()] = struct{}{} + change, err := p.changeForEndpoint(endpoint, action) + if err != nil { + return err + } + changes = append(changes, change) + } + + // Delete any previously published records that are no longer present in record.Spec.Endpoints + if action != string(deleteAction) { + lastPublishedEndpoints := record.Status.Endpoints + for _, endpoint := range lastPublishedEndpoints { + if _, found := expectedEndpointsMap[endpoint.SetID()]; !found { + change, err := p.changeForEndpoint(endpoint, string(deleteAction)) + if err != nil { + return err + } + changes = append(changes, change) + } + } + } + + if len(changes) == 0 { + return nil + } + input.ChangeBatch = &route53.ChangeBatch{ + Changes: changes, + } + resp, err := p.client.ChangeResourceRecordSets(&input) + if err != nil { + return fmt.Errorf("couldn't update DNS record %s in zone %s: %v", record.Name, zoneID, err) + } + p.logger.Info("Updated DNS record", "record", record, "zone", zoneID, "response", resp) + return nil +} + +func (p *Route53DNSProvider) changeForEndpoint(endpoint *v1alpha1.Endpoint, action string) (*route53.Change, error) { + if endpoint.RecordType != string(v1alpha1.ARecordType) && endpoint.RecordType != string(v1alpha1.CNAMERecordType) && endpoint.RecordType != string(v1alpha1.NSRecordType) { + return nil, fmt.Errorf("unsupported record type %s", endpoint.RecordType) + } + domain, targets := endpoint.DNSName, endpoint.Targets + if len(domain) == 0 { + return nil, fmt.Errorf("domain is required") + } + if len(targets) == 0 { + return nil, fmt.Errorf("targets is required") + } + + var resourceRecords []*route53.ResourceRecord + for _, target := range endpoint.Targets { + resourceRecords = append(resourceRecords, &route53.ResourceRecord{Value: aws.String(target)}) + } + + resourceRecordSet := &route53.ResourceRecordSet{ + Name: aws.String(endpoint.DNSName), + Type: aws.String(endpoint.RecordType), + TTL: aws.Int64(int64(endpoint.RecordTTL)), + ResourceRecords: resourceRecords, + } + + if endpoint.SetIdentifier != "" { + resourceRecordSet.SetIdentifier = aws.String(endpoint.SetIdentifier) + } + if prop, ok := endpoint.GetProviderSpecificProperty(provider.ProviderSpecificWeight); ok { + weight, err := strconv.ParseInt(prop.Value, 10, 64) + if err != nil { + p.logger.Error(err, "Failed parsing value, using weight of 0", "weight", provider.ProviderSpecificWeight, "value", prop.Value) + weight = 0 + } + resourceRecordSet.Weight = aws.Int64(weight) + } + if prop, ok := endpoint.GetProviderSpecificProperty(ProviderSpecificRegion); ok { + resourceRecordSet.Region = aws.String(prop.Value) + } + if prop, ok := endpoint.GetProviderSpecificProperty(ProviderSpecificFailover); ok { + resourceRecordSet.Failover = aws.String(prop.Value) + } + if _, ok := endpoint.GetProviderSpecificProperty(ProviderSpecificMultiValueAnswer); ok { + resourceRecordSet.MultiValueAnswer = aws.Bool(true) + } + + var geolocation = &route53.GeoLocation{} + useGeolocation := false + + if prop, ok := endpoint.GetProviderSpecificProperty(provider.ProviderSpecificGeoCode); ok { + if provider.IsISO3166Alpha2Code(prop.Value) || prop.Value == "*" { + geolocation.CountryCode = aws.String(prop.Value) + } else { + geolocation.ContinentCode = aws.String(prop.Value) + } + useGeolocation = true + } + + if geolocation.ContinentCode == nil { + if prop, ok := endpoint.GetProviderSpecificProperty(ProviderSpecificGeolocationSubdivisionCode); ok { + geolocation.SubdivisionCode = aws.String(prop.Value) + useGeolocation = true + } + } + if useGeolocation { + resourceRecordSet.GeoLocation = geolocation + } + + if prop, ok := endpoint.GetProviderSpecificProperty(ProviderSpecificHealthCheckID); ok { + resourceRecordSet.HealthCheckId = aws.String(prop.Value) + } + + change := &route53.Change{ + Action: aws.String(action), + ResourceRecordSet: resourceRecordSet, + } + return change, nil +} + +// validateServiceEndpoints validates that provider clients can communicate with +// associated API endpoints by having each client make a list/describe/get call. +func validateServiceEndpoints(provider *Route53DNSProvider) error { + var errs []error + zoneInput := route53.ListHostedZonesInput{MaxItems: aws.String("1")} + if _, err := provider.client.ListHostedZones(&zoneInput); err != nil { + errs = append(errs, fmt.Errorf("failed to list route53 hosted zones: %v", err)) + } + return kerrors.NewAggregate(errs) +} + +// Register this Provider with the provider factory +func init() { + provider.RegisterProvider("aws", NewProviderFromSecret) +} diff --git a/internal/provider/aws/client.go b/internal/provider/aws/client.go new file mode 100644 index 00000000..d5261a31 --- /dev/null +++ b/internal/provider/aws/client.go @@ -0,0 +1,118 @@ +/* +Copyright 2024. + +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 aws + +import ( + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/route53" +) + +type InstrumentedRoute53 struct { + route53 *route53.Route53 +} + +func observe(operation string, f func() error) { + start := time.Now() + route53RequestCount.WithLabelValues(operation).Inc() + defer route53RequestCount.WithLabelValues(operation).Dec() + err := f() + duration := time.Since(start).Seconds() + code := returnCodeLabelDefault + if err != nil { + route53RequestErrors.WithLabelValues(operation, code).Inc() + if awsErr, ok := err.(awserr.Error); ok { + if reqErr, ok := err.(awserr.RequestFailure); ok { + // A service error occurred + code = strconv.Itoa(reqErr.StatusCode()) + } else { + // Generic AWS Error with Code, Message, and original error (if any) + code = awsErr.Code() + } + } + } + route53RequestDuration.WithLabelValues(operation, code).Observe(duration) + route53RequestTotal.WithLabelValues(operation, code).Inc() +} + +func (c *InstrumentedRoute53) ListHostedZones(input *route53.ListHostedZonesInput) (output *route53.ListHostedZonesOutput, err error) { + observe("ListHostedZones", func() error { + output, err = c.route53.ListHostedZones(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) ChangeResourceRecordSets(input *route53.ChangeResourceRecordSetsInput) (output *route53.ChangeResourceRecordSetsOutput, err error) { + observe("ChangeResourceRecordSets", func() error { + output, err = c.route53.ChangeResourceRecordSets(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) CreateHealthCheck(input *route53.CreateHealthCheckInput) (output *route53.CreateHealthCheckOutput, err error) { + observe("CreateHealthCheck", func() error { + output, err = c.route53.CreateHealthCheck(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) GetHostedZone(input *route53.GetHostedZoneInput) (output *route53.GetHostedZoneOutput, err error) { + observe("GetHostedZone", func() error { + output, err = c.route53.GetHostedZone(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) UpdateHostedZoneComment(input *route53.UpdateHostedZoneCommentInput) (output *route53.UpdateHostedZoneCommentOutput, err error) { + observe("UpdateHostedZoneComment", func() error { + output, err = c.route53.UpdateHostedZoneComment(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) CreateHostedZone(input *route53.CreateHostedZoneInput) (output *route53.CreateHostedZoneOutput, err error) { + observe("CreateHostedZone", func() error { + output, err = c.route53.CreateHostedZone(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) DeleteHostedZone(input *route53.DeleteHostedZoneInput) (output *route53.DeleteHostedZoneOutput, err error) { + observe("DeleteHostedZone", func() error { + output, err = c.route53.DeleteHostedZone(input) + return err + }) + return +} + +func (c *InstrumentedRoute53) ChangeTagsForResourceWithContext(ctx aws.Context, input *route53.ChangeTagsForResourceInput, opts ...request.Option) (output *route53.ChangeTagsForResourceOutput, err error) { + observe("ChangeTagsForResourceWithContext", func() error { + output, err = c.route53.ChangeTagsForResourceWithContext(ctx, input, opts...) + return err + }) + return +} diff --git a/internal/provider/aws/metrics.go b/internal/provider/aws/metrics.go new file mode 100644 index 00000000..9861ac60 --- /dev/null +++ b/internal/provider/aws/metrics.go @@ -0,0 +1,105 @@ +/* +Copyright 2024. + +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 aws + +import ( + "reflect" + + "github.com/prometheus/client_golang/prometheus" + + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +const ( + operationLabel = "operation" + returnCodeLabel = "code" + // The default return code + returnCodeLabelDefault = "" +) + +var ( + // route53RequestCount is a prometheus metric which holds the number of + // concurrent inflight requests to Route53. + route53RequestCount = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "mgc_aws_route53_inflight_request_count", + Help: "MGC AWS Route53 inflight request count", + }, + []string{operationLabel}, + ) + + // route53RequestTotal is a prometheus counter metrics which holds the total + // number of requests to Route53. + route53RequestTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mgc_aws_route53_request_total", + Help: "MGC AWS Route53 total number of requests", + }, + []string{operationLabel, returnCodeLabel}, + ) + + // route53RequestErrors is a prometheus counter metrics which holds the total + // number of failed requests to Route53. + route53RequestErrors = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mgc_aws_route53_request_errors_total", + Help: "MGC AWS Route53 total number of errors", + }, + []string{operationLabel, returnCodeLabel}, + ) + + // route53RequestDuration is a prometheus metric which records the duration + // of the requests to Route53. + route53RequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "mgc_aws_route53_request_duration_seconds", + Help: "MGC AWS Route53 request duration", + Buckets: []float64{ + 0.005, 0.01, 0.025, 0.05, 0.1, + 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45, + 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, + 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, + 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 40, 50, 60, + }, + }, + []string{operationLabel, returnCodeLabel}, + ) +) + +var operationLabelValues []string + +func init() { + // Register metrics into the global prometheus registry + metrics.Registry.MustRegister( + route53RequestCount, + route53RequestTotal, + route53RequestErrors, + route53RequestDuration, + ) + + monitoredRoute53 := reflect.PtrTo(reflect.TypeOf(InstrumentedRoute53{})) + for i := 0; i < monitoredRoute53.NumMethod(); i++ { + operationLabelValues = append(operationLabelValues, monitoredRoute53.Method(i).Name) + } + + // Initialize metrics + for _, operation := range operationLabelValues { + route53RequestCount.WithLabelValues(operation).Set(0) + route53RequestTotal.WithLabelValues(operation, returnCodeLabelDefault).Add(0) + route53RequestErrors.WithLabelValues(operation, returnCodeLabelDefault).Add(0) + } +} diff --git a/internal/provider/factory.go b/internal/provider/factory.go new file mode 100644 index 00000000..2ba1c672 --- /dev/null +++ b/internal/provider/factory.go @@ -0,0 +1,85 @@ +package provider + +import ( + "context" + "fmt" + "sync" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +) + +var errUnsupportedProvider = fmt.Errorf("provider type given is not supported") + +// ProviderConstructor constructs a provider given a Secret resource and a Context. +// An error will be returned if the appropriate provider is not registered. +type ProviderConstructor func(context.Context, *v1.Secret) (Provider, error) + +var ( + constructors = make(map[string]ProviderConstructor) + constructorsLock sync.RWMutex +) + +// RegisterProvider will register a provider constructor, so it can be used within the application. +// 'name' should be unique, and should be used to identify this provider. +func RegisterProvider(name string, c ProviderConstructor) { + constructorsLock.Lock() + defer constructorsLock.Unlock() + constructors[name] = c +} + +// Factory is an interface that can be used to obtain Provider implementations. +// It determines which provider implementation to use by introspecting the given ProviderAccessor resource. +type Factory interface { + ProviderFor(context.Context, v1alpha1.ProviderAccessor) (Provider, error) +} + +// factory is the default Factory implementation +type factory struct { + client.Client +} + +// NewFactory returns a new provider factory with the given client. +func NewFactory(c client.Client) Factory { + return &factory{Client: c} +} + +// ProviderFor will return a Provider interface for the given ProviderAccessor secret. +// If the requested ProviderAccessor secret does not exist, an error will be returned. +func (f *factory) ProviderFor(ctx context.Context, pa v1alpha1.ProviderAccessor) (Provider, error) { + providerSecret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pa.GetProviderRef().Name, + Namespace: pa.GetNamespace(), + }} + + if err := f.Client.Get(ctx, client.ObjectKeyFromObject(providerSecret), providerSecret); err != nil { + return nil, err + } + + providerType, err := nameForProviderSecret(providerSecret) + if err != nil { + return nil, err + } + + constructorsLock.RLock() + defer constructorsLock.RUnlock() + if constructor, ok := constructors[providerType]; ok { + return constructor(ctx, providerSecret) + } + + return nil, fmt.Errorf("provider '%s' not registered", providerType) +} + +func nameForProviderSecret(secret *v1.Secret) (string, error) { + switch secret.Type { + case "kuadrant.io/aws": + return "aws", nil + case "kuadrant.io/gcp": + return "google", nil + } + return "", errUnsupportedProvider +} diff --git a/internal/provider/fake/factory.go b/internal/provider/fake/factory.go new file mode 100644 index 00000000..8a69fdd6 --- /dev/null +++ b/internal/provider/fake/factory.go @@ -0,0 +1,18 @@ +package fake + +import ( + "context" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +type Factory struct { + ProviderForFunc func(ctx context.Context, pa v1alpha1.ProviderAccessor) (provider.Provider, error) +} + +var _ provider.Factory = &Factory{} + +func (f *Factory) ProviderFor(ctx context.Context, pa v1alpha1.ProviderAccessor) (provider.Provider, error) { + return f.ProviderForFunc(ctx, pa) +} diff --git a/internal/provider/fake/provider.go b/internal/provider/fake/provider.go new file mode 100644 index 00000000..a11ce4bc --- /dev/null +++ b/internal/provider/fake/provider.go @@ -0,0 +1,31 @@ +package fake + +import ( + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +type Provider struct { + EnsureFunc func(*v1alpha1.DNSRecord, *v1alpha1.ManagedZone) error + DeleteFunc func(*v1alpha1.DNSRecord, *v1alpha1.ManagedZone) error + EnsureManagedZoneFunc func(*v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) + DeleteManagedZoneFunc func(*v1alpha1.ManagedZone) error +} + +var _ provider.Provider = &Provider{} + +func (p Provider) Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return p.EnsureFunc(record, managedZone) +} + +func (p Provider) Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return p.DeleteFunc(record, managedZone) +} + +func (p Provider) EnsureManagedZone(managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + return p.EnsureManagedZoneFunc(managedZone) +} + +func (p Provider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { + return p.DeleteManagedZoneFunc(managedZone) +} diff --git a/internal/provider/google/google.go b/internal/provider/google/google.go new file mode 100644 index 00000000..a28d640b --- /dev/null +++ b/internal/provider/google/google.go @@ -0,0 +1,503 @@ +/* +Copyright 2024. + +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 google + +import ( + "context" + "fmt" + "net" + "sort" + "strconv" + "strings" + "time" + + "github.com/go-logr/logr" + dnsv1 "google.golang.org/api/dns/v1" + googleapi "google.golang.org/api/googleapi" + "google.golang.org/api/option" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +type action string + +const ( + GoogleBatchChangeSize = 1000 + GoogleBatchChangeInterval = time.Second + DryRun = false + upsertAction action = "UPSERT" + deleteAction action = "DELETE" + defaultGeo = "europe-west1" +) + +// Based on the external-dns google provider https://github.com/kubernetes-sigs/external-dns/blob/master/provider/google/google.go + +// Managed zone interfaces +type managedZonesCreateCallInterface interface { + Do(opts ...googleapi.CallOption) (*dnsv1.ManagedZone, error) +} + +type managedZonesGetCallInterface interface { + Do(opts ...googleapi.CallOption) (*dnsv1.ManagedZone, error) +} +type managedZonesDeleteCallInterface interface { + Do(opts ...googleapi.CallOption) error +} + +type managedZonesListCallInterface interface { + Pages(ctx context.Context, f func(*dnsv1.ManagedZonesListResponse) error) error +} + +type managedZonesServiceInterface interface { + Create(project string, managedzone *dnsv1.ManagedZone) managedZonesCreateCallInterface + Get(project string, managedZone string) managedZonesGetCallInterface + List(project string) managedZonesListCallInterface + Delete(project string, managedzone string) managedZonesDeleteCallInterface +} + +type managedZonesService struct { + service *dnsv1.ManagedZonesService +} + +func (m managedZonesService) Create(project string, managedzone *dnsv1.ManagedZone) managedZonesCreateCallInterface { + return m.service.Create(project, managedzone) +} + +func (m managedZonesService) Get(project string, managedZone string) managedZonesGetCallInterface { + return m.service.Get(project, managedZone) +} + +func (m managedZonesService) List(project string) managedZonesListCallInterface { + return m.service.List(project) +} +func (m managedZonesService) Delete(project string, managedzone string) managedZonesDeleteCallInterface { + return m.service.Delete(project, managedzone) +} + +// Record set interfaces +type resourceRecordSetsListCallInterface interface { + Pages(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error +} + +type resourceRecordSetsClientInterface interface { + List(project string, managedZone string) resourceRecordSetsListCallInterface +} + +type changesCreateCallInterface interface { + Do(opts ...googleapi.CallOption) (*dnsv1.Change, error) +} + +type changesServiceInterface interface { + Create(project string, managedZone string, change *dnsv1.Change) changesCreateCallInterface +} + +type changesService struct { + service *dnsv1.ChangesService +} + +func (c changesService) Create(project string, managedZone string, change *dnsv1.Change) changesCreateCallInterface { + return c.service.Create(project, managedZone, change) +} + +type resourceRecordSetsService struct { + service *dnsv1.ResourceRecordSetsService +} + +func (r resourceRecordSetsService) List(project string, managedZone string) resourceRecordSetsListCallInterface { + return r.service.List(project, managedZone) +} + +type GoogleDNSProvider struct { + logger logr.Logger + // The Google project to work in + project string + // Enabled dry-run will print any modifying actions rather than execute them. + dryRun bool + // Max batch size to submit to Google Cloud DNS per transaction. + batchChangeSize int + // Interval between batch updates. + batchChangeInterval time.Duration + // A client for managing resource record sets + resourceRecordSetsClient resourceRecordSetsClientInterface + // A client for managing hosted zones + managedZonesClient managedZonesServiceInterface + // A client for managing change sets + changesClient changesServiceInterface + // The context parameter to be passed for gcloud API calls. + ctx context.Context +} + +var _ provider.Provider = &GoogleDNSProvider{} + +func NewProviderFromSecret(ctx context.Context, s *v1.Secret) (provider.Provider, error) { + + if string(s.Data["GOOGLE"]) == "" || string(s.Data["PROJECT_ID"]) == "" { + return nil, fmt.Errorf("GCP Provider credentials is empty") + } + + dnsClient, err := dnsv1.NewService(ctx, option.WithCredentialsJSON(s.Data["GOOGLE"])) + if err != nil { + return nil, err + } + + var project = string(s.Data["PROJECT_ID"]) + + p := &GoogleDNSProvider{ + logger: log.Log.WithName("google-dns").WithValues("project", project), + project: project, + dryRun: DryRun, + batchChangeSize: GoogleBatchChangeSize, + batchChangeInterval: GoogleBatchChangeInterval, + resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets}, + managedZonesClient: managedZonesService{dnsClient.ManagedZones}, + changesClient: changesService{dnsClient.Changes}, + ctx: ctx, + } + + return p, nil +} + +// ManagedZones + +func (g *GoogleDNSProvider) DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error { + return g.managedZonesClient.Delete(g.project, managedZone.Status.ID).Do() +} + +func (g *GoogleDNSProvider) EnsureManagedZone(managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + var zoneID string + + if managedZone.Spec.ID != "" { + zoneID = managedZone.Spec.ID + } else { + zoneID = managedZone.Status.ID + } + + if zoneID != "" { + //Get existing managed zone + return g.getManagedZone(zoneID) + } + //Create new managed zone + return g.createManagedZone(managedZone) +} + +func (g *GoogleDNSProvider) createManagedZone(managedZone *v1alpha1.ManagedZone) (provider.ManagedZoneOutput, error) { + zoneID := strings.Replace(managedZone.Spec.DomainName, ".", "-", -1) + zone := dnsv1.ManagedZone{ + Name: zoneID, + DnsName: ensureTrailingDot(managedZone.Spec.DomainName), + Description: managedZone.Spec.Description, + } + mz, err := g.managedZonesClient.Create(g.project, &zone).Do() + if err != nil { + return provider.ManagedZoneOutput{}, err + } + return g.toManagedZoneOutput(mz) +} + +func (g *GoogleDNSProvider) getManagedZone(zoneID string) (provider.ManagedZoneOutput, error) { + mz, err := g.managedZonesClient.Get(g.project, zoneID).Do() + if err != nil { + return provider.ManagedZoneOutput{}, err + } + return g.toManagedZoneOutput(mz) +} + +func (g *GoogleDNSProvider) toManagedZoneOutput(mz *dnsv1.ManagedZone) (provider.ManagedZoneOutput, error) { + var managedZoneOutput provider.ManagedZoneOutput + + zoneID := mz.Name + var nameservers []*string + for i := range mz.NameServers { + nameservers = append(nameservers, &mz.NameServers[i]) + } + managedZoneOutput.ID = zoneID + managedZoneOutput.NameServers = nameservers + + currentRecords, err := g.getResourceRecordSets(g.ctx, zoneID) + if err != nil { + return managedZoneOutput, err + } + managedZoneOutput.RecordCount = int64(len(currentRecords)) + + return managedZoneOutput, nil +} + +//DNSRecords + +func (g *GoogleDNSProvider) Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return g.updateRecord(record, managedZone.Status.ID, upsertAction) +} + +func (g *GoogleDNSProvider) Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error { + return g.updateRecord(record, managedZone.Status.ID, deleteAction) +} + +func (g *GoogleDNSProvider) updateRecord(dnsRecord *v1alpha1.DNSRecord, zoneID string, action action) error { + // When updating records the Google DNS API expects you to delete any existing record and add the new one as part of + // the same change request. The record to be deleted must match exactly what currently exists in the provider or the + // change request will fail. To make sure we can always remove the records, we first get all records that exist in + // the zone and build up the deleting list from `dnsRecord.Status` but use the most recent version of it retrieved + // from the provider in the change request. + currentRecords, err := g.getResourceRecordSets(g.ctx, zoneID) + if err != nil { + return err + } + currentRecordsMap := make(map[string]*dnsv1.ResourceRecordSet) + for _, record := range currentRecords { + currentRecordsMap[record.Name] = record + } + statusRecords := toResourceRecordSets(dnsRecord.Status.Endpoints) + statusRecordsMap := make(map[string]*dnsv1.ResourceRecordSet) + for _, record := range statusRecords { + statusRecordsMap[record.Name] = record + } + + var deletingRecords []*dnsv1.ResourceRecordSet + for name := range statusRecordsMap { + if record, ok := currentRecordsMap[name]; ok { + deletingRecords = append(deletingRecords, record) + } + } + addingRecords := toResourceRecordSets(dnsRecord.Spec.Endpoints) + + g.logger.V(1).Info("updateRecord", "currentRecords", currentRecords, "deletingRecords", deletingRecords, "addingRecords", addingRecords) + + change := &dnsv1.Change{} + if action == deleteAction { + change.Deletions = deletingRecords + } else { + change.Deletions = deletingRecords + change.Additions = addingRecords + } + + return g.submitChange(change, zoneID) +} + +func (g *GoogleDNSProvider) submitChange(change *dnsv1.Change, zone string) error { + if len(change.Additions) == 0 && len(change.Deletions) == 0 { + g.logger.Info("All records are already up to date") + return nil + } + + for batch, c := range g.batchChange(change, g.batchChangeSize) { + g.logger.V(1).Info("Change zone", "zone", zone, "batch", batch) + for _, del := range c.Deletions { + g.logger.V(1).Info("Del records", "name", del.Name, "type", del.Type, "Rrdatas", + del.Rrdatas, "RoutingPolicy", del.RoutingPolicy, "ttl", del.Ttl) + } + for _, add := range c.Additions { + g.logger.V(1).Info("Add records", "name", add.Name, "type", add.Type, "Rrdatas", + add.Rrdatas, "RoutingPolicy", add.RoutingPolicy, "ttl", add.Ttl) + } + if g.dryRun { + continue + } + + if _, err := g.changesClient.Create(g.project, zone, c).Do(); err != nil { + return err + } + time.Sleep(g.batchChangeInterval) + } + return nil +} + +func (g *GoogleDNSProvider) batchChange(change *dnsv1.Change, batchSize int) []*dnsv1.Change { + changes := []*dnsv1.Change{} + + if batchSize == 0 { + return append(changes, change) + } + + type dnsv1Change struct { + additions []*dnsv1.ResourceRecordSet + deletions []*dnsv1.ResourceRecordSet + } + + changesByName := map[string]*dnsv1Change{} + + for _, a := range change.Additions { + change, ok := changesByName[a.Name] + if !ok { + change = &dnsv1Change{} + changesByName[a.Name] = change + } + + change.additions = append(change.additions, a) + } + + for _, a := range change.Deletions { + change, ok := changesByName[a.Name] + if !ok { + change = &dnsv1Change{} + changesByName[a.Name] = change + } + + change.deletions = append(change.deletions, a) + } + + names := make([]string, 0) + for v := range changesByName { + names = append(names, v) + } + sort.Strings(names) + + currentChange := &dnsv1.Change{} + var totalChanges int + for _, name := range names { + c := changesByName[name] + + totalChangesByName := len(c.additions) + len(c.deletions) + + if totalChangesByName > batchSize { + g.logger.V(1).Info("Total changes for %s exceeds max batch size of %d, total changes: %d", name, + batchSize, totalChangesByName) + continue + } + + if totalChanges+totalChangesByName > batchSize { + totalChanges = 0 + changes = append(changes, currentChange) + currentChange = &dnsv1.Change{} + } + + currentChange.Additions = append(currentChange.Additions, c.additions...) + currentChange.Deletions = append(currentChange.Deletions, c.deletions...) + + totalChanges += totalChangesByName + } + + if totalChanges > 0 { + changes = append(changes, currentChange) + } + + return changes +} + +// getResourceRecordSets returns the records for a managed zone of the currently configured provider. +func (g *GoogleDNSProvider) getResourceRecordSets(ctx context.Context, zoneID string) ([]*dnsv1.ResourceRecordSet, error) { + var records []*dnsv1.ResourceRecordSet + + f := func(resp *dnsv1.ResourceRecordSetsListResponse) error { + records = append(records, resp.Rrsets...) + return nil + } + + if err := g.resourceRecordSetsClient.List(g.project, zoneID).Pages(ctx, f); err != nil { + return nil, err + } + + return records, nil +} + +// toResourceRecordSets converts a list of endpoints into `ResourceRecordSet` resources. +func toResourceRecordSets(allEndpoints []*v1alpha1.Endpoint) []*dnsv1.ResourceRecordSet { + var records []*dnsv1.ResourceRecordSet + + // Google DNS requires a record to be created per `dnsName`, so the first thing we need to do is group all the + // endpoints with the same dnsName together. + endpointMap := make(map[string][]*v1alpha1.Endpoint) + for _, ep := range allEndpoints { + endpointMap[ep.DNSName] = append(endpointMap[ep.DNSName], ep) + } + + for dnsName, endpoints := range endpointMap { + // A set of endpoints belonging to the same group(`dnsName`) must always be of the same type, have the same ttl + // and contain the same rrdata (weighted or geo), so we can just get that from the first endpoint in the list. + ttl := int64(endpoints[0].RecordTTL) + recordType := endpoints[0].RecordType + _, weighted := endpoints[0].GetProviderSpecificProperty(provider.ProviderSpecificWeight) + _, geoCode := endpoints[0].GetProviderSpecificProperty(provider.ProviderSpecificGeoCode) + + record := &dnsv1.ResourceRecordSet{ + Name: ensureTrailingDot(dnsName), + Ttl: ttl, + Type: recordType, + } + if weighted { + record.RoutingPolicy = &dnsv1.RRSetRoutingPolicy{ + Wrr: &dnsv1.RRSetRoutingPolicyWrrPolicy{}, + } + } else if geoCode { + record.RoutingPolicy = &dnsv1.RRSetRoutingPolicy{ + Geo: &dnsv1.RRSetRoutingPolicyGeoPolicy{}, + } + } + + for _, ep := range endpoints { + targets := make([]string, len(ep.Targets)) + copy(targets, ep.Targets) + if ep.RecordType == string(v1alpha1.CNAMERecordType) { + targets[0] = ensureTrailingDot(targets[0]) + } + + if !weighted && !geoCode { + record.Rrdatas = targets + } + if weighted { + weightProp, _ := ep.GetProviderSpecificProperty(provider.ProviderSpecificWeight) + weight, err := strconv.ParseFloat(weightProp.Value, 64) + if err != nil { + weight = 0 + } + item := &dnsv1.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + Rrdatas: targets, + Weight: weight, + } + record.RoutingPolicy.Wrr.Items = append(record.RoutingPolicy.Wrr.Items, item) + } + if geoCode { + geoCodeProp, _ := ep.GetProviderSpecificProperty(provider.ProviderSpecificGeoCode) + geoCodeValue := geoCodeProp.Value + targetIsDefaultGroup := strings.HasPrefix(ep.Targets[0], v1alpha1.DefaultGeo) + // GCP doesn't accept * as value for default geolocations like AWS does. + // To ensure the dns chain doesn't break if a * is given we map the value to europe-west1 instead + // We cant drop the record as the chain will break + if geoCodeValue == "*" { + if !targetIsDefaultGroup { + continue + } + geoCodeValue = defaultGeo + } + item := &dnsv1.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + Location: geoCodeValue, + Rrdatas: targets, + } + record.RoutingPolicy.Geo.Items = append(record.RoutingPolicy.Geo.Items, item) + } + } + records = append(records, record) + } + return records +} + +// ensureTrailingDot ensures that the hostname receives a trailing dot if it hasn't already. +func ensureTrailingDot(hostname string) string { + if net.ParseIP(hostname) != nil { + return hostname + } + + return strings.TrimSuffix(hostname, ".") + "." +} + +// Register this Provider with the provider factory +func init() { + provider.RegisterProvider("google", NewProviderFromSecret) +} diff --git a/internal/provider/google/google_test.go b/internal/provider/google/google_test.go new file mode 100644 index 00000000..e7f449f8 --- /dev/null +++ b/internal/provider/google/google_test.go @@ -0,0 +1,544 @@ +// //go:build unit + +package google + +import ( + "context" + "fmt" + "reflect" + "sort" + "testing" + + "github.com/aws/aws-sdk-go/aws" + dnsv1 "google.golang.org/api/dns/v1" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-dns-operator/internal/provider" +) + +func TestGoogleDNSProvider_toManagedZoneOutput(t *testing.T) { + mockListCall := &MockResourceRecordSetsListCall{ + PagesFunc: func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error { + mockResponse := &dnsv1.ResourceRecordSetsListResponse{ + Rrsets: []*dnsv1.ResourceRecordSet{ + { + Name: "TestRecordSet1", + }, + { + Name: "TestRecordSet2", + }, + }, + } + return f(mockResponse) + }, + } + mockClient := &MockResourceRecordSetsClient{ + ListFunc: func(project string, managedZone string) resourceRecordSetsListCallInterface { + return mockListCall + }, + } + + mockListCallErr := &MockResourceRecordSetsListCall{ + PagesFunc: func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error { + + error := fmt.Errorf("status 400 ") + return error + }, + } + mockClientErr := &MockResourceRecordSetsClient{ + ListFunc: func(project string, managedZone string) resourceRecordSetsListCallInterface { + return mockListCallErr + }, + } + + type fields struct { + resourceRecordSetsClient resourceRecordSetsClientInterface + } + type args struct { + mz *dnsv1.ManagedZone + } + tests := []struct { + name string + fields fields + args args + want provider.ManagedZoneOutput + wantErr bool + }{ + + { + name: "Successful test", + fields: fields{ + resourceRecordSetsClient: mockClient, + }, + args: args{ + &dnsv1.ManagedZone{ + Name: "testname", + NameServers: []string{ + "nameserver1", + "nameserver2", + }, + }, + }, + want: provider.ManagedZoneOutput{ + ID: "testname", + NameServers: []*string{ + aws.String("nameserver1"), + aws.String("nameserver2"), + }, + RecordCount: 2, + }, + wantErr: false, + }, + { + name: "UnSuccessful test", + fields: fields{ + resourceRecordSetsClient: mockClientErr, + }, + args: args{ + &dnsv1.ManagedZone{ + Name: "testname", + NameServers: []string{ + "nameserver1", + "nameserver2", + }, + }, + }, + want: provider.ManagedZoneOutput{ + ID: "testname", + NameServers: []*string{ + aws.String("nameserver1"), + aws.String("nameserver2"), + }, + RecordCount: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := &GoogleDNSProvider{ + resourceRecordSetsClient: tt.fields.resourceRecordSetsClient, + } + got, err := g.toManagedZoneOutput(tt.args.mz) + if (err != nil) != tt.wantErr { + t.Errorf("GoogleDNSProvider.toManagedZoneOutput() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GoogleDNSProvider.toManagedZoneOutput() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_toResourceRecordSets(t *testing.T) { + type args struct { + allEndpoints []*v1alpha1.Endpoint + } + tests := []struct { + name string + args args + want []*dnsv1.ResourceRecordSet + }{ + { + name: "Successful test Geo & weight", + args: args{ + allEndpoints: []*v1alpha1.Endpoint{ + { + DNSName: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "A", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "0.0.0.0", + }, + ProviderSpecific: v1alpha1.ProviderSpecific{}, + SetIdentifier: "", + }, + { + DNSName: "europe-west1.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + }, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "weight", + Value: "60", + }, + }, + }, + { + DNSName: "lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "europe-west1", + Targets: []string{ + "europe-west1.lb-4ej5le.unittest.google.hcpapps.net", + }, + RecordTTL: 300, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "geo-code", + Value: "europe-west1", + }, + }, + }, + { + DNSName: "unittest.google.hcpapps.net", + RecordType: "CNAME", + RecordTTL: 300, + Targets: []string{ + "lb-4ej5le.unittest.google.hcpapps.net", + }, + SetIdentifier: "", + }, + }, + }, + want: []*dnsv1.ResourceRecordSet{ + { + Name: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + Rrdatas: []string{ + "0.0.0.0", + }, + Ttl: 60, + Type: "A", + }, + { + Name: "europe-west1.lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Wrr: &dnsv1.RRSetRoutingPolicyWrrPolicy{ + Items: []*dnsv1.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + { + Rrdatas: []string{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + }, + Weight: 60, + }, + }, + }, + }, + Ttl: 60, + Type: "CNAME", + }, + + { + Name: "lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Geo: &dnsv1.RRSetRoutingPolicyGeoPolicy{ + EnableFencing: false, + Items: []*dnsv1.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + { + Location: "europe-west1", + Rrdatas: []string{ + "europe-west1.lb-4ej5le.unittest.google.hcpapps.net.", + }, + }, + }, + }, + }, + Ttl: 300, + Type: "CNAME", + }, + + { + Name: "unittest.google.hcpapps.net.", + Rrdatas: []string{ + "lb-4ej5le.unittest.google.hcpapps.net.", + }, + Ttl: 300, + Type: "CNAME", + }, + }, + }, + { + name: "Successful test no Geo & weight", + args: args{ + allEndpoints: []*v1alpha1.Endpoint{ + { + DNSName: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "A", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "0.0.0.0", + }, + SetIdentifier: "", + }, + { + DNSName: "default.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + }, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "weight", + Value: "120", + }, + }, + }, + { + DNSName: "lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "default", + Targets: []string{ + "default.lb-4ej5le.unittest.google.hcpapps.net", + }, + RecordTTL: 300, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "geo-code", + Value: "*", + }, + }, + }, + { + DNSName: "unittest.google.hcpapps.net", + RecordType: "CNAME", + RecordTTL: 300, + Targets: []string{ + "lb-4ej5le.unittest.google.hcpapps.net", + }, + SetIdentifier: "", + }, + }, + }, + want: []*dnsv1.ResourceRecordSet{ + { + Name: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + Rrdatas: []string{ + "0.0.0.0", + }, + Ttl: 60, + Type: "A", + }, + { + Name: "default.lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Wrr: &dnsv1.RRSetRoutingPolicyWrrPolicy{ + Items: []*dnsv1.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + { + Rrdatas: []string{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + }, + Weight: 120, + }, + }, + }, + }, + Ttl: 60, + Type: "CNAME", + }, + { + Name: "lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Geo: &dnsv1.RRSetRoutingPolicyGeoPolicy{ + EnableFencing: false, + Items: []*dnsv1.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + { + Location: "europe-west1", + Rrdatas: []string{ + "default.lb-4ej5le.unittest.google.hcpapps.net.", + }, + }, + }, + }, + }, + Ttl: 300, + Type: "CNAME", + }, + { + Name: "unittest.google.hcpapps.net.", + Rrdatas: []string{ + "lb-4ej5le.unittest.google.hcpapps.net.", + }, + Ttl: 300, + Type: "CNAME", + }, + }, + }, + { + name: "Successful test weight round robin with multiple targets", + args: args{ + allEndpoints: []*v1alpha1.Endpoint{ + { + DNSName: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "A", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "0.0.0.0", + }, + SetIdentifier: "", + }, + { + DNSName: "lrnse3.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "A", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "0.0.0.1", + }, + SetIdentifier: "", + }, + { + DNSName: "default.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net", + }, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "weight", + Value: "120", + }, + }, + }, + { + DNSName: "default.lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "lrnse3.lb-4ej5le.unittest.google.hcpapps.net", + RecordTTL: 60, + Targets: v1alpha1.Targets{ + "lrnse3.lb-4ej5le.unittest.google.hcpapps.net", + }, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "weight", + Value: "120", + }, + }, + }, + { + DNSName: "lb-4ej5le.unittest.google.hcpapps.net", + RecordType: "CNAME", + SetIdentifier: "default", + Targets: []string{ + "default.lb-4ej5le.unittest.google.hcpapps.net", + }, + RecordTTL: 300, + ProviderSpecific: v1alpha1.ProviderSpecific{ + v1alpha1.ProviderSpecificProperty{ + Name: "geo-code", + Value: "*", + }, + }, + }, + { + DNSName: "unittest.google.hcpapps.net", + RecordType: "CNAME", + RecordTTL: 300, + Targets: []string{ + "lb-4ej5le.unittest.google.hcpapps.net", + }, + SetIdentifier: "", + }, + }, + }, + want: []*dnsv1.ResourceRecordSet{ + { + Name: "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + Rrdatas: []string{ + "0.0.0.0", + }, + Ttl: 60, + Type: "A", + }, + { + Name: "lrnse3.lb-4ej5le.unittest.google.hcpapps.net.", + Rrdatas: []string{ + "0.0.0.1", + }, + Ttl: 60, + Type: "A", + }, + { + Name: "default.lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Wrr: &dnsv1.RRSetRoutingPolicyWrrPolicy{ + Items: []*dnsv1.RRSetRoutingPolicyWrrPolicyWrrPolicyItem{ + { + Rrdatas: []string{ + "2c71gf.lb-4ej5le.unittest.google.hcpapps.net.", + }, + Weight: 120, + }, + { + Rrdatas: []string{ + "lrnse3.lb-4ej5le.unittest.google.hcpapps.net.", + }, + Weight: 120, + }, + }, + }, + }, + Ttl: 60, + Type: "CNAME", + }, + { + Name: "lb-4ej5le.unittest.google.hcpapps.net.", + RoutingPolicy: &dnsv1.RRSetRoutingPolicy{ + Geo: &dnsv1.RRSetRoutingPolicyGeoPolicy{ + EnableFencing: false, + Items: []*dnsv1.RRSetRoutingPolicyGeoPolicyGeoPolicyItem{ + { + Location: "europe-west1", + Rrdatas: []string{ + "default.lb-4ej5le.unittest.google.hcpapps.net.", + }, + }, + }, + }, + }, + Ttl: 300, + Type: "CNAME", + }, + { + Name: "unittest.google.hcpapps.net.", + Rrdatas: []string{ + "lb-4ej5le.unittest.google.hcpapps.net.", + }, + Ttl: 300, + Type: "CNAME", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := toResourceRecordSets(tt.args.allEndpoints) + sorted(got) + sorted(tt.want) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toResourceRecordSets() = %+v, want %+v", got, tt.want) + } + + }) + } +} +func sorted(rrset []*dnsv1.ResourceRecordSet) { + sort.Slice(rrset, func(i, j int) bool { + return rrset[i].Name < rrset[j].Name + }) +} + +type MockResourceRecordSetsListCall struct { + PagesFunc func(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error +} + +func (m *MockResourceRecordSetsListCall) Pages(ctx context.Context, f func(*dnsv1.ResourceRecordSetsListResponse) error) error { + return m.PagesFunc(ctx, f) + +} + +type MockResourceRecordSetsClient struct { + ListFunc func(project string, managedZone string) resourceRecordSetsListCallInterface +} + +func (m *MockResourceRecordSetsClient) List(project string, managedZone string) resourceRecordSetsListCallInterface { + + return m.ListFunc(project, managedZone) + +} diff --git a/internal/provider/iso3166.go b/internal/provider/iso3166.go new file mode 100644 index 00000000..e97bea78 --- /dev/null +++ b/internal/provider/iso3166.go @@ -0,0 +1,272 @@ +package provider + +import "github.com/kuadrant/kuadrant-dns-operator/internal/common/slice" + +// nolint +type countryCodes struct { + alpha2Code string +} + +var iso3166Codes = []*countryCodes{ + {"AF"}, + {"AL"}, + {"DZ"}, + {"AS"}, + {"AD"}, + {"AO"}, + {"AI"}, + {"AQ"}, + {"AG"}, + {"AR"}, + {"AM"}, + {"AW"}, + {"AU"}, + {"AT"}, + {"AZ"}, + {"BS"}, + {"BH"}, + {"BD"}, + {"BB"}, + {"BY"}, + {"BE"}, + {"BZ"}, + {"BJ"}, + {"BM"}, + {"BT"}, + {"BO"}, + {"BQ"}, + {"BA"}, + {"BW"}, + {"BV"}, + {"BR"}, + {"IO"}, + {"BN"}, + {"BG"}, + {"BF"}, + {"BI"}, + {"CV"}, + {"KH"}, + {"CM"}, + {"CA"}, + {"KY"}, + {"CF"}, + {"TD"}, + {"CL"}, + {"CN"}, + {"CX"}, + {"CC"}, + {"CO"}, + {"KM"}, + {"CD"}, + {"CG"}, + {"CK"}, + {"CR"}, + {"HR"}, + {"CU"}, + {"CW"}, + {"CY"}, + {"CZ"}, + {"CI"}, + {"DK"}, + {"DJ"}, + {"DM"}, + {"DO"}, + {"EC"}, + {"EG"}, + {"SV"}, + {"GQ"}, + {"ER"}, + {"EE"}, + {"SZ"}, + {"ET"}, + {"FK"}, + {"FO"}, + {"FJ"}, + {"FI"}, + {"FR"}, + {"GF"}, + {"PF"}, + {"TF"}, + {"GA"}, + {"GM"}, + {"GE"}, + {"DE"}, + {"GH"}, + {"GI"}, + {"GR"}, + {"GL"}, + {"GD"}, + {"GP"}, + {"GU"}, + {"GT"}, + {"GG"}, + {"GN"}, + {"GW"}, + {"GY"}, + {"HT"}, + {"HM"}, + {"VA"}, + {"HN"}, + {"HK"}, + {"HU"}, + {"IS"}, + {"IN"}, + {"ID"}, + {"IR"}, + {"IQ"}, + {"IE"}, + {"IM"}, + {"IL"}, + {"IT"}, + {"JM"}, + {"JP"}, + {"JE"}, + {"JO"}, + {"KZ"}, + {"KE"}, + {"KI"}, + {"KP"}, + {"KR"}, + {"KW"}, + {"KG"}, + {"LA"}, + {"LV"}, + {"LB"}, + {"LS"}, + {"LR"}, + {"LY"}, + {"LI"}, + {"LT"}, + {"LU"}, + {"MO"}, + {"MG"}, + {"MW"}, + {"MY"}, + {"MV"}, + {"ML"}, + {"MT"}, + {"MH"}, + {"MQ"}, + {"MR"}, + {"MU"}, + {"YT"}, + {"MX"}, + {"FM"}, + {"MD"}, + {"MC"}, + {"MN"}, + {"ME"}, + {"MS"}, + {"MA"}, + {"MZ"}, + {"MM"}, + {"NA"}, + {"NR"}, + {"NP"}, + {"NL"}, + {"NC"}, + {"NZ"}, + {"NI"}, + {"NE"}, + {"NG"}, + {"NU"}, + {"NF"}, + {"MK"}, + {"MP"}, + {"NO"}, + {"OM"}, + {"PK"}, + {"PW"}, + {"PS"}, + {"PA"}, + {"PG"}, + {"PY"}, + {"PE"}, + {"PH"}, + {"PN"}, + {"PL"}, + {"PT"}, + {"PR"}, + {"QA"}, + {"RO"}, + {"RU"}, + {"RW"}, + {"RE"}, + {"BL"}, + {"SH"}, + {"KN"}, + {"LC"}, + {"MF"}, + {"PM"}, + {"VC"}, + {"WS"}, + {"SM"}, + {"ST"}, + {"SA"}, + {"SN"}, + {"RS"}, + {"SC"}, + {"SL"}, + {"SG"}, + {"SX"}, + {"SK"}, + {"SI"}, + {"SB"}, + {"SO"}, + {"ZA"}, + {"GS"}, + {"SS"}, + {"ES"}, + {"LK"}, + {"SD"}, + {"SR"}, + {"SJ"}, + {"SE"}, + {"CH"}, + {"SY"}, + {"TW"}, + {"TJ"}, + {"TZ"}, + {"TH"}, + {"TL"}, + {"TG"}, + {"TK"}, + {"TO"}, + {"TT"}, + {"TN"}, + {"TR"}, + {"TM"}, + {"TC"}, + {"TV"}, + {"UG"}, + {"UA"}, + {"AE"}, + {"GB"}, + {"UM"}, + {"US"}, + {"UY"}, + {"UZ"}, + {"VU"}, + {"VE"}, + {"VN"}, + {"VG"}, + {"VI"}, + {"WF"}, + {"EH"}, + {"YE"}, + {"ZM"}, + {"ZW"}, +} + +func GetISO3166Alpha2Codes() []string { + var codes []string + for _, v := range iso3166Codes { + codes = append(codes, v.alpha2Code) + } + return codes +} + +// IsISO3166Alpha2Code returns true if it's a valid ISO_3166 Alpha 2 country code (https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) +func IsISO3166Alpha2Code(code string) bool { + return slice.ContainsString(GetISO3166Alpha2Codes(), code) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 00000000..66165e98 --- /dev/null +++ b/internal/provider/provider.go @@ -0,0 +1,47 @@ +package provider + +import ( + "errors" + "regexp" + + "github.com/kuadrant/kuadrant-dns-operator/api/v1alpha1" +) + +const ( + ProviderSpecificWeight = "weight" + ProviderSpecificGeoCode = "geo-code" +) + +// Provider knows how to manage DNS zones only as pertains to routing. +type Provider interface { + + // Ensure will create or update record. + Ensure(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error + + // Delete will delete record. + Delete(record *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone) error + + // Ensure will create or update a managed zone, returns an array of NameServers for that zone. + EnsureManagedZone(managedZone *v1alpha1.ManagedZone) (ManagedZoneOutput, error) + + // Delete will delete a managed zone. + DeleteManagedZone(managedZone *v1alpha1.ManagedZone) error +} + +type ProviderSpecificLabels struct { + Weight string + HealthCheckID string +} + +type ManagedZoneOutput struct { + ID string + NameServers []*string + RecordCount int64 +} + +// SanitizeError removes request specific data from error messages in order to make them consistent across multiple similar requests to the provider. e.g AWS SDK Request ids `request id: 051c860b-9b30-4c19-be1a-1280c3e9fdc4` +func SanitizeError(err error) error { + re := regexp.MustCompile(`request id: [^\s]+`) + sanitizedErr := re.ReplaceAllString(err.Error(), "") + return errors.New(sanitizedErr) +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 00000000..36764120 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,35 @@ +//go:build unit + +package provider + +import ( + "errors" + "testing" +) + +func TestSanitizeError(t *testing.T) { + testCases := []struct { + name string + err error + expectedError string + }{ + { + name: "error message with request id", + err: errors.New("An error has occurred, request id: 12345abcd"), + expectedError: "An error has occurred, ", + }, + { + name: "error message without request id", + err: errors.New("An error has occurred"), + expectedError: "An error has occurred", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := SanitizeError(testCase.err) + if got.Error() != testCase.expectedError { + t.Errorf("expected '%v' got '%v'", testCase.expectedError, got) + } + }) + } +}