diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e4101ba49..e76ece1ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,3 +23,11 @@ repos: entry: ruff format --force-exclude types: [python] require_serial: true + + - repo: https://github.com/norwoodj/helm-docs + rev: "" + hooks: + - id: helm-docs-container + args: + # Make the tool search for charts only under the `helm` directory + - --chart-search-root=helm diff --git a/helm/blueapi/Chart.yaml b/helm/blueapi/Chart.yaml index c8d5dd234..08097921f 100644 --- a/helm/blueapi/Chart.yaml +++ b/helm/blueapi/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: blueapi home: https://github.com/DiamondLightSource/blueapi -description: A helm chart deploying a worker pod that runs Bluesky plans +description: A Helm chart deploying a worker pod that runs Bluesky plans # A chart can be either an 'application' or a 'library' chart. # @@ -13,8 +13,11 @@ description: A helm chart deploying a worker pod that runs Bluesky plans # pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. +# This is the chart version. This version number is incremented by the release process. # Versions are expected to follow Semantic Versioning (https://semver.org/) -# This is updated automatically by CI, the appVersion is automatically inserted by CI version: 0.1.0 + +# This is the version number of the application being deployed. This version number is incremented by the release process. +# Versions are not expected to follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/helm/blueapi/README.md b/helm/blueapi/README.md new file mode 100644 index 000000000..dc9ef0790 --- /dev/null +++ b/helm/blueapi/README.md @@ -0,0 +1,55 @@ +# blueapi + +A Helm chart deploying a worker pod that runs Bluesky plans + +**Homepage:** + +## Values + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| affinity | object | `{}` | May be required to run on specific nodes (e.g. the control machine) | +| debug.enabled | bool | `false` | If enabled, disables liveness and readiness probes, and does not start the service on startup This allows connecting to the pod and starting the service manually to allow debugging on the cluster | +| extraEnvVars | list | `[]` | Additional envVars to mount to the pod | +| fullnameOverride | string | `""` | | +| hostNetwork | bool | `false` | May be needed for EPICS depending on gateway configuration | +| image.pullPolicy | string | `"IfNotPresent"` | | +| image.repository | string | `"ghcr.io/diamondlightsource/blueapi"` | To use a container image that extends the blueapi one, set it here | +| image.tag | string | `""` | | +| imagePullSecrets | list | `[]` | | +| ingress | object | `{"annotations":{},"className":"nginx","enabled":false,"hosts":[{"host":"example.diamond.ac.uk","paths":[{"path":"/","pathType":"Prefix"}]}],"tls":[]}` | Configuring and enabling an ingress allows blueapi to be served at a nicer address, e.g. ixx-blueapi.diamond.ac.uk | +| ingress.hosts[0] | object | `{"host":"example.diamond.ac.uk","paths":[{"path":"/","pathType":"Prefix"}]}` | Request a host from https://jira.diamond.ac.uk/servicedesk/customer/portal/2/create/91 of the form ixx-blueapi.diamond.ac.uk. Note: pathType: Prefix is required in Diamond's clusters | +| initContainer | object | `{"enabled":false,"persistentVolume":{"enabled":false,"existingClaimName":""}}` | Configure the initContainer that checks out the scratch configuration repositories | +| initContainer.persistentVolume.enabled | bool | `false` | Whether to use a persistent volume in the cluster or check out onto the mounted host filesystem If persistentVolume.enabled: False, mounts scratch.root as scratch.root in the container | +| initContainer.persistentVolume.existingClaimName | string | `""` | May be set to an existing persistent volume claim to re-use the volume, else a new one is created for each blueapi release | +| livenessProbe | object | `{"failureThreshold":3,"httpGet":{"path":"/healthz","port":"http"},"periodSeconds":10}` | Liveness probe, if configured kubernetes will kill the pod and start a new one if failed consecutively. This is automatically disabled when in debug mode. | +| nameOverride | string | `""` | | +| nodeSelector | object | `{}` | May be required to run on specific nodes (e.g. the control machine) | +| podAnnotations | object | `{}` | | +| podLabels | object | `{}` | | +| podSecurityContext | object | `{}` | | +| readinessProbe | object | `{"failureThreshold":2,"httpGet":{"path":"/healthz","port":"http"},"periodSeconds":10}` | Readiness probe, if configured kubernetes will not route traffic to this pod if failed consecutively. This could allow the service time to recover if it is being overwhelmed by traffic, but without the to ability to load balance or scale up/outwards, upstream services will need to know to back off. This is automatically disabled when in debug mode. | +| resources | object | `{"limits":{"cpu":"2000m","memory":"4000Mi"},"requests":{"cpu":"200m","memory":"400Mi"}}` | Sets the compute resources available to the pod. These defaults are appropriate when using debug mode or an internal PVC and therefore running VS Code server in the pod. In the Diamond cluster, requests must be >= 0.1*limits When not using either of the above, the limits may be lowered. When idle but connected, blueapi consumes ~400MB of memory and 1% cpu and may struggle when allocated less. | +| restartOnConfigChange | bool | `true` | If enabled the blueapi pod will restart on changes to `worker` | +| securityContext.runAsNonRoot | bool | `true` | | +| securityContext.runAsUser | int | `1000` | | +| service.port | int | `80` | | +| service.type | string | `"ClusterIP"` | To make blueapi available on an IP outside of the cluster prior to an Ingress being created, change this to LoadBalancer | +| serviceAccount.annotations | object | `{}` | | +| serviceAccount.automount | bool | `true` | | +| serviceAccount.create | bool | `false` | | +| serviceAccount.name | string | `""` | | +| startupProbe | object | `{"failureThreshold":5,"httpGet":{"path":"/healthz","port":"http"},"periodSeconds":10}` | A more lenient livenessProbe to allow the service to start fully. This is automatically disabled when in debug mode. | +| tolerations | list | `[]` | May be required to run on specific nodes (e.g. the control machine) | +| tracing | object | `{"otlp":{"enabled":false,"protocol":"http/protobuf","server":{"host":"http://opentelemetry-collector.tracing","port":4318}}}` | Configure tracing: opentelemetry-collector.tracing should be available in all Diamond clusters | +| volumeMounts | list | `[{"mountPath":"/config","name":"worker-config","readOnly":true}]` | Additional volumeMounts on the output StatefulSet definition. Define how volumes are mounted to the container referenced by using the same name. | +| volumes | list | `[]` | Additional volumes on the output StatefulSet definition. Define volumes from e.g. Secrets, ConfigMaps or the Filesystem | +| worker | object | `{"api":{"url":"http://0.0.0.0:8000/"},"env":{"sources":[{"kind":"planFunctions","module":"dodal.plans"},{"kind":"planFunctions","module":"dodal.plan_stubs.wrapped"}]},"logging":{"graylog":{"enabled":false,"url":"http://graylog-log-target.diamond.ac.uk:12232/"},"level":"INFO"},"scratch":{"repositories":[],"root":"/blueapi-plugins/scratch"},"stomp":{"auth":{"password":"guest","username":"guest"},"enabled":false,"url":"http://rabbitmq:61613/"}}` | Config for the worker goes here, will be mounted into a config file | +| worker.api.url | string | `"http://0.0.0.0:8000/"` | 0.0.0.0 required to allow non-loopback traffic If using hostNetwork, the port must be free on the host | +| worker.env.sources | list | `[{"kind":"planFunctions","module":"dodal.plans"},{"kind":"planFunctions","module":"dodal.plan_stubs.wrapped"}]` | modules (must be installed in the venv) to fetch devices/plans from | +| worker.logging | object | `{"graylog":{"enabled":false,"url":"http://graylog-log-target.diamond.ac.uk:12232/"},"level":"INFO"}` | Configures logging. Port 12231 is the `dodal` input on graylog which will be renamed `blueapi` | +| worker.scratch | object | `{"repositories":[],"root":"/blueapi-plugins/scratch"}` | If initContainer is enabled the default branch of python projects in this section are installed into the venv *without their dependencies* | +| worker.stomp | object | `{"auth":{"password":"guest","username":"guest"},"enabled":false,"url":"http://rabbitmq:61613/"}` | Message bus configuration for returning status to GDA/forwarding documents downstream Password may be in the form ${ENV_VAR} to be fetched from an environment variable e.g. mounted from a SealedSecret | + +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/helm/blueapi/README.md.gotmpl b/helm/blueapi/README.md.gotmpl new file mode 100644 index 000000000..3ca63f059 --- /dev/null +++ b/helm/blueapi/README.md.gotmpl @@ -0,0 +1,16 @@ +{{ template "chart.header" . }} +{{ template "chart.deprecationWarning" . }} + +{{ template "chart.description" . }} + +{{ template "chart.homepageLine" . }} + +{{ template "chart.maintainersSection" . }} + +{{ template "chart.sourcesSection" . }} + +{{ template "chart.requirementsSection" . }} + +{{ template "chart.valuesSection" . }} + +{{ template "helm-docs.versionFooter" . }} diff --git a/helm/blueapi/templates/NOTES.txt b/helm/blueapi/templates/NOTES.txt index 85909b5d6..f1c272878 100644 --- a/helm/blueapi/templates/NOTES.txt +++ b/helm/blueapi/templates/NOTES.txt @@ -1,2 +1,25 @@ -1. Worker will be deployed with the following config: +Worker will be deployed with the following config: {{- .Values.worker }} + +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "blueapi.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "blueapi.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "blueapi.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "blueapi.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/helm/blueapi/templates/configmap.yaml b/helm/blueapi/templates/configmap.yaml index dff3f2d35..d22583bff 100644 --- a/helm/blueapi/templates/configmap.yaml +++ b/helm/blueapi/templates/configmap.yaml @@ -18,3 +18,18 @@ data: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: {{ .Values.tracing.otlp.protocol | default "http/protobuf" }} OTEL_EXPORTER_OTLP_ENDPOINT: {{ required "OTLP export enabled but server address not set" .Values.tracing.otlp.server.host }}:{{ .Values.tracing.otlp.server.port | default 4318 }} {{ end }} + +--- + +{{- if .Values.initContainer.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "blueapi.fullname" . }}-init-config +data: + init_config.yaml: |- + scratch: + {{- toYaml .Values.worker.scratch | nindent 6 }} +{{- end }} + +--- diff --git a/helm/blueapi/templates/ingress.yaml b/helm/blueapi/templates/ingress.yaml index 8d822aff5..234c779f0 100644 --- a/helm/blueapi/templates/ingress.yaml +++ b/helm/blueapi/templates/ingress.yaml @@ -1,22 +1,43 @@ -{{- if .Values.ingress.create -}} +{{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include "blueapi.fullname" . }} + labels: + {{- include "blueapi.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} spec: - ingressClassName: nginx + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} tls: - - hosts: - - {{ required "A valid hostname must be provided" .Values.ingress.host }} + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} rules: - - host: {{ required "A valid hostname must be provided" .Values.ingress.host }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: {{ include "blueapi.fullname" . }} - port: - number: {{ .Values.service.port }} + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "blueapi.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} {{- end }} diff --git a/helm/blueapi/templates/init-configmap.yaml b/helm/blueapi/templates/init-configmap.yaml deleted file mode 100644 index faeb8cefc..000000000 --- a/helm/blueapi/templates/init-configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if .Values.initContainer.enabled }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "blueapi.fullname" . }}-initconfig -data: - initconfig.yaml: |- - scratch: - {{- toYaml .Values.worker.scratch | nindent 6 }} -{{- end }} ---- diff --git a/helm/blueapi/templates/service.yaml b/helm/blueapi/templates/service.yaml index d51bf9b36..464b9028b 100644 --- a/helm/blueapi/templates/service.yaml +++ b/helm/blueapi/templates/service.yaml @@ -2,12 +2,14 @@ apiVersion: v1 kind: Service metadata: name: {{ include "blueapi.fullname" . }} + labels: + {{- include "blueapi.labels" . | nindent 4 }} spec: + type: {{ .Values.service.type }} ports: - - name: http - port: {{ .Values.service.port }} - protocol: TCP - targetPort: http + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http selector: {{- include "blueapi.selectorLabels" . | nindent 4 }} - type: {{ .Values.service.type }} diff --git a/helm/blueapi/templates/serviceaccount.yaml b/helm/blueapi/templates/serviceaccount.yaml index 6558cf145..6ecc1e731 100644 --- a/helm/blueapi/templates/serviceaccount.yaml +++ b/helm/blueapi/templates/serviceaccount.yaml @@ -9,4 +9,5 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} diff --git a/helm/blueapi/templates/statefulset.yaml b/helm/blueapi/templates/statefulset.yaml index 534ee3c21..4465650f8 100644 --- a/helm/blueapi/templates/statefulset.yaml +++ b/helm/blueapi/templates/statefulset.yaml @@ -5,7 +5,6 @@ metadata: labels: {{- include "blueapi.labels" . | nindent 4 }} spec: - replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "blueapi.selectorLabels" . | nindent 6 }} @@ -22,17 +21,28 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} labels: - {{- include "blueapi.selectorLabels" . | nindent 8 }} + {{- include "blueapi.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} spec: - hostNetwork: {{ .Values.hostNetwork }} + {{- if .Values.hostNetwork }} + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "blueapi.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- toYaml . | nindent 8 }} + {{- end }} volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} - name: worker-config projected: sources: @@ -43,7 +53,7 @@ spec: projected: sources: - configMap: - name: {{ include "blueapi.fullname" . }}-initconfig + name: {{ include "blueapi.fullname" . }}-init-config - name: venv emptyDir: sizeLimit: 5Gi @@ -65,7 +75,7 @@ spec: - name: nslcd # Shared volume between main and sidecar container emptyDir: sizeLimit: 5Mi - {{- end }} + {{- end }} {{- if .Values.initContainer.enabled }} initContainers: - name: setup-scratch @@ -73,11 +83,11 @@ spec: imagePullPolicy: {{ .Values.image.pullPolicy }} resources: {{- .Values.initResources | default .Values.resources | toYaml | nindent 12 }} - command: [/bin/sh, -c] + command: ["/bin/sh", "-c"] args: - | echo "Setting up scratch area" - blueapi -c /config/initconfig.yaml setup-scratch + blueapi -c /config/init_config.yaml setup-scratch if [ $? -ne 0 ]; then echo 'Blueapi failed'; exit 1; fi; echo "Exporting venv as artefact" cp -r /venv/* /artefacts @@ -95,12 +105,13 @@ spec: mountPath: {{ .Values.worker.scratch.root }} mountPropagation: HostToContainer {{- end }} - {{- end }} containers: - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}{{ ternary "-debug" "" .Values.debug.enabled }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: @@ -116,12 +127,14 @@ spec: containerPort: 8000 {{- end }} protocol: TCP + {{- with .Values.resources }} resources: - {{- toYaml .Values.resources | nindent 12 }} + {{- toYaml . | nindent 12 }} + {{- end }} volumeMounts: - - name: worker-config - mountPath: "/config" - readOnly: true + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} {{- if .Values.initContainer.enabled }} {{- if .Values.initContainer.persistentVolume.enabled }} - name: scratch @@ -145,6 +158,18 @@ spec: - "-c" - "/config/config.yaml" - "serve" + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.startupProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- end }} envFrom: - configMapRef: diff --git a/helm/blueapi/templates/tests/test-ping.yaml b/helm/blueapi/templates/tests/test-connection.yaml similarity index 50% rename from helm/blueapi/templates/tests/test-ping.yaml rename to helm/blueapi/templates/tests/test-connection.yaml index 5e238a59c..c4e8330e7 100644 --- a/helm/blueapi/templates/tests/test-ping.yaml +++ b/helm/blueapi/templates/tests/test-connection.yaml @@ -1,23 +1,18 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "blueapi.fullname" . }}-test-ping" + name: "{{ include "blueapi.fullname" . }}-test-connection" labels: {{- include "blueapi.labels" . | nindent 4 }} annotations: "helm.sh/hook": test - "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: volumes: - - name: worker-config + - name: test-config projected: sources: - configMap: - name: {{ include "blueapi.fullname" . }}-config - {{- with .Values.existingSecret }} - - secret: - name: {{ . }} - {{- end }} + name: {{ include "blueapi.fullname" . }}-test-config containers: - name: ping volumeMounts: @@ -30,10 +25,28 @@ spec: args: - "-c" - "/config/config.yaml" - {{- with .Values.existingSecret }} - - "-c" - - "/config/secret.yaml" - {{- end }} - "controller" - "plans" restartPolicy: Never + +--- + +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "blueapi.fullname" . }}-test-config +data: + init_config.yaml: |- + api: + url: http://{{ include "blueapi.fullname" . }}:{{ .Values.service.port }}/ + stomp: + enabled: false + auth: + username: guest + password: guest + url: http://rabbitmq:61613/ + logging: + level: "INFO" + graylog: + enabled: False + url: http://graylog-log-target.diamond.ac.uk:12232/ diff --git a/helm/blueapi/values.yaml b/helm/blueapi/values.yaml index 76544e557..efe9c003f 100644 --- a/helm/blueapi/values.yaml +++ b/helm/blueapi/values.yaml @@ -2,81 +2,158 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. -replicaCount: 1 - +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: + # -- To use a container image that extends the blueapi one, set it here repository: ghcr.io/diamondlightsource/blueapi - pullPolicy: Always + # This sets the pull policy for images. + pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - # Do not set this to a package hash: https://github.com/DiamondLightSource/blueapi/issues/1046 tag: "" +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] +# This is to override the chart name. nameOverride: "" fullnameOverride: "" +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ serviceAccount: # Specifies whether a service account should be created create: false + # Automatically mount a ServiceAccount's API credentials? + automount: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} podSecurityContext: {} -# fsGroup: 2000 - -securityContext: {} -# capabilities: -# drop: -# - ALL -# readOnlyRootFilesystem: true -# runAsNonRoot: true -# runAsUser: 1000 - -# Recommended for production to change service.type to ClusterIP and set up an Ingress + # fsGroup: 2000 + +securityContext: + # https://github.com/DiamondLightSource/blueapi/issues/1096 + # readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + # capabilities: + # drop: + # - ALL + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + # -- To make blueapi available on an IP outside of the cluster prior to an Ingress being created, change this to LoadBalancer type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports port: 80 +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +# -- Configuring and enabling an ingress allows blueapi to be served at a nicer address, e.g. ixx-blueapi.diamond.ac.uk ingress: - create: false -# host: foo.diamond.ac.uk (assumes port = service.port) - -resources: {} -# We usually recommend not to specify default resources and to leave this as a conscious -# choice for the user. This also increases chances charts run on environments with little -# resources, such as Minikube. If you do want to specify resources, uncomment the following -# lines, adjust them as necessary, and remove the curly braces after 'resources:'. -# limits: -# cpu: 100m -# memory: 128Mi -# requests: -# cpu: 100m -# memory: 128Mi - -initResources: {} -# Can optionally specify separate resource constraints for the scratch setup container. -# If left empty this defaults to the same as resources above. - + enabled: false + className: "nginx" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + # -- Request a host from https://jira.diamond.ac.uk/servicedesk/customer/portal/2/create/91 + # of the form ixx-blueapi.diamond.ac.uk. Note: pathType: Prefix is required in Diamond's clusters + - host: example.diamond.ac.uk + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Sets the compute resources available to the pod. +# These defaults are appropriate when using debug mode or an internal PVC and therefore +# running VS Code server in the pod. +# In the Diamond cluster, requests must be >= 0.1*limits +# When not using either of the above, the limits may be lowered. +# When idle but connected, blueapi consumes ~400MB of memory and 1% cpu +# and may struggle when allocated less. +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 2000m + memory: 4000Mi + requests: + cpu: 200m + memory: 400Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +# -- Liveness probe, if configured kubernetes will kill the pod and start a new one if failed consecutively. +# This is automatically disabled when in debug mode. +livenessProbe: + httpGet: + path: /healthz + port: http + failureThreshold: 3 + periodSeconds: 10 + +# -- Readiness probe, if configured kubernetes will not route traffic to this pod if failed consecutively. +# This could allow the service time to recover if it is being overwhelmed by traffic, but without the +# to ability to load balance or scale up/outwards, upstream services will need to know to back off. +# This is automatically disabled when in debug mode. +readinessProbe: + httpGet: + path: /healthz + port: http + failureThreshold: 2 + periodSeconds: 10 + +# -- A more lenient livenessProbe to allow the service to start fully. +# This is automatically disabled when in debug mode. +startupProbe: + httpGet: + path: /healthz + port: http + failureThreshold: 5 + periodSeconds: 10 + + + +# -- Additional volumes on the output StatefulSet definition. +# Define volumes from e.g. Secrets, ConfigMaps or the Filesystem +volumes: [] + +# -- Additional volumeMounts on the output StatefulSet definition. +# Define how volumes are mounted to the container referenced by using the same name. +volumeMounts: +- name: worker-config + mountPath: "/config" + readOnly: true + +# -- May be required to run on specific nodes (e.g. the control machine) nodeSelector: {} - +# -- May be required to run on specific nodes (e.g. the control machine) tolerations: [] - +# -- May be required to run on specific nodes (e.g. the control machine) affinity: {} -hostNetwork: false # May be needed for talking to arcane protocols such as EPICS +# blueapi specific fields -restartOnConfigChange: true +# -- May be needed for EPICS depending on gateway configuration +hostNetwork: false -listener: - enabled: true - resources: {} +# -- If enabled the blueapi pod will restart on changes to `worker` +restartOnConfigChange: true -# Additional envVars to mount to the pod as a String +# -- Additional envVars to mount to the pod extraEnvVars: [] # - name: RABBITMQ_PASSWORD # valueFrom: @@ -84,19 +161,23 @@ extraEnvVars: [] # name: rabbitmq-password # key: rabbitmq-password +# -- Configure tracing: opentelemetry-collector.tracing should be available in all Diamond clusters tracing: otlp: enabled: false protocol: http/protobuf server: - host: https://daq-services-jaeger # replace with central instance + host: http://opentelemetry-collector.tracing port: 4318 -# Config for the worker goes here, will be mounted into a config file +# -- Config for the worker goes here, will be mounted into a config file worker: api: - url: http://0.0.0.0:8000/ # Allow non-loopback traffic + # -- 0.0.0.0 required to allow non-loopback traffic + # If using hostNetwork, the port must be free on the host + url: http://0.0.0.0:8000/ env: + # -- modules (must be installed in the venv) to fetch devices/plans from sources: # - kind: dodal # module: dodal.beamlines.adsim @@ -104,27 +185,39 @@ worker: module: dodal.plans - kind: planFunctions module: dodal.plan_stubs.wrapped + # -- Message bus configuration for returning status to GDA/forwarding documents downstream + # Password may be in the form ${ENV_VAR} to be fetched from an environment variable e.g. mounted from a SealedSecret stomp: enabled: false auth: username: guest password: guest url: http://rabbitmq:61613/ + # -- If initContainer is enabled the default branch of python projects in this section are installed + # into the venv *without their dependencies* scratch: root: /blueapi-plugins/scratch repositories: [] # - name: "dodal" # remote_url: https://github.com/DiamondLightSource/dodal.git + # -- Configures logging. Port 12231 is the `dodal` input on graylog which will be renamed `blueapi` logging: level: "INFO" graylog: enabled: False url: http://graylog-log-target.diamond.ac.uk:12232/ + +# -- Configure the initContainer that checks out the scratch configuration repositories initContainer: enabled: false persistentVolume: + # -- Whether to use a persistent volume in the cluster or check out onto the mounted host filesystem + # If persistentVolume.enabled: False, mounts scratch.root as scratch.root in the container enabled: false - # existingClaimName: foo + # -- May be set to an existing persistent volume claim to re-use the volume, else a new one is created for each blueapi release + existingClaimName: "" debug: + # -- If enabled, disables liveness and readiness probes, and does not start the service on startup + # This allows connecting to the pod and starting the service manually to allow debugging on the cluster enabled: false diff --git a/tests/unit_tests/test_helm_chart.py b/tests/unit_tests/test_helm_chart.py index 601d88155..47e58516c 100644 --- a/tests/unit_tests/test_helm_chart.py +++ b/tests/unit_tests/test_helm_chart.py @@ -139,7 +139,7 @@ def test_helm_chart_creates_config_map(worker_config: ApplicationConfig): def test_helm_chart_creates_init_config_map(values: Values): manifests = render_chart(values=values) rendered_config = yaml.safe_load( - manifests["ConfigMap"]["blueapi-initconfig"]["data"]["initconfig.yaml"] + manifests["ConfigMap"]["blueapi-init-config"]["data"]["init_config.yaml"] ) assert rendered_config["scratch"] == values["worker"]["scratch"] @@ -333,7 +333,7 @@ def test_worker_scratch_config_used_when_init_container_enabled(): manifests["ConfigMap"]["blueapi-config"]["data"]["config.yaml"] ) init_config = yaml.safe_load( - manifests["ConfigMap"]["blueapi-initconfig"]["data"]["initconfig.yaml"] + manifests["ConfigMap"]["blueapi-init-config"]["data"]["init_config.yaml"] ) type_adapter = TypeAdapter(ApplicationConfig) @@ -589,11 +589,11 @@ def test_persistent_volume_claim_exists( ) persistent_volume_claim = { - "scratch-": { + "scratch-0.1.0": { "apiVersion": "v1", "kind": "PersistentVolumeClaim", "metadata": { - "name": "scratch-", + "name": "scratch-0.1.0", "annotations": {"helm.sh/resource-policy": "keep"}, }, "spec": { @@ -832,7 +832,7 @@ def test_scratch_volume_uses_correct_claimName( assert claim_name == existingClaimName assert "PersistentVolumeClaim" not in manifests else: - assert claim_name == "scratch-" + assert claim_name == "scratch-0.1.0" assert claim_name in manifests["PersistentVolumeClaim"] @@ -848,7 +848,7 @@ def worker_config_volume(): def init_config_volume(): return { "name": "init-config", - "projected": {"sources": [{"configMap": {"name": "blueapi-initconfig"}}]}, + "projected": {"sources": [{"configMap": {"name": "blueapi-init-config"}}]}, } @@ -1094,6 +1094,8 @@ def render_chart( def group_manifests(ungrouped: Iterable[Mapping[str, Any]]) -> GroupedManifests: groups = {} for manifest in ungrouped: + if manifest is None: + continue name = manifest["metadata"]["name"] kind = manifest["kind"] group = groups.setdefault(kind, {}) @@ -1138,8 +1140,119 @@ def test_init_container_config_copied_from_worker_when_enabled(): ) init_config = ApplicationConfig.model_validate( yaml.safe_load( - manifests["ConfigMap"]["blueapi-initconfig"]["data"]["initconfig.yaml"] + manifests["ConfigMap"]["blueapi-init-config"]["data"]["init_config.yaml"] ) ) assert config.scratch == init_config.scratch + + +@pytest.mark.parametrize("service_port", [80, 800]) +@pytest.mark.parametrize("service_type", ["LoadBalancer", "ClusterIP"]) +def test_service_created(service_type: str, service_port: int): + manifests = render_chart( + values={ + "service": {"type": service_type, "port": service_port}, + } + ) + spec = manifests["Service"]["blueapi"]["spec"] + assert spec["type"] == service_type + assert spec["ports"][0] == { + "name": "http", + "port": service_port, + "protocol": "TCP", + "targetPort": "http", + } + + +@pytest.mark.parametrize("ingress_host", ["blueapi.diamond.ac.uk", "ixx.diamond.ac.uk"]) +@pytest.mark.parametrize("service_type", ["LoadBalancer", "ClusterIP"]) +@pytest.mark.parametrize("service_port", [80, 800]) +def test_ingress_created(service_type: str, service_port: int, ingress_host: str): + manifests = render_chart( + values={ + "service": {"type": service_type, "port": service_port}, + "ingress": { + "enabled": True, + "hosts": [ + { + "host": ingress_host, + "paths": [{"path": "/", "pathType": "Prefix"}], + } + ], + }, + } + ) + spec = manifests["Ingress"]["blueapi"]["spec"] + assert spec["ingressClassName"] == "nginx" + assert spec["rules"][0] == { + "host": ingress_host, + "http": { + "paths": [ + { + "path": "/", + "pathType": "Prefix", + "backend": { + "service": { + "name": "blueapi", + "port": {"number": service_port}, + } + }, + } + ] + }, + } + + +def test_ingress_not_created(): + manifests = render_chart( + values={ + "ingress": {"enabled": False}, + } + ) + assert "Ingress" not in manifests + + +@pytest.mark.parametrize("service_port", [80, 800]) +@pytest.mark.parametrize( + "worker_api_url", + [ + "https://0.0.0.0", + "http://0.0.0.0", + "http://0.0.0.0:800", + "https://0.0.0.0:800", + None, + ], +) +def test_service_linked_to_api(worker_api_url: str | None, service_port: int): + manifests = render_chart( + values={ + "service": {"port": service_port}, + "worker": {"api": {"url": worker_api_url}} if worker_api_url else {}, + } + ) + service_spec = manifests["Service"]["blueapi"]["spec"] + assert service_spec["ports"][0] == { + "name": "http", + "port": service_port, + "protocol": "TCP", + "targetPort": "http", + } + + expected_container_port = { + "https://0.0.0.0": 443, + "http://0.0.0.0": 80, + "http://0.0.0.0:800": 800, + "https://0.0.0.0:800": 800, + None: 8000, + } + + container_ports = manifests["StatefulSet"]["blueapi"]["spec"]["template"]["spec"][ + "containers" + ][0]["ports"] + assert len(container_ports) == 1 + assert container_ports[0] == { + "name": "http", + "containerPort": expected_container_port[worker_api_url], + "protocol": "TCP", + }