diff --git a/library/common-test/tests/service/metadata_test.yaml b/library/common-test/tests/service/metadata_test.yaml new file mode 100644 index 0000000000..59ac41bd0c --- /dev/null +++ b/library/common-test/tests/service/metadata_test.yaml @@ -0,0 +1,97 @@ +suite: service metadata test +templates: + - common.yaml +chart: + appVersion: &appVer v9.9.9 +tests: + - it: should pass with service created with labels and annotations + set: + label1: label1 + label2: global_label2 + annotation1: annotation1 + annotation2: global_annotation2 + global: + labels: + g_label1: global_label1 + g_label2: "{{ .Values.label2 }}" + annotations: + g_annotation1: global_annotation1 + g_annotation2: "{{ .Values.annotation2 }}" + service: + my-service1: + enabled: true + primary: true + labels: + label1: "{{ .Values.label1 }}" + label2: label2 + annotations: + annotation1: "{{ .Values.annotation1 }}" + annotation2: annotation2 + ports: + port-name: + enabled: true + primary: true + workload: + my-workload: + enabled: true + primary: true + type: Deployment + podSpec: {} + asserts: + - documentIndex: &serviceDoc 1 + isKind: + of: Service + - documentIndex: *serviceDoc + equal: + path: metadata.annotations + value: + annotation1: annotation1 + annotation2: annotation2 + g_annotation1: global_annotation1 + g_annotation2: global_annotation2 + - documentIndex: *serviceDoc + equal: + path: metadata.labels + value: + app: common-test-1.0.0 + release: release-name + helm-revision: 0 + helm.sh/chart: common-test-1.0.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/version: *appVer + g_label1: global_label1 + g_label2: global_label2 + label1: label1 + label2: label2 + + - it: should pass with service type LoadBalancer, with HTTPS port and addMetalLBAnnotations/Traefik true + set: + global: + addMetalLBAnnotations: true + addTraefikAnnotations: true + service: + my-service1: + enabled: true + primary: true + type: LoadBalancer + ports: + port-name: + enabled: true + primary: true + protocol: HTTPS + workload: + my-workload: + enabled: true + primary: true + type: Deployment + podSpec: {} + asserts: + - documentIndex: *serviceDoc + isKind: + of: Service + - documentIndex: *serviceDoc + isSubset: + path: metadata.annotations + content: + metallb.universe.tf/allow-shared-ip: release-name-common-test + traefik.ingress.kubernetes.io/service.serversscheme: https diff --git a/library/common-test/tests/service/names_test.yaml b/library/common-test/tests/service/names_test.yaml new file mode 100644 index 0000000000..cc2c889a28 --- /dev/null +++ b/library/common-test/tests/service/names_test.yaml @@ -0,0 +1,47 @@ +suite: service name test +templates: + - common.yaml +tests: + - it: should generate correct name + set: + service: + my-service1: + enabled: true + primary: true + ports: + port-name: + enabled: true + primary: true + my-service2: + enabled: true + ports: + port-name: + enabled: true + primary: true + workload: + my-workload: + enabled: true + primary: true + type: Deployment + podSpec: {} + asserts: + - documentIndex: &serviceDoc 1 + isKind: + of: Service + - documentIndex: *serviceDoc + isAPIVersion: + of: v1 + - documentIndex: *serviceDoc + equal: + path: metadata.name + value: release-name-common-test + - documentIndex: &otherServiceDoc 2 + isKind: + of: Service + - documentIndex: *otherServiceDoc + isAPIVersion: + of: v1 + - documentIndex: *otherServiceDoc + equal: + path: metadata.name + value: release-name-common-test-my-service2 diff --git a/library/common-test/tests/service/validation_test.yaml b/library/common-test/tests/service/validation_test.yaml new file mode 100644 index 0000000000..af4132537f --- /dev/null +++ b/library/common-test/tests/service/validation_test.yaml @@ -0,0 +1,191 @@ +suite: service validation test +templates: + - common.yaml +tests: + - it: should fail without primary service + set: + service: + service-name: + enabled: true + asserts: + - failedTemplate: + errorMessage: Service - At least one enabled service must be primary + + - it: should fail with more than one primary service + set: + service: + service-name1: + enabled: true + primary: true + service-name2: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Only one service can be primary + + - it: should fail without primary port in service + set: + service: + service-name1: + enabled: true + primary: true + ports: + port-name: + enabled: true + asserts: + - failedTemplate: + errorMessage: Service - At least one enabled port in service must be primary + + - it: should fail with more than one primary port in service + set: + service: + service-name1: + enabled: true + primary: true + ports: + port-name1: + enabled: true + primary: true + port-name2: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Only one port per service can be primary + + - it: should fail with no enabled ports in enabled service + set: + service: + service-name1: + enabled: true + primary: true + ports: + port-name1: + enabled: true + primary: true + port-name2: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Only one port per service can be primary + + - it: should fail with annotations not a dict + set: + service: + service-name1: + enabled: true + primary: true + annotations: not-a-dict + ports: + port-name1: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Expected to be a dictionary, but got [string] + + - it: should fail with labels not a dict + set: + service: + service-name1: + enabled: true + primary: true + labels: not-a-dict + ports: + port-name1: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Expected to be a dictionary, but got [string] + + - it: should fail with pod targetSelector not a string + set: + service: + service-name1: + enabled: true + primary: true + targetSelector: + pod: not-a-string + ports: + port-name1: + enabled: true + primary: true + asserts: + - failedTemplate: + errorMessage: Service - Expected service's to be [string], but got [map] + + - it: should fail with container targetSelector not a string + set: + service: + service-name1: + enabled: true + primary: true + ports: + port-name1: + enabled: true + primary: true + targetSelector: + container: not-a-string + asserts: + - failedTemplate: + errorMessage: Service - Expected port's to be [string], but got [map] + + - it: should fail with selected pod not defined + set: + service: + service-name1: + enabled: true + primary: true + targetSelector: some-pod-name + ports: + port-name: + enabled: true + primary: true + workload: + main: + enabled: true + primary: true + type: Deployment + podSpec: {} + asserts: + - failedTemplate: + errorMessage: Service - Selected pod [some-pod-name] is not defined + + - it: should fail with selected pod not enabled + set: + service: + service-name1: + enabled: true + primary: true + targetSelector: some-pod-name + ports: + port-name: + enabled: true + primary: true + workload: + some-pod-name: + enabled: false + primary: true + type: Deployment + podSpec: {} + asserts: + - failedTemplate: + errorMessage: Service - Selected pod [some-pod-name] is not enabled + + - it: should fail with invalid port protocol + set: + service: + service-name1: + enabled: true + primary: true + ports: + port-name1: + enabled: true + primary: true + protocol: not-a-protocol + asserts: + - failedTemplate: + errorMessage: Service - Expected port's protocol to be one of [TCP, UDP, HTTP, HTTPS] but got [not-a-protocol] diff --git a/library/common-test/values.yaml b/library/common-test/values.yaml index a6be06a4cc..65160ce923 100644 --- a/library/common-test/values.yaml +++ b/library/common-test/values.yaml @@ -1,7 +1,11 @@ # As we have by default enabled -# a "main" workload" for all charts +# a "main" workload and service" for all charts # We disable it so tests run with their defined -# workload (Tests use a different named workload) +# values (Tests use a different named values) workload: main: enabled: false + +service: + main: + enabled: false diff --git a/library/common/1.0.0/templates/classes/_service.tpl b/library/common/1.0.0/templates/classes/_service.tpl index e69de29bb2..6e9b12321a 100644 --- a/library/common/1.0.0/templates/classes/_service.tpl +++ b/library/common/1.0.0/templates/classes/_service.tpl @@ -0,0 +1,70 @@ +{{/* Service Class */}} +{{/* Call this template: +{{ include "ix.v1.common.class.service" (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.service" -}} + + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- $svcType := $objectData.type | default "ClusterIP" -}} + + {{/* Get Pod Values based on the selector (or the absence of it) */}} + {{- $podValues := fromJson (include "ix.v1.common.lib.service.getSelectedPodValues" (dict "rootCtx" $rootCtx "objectData" $objectData)) -}} + + {{/* Get Pod's hostNetwork configuration */}} + {{- $hostNetwork := include "ix.v1.common.lib.pod.hostNetwork" (dict "rootCtx" $rootCtx "objectData" $podValues) -}} + + {{- $hasHTTPSPort := false -}} + {{- $hasHostPort := false -}} + + {{- range $portName, $port := $objectData.ports -}} + {{- if $port.enabled -}} + {{- if eq ($port.protocol | default "") "HTTPS" -}} + {{- $hasHTTPSPort = true -}} + {{- end -}} + + {{- if and (hasKey $port "hostPort") $port.hostPort -}} + {{- $hasHostPort = true -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{/* When hostNetwork is set on the pod, force ClusterIP, so services won't try to bind the same ports on the host */}} + {{- if or (and (kindIs "bool" $hostNetwork) $hostNetwork) (and (kindIs "string" $hostNetwork) (eq $hostNetwork "true")) -}} + {{- $svcType = "ClusterIP" -}} + {{- end -}} + + {{/* When hostPort is defined, force ClusterIP aswell */}} + {{- if $hasHostPort -}} + {{- $svcType = "ClusterIP" -}} + {{- end }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $objectData.name }} + {{- $labels := (mustMerge ($objectData.labels | default dict) (include "ix.v1.common.lib.metadata.allLabels" $rootCtx | fromYaml)) -}} + {{- 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)) -}} + {{- if eq $svcType "LoadBalancer" -}} + {{- include "ix.v1.common.lib.service.metalLBAnnotations" (dict "rootCtx" $rootCtx "annotations" $annotations) -}} + {{- end -}} + {{- if and $hasHTTPSPort -}} + {{- include "ix.v1.common.lib.service.traefikAnnotations" (dict "rootCtx" $rootCtx "annotations" $annotations) -}} + {{- end -}} + {{- with (include "ix.v1.common.lib.metadata.render" (dict "rootCtx" $rootCtx "annotations" $annotations) | trim) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: +{{/* TODO: */}} +{{- end -}} diff --git a/library/common/1.0.0/templates/lib/metadata/_selectorLabels.tpl b/library/common/1.0.0/templates/lib/metadata/_selectorLabels.tpl index eb6cad2d4b..a94adb4e5b 100644 --- a/library/common/1.0.0/templates/lib/metadata/_selectorLabels.tpl +++ b/library/common/1.0.0/templates/lib/metadata/_selectorLabels.tpl @@ -1,6 +1,7 @@ {{/* Labels that are used on selectors */}} {{/* Call this template: {{ include "ix.v1.common.lib.metadata.selectorLabels" (dict "rootCtx" $rootCtx "podName" $podName) }} +podName is the "shortName" of the pod. The one you define in the .Values.workload */}} {{- define "ix.v1.common.lib.metadata.selectorLabels" -}} {{- $rootCtx := .rootCtx -}} diff --git a/library/common/1.0.0/templates/lib/service/_additionalAnnotations.tpl b/library/common/1.0.0/templates/lib/service/_additionalAnnotations.tpl new file mode 100644 index 0000000000..be701b162c --- /dev/null +++ b/library/common/1.0.0/templates/lib/service/_additionalAnnotations.tpl @@ -0,0 +1,29 @@ +{{/* Service - MetalLB Annotations */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.metalLBAnnotations" (dict "rootCtx" $rootCtx "annotations" $annotations) -}} +rootCtx: The root context of the service +*/}} + +{{- define "ix.v1.common.lib.service.metalLBAnnotations" -}} + {{- $rootCtx := .rootCtx -}} + {{- $annotations := .annotations -}} + + {{- if $rootCtx.Values.global.addMetalLBAnnotations -}} + {{- $_ := set $annotations "metallb.universe.tf/allow-shared-ip" (include "ix.v1.common.lib.chart.names.fullname" $rootCtx) -}} + {{- end -}} +{{- end -}} + +{{/* Service - Traefik Annotations */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.traefikAnnotations" (dict "rootCtx" $rootCtx "annotations" $annotations) -}} +rootCtx: The root context of the service +*/}} + +{{- define "ix.v1.common.lib.service.traefikAnnotations" -}} + {{- $rootCtx := .rootCtx -}} + {{- $annotations := .annotations -}} + + {{- if $rootCtx.Values.global.addTraefikAnnotations -}} + {{- $_ := set $annotations "traefik.ingress.kubernetes.io/service.serversscheme" "https" -}} + {{- end -}} +{{- end -}} diff --git a/library/common/1.0.0/templates/lib/service/_getSelectedPod.tpl b/library/common/1.0.0/templates/lib/service/_getSelectedPod.tpl new file mode 100644 index 0000000000..c8fecbd39f --- /dev/null +++ b/library/common/1.0.0/templates/lib/service/_getSelectedPod.tpl @@ -0,0 +1,46 @@ +{{/* Service - Get Selected Pod */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.getSelectedPodValues" (dict "rootCtx" $rootCtx "objectData" $objectData) -}} +objectData: The object data of the service +rootCtx: The root context of the service +*/}} + +{{- define "ix.v1.common.lib.service.getSelectedPodValues" -}} + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- $podValues := dict -}} + {{- with $objectData.targetSelector -}} + {{- $podValues = mustDeepCopy (get $rootCtx.Values.workload .) -}} + + {{- if not $podValues -}} + {{- fail (printf "Service - Selected pod [%s] is not defined" .) -}} + {{- end -}} + + {{- if not $podValues.enabled -}} + {{- fail (printf "Service - Selected pod [%s] is not enabled" .) -}} + {{- end -}} + + {{/* While we know the shortName from targetSelector, let's set it explicitly + So service can reference this directly, to match the behaviour of a service + without targetSelector defined (assumes "use primary") */}} + {{- $_ := set $podValues "shortName" . -}} + {{- else -}} + + {{/* If no targetSelector is defined, we assume the service is using the primary pod */}} + {{/* Also no need to check for multiple primaries here, it's already done on the workload validation */}} + {{- range $podName, $pod := $rootCtx.Values.workload -}} + {{- if $pod.enabled -}} + {{- if $pod.primary -}} + {{- $podValues = mustDeepCopy $pod -}} + {{/* Set the shortName so service can use this on selector */}} + {{- $_ := set $podValues "shortName" $podName -}} + {{- end -}} + {{- end -}} + {{- end -}} + + {{- end -}} + + {{/* Return values in Json, to preserve types */}} + {{ $podValues | toJson }} +{{- end -}} diff --git a/library/common/1.0.0/templates/lib/service/_validation.tpl b/library/common/1.0.0/templates/lib/service/_validation.tpl new file mode 100644 index 0000000000..8613d57cce --- /dev/null +++ b/library/common/1.0.0/templates/lib/service/_validation.tpl @@ -0,0 +1,126 @@ +{{/* Service Validation */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.validation" (dict "objectData" $objectData) -}} +objectData: + labels: The labels of the service. + annotations: The annotations of the service. +*/}} + +{{- define "ix.v1.common.lib.service.validation" -}} + {{- $objectData := .objectData -}} + + {{- if and $objectData.labels (not (kindIs "map" $objectData.labels)) -}} + {{- fail (printf "Service - Expected to be a dictionary, but got [%v]" (kindOf $objectData.labels)) -}} + {{- end -}} + + {{- if and $objectData.annotations (not (kindIs "map" $objectData.annotations)) -}} + {{- fail (printf "Service - Expected to be a dictionary, but got [%v]" (kindOf $objectData.annotations)) -}} + {{- end -}} + + {{- if and $objectData.targetSelector (not (kindIs "string" $objectData.targetSelector)) -}} + {{- fail (printf "Service - Expected service's to be [string], but got [%s]" (kindOf $objectData.targetSelector)) -}} + {{- end -}} + + {{- $hasEnabledPort := false -}} + {{- range $name, $port := $objectData.ports -}} + {{- if $port.enabled -}} + {{- $hasEnabledPort = true -}} + + {{- if and $port.targetSelector (not (kindIs "string" $port.targetSelector)) -}} + {{- fail (printf "Service - Expected port's to be [string], but got [%s]" (kindOf $port.targetSelector)) -}} + {{- end -}} + + {{- if and $port.protocol (not (mustHas $port.protocol (list "TCP" "UDP" "HTTP" "HTTPS"))) -}} + {{- fail (printf "Service - Expected port's protocol to be one of [TCP, UDP, HTTP, HTTPS] but got [%s]" $port.protocol) -}} + {{- end -}} + + {{- end -}} + {{- end -}} + + {{- if not $hasEnabledPort -}} + {{- fail "Service - Expected enabled service to have at least one port" -}} + {{- end -}} + +{{- end -}} + +{{/* Service Primary Validation */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.primaryValidation" $ -}} +*/}} + +{{- define "ix.v1.common.lib.service.primaryValidation" -}} + + {{/* Initialize values */}} + {{- $hasPrimary := false -}} + {{- $hasEnabled := false -}} + + {{- range $name, $service := .Values.service -}} + + {{/* If service is enabled */}} + {{- if $service.enabled -}} + {{- $hasEnabled = true -}} + + {{/* And service is primary */}} + {{- if and (hasKey $service "primary") ($service.primary) -}} + {{/* Fail if there is already a primary service */}} + {{- if $hasPrimary -}} + {{- fail "Service - Only one service can be primary" -}} + {{- end -}} + + {{- $hasPrimary = true -}} + + {{- include "ix.v1.common.lib.servicePort.primaryValidation" (dict "objectData" $service.ports) -}} + + {{- end -}} + + {{- end -}} + {{- end -}} + + {{/* Require at least one primary service, if any enabled */}} + {{- if and $hasEnabled (not $hasPrimary) -}} + {{- fail "Service - At least one enabled service must be primary" -}} + {{- end -}} + +{{- end -}} + +{{/* Service Port Primary Validation */}} +{{/* Call this template: +{{ include "ix.v1.common.lib.service.primaryValidation" (dict "objectData" $objectData -}} +objectData: + The ports of the service. +*/}} + +{{- define "ix.v1.common.lib.servicePort.primaryValidation" -}} + {{- $objectData := .objectData -}} + + {{/* Initialize values */}} + {{- $hasPrimary := false -}} + {{- $hasEnabled := false -}} + + {{- range $name, $port := $objectData -}} + + {{/* If service is enabled */}} + {{- if $port.enabled -}} + {{- $hasEnabled = true -}} + + {{/* And service is primary */}} + {{- if and (hasKey $port "primary") ($port.primary) -}} + + {{/* Fail if there is already a primary port */}} + {{- if $hasPrimary -}} + {{- fail "Service - Only one port per service can be primary" -}} + {{- end -}} + + {{- $hasPrimary = true -}} + + {{- end -}} + + {{- end -}} + {{- end -}} + + {{/* Require at least one primary service, if any enabled */}} + {{- if and $hasEnabled (not $hasPrimary) -}} + {{- fail "Service - At least one enabled port in service must be primary" -}} + {{- end -}} + +{{- end -}} diff --git a/library/common/1.0.0/templates/loader/_apply.tpl b/library/common/1.0.0/templates/loader/_apply.tpl index 49242991c3..eaff583191 100644 --- a/library/common/1.0.0/templates/loader/_apply.tpl +++ b/library/common/1.0.0/templates/loader/_apply.tpl @@ -19,4 +19,7 @@ {{/* Render Workload(s) */}} {{- include "ix.v1.common.spawner.workload" . | nindent 0 -}} + {{/* Render Services(s) */}} + {{- include "ix.v1.common.spawner.service" . | nindent 0 -}} + {{- end -}} diff --git a/library/common/1.0.0/templates/spawner/_service.tpl b/library/common/1.0.0/templates/spawner/_service.tpl index e69de29bb2..8a6a9eb12a 100644 --- a/library/common/1.0.0/templates/spawner/_service.tpl +++ b/library/common/1.0.0/templates/spawner/_service.tpl @@ -0,0 +1,38 @@ +{{/* Service Spawner */}} +{{/* Call this template: +{{ include "ix.v1.common.spawner.service" $ -}} +*/}} + +{{- define "ix.v1.common.spawner.service" -}} + + {{/* Primary validation for enabled service. */}} + {{- include "ix.v1.common.lib.service.primaryValidation" $ -}} + + {{- range $name, $service := .Values.service -}} + + {{- if $service.enabled -}} + + {{/* Create a copy of the configmap */}} + {{- $objectData := (mustDeepCopy $service) -}} + + {{- $objectName := include "ix.v1.common.lib.chart.names.fullname" $ -}} + {{- if not $objectData.primary -}} + {{- $objectName = (printf "%s-%s" (include "ix.v1.common.lib.chart.names.fullname" $) $name) -}} + {{- end -}} + + {{/* Perform validations */}} + {{- include "ix.v1.common.lib.chart.names.validation" (dict "name" $objectName) -}} + {{- include "ix.v1.common.lib.service.validation" (dict "objectData" $objectData) -}} + + {{/* Set the name of the service account */}} + {{- $_ := set $objectData "name" $objectName -}} + {{- $_ := set $objectData "shortName" $name -}} + + {{/* Call class to create the object */}} + {{- include "ix.v1.common.class.service" (dict "rootCtx" $ "objectData" $objectData) -}} + + {{- end -}} + + {{- end -}} + +{{- end -}} diff --git a/library/common/1.0.0/values.yaml b/library/common/1.0.0/values.yaml index bfba184cc8..342a6e502e 100644 --- a/library/common/1.0.0/values.yaml +++ b/library/common/1.0.0/values.yaml @@ -4,11 +4,12 @@ global: labels: {} # -- Set additional global annotations annotations: {} + # -- Adds metalLB annotations to services + addMetalLBAnnotations: false + # -- Adds traefik annotations to services + addTraefikAnnotations: false # -- Scale Storage Class scaleZFSStorageClass: '{{ printf "ix-storage-class-%v" .Release.Name }}' - ixChartContext: - addNvidiaRuntimeClass: true - nvidiaRuntimeClassName: asdf # TODO: fallbackDefaults: @@ -251,16 +252,20 @@ persistence: subPath: "" mountPropagation: "" -# -- Services -services: +# -- Service +service: # -- Service name - service-name: + main: # -- Enables the service - enabled: true + enabled: false # -- Primary service primary: true + # -- Service labels + labels: {} + # -- Service annotations + annotations: {} # -- Pod to connect - targetSelector: pod-name + targetSelector: main # -- Ports ports: port-name: