diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml index 054d95b96396..c32a418a90ac 100644 --- a/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml +++ b/app/kumactl/cmd/install/testdata/install-control-plane.defaults.golden.yaml @@ -485,11 +485,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -497,11 +494,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: @@ -5120,7 +5176,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: @@ -7243,6 +7299,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml index a63b6ee2ddee..8176057d7a6f 100644 --- a/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml +++ b/app/kumactl/cmd/install/testdata/install-control-plane.gateway-api-present.yaml @@ -485,11 +485,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -497,11 +494,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: @@ -5120,7 +5176,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: @@ -7243,6 +7299,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml b/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml index 9a6a54c80c93..42230059fb86 100644 --- a/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml +++ b/app/kumactl/cmd/install/testdata/install-control-plane.with-helm-set.yaml @@ -505,11 +505,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -517,11 +514,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: @@ -5140,7 +5196,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: @@ -7263,6 +7319,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml b/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml index c555e81bf75f..c4fa530894dc 100644 --- a/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml +++ b/app/kumactl/cmd/install/testdata/install-crds.all.golden.yaml @@ -477,6 +477,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: @@ -1967,11 +1976,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -1979,11 +1985,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: @@ -6552,7 +6617,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: diff --git a/deployments/charts/kuma/crds/kuma.io_hostnamegenerators.yaml b/deployments/charts/kuma/crds/kuma.io_hostnamegenerators.yaml index 33e6e2e1d3ff..c1d59d39cc8c 100644 --- a/deployments/charts/kuma/crds/kuma.io_hostnamegenerators.yaml +++ b/deployments/charts/kuma/crds/kuma.io_hostnamegenerators.yaml @@ -42,6 +42,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml b/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml index 0347bd0a2243..013c20207bda 100644 --- a/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml +++ b/deployments/charts/kuma/crds/kuma.io_meshexternalservices.yaml @@ -238,11 +238,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -250,11 +247,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: diff --git a/deployments/charts/kuma/crds/kuma.io_meshservices.yaml b/deployments/charts/kuma/crds/kuma.io_meshservices.yaml index b7d748923186..247db0a8d4ff 100644 --- a/deployments/charts/kuma/crds/kuma.io_meshservices.yaml +++ b/deployments/charts/kuma/crds/kuma.io_meshservices.yaml @@ -99,7 +99,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: diff --git a/docs/generated/raw/crds/kuma.io_hostnamegenerators.yaml b/docs/generated/raw/crds/kuma.io_hostnamegenerators.yaml index 33e6e2e1d3ff..c1d59d39cc8c 100644 --- a/docs/generated/raw/crds/kuma.io_hostnamegenerators.yaml +++ b/docs/generated/raw/crds/kuma.io_hostnamegenerators.yaml @@ -42,6 +42,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml b/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml index 0347bd0a2243..013c20207bda 100644 --- a/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml +++ b/docs/generated/raw/crds/kuma.io_meshexternalservices.yaml @@ -238,11 +238,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -250,11 +247,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: diff --git a/docs/generated/raw/crds/kuma.io_meshservices.yaml b/docs/generated/raw/crds/kuma.io_meshservices.yaml index b7d748923186..247db0a8d4ff 100644 --- a/docs/generated/raw/crds/kuma.io_meshservices.yaml +++ b/docs/generated/raw/crds/kuma.io_meshservices.yaml @@ -99,7 +99,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: diff --git a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/hostnamegenerator.go b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/hostnamegenerator.go index 2afd984d278e..005c25e0913a 100644 --- a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/hostnamegenerator.go +++ b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/hostnamegenerator.go @@ -1,12 +1,38 @@ // +kubebuilder:object:generate=true package v1alpha1 +import ( + kube_meta "k8s.io/apimachinery/pkg/apis/meta/v1" +) + type LabelSelector struct { MatchLabels map[string]string `json:"matchLabels,omitempty"` } +type NameLabelsSelector struct { + MatchName string `json:"matchName,omitempty"` + MatchLabels map[string]string `json:"matchLabels,omitempty"` +} + type Selector struct { - MeshService LabelSelector `json:"meshService,omitempty"` + MeshService LabelSelector `json:"meshService,omitempty"` + MeshExternalService NameLabelsSelector `json:"meshExternalService,omitempty"` +} + +func (s NameLabelsSelector) Matches(name string, labels map[string]string) bool { + if s.MatchName != "" && s.MatchName != name { + return false + } + for tag, matchValue := range s.MatchLabels { + labelValue, exist := labels[tag] + if !exist { + return false + } + if matchValue != labelValue { + return false + } + } + return true } func (s LabelSelector) Matches(labels map[string]string) bool { @@ -28,3 +54,78 @@ type HostnameGenerator struct { Selector Selector `json:"selector,omitempty"` Template string `json:"template,omitempty"` } + +type Origin string + +const ( + OriginGenerator Origin = "HostnameGenerator" + OriginKubernetes Origin = "Kubernetes" +) + +type Address struct { + Hostname string `json:"hostname,omitempty"` + Origin Origin `json:"origin,omitempty"` + HostnameGeneratorRef HostnameGeneratorRef `json:"hostnameGeneratorRef,omitempty"` +} + +const ( + GeneratedCondition string = "Generated" +) + +const ( + GeneratedReason string = "Generated" + TemplateErrorReason string = "TemplateError" + CollisionReason string = "Collision" +) + +type HostnameGeneratorRef struct { + CoreName string `json:"name"` +} + +type HostnameGeneratorStatus struct { + HostnameGeneratorRef HostnameGeneratorRef `json:"hostnameGeneratorRef"` + + // Conditions is an array of hostname generator conditions. + // + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` +} + +type Condition struct { + // 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) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation: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])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status kube_meta.ConditionStatus `json:"status"` + // 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. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/schema.yaml b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/schema.yaml index 1458a8b38df9..ce5b5ad9850a 100644 --- a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/schema.yaml +++ b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/schema.yaml @@ -17,6 +17,15 @@ properties: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/zz_generated.deepcopy.go b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/zz_generated.deepcopy.go index f211d07598ce..cd9bdce58020 100644 --- a/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1/zz_generated.deepcopy.go @@ -6,6 +6,37 @@ package v1alpha1 import () +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Address) DeepCopyInto(out *Address) { + *out = *in + out.HostnameGeneratorRef = in.HostnameGeneratorRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. +func (in *Address) DeepCopy() *Address { + if in == nil { + return nil + } + out := new(Address) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostnameGenerator) DeepCopyInto(out *HostnameGenerator) { *out = *in @@ -22,6 +53,42 @@ func (in *HostnameGenerator) DeepCopy() *HostnameGenerator { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostnameGeneratorRef) DeepCopyInto(out *HostnameGeneratorRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnameGeneratorRef. +func (in *HostnameGeneratorRef) DeepCopy() *HostnameGeneratorRef { + if in == nil { + return nil + } + out := new(HostnameGeneratorRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostnameGeneratorStatus) DeepCopyInto(out *HostnameGeneratorStatus) { + *out = *in + out.HostnameGeneratorRef = in.HostnameGeneratorRef + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnameGeneratorStatus. +func (in *HostnameGeneratorStatus) DeepCopy() *HostnameGeneratorStatus { + if in == nil { + return nil + } + out := new(HostnameGeneratorStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LabelSelector) DeepCopyInto(out *LabelSelector) { *out = *in @@ -44,10 +111,33 @@ func (in *LabelSelector) DeepCopy() *LabelSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameLabelsSelector) DeepCopyInto(out *NameLabelsSelector) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameLabelsSelector. +func (in *NameLabelsSelector) DeepCopy() *NameLabelsSelector { + if in == nil { + return nil + } + out := new(NameLabelsSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Selector) DeepCopyInto(out *Selector) { *out = *in in.MeshService.DeepCopyInto(&out.MeshService) + in.MeshExternalService.DeepCopyInto(&out.MeshExternalService) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector. diff --git a/pkg/core/resources/apis/hostnamegenerator/hostname/generator.go b/pkg/core/resources/apis/hostnamegenerator/hostname/generator.go index de7f2d45da40..d4ece35037d4 100644 --- a/pkg/core/resources/apis/hostnamegenerator/hostname/generator.go +++ b/pkg/core/resources/apis/hostnamegenerator/hostname/generator.go @@ -3,10 +3,8 @@ package hostname import ( "context" "fmt" - "reflect" "slices" "strings" - "text/template" "time" "github.com/go-logr/logr" @@ -16,7 +14,6 @@ import ( mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1" hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" - meshservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/api/v1alpha1" "github.com/kumahq/kuma/pkg/core/resources/manager" "github.com/kumahq/kuma/pkg/core/resources/model" "github.com/kumahq/kuma/pkg/core/runtime/component" @@ -25,11 +22,19 @@ import ( "github.com/kumahq/kuma/pkg/util/maps" ) +type HostnameGenerator interface { + GetResources(context.Context) (model.ResourceList, error) + UpdateResourceStatus(context.Context, model.Resource, []hostnamegenerator_api.HostnameGeneratorStatus, []hostnamegenerator_api.Address) error + HasStatusChanged(model.Resource, []hostnamegenerator_api.HostnameGeneratorStatus, []hostnamegenerator_api.Address) (bool, error) + GenerateHostname(*hostnamegenerator_api.HostnameGeneratorResource, model.Resource) (string, error) +} + type Generator struct { logger logr.Logger interval time.Duration metric prometheus.Summary resManager manager.ResourceManager + generators []HostnameGenerator } var _ component.Component = &Generator{} @@ -39,21 +44,22 @@ func NewGenerator( metrics core_metrics.Metrics, resManager manager.ResourceManager, interval time.Duration, + generators []HostnameGenerator, ) (*Generator, error) { metric := prometheus.NewSummary(prometheus.SummaryOpts{ - Name: "component_hostname_generator_ms", + Name: "component_hostname_generator", Help: "Summary of hostname generator interval", Objectives: core_metrics.DefaultObjectives, }) if err := metrics.Register(metric); err != nil { return nil, err } - return &Generator{ logger: logger, resManager: resManager, interval: interval, metric: metric, + generators: generators, }, nil } @@ -77,42 +83,6 @@ func (g *Generator) Start(stop <-chan struct{}) error { } } -func apply(generator *hostnamegenerator_api.HostnameGeneratorResource, service *meshservice_api.MeshServiceResource) (string, error) { - if !generator.Spec.Selector.MeshService.Matches(service.Meta.GetLabels()) { - return "", nil - } - sb := strings.Builder{} - tmpl := template.New("").Funcs( - map[string]any{ - "label": func(key string) (string, error) { - val, ok := service.GetMeta().GetLabels()[key] - if !ok { - return "", errors.Errorf("label %s not found", key) - } - return val, nil - }, - }, - ) - tmpl, err := tmpl.Parse(generator.Spec.Template) - if err != nil { - return "", fmt.Errorf("failed compiling gotemplate error=%q", err.Error()) - } - type meshedName struct { - Name string - Namespace string - Mesh string - } - err = tmpl.Execute(&sb, meshedName{ - Name: service.GetMeta().GetNameExtensions()[model.K8sNameComponent], - Namespace: service.GetMeta().GetNameExtensions()[model.K8sNamespaceComponent], - Mesh: service.GetMeta().GetMesh(), - }) - if err != nil { - return "", fmt.Errorf("pre evaluation of template with parameters failed with error=%q", err.Error()) - } - return sb.String(), nil -} - func sortGenerators(generators []*hostnamegenerator_api.HostnameGeneratorResource) []*hostnamegenerator_api.HostnameGeneratorResource { sorted := slices.Clone(generators) slices.SortFunc(sorted, func(a, b *hostnamegenerator_api.HostnameGeneratorResource) int { @@ -134,129 +104,129 @@ func sortGenerators(generators []*hostnamegenerator_api.HostnameGeneratorResourc } func (g *Generator) generateHostnames(ctx context.Context) error { - services := &meshservice_api.MeshServiceResourceList{} - if err := g.resManager.List(ctx, services); err != nil { - return errors.Wrap(err, "could not list MeshServices") - } - generators := &hostnamegenerator_api.HostnameGeneratorResourceList{} if err := g.resManager.List(ctx, generators); err != nil { return errors.Wrap(err, "could not list HostnameGenerators") } - type serviceKey struct { name string mesh string } type status struct { hostname string - conditions []meshservice_api.Condition + conditions []hostnamegenerator_api.Condition } - type meshName string - type serviceName string - type hostname string - generatedHostnames := map[meshName]map[hostname]serviceName{} - newStatuses := map[serviceKey]map[string]status{} - for _, generator := range sortGenerators(generators.Items) { - for _, service := range services.Items { - serviceKey := serviceKey{ - name: service.GetMeta().GetName(), - mesh: service.GetMeta().GetMesh(), - } - generatorStatuses, ok := newStatuses[serviceKey] - if !ok { - generatorStatuses = map[string]status{} - } + for _, generatorType := range g.generators { + resources, err := generatorType.GetResources(ctx) + if err != nil { + return err + } + type meshName string + type serviceName string + type hostname string + generatedHostnames := map[meshName]map[hostname]serviceName{} + newStatuses := map[serviceKey]map[string]status{} + for _, generator := range sortGenerators(generators.Items) { + for _, service := range resources.GetItems() { + serviceKey := serviceKey{ + name: service.GetMeta().GetName(), + mesh: service.GetMeta().GetMesh(), + } + generatorStatuses, ok := newStatuses[serviceKey] + if !ok { + generatorStatuses = map[string]status{} + } - generated, err := apply(generator, service) + generated, err := generatorType.GenerateHostname(generator, service) - var conditions []meshservice_api.Condition - if generated != "" || err != nil { - generationConditionStatus := kube_meta.ConditionUnknown - reason := "Pending" - var message string - if err != nil { - generationConditionStatus = kube_meta.ConditionFalse - reason = meshservice_api.TemplateErrorReason - message = err.Error() - } - if generated != "" { - if svcName, ok := generatedHostnames[meshName(serviceKey.mesh)][hostname(generated)]; ok && string(svcName) != serviceKey.name { + var conditions []hostnamegenerator_api.Condition + if generated != "" || err != nil { + generationConditionStatus := kube_meta.ConditionUnknown + reason := "Pending" + var message string + if err != nil { generationConditionStatus = kube_meta.ConditionFalse - reason = meshservice_api.CollisionReason - message = fmt.Sprintf("Hostname collision with MeshService %s", serviceKey.name) - generated = "" - } else { - generationConditionStatus = kube_meta.ConditionTrue - reason = meshservice_api.GeneratedReason - meshHostnames, ok := generatedHostnames[meshName(serviceKey.mesh)] - if !ok { - meshHostnames = map[hostname]serviceName{} + reason = hostnamegenerator_api.TemplateErrorReason + message = err.Error() + } + if generated != "" { + if svcName, ok := generatedHostnames[meshName(serviceKey.mesh)][hostname(generated)]; ok && string(svcName) != serviceKey.name { + generationConditionStatus = kube_meta.ConditionFalse + reason = hostnamegenerator_api.CollisionReason + message = fmt.Sprintf("Hostname collision with %s: %s", resources.GetItemType(), serviceKey.name) + generated = "" + } else { + generationConditionStatus = kube_meta.ConditionTrue + reason = hostnamegenerator_api.GeneratedReason + meshHostnames, ok := generatedHostnames[meshName(serviceKey.mesh)] + if !ok { + meshHostnames = map[hostname]serviceName{} + } + meshHostnames[hostname(generated)] = serviceName(serviceKey.name) + generatedHostnames[meshName(serviceKey.mesh)] = meshHostnames } - meshHostnames[hostname(generated)] = serviceName(serviceKey.name) - generatedHostnames[meshName(serviceKey.mesh)] = meshHostnames + } + condition := hostnamegenerator_api.Condition{ + Type: hostnamegenerator_api.GeneratedCondition, + Status: generationConditionStatus, + Reason: reason, + Message: message, + } + conditions = []hostnamegenerator_api.Condition{ + condition, } } - condition := meshservice_api.Condition{ - Type: meshservice_api.GeneratedCondition, - Status: generationConditionStatus, - Reason: reason, - Message: message, - } - conditions = []meshservice_api.Condition{ - condition, - } - } - generatorStatuses[generator.GetMeta().GetName()] = status{ - hostname: generated, - conditions: conditions, + generatorStatuses[generator.GetMeta().GetName()] = status{ + hostname: generated, + conditions: conditions, + } + newStatuses[serviceKey] = generatorStatuses } - newStatuses[serviceKey] = generatorStatuses } - } - - for _, service := range services.Items { - statuses := newStatuses[serviceKey{ - name: service.GetMeta().GetName(), - mesh: service.GetMeta().GetMesh(), - }] - var addresses []meshservice_api.Address - var generatorStatuses []meshservice_api.HostnameGeneratorStatus - - for _, generator := range maps.SortedKeys(statuses) { - status := statuses[generator] - ref := meshservice_api.HostnameGeneratorRef{ - CoreName: generator, + for _, service := range resources.GetItems() { + statuses := newStatuses[serviceKey{ + name: service.GetMeta().GetName(), + mesh: service.GetMeta().GetMesh(), + }] + var addresses []hostnamegenerator_api.Address + var generatorStatuses []hostnamegenerator_api.HostnameGeneratorStatus + + for _, generator := range maps.SortedKeys(statuses) { + status := statuses[generator] + ref := hostnamegenerator_api.HostnameGeneratorRef{ + CoreName: generator, + } + if status.hostname == "" && len(status.conditions) == 0 { + continue + } + if status.hostname != "" { + addresses = append( + addresses, + hostnamegenerator_api.Address{ + Hostname: status.hostname, + Origin: hostnamegenerator_api.OriginGenerator, + HostnameGeneratorRef: ref, + }, + ) + } + generatorStatuses = append(generatorStatuses, hostnamegenerator_api.HostnameGeneratorStatus{ + HostnameGeneratorRef: ref, + Conditions: status.conditions, + }) + } + changed, changedErr := generatorType.HasStatusChanged(service, generatorStatuses, addresses) + if changedErr != nil { + return errors.Wrapf(changedErr, "couldn't check %s status", resources.GetItemType()) } - if status.hostname == "" && len(status.conditions) == 0 { + if !changed { continue } - if status.hostname != "" { - addresses = append( - addresses, - meshservice_api.Address{ - Hostname: status.hostname, - Origin: meshservice_api.OriginGenerator, - HostnameGeneratorRef: ref, - }, - ) + if err := generatorType.UpdateResourceStatus(ctx, service, generatorStatuses, addresses); err != nil { + return errors.Wrapf(err, "couldn't update %s status", resources.GetItemType()) } - generatorStatuses = append(generatorStatuses, meshservice_api.HostnameGeneratorStatus{ - HostnameGeneratorRef: ref, - Conditions: status.conditions, - }) - } - if reflect.DeepEqual(addresses, service.Status.Addresses) && reflect.DeepEqual(generatorStatuses, service.Status.HostnameGenerators) { - continue - } - service.Status.Addresses = addresses - service.Status.HostnameGenerators = generatorStatuses - if err := g.resManager.Update(ctx, service); err != nil { - return errors.Wrap(err, "couldn't update MeshService status") } } - return nil } diff --git a/pkg/core/resources/apis/hostnamegenerator/k8s/crd/kuma.io_hostnamegenerators.yaml b/pkg/core/resources/apis/hostnamegenerator/k8s/crd/kuma.io_hostnamegenerators.yaml index 33e6e2e1d3ff..c1d59d39cc8c 100644 --- a/pkg/core/resources/apis/hostnamegenerator/k8s/crd/kuma.io_hostnamegenerators.yaml +++ b/pkg/core/resources/apis/hostnamegenerator/k8s/crd/kuma.io_hostnamegenerators.yaml @@ -42,6 +42,15 @@ spec: properties: selector: properties: + meshExternalService: + properties: + matchLabels: + additionalProperties: + type: string + type: object + matchName: + type: string + type: object meshService: properties: matchLabels: diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go index de485e8d2e14..3e0c74dbff3f 100644 --- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go +++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/meshexternalservice.go @@ -5,6 +5,7 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "github.com/kumahq/kuma/api/common/v1alpha1" + hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" ) // MeshExternalService @@ -156,7 +157,8 @@ type MeshExternalServiceStatus struct { // Vip section for allocated IP VIP VIP `json:"vip,omitempty"` // Addresses section for generated domains - Addresses []Address `json:"addresses,omitempty"` + Addresses []hostnamegenerator_api.Address `json:"addresses,omitempty"` + HostnameGenerators []hostnamegenerator_api.HostnameGeneratorStatus `json:"hostnameGenerators,omitempty"` } // +kubebuilder:validation:Enum=Kuma @@ -166,32 +168,3 @@ type VIP struct { // Value allocated IP for a provided domain with `HostnameGenerator` type in a match section. IP string `json:"ip,omitempty"` } - -type Origin string - -const ( - OriginGenerator Origin = "HostnameGenerator" -) - -type Address struct { - // Hostname of the generated domain - Hostname string `json:"hostname,omitempty"` - // Origin provides information what generated the vip - Origin Origin `json:"origin"` - // HostnameGeneratorRef informes which generator was used - HostnameGeneratorRef HostnameGeneratorRef `json:"hostnameGeneratorRef,omitempty"` -} - -type HostnameGeneratorRef struct { - CoreName string `json:"name"` -} - -// +kubebuilder:validation:Enum=HostnameGenerator -type OriginKind string - -type AddressOrigin struct { - // Kind points to entity kind that generated the domain. - Kind OriginKind `json:"kind"` - // Name of the entity that generated the domain. - Name string `json:"name"` -} diff --git a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go index a704073b4107..d3cbd00f60ad 100644 --- a/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/core/resources/apis/meshexternalservice/api/v1alpha1/zz_generated.deepcopy.go @@ -6,40 +6,10 @@ package v1alpha1 import ( commonv1alpha1 "github.com/kumahq/kuma/api/common/v1alpha1" + apiv1alpha1 "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Address) DeepCopyInto(out *Address) { - *out = *in - out.HostnameGeneratorRef = in.HostnameGeneratorRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. -func (in *Address) DeepCopy() *Address { - if in == nil { - return nil - } - out := new(Address) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AddressOrigin) DeepCopyInto(out *AddressOrigin) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressOrigin. -func (in *AddressOrigin) DeepCopy() *AddressOrigin { - if in == nil { - return nil - } - out := new(AddressOrigin) - in.DeepCopyInto(out) - 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 @@ -80,21 +50,6 @@ func (in *Extension) DeepCopy() *Extension { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HostnameGeneratorRef) DeepCopyInto(out *HostnameGeneratorRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnameGeneratorRef. -func (in *HostnameGeneratorRef) DeepCopy() *HostnameGeneratorRef { - if in == nil { - return nil - } - out := new(HostnameGeneratorRef) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Match) DeepCopyInto(out *Match) { *out = *in @@ -149,9 +104,16 @@ func (in *MeshExternalServiceStatus) DeepCopyInto(out *MeshExternalServiceStatus out.VIP = in.VIP if in.Addresses != nil { in, out := &in.Addresses, &out.Addresses - *out = make([]Address, len(*in)) + *out = make([]apiv1alpha1.Address, len(*in)) copy(*out, *in) } + if in.HostnameGenerators != nil { + in, out := &in.HostnameGenerators, &out.HostnameGenerators + *out = make([]apiv1alpha1.HostnameGeneratorStatus, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MeshExternalServiceStatus. diff --git a/pkg/core/resources/apis/meshexternalservice/hostname/generator.go b/pkg/core/resources/apis/meshexternalservice/hostname/generator.go new file mode 100644 index 000000000000..c991223c8e6f --- /dev/null +++ b/pkg/core/resources/apis/meshexternalservice/hostname/generator.go @@ -0,0 +1,104 @@ +package hostname + +import ( + "context" + "fmt" + "reflect" + "strings" + "text/template" + + "github.com/pkg/errors" + + hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" + "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/hostname" + meshexternalservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshexternalservice/api/v1alpha1" + "github.com/kumahq/kuma/pkg/core/resources/manager" + "github.com/kumahq/kuma/pkg/core/resources/model" +) + +type MeshExternalServiceHostnameGenerator struct { + resManager manager.ResourceManager +} + +var _ hostname.HostnameGenerator = &MeshExternalServiceHostnameGenerator{} + +func NewMeshExternalServiceHostnameGenerator( + resManager manager.ResourceManager, +) *MeshExternalServiceHostnameGenerator { + return &MeshExternalServiceHostnameGenerator{ + resManager: resManager, + } +} + +func (g *MeshExternalServiceHostnameGenerator) GetResources(ctx context.Context) (model.ResourceList, error) { + resources := &meshexternalservice_api.MeshExternalServiceResourceList{} + if err := g.resManager.List(ctx, resources); err != nil { + return nil, errors.Wrap(err, "could not list MeshExternalServices") + } + return resources, nil +} + +func (g *MeshExternalServiceHostnameGenerator) UpdateResourceStatus(ctx context.Context, resource model.Resource, statuses []hostnamegenerator_api.HostnameGeneratorStatus, addresses []hostnamegenerator_api.Address) error { + externalService, ok := resource.(*meshexternalservice_api.MeshExternalServiceResource) + if !ok { + return errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshexternalservice_api.MeshExternalServiceResource)(nil), resource) + } + externalService.Status.Addresses = addresses + externalService.Status.HostnameGenerators = statuses + if err := g.resManager.Update(ctx, externalService); err != nil { + return errors.Wrap(err, "couldn't update MeshExternalService status") + } + return nil +} + +func (g *MeshExternalServiceHostnameGenerator) HasStatusChanged(resource model.Resource, generatorStatuses []hostnamegenerator_api.HostnameGeneratorStatus, addresses []hostnamegenerator_api.Address) (bool, error) { + es, ok := resource.(*meshexternalservice_api.MeshExternalServiceResource) + if !ok { + return false, errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshexternalservice_api.MeshExternalServiceResource)(nil), resource) + } + return !reflect.DeepEqual(addresses, es.Status.Addresses) || !reflect.DeepEqual(generatorStatuses, es.Status.HostnameGenerators), nil +} + +func (g *MeshExternalServiceHostnameGenerator) GenerateHostname(generator *hostnamegenerator_api.HostnameGeneratorResource, resource model.Resource) (string, error) { + es, ok := resource.(*meshexternalservice_api.MeshExternalServiceResource) + if !ok { + return "", errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshexternalservice_api.MeshExternalServiceResource)(nil), resource) + } + if !generator.Spec.Selector.MeshExternalService.Matches(es.Meta.GetName(), es.Meta.GetLabels()) { + return "", nil + } + sb := strings.Builder{} + tmpl := template.New("").Funcs( + map[string]any{ + "label": func(key string) (string, error) { + val, ok := es.GetMeta().GetLabels()[key] + if !ok { + return "", errors.Errorf("label %s not found", key) + } + return val, nil + }, + }, + ) + tmpl, err := tmpl.Parse(generator.Spec.Template) + if err != nil { + return "", fmt.Errorf("failed compiling gotemplate error=%q", err.Error()) + } + type meshedName struct { + Name string + Namespace string + Mesh string + } + name := es.GetMeta().GetNameExtensions()[model.K8sNameComponent] + if name == "" { + name = es.GetMeta().GetName() + } + err = tmpl.Execute(&sb, meshedName{ + Name: name, + Namespace: es.GetMeta().GetNameExtensions()[model.K8sNamespaceComponent], + Mesh: es.GetMeta().GetMesh(), + }) + if err != nil { + return "", fmt.Errorf("pre evaluation of template with parameters failed with error=%q", err.Error()) + } + return sb.String(), nil +} diff --git a/pkg/core/resources/apis/meshexternalservice/hostname/generator_test.go b/pkg/core/resources/apis/meshexternalservice/hostname/generator_test.go new file mode 100644 index 000000000000..12e6d6fbe608 --- /dev/null +++ b/pkg/core/resources/apis/meshexternalservice/hostname/generator_test.go @@ -0,0 +1,193 @@ +package hostname_test + +import ( + "context" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + kube_meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" + "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/hostname" + meshexternalservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshexternalservice/api/v1alpha1" + mes_hostname "github.com/kumahq/kuma/pkg/core/resources/apis/meshexternalservice/hostname" + "github.com/kumahq/kuma/pkg/core/resources/manager" + "github.com/kumahq/kuma/pkg/core/resources/model" + core_model "github.com/kumahq/kuma/pkg/core/resources/model" + "github.com/kumahq/kuma/pkg/core/resources/store" + core_metrics "github.com/kumahq/kuma/pkg/metrics" + "github.com/kumahq/kuma/pkg/plugins/resources/memory" + test_model "github.com/kumahq/kuma/pkg/test/resources/model" + "github.com/kumahq/kuma/pkg/test/resources/samples" +) + +var _ = Describe("MeshExternalService Hostname Generator", func() { + var stopChSend chan<- struct{} + var resManager manager.ResourceManager + + BeforeEach(func() { + m, err := core_metrics.NewMetrics("") + Expect(err).ToNot(HaveOccurred()) + resManager = manager.NewResourceManager(memory.NewStore()) + allocator, err := hostname.NewGenerator( + logr.Discard(), m, resManager, 50*time.Millisecond, + []hostname.HostnameGenerator{mes_hostname.NewMeshExternalServiceHostnameGenerator(resManager)}, + ) + Expect(err).ToNot(HaveOccurred()) + ch := make(chan struct{}) + var stopChRecv <-chan struct{} + stopChSend, stopChRecv = ch, ch + go func() { + defer GinkgoRecover() + Expect(allocator.Start(stopChRecv)).To(Succeed()) + }() + + Expect(samples.MeshDefaultBuilder().Create(resManager)).To(Succeed()) + + generator := hostnamegenerator_api.NewHostnameGeneratorResource() + generator.Meta = &test_model.ResourceMeta{ + Mesh: core_model.DefaultMesh, + Name: "byname", + } + generator.Spec = &hostnamegenerator_api.HostnameGenerator{ + Template: "{{ .Name }}.byname.mesh", + Selector: hostnamegenerator_api.Selector{ + MeshExternalService: hostnamegenerator_api.NameLabelsSelector{ + MatchName: "test-external-svc", + }, + }, + } + Expect(resManager.Create(context.Background(), generator, store.CreateBy(core_model.MetaToResourceKey(generator.GetMeta())))).To(Succeed()) + generator = hostnamegenerator_api.NewHostnameGeneratorResource() + generator.Meta = &test_model.ResourceMeta{ + Mesh: core_model.DefaultMesh, + Name: "example", + } + generator.Spec = &hostnamegenerator_api.HostnameGenerator{ + Template: "{{ .Name }}.mesh", + Selector: hostnamegenerator_api.Selector{ + MeshExternalService: hostnamegenerator_api.NameLabelsSelector{ + MatchLabels: map[string]string{ + "label": "value", + }, + }, + }, + } + Expect(resManager.Create(context.Background(), generator, store.CreateBy(core_model.MetaToResourceKey(generator.GetMeta())))).To(Succeed()) + generator = hostnamegenerator_api.NewHostnameGeneratorResource() + generator.Meta = &test_model.ResourceMeta{ + Mesh: core_model.DefaultMesh, + Name: "static", + } + generator.Spec = &hostnamegenerator_api.HostnameGenerator{ + Template: "static.mesh", + Selector: hostnamegenerator_api.Selector{ + MeshExternalService: hostnamegenerator_api.NameLabelsSelector{ + MatchLabels: map[string]string{ + "generate": "static", + }, + }, + }, + } + Expect(resManager.Create(context.Background(), generator, store.CreateBy(core_model.MetaToResourceKey(generator.GetMeta())))).To(Succeed()) + }) + + AfterEach(func() { + close(stopChSend) + Expect(resManager.DeleteAll(context.Background(), &meshexternalservice_api.MeshExternalServiceResourceList{})).To(Succeed()) + }) + + vipOfMeshExternalService := func(name string) *meshexternalservice_api.MeshExternalServiceStatus { + ms := meshexternalservice_api.NewMeshExternalServiceResource() + err := resManager.Get(context.Background(), ms, store.GetByKey(name, model.DefaultMesh)) + Expect(err).ToNot(HaveOccurred()) + return ms.Status + } + + It("should not generate hostname if no generator selects a given MeshExternalService", func() { + // when + err := samples.MeshExternalServiceExampleBuilder().WithoutVIP().WithName("example").Create(resManager) + Expect(err).ToNot(HaveOccurred()) + + // then + Consistently(func(g Gomega) { + status := vipOfMeshExternalService("example") + g.Expect(status.Addresses).Should(BeEmpty()) + g.Expect(status.HostnameGenerators).Should(BeEmpty()) + }, "10s", "100ms").Should(Succeed()) + }) + + It("should generate hostname if a generator selects a given MeshExternalService", func() { + // when + err := samples.MeshExternalServiceExampleBuilder().WithoutVIP().WithLabels(map[string]string{ + "label": "value", + }).Create(resManager) + Expect(err).ToNot(HaveOccurred()) + + // then + Eventually(func(g Gomega) { + status := vipOfMeshExternalService("example") + g.Expect(status.Addresses).Should(Not(BeEmpty())) + g.Expect(status.HostnameGenerators).Should(Not(BeEmpty())) + }, "2s", "100ms").Should(Succeed()) + }) + + It("should generate hostname if a generator selects a given MeshExternalService by name", func() { + // when + err := samples.MeshExternalServiceExampleBuilder().WithoutVIP().WithName("test-external-svc").Create(resManager) + Expect(err).ToNot(HaveOccurred()) + + // then + Eventually(func(g Gomega) { + status := vipOfMeshExternalService("test-external-svc") + g.Expect(status.Addresses).Should(Not(BeEmpty())) + g.Expect(status.Addresses[0].Hostname).Should(Equal("test-external-svc.byname.mesh")) + g.Expect(status.HostnameGenerators).Should(Not(BeEmpty())) + }, "2000s", "100ms").Should(Succeed()) + }) + + It("should set an error if there's a collision", func() { + // when + Expect( + samples.MeshExternalServiceExampleBuilder().WithoutVIP().WithLabels(map[string]string{ + "generate": "static", + }).Create(resManager), + ).To(Succeed()) + Expect( + samples.MeshExternalServiceExampleBuilder().WithoutVIP().WithLabels(map[string]string{ + "generate": "static", + }).WithName("other").Create(resManager), + ).To(Succeed()) + + // then + Eventually(func(g Gomega) { + otherStatus := vipOfMeshExternalService("other") + exampleStatus := vipOfMeshExternalService("example") + g.Expect(otherStatus.Addresses).Should(BeEmpty()) + g.Expect(otherStatus.HostnameGenerators).Should(ConsistOf( + hostnamegenerator_api.HostnameGeneratorStatus{ + HostnameGeneratorRef: hostnamegenerator_api.HostnameGeneratorRef{CoreName: "static"}, + Conditions: []hostnamegenerator_api.Condition{{ + Type: hostnamegenerator_api.GeneratedCondition, + Status: kube_meta.ConditionFalse, + Reason: hostnamegenerator_api.CollisionReason, + Message: "Hostname collision with MeshExternalService: other", + }}, + }, + )) + g.Expect(exampleStatus.Addresses).Should(Not(BeEmpty())) + g.Expect(exampleStatus.HostnameGenerators).Should(ConsistOf( + hostnamegenerator_api.HostnameGeneratorStatus{ + HostnameGeneratorRef: hostnamegenerator_api.HostnameGeneratorRef{CoreName: "static"}, + Conditions: []hostnamegenerator_api.Condition{{ + Type: hostnamegenerator_api.GeneratedCondition, + Status: kube_meta.ConditionTrue, + Reason: hostnamegenerator_api.GeneratedReason, + }}, + }, + )) + }, "2s", "100ms").Should(Succeed()) + }) +}) diff --git a/pkg/core/resources/apis/meshexternalservice/hostname/suite_test.go b/pkg/core/resources/apis/meshexternalservice/hostname/suite_test.go new file mode 100644 index 000000000000..d1a81c3e878a --- /dev/null +++ b/pkg/core/resources/apis/meshexternalservice/hostname/suite_test.go @@ -0,0 +1,11 @@ +package hostname_test + +import ( + "testing" + + "github.com/kumahq/kuma/pkg/test" +) + +func TestHostnameGenerator(t *testing.T) { + test.RunSpecs(t, "MeshExternalServiceHostnameGenerator Suite") +} diff --git a/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml b/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml index 0347bd0a2243..013c20207bda 100644 --- a/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml +++ b/pkg/core/resources/apis/meshexternalservice/k8s/crd/kuma.io_meshexternalservices.yaml @@ -238,11 +238,8 @@ spec: items: properties: hostname: - description: Hostname of the generated domain type: string hostnameGeneratorRef: - description: HostnameGeneratorRef informes which generator was - used properties: name: type: string @@ -250,11 +247,70 @@ spec: - name type: object origin: - description: Origin provides information what generated the - vip type: string + type: object + type: array + hostnameGenerators: + items: + properties: + conditions: + description: Conditions is an array of hostname generator conditions. + items: + properties: + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + 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: + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + hostnameGeneratorRef: + properties: + name: + type: string + required: + - name + type: object required: - - origin + - hostnameGeneratorRef type: object type: array vip: diff --git a/pkg/core/resources/apis/meshservice/api/v1alpha1/meshservice.go b/pkg/core/resources/apis/meshservice/api/v1alpha1/meshservice.go index f441a20d7967..2a7ab0e8f579 100644 --- a/pkg/core/resources/apis/meshservice/api/v1alpha1/meshservice.go +++ b/pkg/core/resources/apis/meshservice/api/v1alpha1/meshservice.go @@ -2,9 +2,9 @@ package v1alpha1 import ( - kube_meta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh" ) @@ -39,19 +39,6 @@ type MeshService struct { Ports []Port `json:"ports,omitempty"` } -type Origin string - -const ( - OriginGenerator Origin = "HostnameGenerator" - OriginKubernetes Origin = "Kubernetes" -) - -type Address struct { - Hostname string `json:"hostname,omitempty"` - Origin Origin `json:"origin,omitempty"` - HostnameGeneratorRef HostnameGeneratorRef `json:"hostnameGeneratorRef,omitempty"` -} - type VIP struct { IP string `json:"ip,omitempty"` } @@ -68,71 +55,9 @@ type TLS struct { Status TLSStatus `json:"status,omitempty"` } -type HostnameGeneratorRef struct { - CoreName string `json:"name"` -} - -const ( - GeneratedCondition string = "Generated" -) - -const ( - GeneratedReason string = "Generated" - TemplateErrorReason string = "TemplateError" - CollisionReason string = "Collision" -) - -type Condition struct { - // 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) - // +required - // +kubebuilder:validation:Required - // +kubebuilder:validation: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])$` - // +kubebuilder:validation:MaxLength=316 - Type string `json:"type"` - // status of the condition, one of True, False, Unknown. - // +required - // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=True;False;Unknown - Status kube_meta.ConditionStatus `json:"status"` - // 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. - // +required - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=1024 - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` - Reason string `json:"reason"` - // message is a human readable message indicating details about the transition. - // This may be an empty string. - // +required - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=32768 - Message string `json:"message"` -} - -type HostnameGeneratorStatus struct { - HostnameGeneratorRef HostnameGeneratorRef `json:"hostnameGeneratorRef"` - - // Conditions is an array of gateway instance conditions. - // - // +optional - // +patchMergeKey=type - // +patchStrategy=merge - // +listType=map - // +listMapKey=type - Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` -} - type MeshServiceStatus struct { - Addresses []Address `json:"addresses,omitempty"` - VIPs []VIP `json:"vips,omitempty"` - TLS TLS `json:"tls,omitempty"` - HostnameGenerators []HostnameGeneratorStatus `json:"hostnameGenerators,omitempty"` + Addresses []hostnamegenerator_api.Address `json:"addresses,omitempty"` + VIPs []VIP `json:"vips,omitempty"` + TLS TLS `json:"tls,omitempty"` + HostnameGenerators []hostnamegenerator_api.HostnameGeneratorStatus `json:"hostnameGenerators,omitempty"` } diff --git a/pkg/core/resources/apis/meshservice/api/v1alpha1/zz_generated.deepcopy.go b/pkg/core/resources/apis/meshservice/api/v1alpha1/zz_generated.deepcopy.go index 9bc079231e5e..6aac3bfd7211 100644 --- a/pkg/core/resources/apis/meshservice/api/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/core/resources/apis/meshservice/api/v1alpha1/zz_generated.deepcopy.go @@ -4,38 +4,9 @@ package v1alpha1 -import () - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Address) DeepCopyInto(out *Address) { - *out = *in - out.HostnameGeneratorRef = in.HostnameGeneratorRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. -func (in *Address) DeepCopy() *Address { - if in == nil { - return nil - } - out := new(Address) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Condition) DeepCopyInto(out *Condition) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. -func (in *Condition) DeepCopy() *Condition { - if in == nil { - return nil - } - out := new(Condition) - in.DeepCopyInto(out) - return out -} +import ( + apiv1alpha1 "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" +) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DataplaneRef) DeepCopyInto(out *DataplaneRef) { @@ -73,42 +44,6 @@ func (in DataplaneTags) DeepCopy() DataplaneTags { return *out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HostnameGeneratorRef) DeepCopyInto(out *HostnameGeneratorRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnameGeneratorRef. -func (in *HostnameGeneratorRef) DeepCopy() *HostnameGeneratorRef { - if in == nil { - return nil - } - out := new(HostnameGeneratorRef) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HostnameGeneratorStatus) DeepCopyInto(out *HostnameGeneratorStatus) { - *out = *in - out.HostnameGeneratorRef = in.HostnameGeneratorRef - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]Condition, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostnameGeneratorStatus. -func (in *HostnameGeneratorStatus) DeepCopy() *HostnameGeneratorStatus { - if in == nil { - return nil - } - out := new(HostnameGeneratorStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MeshService) DeepCopyInto(out *MeshService) { *out = *in @@ -135,7 +70,7 @@ func (in *MeshServiceStatus) DeepCopyInto(out *MeshServiceStatus) { *out = *in if in.Addresses != nil { in, out := &in.Addresses, &out.Addresses - *out = make([]Address, len(*in)) + *out = make([]apiv1alpha1.Address, len(*in)) copy(*out, *in) } if in.VIPs != nil { @@ -146,7 +81,7 @@ func (in *MeshServiceStatus) DeepCopyInto(out *MeshServiceStatus) { out.TLS = in.TLS if in.HostnameGenerators != nil { in, out := &in.HostnameGenerators, &out.HostnameGenerators - *out = make([]HostnameGeneratorStatus, len(*in)) + *out = make([]apiv1alpha1.HostnameGeneratorStatus, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/pkg/core/resources/apis/meshservice/hostname/generator.go b/pkg/core/resources/apis/meshservice/hostname/generator.go new file mode 100644 index 000000000000..09ec63197786 --- /dev/null +++ b/pkg/core/resources/apis/meshservice/hostname/generator.go @@ -0,0 +1,105 @@ +package hostname + +import ( + "context" + "fmt" + "reflect" + "strings" + "text/template" + + "github.com/pkg/errors" + + hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" + "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/hostname" + meshservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/api/v1alpha1" + "github.com/kumahq/kuma/pkg/core/resources/manager" + "github.com/kumahq/kuma/pkg/core/resources/model" +) + +type MeshServiceHostnameGenerator struct { + resManager manager.ResourceManager +} + +var _ hostname.HostnameGenerator = &MeshServiceHostnameGenerator{} + +func NewMeshServiceHostnameGenerator( + resManager manager.ResourceManager, +) *MeshServiceHostnameGenerator { + return &MeshServiceHostnameGenerator{ + resManager: resManager, + } +} + +func (g *MeshServiceHostnameGenerator) GetResources(ctx context.Context) (model.ResourceList, error) { + resources := &meshservice_api.MeshServiceResourceList{} + if err := g.resManager.List(ctx, resources); err != nil { + return nil, errors.Wrap(err, "could not list MeshServices") + } + return resources, nil +} + +func (g *MeshServiceHostnameGenerator) UpdateResourceStatus(ctx context.Context, resource model.Resource, statuses []hostnamegenerator_api.HostnameGeneratorStatus, addresses []hostnamegenerator_api.Address) error { + service, ok := resource.(*meshservice_api.MeshServiceResource) + if !ok { + return errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshservice_api.MeshServiceResource)(nil), resource) + } + service.Status.Addresses = addresses + service.Status.HostnameGenerators = statuses + if err := g.resManager.Update(ctx, resource); err != nil { + return errors.Wrap(err, "couldn't update MeshService status") + } + return nil +} + +func (g *MeshServiceHostnameGenerator) HasStatusChanged(resource model.Resource, generatorStatuses []hostnamegenerator_api.HostnameGeneratorStatus, addresses []hostnamegenerator_api.Address) (bool, error) { + service, ok := resource.(*meshservice_api.MeshServiceResource) + if !ok { + return false, errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshservice_api.MeshServiceResource)(nil), resource) + } + + return !reflect.DeepEqual(addresses, service.Status.Addresses) || !reflect.DeepEqual(generatorStatuses, service.Status.HostnameGenerators), nil +} + +func (g *MeshServiceHostnameGenerator) GenerateHostname(generator *hostnamegenerator_api.HostnameGeneratorResource, resource model.Resource) (string, error) { + service, ok := resource.(*meshservice_api.MeshServiceResource) + if !ok { + return "", errors.Errorf("invalid resource type: expected=%T, got=%T", (*meshservice_api.MeshServiceResource)(nil), resource) + } + if !generator.Spec.Selector.MeshService.Matches(service.Meta.GetLabels()) { + return "", nil + } + sb := strings.Builder{} + tmpl := template.New("").Funcs( + map[string]any{ + "label": func(key string) (string, error) { + val, ok := service.GetMeta().GetLabels()[key] + if !ok { + return "", errors.Errorf("label %s not found", key) + } + return val, nil + }, + }, + ) + tmpl, err := tmpl.Parse(generator.Spec.Template) + if err != nil { + return "", fmt.Errorf("failed compiling gotemplate error=%q", err.Error()) + } + type meshedName struct { + Name string + Namespace string + Mesh string + } + name := service.GetMeta().GetNameExtensions()[model.K8sNameComponent] + if name == "" { + name = service.GetMeta().GetName() + } + err = tmpl.Execute(&sb, meshedName{ + Name: name, + Namespace: service.GetMeta().GetNameExtensions()[model.K8sNamespaceComponent], + Mesh: service.GetMeta().GetMesh(), + }) + if err != nil { + return "", fmt.Errorf("pre evaluation of template with parameters failed with error=%q", err.Error()) + } + return sb.String(), nil +} diff --git a/pkg/core/resources/apis/hostnamegenerator/hostname/generator_test.go b/pkg/core/resources/apis/meshservice/hostname/generator_test.go similarity index 82% rename from pkg/core/resources/apis/hostnamegenerator/hostname/generator_test.go rename to pkg/core/resources/apis/meshservice/hostname/generator_test.go index bff9c2951401..4d4214cb1e7f 100644 --- a/pkg/core/resources/apis/hostnamegenerator/hostname/generator_test.go +++ b/pkg/core/resources/apis/meshservice/hostname/generator_test.go @@ -12,6 +12,7 @@ import ( hostnamegenerator_api "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/api/v1alpha1" "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/hostname" meshservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/api/v1alpha1" + meshservice_hostname "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/hostname" "github.com/kumahq/kuma/pkg/core/resources/manager" "github.com/kumahq/kuma/pkg/core/resources/model" core_model "github.com/kumahq/kuma/pkg/core/resources/model" @@ -22,7 +23,7 @@ import ( "github.com/kumahq/kuma/pkg/test/resources/samples" ) -var _ = Describe("Hostname Generator", func() { +var _ = Describe("MeshService Hostname Generator", func() { var stopChSend chan<- struct{} var resManager manager.ResourceManager @@ -30,7 +31,10 @@ var _ = Describe("Hostname Generator", func() { m, err := core_metrics.NewMetrics("") Expect(err).ToNot(HaveOccurred()) resManager = manager.NewResourceManager(memory.NewStore()) - allocator, err := hostname.NewGenerator(logr.Discard(), m, resManager, 50*time.Millisecond) + allocator, err := hostname.NewGenerator( + logr.Discard(), m, resManager, 50*time.Millisecond, + []hostname.HostnameGenerator{meshservice_hostname.NewMeshServiceHostnameGenerator(resManager)}, + ) Expect(err).ToNot(HaveOccurred()) ch := make(chan struct{}) var stopChRecv <-chan struct{} @@ -135,24 +139,24 @@ var _ = Describe("Hostname Generator", func() { backendStatus := vipOfMeshService("backend") g.Expect(otherStatus.Addresses).Should(BeEmpty()) g.Expect(otherStatus.HostnameGenerators).Should(ConsistOf( - meshservice_api.HostnameGeneratorStatus{ - HostnameGeneratorRef: meshservice_api.HostnameGeneratorRef{CoreName: "static"}, - Conditions: []meshservice_api.Condition{{ - Type: meshservice_api.GeneratedCondition, + hostnamegenerator_api.HostnameGeneratorStatus{ + HostnameGeneratorRef: hostnamegenerator_api.HostnameGeneratorRef{CoreName: "static"}, + Conditions: []hostnamegenerator_api.Condition{{ + Type: hostnamegenerator_api.GeneratedCondition, Status: kube_meta.ConditionFalse, - Reason: meshservice_api.CollisionReason, - Message: "Hostname collision with MeshService other", + Reason: hostnamegenerator_api.CollisionReason, + Message: "Hostname collision with MeshService: other", }}, }, )) g.Expect(backendStatus.Addresses).Should(Not(BeEmpty())) g.Expect(backendStatus.HostnameGenerators).Should(ConsistOf( - meshservice_api.HostnameGeneratorStatus{ - HostnameGeneratorRef: meshservice_api.HostnameGeneratorRef{CoreName: "static"}, - Conditions: []meshservice_api.Condition{{ - Type: meshservice_api.GeneratedCondition, + hostnamegenerator_api.HostnameGeneratorStatus{ + HostnameGeneratorRef: hostnamegenerator_api.HostnameGeneratorRef{CoreName: "static"}, + Conditions: []hostnamegenerator_api.Condition{{ + Type: hostnamegenerator_api.GeneratedCondition, Status: kube_meta.ConditionTrue, - Reason: meshservice_api.GeneratedReason, + Reason: hostnamegenerator_api.GeneratedReason, }}, }, )) diff --git a/pkg/core/resources/apis/hostnamegenerator/hostname/suite_test.go b/pkg/core/resources/apis/meshservice/hostname/suite_test.go similarity index 69% rename from pkg/core/resources/apis/hostnamegenerator/hostname/suite_test.go rename to pkg/core/resources/apis/meshservice/hostname/suite_test.go index bf3bf8e2fed0..cb8d4ab8fc04 100644 --- a/pkg/core/resources/apis/hostnamegenerator/hostname/suite_test.go +++ b/pkg/core/resources/apis/meshservice/hostname/suite_test.go @@ -7,5 +7,5 @@ import ( ) func TestHostnameGenerator(t *testing.T) { - test.RunSpecs(t, "HostnameGenerator Suite") + test.RunSpecs(t, "MeshService HostnameGenerator Suite") } diff --git a/pkg/core/resources/apis/meshservice/k8s/crd/kuma.io_meshservices.yaml b/pkg/core/resources/apis/meshservice/k8s/crd/kuma.io_meshservices.yaml index b7d748923186..247db0a8d4ff 100644 --- a/pkg/core/resources/apis/meshservice/k8s/crd/kuma.io_meshservices.yaml +++ b/pkg/core/resources/apis/meshservice/k8s/crd/kuma.io_meshservices.yaml @@ -99,7 +99,7 @@ spec: items: properties: conditions: - description: Conditions is an array of gateway instance conditions. + description: Conditions is an array of hostname generator conditions. items: properties: message: diff --git a/pkg/dns/hostname_generator.go b/pkg/dns/hostname_generator.go index 58b82e9bad6b..0c119374edc9 100644 --- a/pkg/dns/hostname_generator.go +++ b/pkg/dns/hostname_generator.go @@ -6,6 +6,8 @@ import ( config_core "github.com/kumahq/kuma/pkg/config/core" "github.com/kumahq/kuma/pkg/core" "github.com/kumahq/kuma/pkg/core/resources/apis/hostnamegenerator/hostname" + mes_hostname "github.com/kumahq/kuma/pkg/core/resources/apis/meshexternalservice/hostname" + ms_hostname "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/hostname" "github.com/kumahq/kuma/pkg/core/runtime" "github.com/kumahq/kuma/pkg/core/runtime/component" ) @@ -19,11 +21,16 @@ func SetupHostnameGenerator(rt runtime.Runtime) error { logger.Info("HostnameGenerator is not enabled, not starting generator") return nil } + mesGenerator := mes_hostname.NewMeshExternalServiceHostnameGenerator(rt.ResourceManager()) + msGenerator := ms_hostname.NewMeshServiceHostnameGenerator(rt.ResourceManager()) generator, err := hostname.NewGenerator( logger, rt.Metrics(), rt.ResourceManager(), rt.Config().IPAM.AllocationInterval.Duration, + []hostname.HostnameGenerator{ + mesGenerator, msGenerator, + }, ) if err != nil { return err diff --git a/pkg/test/resources/builders/meshexternalservice_builder.go b/pkg/test/resources/builders/meshexternalservice_builder.go index a1d775e588f0..4c11c6d74370 100644 --- a/pkg/test/resources/builders/meshexternalservice_builder.go +++ b/pkg/test/resources/builders/meshexternalservice_builder.go @@ -39,6 +39,11 @@ func MeshExternalService() *MeshExternalServiceBuilder { } } +func (m *MeshExternalServiceBuilder) WithLabels(labels map[string]string) *MeshExternalServiceBuilder { + m.res.Meta.(*test_model.ResourceMeta).Labels = labels + return m +} + func (m *MeshExternalServiceBuilder) WithName(name string) *MeshExternalServiceBuilder { m.res.Meta.(*test_model.ResourceMeta).Name = name return m @@ -67,7 +72,13 @@ func (m *MeshExternalServiceBuilder) Build() *v1alpha1.MeshExternalServiceResour } func (m *MeshExternalServiceBuilder) Create(s store.ResourceStore) error { - return s.Create(context.Background(), m.Build(), store.CreateBy(m.Key())) + opts := []store.CreateOptionsFunc{ + store.CreateBy(m.Key()), + } + if ls := m.res.GetMeta().GetLabels(); len(ls) > 0 { + opts = append(opts, store.CreateWithLabels(ls)) + } + return s.Create(context.Background(), m.Build(), opts...) } func (m *MeshExternalServiceBuilder) Key() core_model.ResourceKey {