diff --git a/library/common-test/tests/service/external_ip_test.yaml b/library/common-test/tests/service/external_ip_test.yaml index 053ee540ed..71dee3c157 100644 --- a/library/common-test/tests/service/external_ip_test.yaml +++ b/library/common-test/tests/service/external_ip_test.yaml @@ -1,6 +1,8 @@ suite: service externalIP test templates: - common.yaml +chart: + appVersion: &appVer v9.9.9 tests: - it: should pass with type externalIP set: @@ -9,7 +11,7 @@ tests: enabled: true primary: true type: ExternalIP - externalName: my-external-name + externalIP: 1.1.1.1 ports: port-name: enabled: true @@ -46,6 +48,8 @@ tests: some_port: 12344 some_target_port: 12346 some_protocol: HTTP + some_external_ip: 1.1.1.1 + some_address_type: IPv6 service: my-service: enabled: true @@ -53,6 +57,7 @@ tests: type: ExternalIP publishNotReadyAddresses: true externalTrafficPolicy: "{{ .Values.some_policy }}" + externalIP: "{{ .Values.some_external_ip }}" externalIPs: - "{{ .Values.some_ip }}" - 10.200.34.54 @@ -60,6 +65,7 @@ tests: sessionAffinityConfig: clientIP: timeoutSeconds: "{{ .Values.some_timeout }}" + addressType: "{{ .Values.some_address_type }}" ports: port-name: enabled: true @@ -93,3 +99,46 @@ tests: port: 12344 protocol: TCP targetPort: 12346 + - documentIndex: &endpointSliceDoc 1 + isKind: + of: EndpointSlice + - documentIndex: *endpointSliceDoc + isAPIVersion: + of: discovery.k8s.io/v1 + - documentIndex: *endpointSliceDoc + equal: + path: metadata.name + value: release-name-common-test + - documentIndex: *endpointSliceDoc + equal: + path: metadata.labels + value: + kubernetes.io/service-name: release-name-common-test + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/version: *appVer + helm.sh/chart: common-test-1.0.0 + helm-revision: 0 + app: common-test-1.0.0 + release: RELEASE-NAME + - documentIndex: *endpointSliceDoc + equal: + path: addressType + value: IPv6 + - documentIndex: *endpointSliceDoc + equal: + path: ports + value: + - name: port-name + port: 12345 + protocol: TCP + appProtocol: tcp + - name: port-name2 + port: 12346 + protocol: TCP + appProtocol: http + - documentIndex: *endpointSliceDoc + equal: + path: endpoints + value: + - addresses: + - 1.1.1.1 diff --git a/library/common-test/tests/service/validation_test.yaml b/library/common-test/tests/service/validation_test.yaml index f4dba5d338..afef2d2ab8 100644 --- a/library/common-test/tests/service/validation_test.yaml +++ b/library/common-test/tests/service/validation_test.yaml @@ -246,6 +246,41 @@ tests: - failedTemplate: errorMessage: Service - Expected to be one of [SingleStack, PreferDualStack, RequireDualStack], but got [not-a-policy] + - it: should fail with empty externalIP + set: + service: + service-name1: + enabled: true + primary: true + type: ExternalIP + externalIP: "" + ports: + port-name1: + enabled: true + primary: true + port: 12345 + asserts: + - failedTemplate: + errorMessage: EndpointSlice - Expected non-empty + + - it: should fail with externalIP not a string + set: + service: + service-name1: + enabled: true + primary: true + type: ExternalIP + externalIP: + - not-a-string + ports: + port-name1: + enabled: true + primary: true + port: 12345 + asserts: + - failedTemplate: + errorMessage: EndpointSlice - Expected to be a [string], but got [slice] + - it: should fail with ipFamilies not a list set: service: diff --git a/library/common/1.0.0/docs/service.md b/library/common/1.0.0/docs/service.md index e624646af4..16318a679f 100644 --- a/library/common/1.0.0/docs/service.md +++ b/library/common/1.0.0/docs/service.md @@ -1,35 +1,37 @@ # Service -| Key | Type | Required | Helm Template | Default | Description | -| :------------------------------------------------------------------- | :-------: | :---------------------: | :----------------: | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------ | -| service | `dict` | ❌ | ❌ | `{}` | Define the service as dicts | -| service.[service-name] | `dict` | ✅ | ❌ | `{}` | Holds service definition | -| service.[service-name].enabled | `boolean` | ✅ | ❌ | `false` | Enables or Disables the service | -| service.[service-name].labels | `dict` | ❌ | ✅ (On value only) | `{}` | Additional labels for service | -| service.[service-name].annotations | `dict` | ❌ | ✅ (On value only) | `{}` | Additional annotations for service | -| service.[service-name].type | `string` | ❌ | ✅ | `ClusterIP` | Define the service type (ClusterIP, LoadBalancer, NodePort, ExternalIP, ExternalName) | -| service.[service-name].publishNotReadyAddresses | `boolean` | ❌ | ❌ | `false` | Define whether to publishNotReadyAddresses or not | -| service.[service-name].externalIPs | `list` | ❌ | ❌ | | Define externalIPs | -| service.[service-name].externalIPs.[externalIP] | `string` | ✅ | ✅ | | The external IP | -| service.[service-name].sharedKey | `string` | ❌ | ✅ | `$FullName` | Custom Shared Key for MetalLB Annotation | -| service.[service-name].clusterIP | `string` | ❌ | ✅ | | Custom Cluster IP | -| service.[service-name].ipFamilyPolicy | `string` | ❌ | ✅ | | Define the ipFamilyPolicy (SingleStack, PreferDualStack, RequireDualStack) | -| service.[service-name].ipFamilies | `list` | ❌ | ❌ | | Define the ipFamilies | -| service.[service-name].ipFamilies.[ipFamily] | `string` | ✅ | ✅ | | Define the ipFamily (IPv4, IPv6) | -| service.[service-name].loadBalancerIP | `string` | ❌ | ✅ | | Define the load balancer IP | -| service.[service-name].loadBalancerSourceRanges | `list` | ❌ | ❌ | | Define the load balancer source ranges | -| service.[service-name].loadBalancerSourceRanges.[source-range] | `string` | ✅ | ✅ | | Define the load balancer source range | -| service.[service-name].externalTrafficPolicy | `string` | ❌ | ✅ | | Define the external traffic policy (Cluster, Local) | -| service.[service-name].sessionAffinity | `string` | ❌ | ✅ | | Define the session affinity (ClientIP, None) | -| service.[service-name].sessionAffinityConfig.clientIP.timeoutSeconds | `int` | ❌ | ✅ | | Define the timeout for ClientIP session affinity (0-86400) | -| service.[service-name].targetSelector | `string` | ❌ | ❌ | `""` | Define the pod to link the service, by default will use the primary pod | -| service.[service-name].ports | `list` | ✅ | ❌ | `{}` | Define the ports of the service | -| service.[service-name].ports.[port-name] | `dict` | ✅ | ❌ | `{}` | Define the port dict | -| service.[service-name].ports.[port-name].port | `int` | ✅ | ✅ | | Define the port that will be exposed by the service | -| service.[service-name].ports.[port-name].nodePort | `int` | ✅ (Only NodePort type) | ✅ | | Define the node port that will be exposed on the node | -| service.[service-name].ports.[port-name].targetPort | `int` | ❌ | ✅ | `[port-name].port` | Define the target port (No named ports, as this will be used to assign the containerPort to containers) | -| service.[service-name].ports.[port-name].protocol | `string` | ❌ | ✅ | `TCP` | Define the port protocol (HTTP, HTTPS, TCP, UDP). (Also used by the container probes, HTTP and HTTPS are converted to TCP on service) | -| service.[service-name].ports.[port-name].targetSelector | `string` | ❌ | ❌ | `TCP` | Define the container to link this port (Must be on under the pod linked above) | +| Key | Type | Required | Helm Template | Default | Description | +| :------------------------------------------------------------------- | :-------: | :-----------------------: | :----------------: | :----------------: | :------------------------------------------------------------------------------------------------------------------------------------ | +| service | `dict` | ❌ | ❌ | `{}` | Define the service as dicts | +| service.[service-name] | `dict` | ✅ | ❌ | `{}` | Holds service definition | +| service.[service-name].enabled | `boolean` | ✅ | ❌ | `false` | Enables or Disables the service | +| service.[service-name].labels | `dict` | ❌ | ✅ (On value only) | `{}` | Additional labels for service | +| service.[service-name].annotations | `dict` | ❌ | ✅ (On value only) | `{}` | Additional annotations for service | +| service.[service-name].type | `string` | ❌ | ✅ | `ClusterIP` | Define the service type (ClusterIP, LoadBalancer, NodePort, ExternalIP, ExternalName) | +| service.[service-name].publishNotReadyAddresses | `boolean` | ❌ | ❌ | `false` | Define whether to publishNotReadyAddresses or not | +| service.[service-name].externalIP | `string` | ✅ (Only ExternalIP type) | ✅ | | Define External IP for headless service | +| service.[service-name].addressType | `string` | ✅ (Only ExternalIP type) | ✅ | `IPv4` | Define the addressType for External IP | +| service.[service-name].externalIPs | `list` | ❌ | ❌ | | Define externalIPs | +| service.[service-name].externalIPs.[externalIP] | `string` | ✅ | ✅ | | The external IP | +| service.[service-name].sharedKey | `string` | ❌ | ✅ | `$FullName` | Custom Shared Key for MetalLB Annotation | +| service.[service-name].clusterIP | `string` | ❌ | ✅ | | Custom Cluster IP | +| service.[service-name].ipFamilyPolicy | `string` | ❌ | ✅ | | Define the ipFamilyPolicy (SingleStack, PreferDualStack, RequireDualStack) | +| service.[service-name].ipFamilies | `list` | ❌ | ❌ | | Define the ipFamilies | +| service.[service-name].ipFamilies.[ipFamily] | `string` | ✅ | ✅ | | Define the ipFamily (IPv4, IPv6) | +| service.[service-name].loadBalancerIP | `string` | ❌ | ✅ | | Define the load balancer IP | +| service.[service-name].loadBalancerSourceRanges | `list` | ❌ | ❌ | | Define the load balancer source ranges | +| service.[service-name].loadBalancerSourceRanges.[source-range] | `string` | ✅ | ✅ | | Define the load balancer source range | +| service.[service-name].externalTrafficPolicy | `string` | ❌ | ✅ | | Define the external traffic policy (Cluster, Local) | +| service.[service-name].sessionAffinity | `string` | ❌ | ✅ | | Define the session affinity (ClientIP, None) | +| service.[service-name].sessionAffinityConfig.clientIP.timeoutSeconds | `int` | ❌ | ✅ | | Define the timeout for ClientIP session affinity (0-86400) | +| service.[service-name].targetSelector | `string` | ❌ | ❌ | `""` | Define the pod to link the service, by default will use the primary pod | +| service.[service-name].ports | `list` | ✅ | ❌ | `{}` | Define the ports of the service | +| service.[service-name].ports.[port-name] | `dict` | ✅ | ❌ | `{}` | Define the port dict | +| service.[service-name].ports.[port-name].port | `int` | ✅ | ✅ | | Define the port that will be exposed by the service | +| service.[service-name].ports.[port-name].nodePort | `int` | ✅ (Only NodePort type) | ✅ | | Define the node port that will be exposed on the node | +| service.[service-name].ports.[port-name].targetPort | `int` | ❌ | ✅ | `[port-name].port` | Define the target port (No named ports, as this will be used to assign the containerPort to containers) | +| service.[service-name].ports.[port-name].protocol | `string` | ❌ | ✅ | `TCP` | Define the port protocol (HTTP, HTTPS, TCP, UDP). (Also used by the container probes, HTTP and HTTPS are converted to TCP on service) | +| service.[service-name].ports.[port-name].targetSelector | `string` | ❌ | ❌ | `TCP` | Define the container to link this port (Must be on under the pod linked above) | --- @@ -62,6 +64,9 @@ service: externalIPs: - 10.200.230.34 sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 86400 targetSelector: pod-name ports: port-name: @@ -88,6 +93,9 @@ service: externalIPs: - 10.200.230.34 sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 86400 externalTrafficPolicy: Cluster targetSelector: pod-name ports: @@ -108,6 +116,9 @@ service: externalIPs: - 10.200.230.34 sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 86400 externalTrafficPolicy: Cluster targetSelector: pod-name ports: @@ -120,6 +131,7 @@ service: targetPort: 8080 nodePort: 30080 + # Special type service-externalname: enabled: true primary: true @@ -130,6 +142,29 @@ service: externalIPs: - 10.200.230.34 sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 86400 + externalTrafficPolicy: Cluster + ports: + port-name: + enabled: true + primary: true + targetSelector: container-name + port: 80 + protocol: HTTP + + # Special type + service-externalip: + enabled: true + primary: true + type: ExternalIP + externalIP: 1.1.1.1 + addressType: IPv4 + publishNotReadyAddresses: true + externalIPs: + - 10.200.230.34 + sessionAffinity: ClientIP externalTrafficPolicy: Cluster ports: port-name: @@ -137,6 +172,7 @@ service: primary: true targetSelector: container-name port: 80 + targetPort: 8080 protocol: HTTP other-service-name: diff --git a/library/common/1.0.0/templates/classes/_endpointSlice.tpl b/library/common/1.0.0/templates/classes/_endpointSlice.tpl new file mode 100644 index 0000000000..ca0056ac7f --- /dev/null +++ b/library/common/1.0.0/templates/classes/_endpointSlice.tpl @@ -0,0 +1,40 @@ +{{/* EndpointSlice Class */}} +{{/* Call this template: +{{ include "ix.v1.common.class.endpointSlice" (dict "rootCtx" $ "objectData" $objectData) }} + +rootCtx: The root context of the template. It is used to access the global context. +objectData: The service data, that will be used to render the Service object. +*/}} + +{{- define "ix.v1.common.class.endpointSlice" -}} + + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- $addressType := $objectData.addressType | default "IPv4" -}} + {{- if $objectData.addressType -}} + {{- $addressType = tpl $addressType $rootCtx -}} + {{- end }} + +--- +apiVersion: discovery.k8s.io/v1 +kind: EndpointSlice +metadata: + name: {{ $objectData.name }} + {{- $labels := (mustMerge ($objectData.labels | default dict) (include "ix.v1.common.lib.metadata.allLabels" $rootCtx | fromYaml)) -}} + {{- $_ := set $labels "kubernetes.io/service-name" $objectData.name -}} + {{- with (include "ix.v1.common.lib.metadata.render" (dict "rootCtx" $rootCtx "labels" $labels) | trim) }} + labels: + {{- . | nindent 4 }} + {{- end -}} + {{- $annotations := (mustMerge ($objectData.annotations | default dict) (include "ix.v1.common.lib.metadata.allAnnotations" $rootCtx | fromYaml)) -}} + {{- with (include "ix.v1.common.lib.metadata.render" (dict "rootCtx" $rootCtx "annotations" $annotations) | trim) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +addressType: {{ $addressType }} +ports: +{{- include "ix.v1.common.lib.endpointslice.ports" (dict "rootCtx" $rootCtx "objectData" $objectData) | trim | nindent 2 }} +endpoints: +{{- include "ix.v1.common.lib.endpointslice.endpoints" (dict "rootCtx" $rootCtx "objectData" $objectData) | trim | nindent 2 }} +{{- end -}} diff --git a/library/common/1.0.0/templates/classes/_service.tpl b/library/common/1.0.0/templates/classes/_service.tpl index 110d7cb938..31545a9515 100644 --- a/library/common/1.0.0/templates/classes/_service.tpl +++ b/library/common/1.0.0/templates/classes/_service.tpl @@ -93,6 +93,6 @@ spec: {{- include "ix.v1.common.lib.metadata.selectorLabels" (dict "rootCtx" $rootCtx "podName" $podValues.shortName) | trim | nindent 4 -}} {{- end -}} {{- if eq $svcType "ExternalIP" -}} - {{/* TODO: endpointsslice for externalIP */}} + {{- include "ix.v1.common.class.endpointSlice" (dict "rootCtx" $rootCtx "objectData" $objectData) | trim | nindent 0 }} {{- end -}} {{- end -}} diff --git a/library/common/1.0.0/templates/lib/endpointSlice/_endpoints.tpl b/library/common/1.0.0/templates/lib/endpointSlice/_endpoints.tpl new file mode 100644 index 0000000000..b830a1cecd --- /dev/null +++ b/library/common/1.0.0/templates/lib/endpointSlice/_endpoints.tpl @@ -0,0 +1,21 @@ +{{/* EndpointSlice - endpoints */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.endpointslice.endpoints" (dict "rootCtx" $rootCtx "objectData" $objectData) -}} +rootCtx: The root context of the service +objectData: The object data of the service +*/}} + +{{- define "ix.v1.common.lib.endpointslice.endpoints" -}} + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- if not $objectData.externalIP -}} + {{- fail "EndpointSlice - Expected non-empty " -}} + {{- end -}} + + {{- if not (kindIs "string" $objectData.externalIP) -}} {{/* Only single IP is supported currently on this lib */}} + {{- fail (printf "EndpointSlice - Expected to be a [string], but got [%s]" (kindOf $objectData.externalIP)) -}} + {{- end }} +- addresses: + - {{ tpl $objectData.externalIP $rootCtx }} +{{- end -}} diff --git a/library/common/1.0.0/templates/lib/endpointSlice/_ports.tpl b/library/common/1.0.0/templates/lib/endpointSlice/_ports.tpl new file mode 100644 index 0000000000..e7db9ed1c0 --- /dev/null +++ b/library/common/1.0.0/templates/lib/endpointSlice/_ports.tpl @@ -0,0 +1,44 @@ +{{/* EndpointSlice - Ports */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.endpointslice.ports" (dict "rootCtx" $rootCtx "objectData" $objectData) -}} +rootCtx: The root context +objectData: The object data of the service +*/}} + +{{- define "ix.v1.common.lib.endpointslice.ports" -}} + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- $tcpProtocols := (list "TCP" "HTTP" "HTTPS") -}} + {{- range $name, $portValues := $objectData.ports -}} + {{- if $portValues.enabled -}} + {{- $protocol := $rootCtx.Values.fallbackDefaults.serviceProtocol -}} {{/* Default to fallback protocol, if no protocol is defined */}} + {{- $appProtocol := $rootCtx.Values.fallbackDefaults.serviceProtocol -}} + {{- $targetPort := $portValues.targetPort -}} + + {{- if not $targetPort -}} + {{- $targetPort = $portValues.port -}} + {{- end -}} + + {{/* Expand targetPort */}} + {{- if (kindIs "string" $targetPort) -}} + {{- $targetPort = (tpl $targetPort $rootCtx) -}} + {{- end -}} + {{- $targetPort = int $targetPort -}} + + {{- with $portValues.protocol -}} + {{- $protocol = tpl . $rootCtx -}} + {{- $appProtocol = tpl . $rootCtx -}} + + {{- if mustHas $protocol $tcpProtocols -}} + {{- $protocol = "TCP" -}} + {{- end -}} + {{- end }} +- name: {{ $name }} + port: {{ $targetPort }} + protocol: {{ $protocol }} + appProtocol: {{ $appProtocol | lower }} + {{- end -}} + {{- end -}} + +{{- end -}}