From be152c10058d00018940e07f1d860bdd231cf520 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Apr 2025 16:33:09 +0100 Subject: [PATCH 1/9] Publish debug container image and account-sync sidecar --- .github/workflows/_account_sync.yml | 44 +++++++++++++++++++++++++ .github/workflows/_container.yml | 20 ++++++++++++ .github/workflows/ci.yml | 7 ++++ account-sync/Dockerfile | 10 ++++++ account-sync/dls-nslcd.conf | 4 +++ docs/how-to/debug-in-cluster.md | 47 +++++++++++++++++++++++++++ docs/images/debugging-kubernetes.jpg | Bin 0 -> 55132 bytes template/Dockerfile.jinja | 13 ++++++++ 8 files changed, 145 insertions(+) create mode 100644 .github/workflows/_account_sync.yml create mode 100644 account-sync/Dockerfile create mode 100644 account-sync/dls-nslcd.conf create mode 100644 docs/how-to/debug-in-cluster.md create mode 100644 docs/images/debugging-kubernetes.jpg diff --git a/.github/workflows/_account_sync.yml b/.github/workflows/_account_sync.yml new file mode 100644 index 00000000..29ad7de6 --- /dev/null +++ b/.github/workflows/_account_sync.yml @@ -0,0 +1,44 @@ +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create tags for publishing image + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }}/account-sync + tags: | + type=ref,event=tag + type=raw,value=latest + + - name: Build and publish debug image to container registry + if: github.ref_type == 'tag' + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: account-sync + push: true + tags: ${{ steps.debug-meta.outputs.tags }} diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml index da5e4936..ac409c11 100644 --- a/.github/workflows/_container.yml +++ b/.github/workflows/_container.yml @@ -46,6 +46,26 @@ jobs: type=ref,event=tag type=raw,value=latest + - name: Create tags for publishing debug image + id: debug-meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=tag,suffix=-debug + type=raw,value=latest-debug + + - name: Build and publish debug image to container registry + if: github.ref_type == 'tag' + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + push: true + target: debug + tags: ${{ steps.debug-meta.outputs.tags }} + - name: Push cached image to container registry if: github.ref_type == 'tag' uses: docker/build-push-action@v6 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f198a244..a0abcec2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,3 +45,10 @@ jobs: uses: ./.github/workflows/_release.yml permissions: contents: write + + release-account-sync: + if: github.ref_type == 'tag' + needs: release + uses: ./.github/workflows/_account_sync.yml + permissions: + contents: write diff --git a/account-sync/Dockerfile b/account-sync/Dockerfile new file mode 100644 index 00000000..e997e3e2 --- /dev/null +++ b/account-sync/Dockerfile @@ -0,0 +1,10 @@ +ARG PYTHON_VERSION=3.11 +# Use same base image as debug to prevent incompatibilities +FROM python:${PYTHON_VERSION}-slim + +RUN apt update +RUN DEBIAN_FRONTEND=noninteractive apt install libnss-ldapd -y +COPY dls-nslcd.conf /etc/nslcd.conf + +ENTRYPOINT [ "nslcd" ] +CMD [ "--debug" ] diff --git a/account-sync/dls-nslcd.conf b/account-sync/dls-nslcd.conf new file mode 100644 index 00000000..ffa039b4 --- /dev/null +++ b/account-sync/dls-nslcd.conf @@ -0,0 +1,4 @@ +URI ldap://ldap.diamond.ac.uk ldap://ldap2.diamond.ac.uk +TIMELIMIT 30 +base dc=diamond,dc=ac,dc=uk +tls_reqcert allow diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md new file mode 100644 index 00000000..6ed9648e --- /dev/null +++ b/docs/how-to/debug-in-cluster.md @@ -0,0 +1,47 @@ +# Debug a container within a cluster + +The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach so if you have configured a `livenessProbe` that requires the service to have started it should be disabled. + +With the Kubernetes plugin for vscode it is then possible to attach to the container inside the cluster. This may require that the kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. + +For containers running in the Diamond Kubernetes infrastructure that run as a specific uid (e.g. if mounting the filesystem), it is required to use a sidecar container to provide name resolution with Diamond's LDAP infrastructure and to mount a home directory to download vscode plugins. + +A sidecar for the Debian-based Python image this template uses is published as a container from this repository, the version should match the version of the python-copier-template you are using, to ensure compatibility with the underlying container infrastructure. + +```yaml +- name: debug-account-sync + image: ghcr.io/diamondlightsource/python-copier-template/account-sync: + volumeMounts: + # This allows the nslcd socket to be shared between the main container and the sidecar + - mountPath: /var/run/nslcd + name: nslcd +``` + +The following changes/additions to your `values.yaml` will be required to connect vscode when using the sidecar. + +```yaml +volumes: +- name: home # Required for vscode to install plugins + hostPath: + path: /home/ +- name: nslcd # Shared volume between main and sidecar container + emptyDir: + sizeLimit: 500Mi + +volumeMounts: +- mountPath: /home/ + name: home +- mountPath: /var/run/nslcd + name: nslcd + +# Disable any liveness probe, as will not start service automatically +livenessProbe: + +# Required to mount /home/, /dls/ etc. +podSecurityContext: + runAsUser: + runAsGroup: + +image: + tag: "-debug" +``` diff --git a/docs/images/debugging-kubernetes.jpg b/docs/images/debugging-kubernetes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70718ce26fce93c3b08ed3df3615b120d642132f GIT binary patch literal 55132 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 V|F6Q?+uV+eg}m+yNp&6Lnqpr8q-#g3TJ# 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 literal 0 HcmV?d00001 diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index c5ca109b..fa29fb72 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -18,6 +18,19 @@ COPY . /context WORKDIR /context RUN touch dev-requirements.txt && pip install -c dev-requirements.txt . +FROM build AS debug + +RUN apt update +# TODO: Is this required? +RUN DEBIAN_FRONTEND=noninteractive apt install libnss-ldapd -y +RUN sed -i 's/files/ldap files/g' /etc/nsswitch.conf + +RUN pip install debugpy +RUN pip install -e . + +ENTRYPOINT [ "/bin/bash", "-c", "--" ] +CMD [ "while true; do sleep 30; done;" ] + # The runtime stage copies the built venv into a slim runtime container FROM python:${PYTHON_VERSION}-slim AS runtime # Add apt-get system dependecies for runtime here if needed From ced766691c30aefad840128a4e76daf22b99a0ad Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Apr 2025 17:04:56 +0100 Subject: [PATCH 2/9] Ensure workspace is editable --- template/Dockerfile.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index fa29fb72..add8dc79 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -14,8 +14,8 @@ ENV PATH=/venv/bin:$PATH{% if docker %} # The build stage installs the context into the venv FROM developer AS build -COPY . /context -WORKDIR /context +COPY --chmod=777 . /workspaces/{{ repo_name }} +WORKDIR /workspaces/{{ repo_name }} RUN touch dev-requirements.txt && pip install -c dev-requirements.txt . FROM build AS debug From bb20fd6f9636b6207de6fa802ee02adf877b982f Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Apr 2025 17:05:03 +0100 Subject: [PATCH 3/9] More complete docs --- docs/how-to/debug-in-cluster.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md index 6ed9648e..fe0755d7 100644 --- a/docs/how-to/debug-in-cluster.md +++ b/docs/how-to/debug-in-cluster.md @@ -1,9 +1,19 @@ # Debug a container within a cluster -The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach so if you have configured a `livenessProbe` that requires the service to have started it should be disabled. +The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach: so if you have configured a `livenessProbe` that requires the service to have started it should be disabled. The container also installs debugpy and makes the service install editable. Any custom `command` or `args` defined for the container should be disabled. -With the Kubernetes plugin for vscode it is then possible to attach to the container inside the cluster. This may require that the kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. +With the [Kubernetes plugin for vscode](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) it is then possible to attach to the container inside the cluster. This may require that the kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. It may also be necessary to [add additional contextual information](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_config/kubectl_config_set-context/), such as the namespace in use. +![Location of the Kubernetes plugin in the plugin bar (screen left), with the Clusters>cluster>Workloads>Pods views expanded out to show a pod named "my-service", overlaid with a dropdown box, with the "Attach Visual Studio Code" highlighted](../images/debugging-kubernetes.jpg) +The Kubernetes plugin can be found in the plugin bar. Expanding the Clusters>`cluster`>Workloads>Pods views, your service should be visible. Right Click>Attach Visual Studio Code will initiate connecting to the workspace in the cluster. Select your service container from the top menu. + +After the connection to the cluster has been established, it may be necessary to open the workspace folder by clicking the Explorer option in the plugin bar, it should be mounted at `/workspaces/`, equivalent to a local devcontainer. + +Starting your service with the command usually executed by the container definition starts it on the node, with access to kubernetes resources as usual, however it's also now possible to attach a debugger, configured to autoReload code, or to start and stop the service rapidly to implement prospective changes. + +After you are happy with the changes, commit them and release a new version of your container. Changes will otherwise not be persisted across container restarts! Your git configuration should be mounted inside the container. + +## Debugging containers that run as non-root For containers running in the Diamond Kubernetes infrastructure that run as a specific uid (e.g. if mounting the filesystem), it is required to use a sidecar container to provide name resolution with Diamond's LDAP infrastructure and to mount a home directory to download vscode plugins. A sidecar for the Debian-based Python image this template uses is published as a container from this repository, the version should match the version of the python-copier-template you are using, to ensure compatibility with the underlying container infrastructure. From 586ae066e9b049494afb33f6592941764348811b Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 11 Apr 2025 16:04:29 +0100 Subject: [PATCH 4/9] Update with response to testing --- .github/workflows/_account_sync.yml | 2 +- .github/workflows/ci.yml | 2 +- docs/how-to/debug-in-cluster.md | 40 +++++++++++++++-------------- template/Dockerfile.jinja | 12 +++++++-- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/.github/workflows/_account_sync.yml b/.github/workflows/_account_sync.yml index 29ad7de6..86ae41f6 100644 --- a/.github/workflows/_account_sync.yml +++ b/.github/workflows/_account_sync.yml @@ -41,4 +41,4 @@ jobs: with: context: account-sync push: true - tags: ${{ steps.debug-meta.outputs.tags }} + tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0abcec2..fc936329 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,4 +51,4 @@ jobs: needs: release uses: ./.github/workflows/_account_sync.yml permissions: - contents: write + packages: write diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md index fe0755d7..2819ea29 100644 --- a/docs/how-to/debug-in-cluster.md +++ b/docs/how-to/debug-in-cluster.md @@ -1,56 +1,58 @@ # Debug a container within a cluster -The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach: so if you have configured a `livenessProbe` that requires the service to have started it should be disabled. The container also installs debugpy and makes the service install editable. Any custom `command` or `args` defined for the container should be disabled. +The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach: if you have configured a `livenessProbe` that requires the service to have started it should be disabled. The container also installs debugpy and makes the service install editable. Any custom `command` or `args` defined for the container should be disabled. -With the [Kubernetes plugin for vscode](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) it is then possible to attach to the container inside the cluster. This may require that the kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. It may also be necessary to [add additional contextual information](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_config/kubectl_config_set-context/), such as the namespace in use. +With the [Kubernetes plugin for vscode](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) it is then possible to attach to the container inside the cluster. This may require that your targeted kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. It may also be necessary to [add additional contextual information](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_config/kubectl_config_set-context/), such as the namespace. -![Location of the Kubernetes plugin in the plugin bar (screen left), with the Clusters>cluster>Workloads>Pods views expanded out to show a pod named "my-service", overlaid with a dropdown box, with the "Attach Visual Studio Code" highlighted](../images/debugging-kubernetes.jpg) -The Kubernetes plugin can be found in the plugin bar. Expanding the Clusters>`cluster`>Workloads>Pods views, your service should be visible. Right Click>Attach Visual Studio Code will initiate connecting to the workspace in the cluster. Select your service container from the top menu. +![Location of the Kubernetes plugin in the plugin bar (screen left), with the Clusters>cluster>Workloads>Pods views expanded out to show a pod named "my-service", overlaid with a dropdown box, with "Attach Visual Studio Code" highlighted](../images/debugging-kubernetes.jpg) +The Kubernetes plugin can be found in the plugin bar. Expanding the Clusters>`cluster`>Workloads>Pods views, your service should be visible. Right Click>Attach Visual Studio Code will initiate connecting to the workspace in the cluster. Select your service container from the top menu when prompted. -After the connection to the cluster has been established, it may be necessary to open the workspace folder by clicking the Explorer option in the plugin bar, it should be mounted at `/workspaces/`, equivalent to a local devcontainer. +After the connection to the cluster has been established, it may be necessary to open the workspace folder by clicking the Explorer option in the plugin bar, the repository will be mounted at `/workspaces/`, equivalent to when working with a local devcontainer. -Starting your service with the command usually executed by the container definition starts it on the node, with access to kubernetes resources as usual, however it's also now possible to attach a debugger, configured to autoReload code, or to start and stop the service rapidly to implement prospective changes. +Starting your service with the command in the container definition starts it on the node, with access to Kubernetes resources, however it is also now possible to run with or attach a debugger, potentially configured to autoReload code, or to start and stop the service rapidly to implement prospective changes. -After you are happy with the changes, commit them and release a new version of your container. Changes will otherwise not be persisted across container restarts! Your git configuration should be mounted inside the container. +After you are happy with the changes, commit them and release a new version of your container. Changes will otherwise not be persisted across container restarts. Your git and ssh config will be mounted inside the devcontainer while connected and for containers on github, the remote `origin` will be configured to use ssh. ## Debugging containers that run as non-root -For containers running in the Diamond Kubernetes infrastructure that run as a specific uid (e.g. if mounting the filesystem), it is required to use a sidecar container to provide name resolution with Diamond's LDAP infrastructure and to mount a home directory to download vscode plugins. +For containers running in the Diamond Kubernetes infrastructure that run as a specific uid (e.g. if mounting the filesystem), it is required to use a sidecar container to provide name resolution from Diamond's LDAP infrastructure and to mount a home directory to house vscode plugins. A sidecar for the Debian-based Python image this template uses is published as a container from this repository, the version should match the version of the python-copier-template you are using, to ensure compatibility with the underlying container infrastructure. ```yaml - name: debug-account-sync - image: ghcr.io/diamondlightsource/python-copier-template/account-sync: - volumeMounts: - # This allows the nslcd socket to be shared between the main container and the sidecar - - mountPath: /var/run/nslcd + image: ghcr.io/diamondlightsource/python-copier-template/account-sync: + volumeMounts: + # The nslcd socket will be shared between the service and the sidecar + - mountPath: /var/run/nslcd name: nslcd ``` -The following changes/additions to your `values.yaml` will be required to connect vscode when using the sidecar. +The following changes/additions to your `values.yaml` may be required to connect vscode when using the sidecar. +It is recommended to set the `HOME` environment variable on your container to be debugged to the same value used in the volume below. ```yaml volumes: -- name: home # Required for vscode to install plugins +- name: home # Required for vscode to start and install plugins hostPath: - path: /home/ + path: /home/ - name: nslcd # Shared volume between main and sidecar container emptyDir: sizeLimit: 500Mi volumeMounts: -- mountPath: /home/ +- mountPath: /home/ name: home - mountPath: /var/run/nslcd name: nslcd # Disable any liveness probe, as will not start service automatically -livenessProbe: +livenessProbe: null +readinessProbe: null # Required to mount /home/, /dls/ etc. podSecurityContext: - runAsUser: - runAsGroup: + runAsUser: + runAsGroup: image: tag: "-debug" diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index add8dc79..b6a4455f 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -14,20 +14,28 @@ ENV PATH=/venv/bin:$PATH{% if docker %} # The build stage installs the context into the venv FROM developer AS build -COPY --chmod=777 . /workspaces/{{ repo_name }} +# Requires buildkit 0.17.0 +COPY --chmod=o+wrX . /workspaces/{{ repo_name }} WORKDIR /workspaces/{{ repo_name }} RUN touch dev-requirements.txt && pip install -c dev-requirements.txt . FROM build AS debug +{% if git_platform=="github.com" %} +# Set origin to use ssh +RUN git remote set-url origin git@github.com:{{github_org}}/{{repo_name}}.git +{% endif %} + +# For this pod to understand finding user information from LDAP RUN apt update -# TODO: Is this required? RUN DEBIAN_FRONTEND=noninteractive apt install libnss-ldapd -y RUN sed -i 's/files/ldap files/g' /etc/nsswitch.conf +# Make editable and debuggable RUN pip install debugpy RUN pip install -e . +# Alternate entrypoint to allow devcontainer to attach ENTRYPOINT [ "/bin/bash", "-c", "--" ] CMD [ "while true; do sleep 30; done;" ] From 5545f3eb2cd3d59c011f617dc62dd0804f9e1611 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Tue, 20 May 2025 11:16:52 +0100 Subject: [PATCH 5/9] Remove extracted account-sync pod --- .github/workflows/_account_sync.yml | 44 ----------------------------- .github/workflows/ci.yml | 7 ----- account-sync/Dockerfile | 10 ------- account-sync/dls-nslcd.conf | 4 --- 4 files changed, 65 deletions(-) delete mode 100644 .github/workflows/_account_sync.yml delete mode 100644 account-sync/Dockerfile delete mode 100644 account-sync/dls-nslcd.conf diff --git a/.github/workflows/_account_sync.yml b/.github/workflows/_account_sync.yml deleted file mode 100644 index 86ae41f6..00000000 --- a/.github/workflows/_account_sync.yml +++ /dev/null @@ -1,44 +0,0 @@ -on: - workflow_call: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - # Need this to get version number from last tag - fetch-depth: 0 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Docker Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Create tags for publishing image - id: meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }}/account-sync - tags: | - type=ref,event=tag - type=raw,value=latest - - - name: Build and publish debug image to container registry - if: github.ref_type == 'tag' - uses: docker/build-push-action@v6 - env: - DOCKER_BUILD_RECORD_UPLOAD: false - with: - context: account-sync - push: true - tags: ${{ steps.meta.outputs.tags }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3293a3fd..223b73ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,10 +40,3 @@ jobs: uses: ./.github/workflows/_release.yml permissions: contents: write - - release-account-sync: - if: github.ref_type == 'tag' - needs: release - uses: ./.github/workflows/_account_sync.yml - permissions: - packages: write diff --git a/account-sync/Dockerfile b/account-sync/Dockerfile deleted file mode 100644 index e997e3e2..00000000 --- a/account-sync/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -ARG PYTHON_VERSION=3.11 -# Use same base image as debug to prevent incompatibilities -FROM python:${PYTHON_VERSION}-slim - -RUN apt update -RUN DEBIAN_FRONTEND=noninteractive apt install libnss-ldapd -y -COPY dls-nslcd.conf /etc/nslcd.conf - -ENTRYPOINT [ "nslcd" ] -CMD [ "--debug" ] diff --git a/account-sync/dls-nslcd.conf b/account-sync/dls-nslcd.conf deleted file mode 100644 index ffa039b4..00000000 --- a/account-sync/dls-nslcd.conf +++ /dev/null @@ -1,4 +0,0 @@ -URI ldap://ldap.diamond.ac.uk ldap://ldap2.diamond.ac.uk -TIMELIMIT 30 -base dc=diamond,dc=ac,dc=uk -tls_reqcert allow From ee0816418912d02b0efb67f7a69bb6b57af56f9a Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Mon, 23 Jun 2025 13:14:42 +0100 Subject: [PATCH 6/9] re-order and expand docs --- docs/how-to/debug-in-cluster.md | 134 +++++++++++++++++++++----------- 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md index 2819ea29..3dcd5e35 100644 --- a/docs/how-to/debug-in-cluster.md +++ b/docs/how-to/debug-in-cluster.md @@ -1,59 +1,99 @@ -# Debug a container within a cluster +# Debugging containers -The container build also publishes a debug container for each tagged release of the container with the tag suffixed with `-debug`. This container contains the workspace and has an alternative entrypoint which allows the devcontainer to attach: if you have configured a `livenessProbe` that requires the service to have started it should be disabled. The container also installs debugpy and makes the service install editable. Any custom `command` or `args` defined for the container should be disabled. +The container build also publishes a debug container for each tagged release of the container suffixed with `-debug`. This container contains an editable install of the workspace & debugpy and has an alternate entrypoint which allows the devcontainer to attach. -With the [Kubernetes plugin for vscode](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) it is then possible to attach to the container inside the cluster. This may require that your targeted kubeconfig is at `~/.kube/config`, rather than referenced from the environment variable `KUBECONFIG`. It may also be necessary to [add additional contextual information](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_config/kubectl_config_set-context/), such as the namespace. +## Using Debug image in a Helm chart -![Location of the Kubernetes plugin in the plugin bar (screen left), with the Clusters>cluster>Workloads>Pods views expanded out to show a pod named "my-service", overlaid with a dropdown box, with "Attach Visual Studio Code" highlighted](../images/debugging-kubernetes.jpg) -The Kubernetes plugin can be found in the plugin bar. Expanding the Clusters>`cluster`>Workloads>Pods views, your service should be visible. Right Click>Attach Visual Studio Code will initiate connecting to the workspace in the cluster. Select your service container from the top menu when prompted. +⚠️ If running with the Diamond filesystem mounted or as a specific user, further adjustments are required, as described [below](#using-debug-image-in-a-helm-chart-that-mounts-the-filesystem). -After the connection to the cluster has been established, it may be necessary to open the workspace folder by clicking the Explorer option in the plugin bar, the repository will be mounted at `/workspaces/`, equivalent to when working with a local devcontainer. +To use the debug image in a Helm chart can be as simple as modifying `image.tag` value in values.yaml to the tag with `-debug`, but this may run into issues if you have defined liveness or readiness probes, a custom command or args, or if the container is running as non-root. To make capturing these edge cases easier it's recommended to define a single flag `debug.enabled` in your `values.yaml` and make the following modifications to the `Deployment|ReplicaSet|StatefulSet`: -Starting your service with the command in the container definition starts it on the node, with access to Kubernetes resources, however it is also now possible to run with or attach a debugger, potentially configured to autoReload code, or to start and stop the service rapidly to implement prospective changes. +```helm +spec: + template: + spec: + containers: + - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}{{ ternary "-debug" "" .Values.debug.enabled }}" + {{- if not .Values.debug.enabled }} # If your Helm chart overrides the `CMD` Containerfile instruction, it should not when in debug mode + args: ["some", "example", "args"] + {{- end }} + {{- if not .Values.debug.enabled }} # prevent probes causing issues before attaching and starting the service + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + volumeMounts: + {{- if .Values.debug.enabled }} + - mountPath: /home # required for VSCode to install extensions if running as non-root + name: home + {{- end }} + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumes: + {{- if .Values.debug.enabled }} + - name: home # mount /home as an editable volume to prevent permission issues + emptyDir: + sizeLimit: 500Mi + {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} +``` -After you are happy with the changes, commit them and release a new version of your container. Changes will otherwise not be persisted across container restarts. Your git and ssh config will be mounted inside the devcontainer while connected and for containers on github, the remote `origin` will be configured to use ssh. +## Using Debug image in a Helm chart that mounts the filesystem -## Debugging containers that run as non-root -For containers running in the Diamond Kubernetes infrastructure that run as a specific uid (e.g. if mounting the filesystem), it is required to use a sidecar container to provide name resolution from Diamond's LDAP infrastructure and to mount a home directory to house vscode plugins. +Containers running in the Diamond Kubernetes infrastructure as a specific uid (e.g. when mounting the filesystem) must provide name resolution from Diamond's LDAP infrastructure: inside the cluster the VSCode server will be running as that user, but requires that the name & home directory of the user can be found. The debug image configures the name lookup service to try finding the user internally (i.e. from `/etc/passwd`) then fall back to calling LDAP through a service called `libnss-ldapd`. As containers are designed to run a single process, this service is run in a sidecar container which must mutually mount the `/var/run/nslcd` socket with the primary container. -A sidecar for the Debian-based Python image this template uses is published as a container from this repository, the version should match the version of the python-copier-template you are using, to ensure compatibility with the underlying container infrastructure. +It therefore requires the further additions to the template modified above: -```yaml -- name: debug-account-sync - image: ghcr.io/diamondlightsource/python-copier-template/account-sync: - volumeMounts: - # The nslcd socket will be shared between the service and the sidecar - - mountPath: /var/run/nslcd - name: nslcd +```helm +spec: + template: + spec: + containers: + - volumeMounts: + {{- if .Values.debug.enabled }} + - mountPath: /var/run/nslcd # socket to place query for user information + name: nslcd + [...] + {{- if .Values.debug.enabled }} + - name: debug-account-sync + image: ghcr.io/diamondlightsource/account-sync-sidecar:3.0.0 + volumeMounts: + - mountPath: /var/run/nslcd # socket to pick queries for user information + name: nslcd + {{- end }} + volumes: + {{- if .Values.debug.enabled }} + - name: nslcd # mutually mounted filesystem to both containers + emptyDir: + sizeLimit: 5Mi + [...] ``` -The following changes/additions to your `values.yaml` may be required to connect vscode when using the sidecar. -It is recommended to set the `HOME` environment variable on your container to be debugged to the same value used in the volume below. - -```yaml -volumes: -- name: home # Required for vscode to start and install plugins - hostPath: - path: /home/ -- name: nslcd # Shared volume between main and sidecar container - emptyDir: - sizeLimit: 500Mi - -volumeMounts: -- mountPath: /home/ - name: home -- mountPath: /var/run/nslcd - name: nslcd - -# Disable any liveness probe, as will not start service automatically -livenessProbe: null -readinessProbe: null - -# Required to mount /home/, /dls/ etc. -podSecurityContext: - runAsUser: - runAsGroup: - -image: - tag: "-debug" +# Debugging in the cluster + +With the [Kubernetes plugin for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-kubernetes-tools.vscode-kubernetes-tools) it is then possible to attach to the container inside the cluster. From the VSCode Command Palette (Ctrl+Shift+P) use the `Kubernetes: Set Kubeconfig` to configure VSCode with the server to use, then`Kubernetes: Use Namespace`. + +```sh +# To find the KUBECONFIG to use from a Diamond machine +$ module load pollux +... +$ echo $KUBECONFIG +~/.kube/config_pollux ``` + +![Location of the Kubernetes plugin in the plugin bar (screen left), with the Clusters>cluster>Workloads>Pods views expanded out to show a pod named "my-service", overlaid with a dropdown box, with "Attach Visual Studio Code" highlighted](../images/debugging-kubernetes.jpg) +The Kubernetes plugin can be found in the plugin bar. Expanding the Clusters>`cluster`>Workloads>Pods views, your service should be visible. Right Click>Attach Visual Studio Code will initiate connecting to the workspace in the cluster. Select your service container from the top menu when prompted. + +After the connection to the cluster has been established open the workspace folder by clicking the Explorer option in the plugin bar, the repository will be mounted at `/workspaces/`, equivalent to when working with a local devcontainer. + +Starting your service with the command in the container definition starts it on the node, with access to Kubernetes resources, however it is also now possible to run with or attach a debugger, potentially configured to autoReload code, or to start and stop the service rapidly to implement prospective changes. + +After you are happy with the changes, commit them and release a new version of your container. Changes will otherwise not be persisted across container restarts. Your git and ssh config will be mounted inside the devcontainer while connected and for containers on github, the remote `origin` will be configured to use ssh. From d6515e5ccf1111ded6418ba26c8fcbe5ceb1228b Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Mon, 23 Jun 2025 13:27:21 +0100 Subject: [PATCH 7/9] Prevent docs build failures --- docs/how-to/debug-in-cluster.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md index 3dcd5e35..037eeecd 100644 --- a/docs/how-to/debug-in-cluster.md +++ b/docs/how-to/debug-in-cluster.md @@ -2,13 +2,13 @@ The container build also publishes a debug container for each tagged release of the container suffixed with `-debug`. This container contains an editable install of the workspace & debugpy and has an alternate entrypoint which allows the devcontainer to attach. -## Using Debug image in a Helm chart +# Using Debug image in a Helm chart -⚠️ If running with the Diamond filesystem mounted or as a specific user, further adjustments are required, as described [below](#using-debug-image-in-a-helm-chart-that-mounts-the-filesystem). +⚠️ If running with the Diamond filesystem mounted or as a specific user, further adjustments are required, as described in the next section. To use the debug image in a Helm chart can be as simple as modifying `image.tag` value in values.yaml to the tag with `-debug`, but this may run into issues if you have defined liveness or readiness probes, a custom command or args, or if the container is running as non-root. To make capturing these edge cases easier it's recommended to define a single flag `debug.enabled` in your `values.yaml` and make the following modifications to the `Deployment|ReplicaSet|StatefulSet`: -```helm +```yaml spec: template: spec: @@ -46,13 +46,13 @@ spec: {{- end }} ``` -## Using Debug image in a Helm chart that mounts the filesystem +# Using Debug image in a Helm chart that mounts the filesystem Containers running in the Diamond Kubernetes infrastructure as a specific uid (e.g. when mounting the filesystem) must provide name resolution from Diamond's LDAP infrastructure: inside the cluster the VSCode server will be running as that user, but requires that the name & home directory of the user can be found. The debug image configures the name lookup service to try finding the user internally (i.e. from `/etc/passwd`) then fall back to calling LDAP through a service called `libnss-ldapd`. As containers are designed to run a single process, this service is run in a sidecar container which must mutually mount the `/var/run/nslcd` socket with the primary container. It therefore requires the further additions to the template modified above: -```helm +```yaml spec: template: spec: From 77fe321754d4da37133a9ef506e8501e9791a851 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Mon, 30 Jun 2025 10:01:09 +0100 Subject: [PATCH 8/9] Publish debug container only if requested --- .github/workflows/_container.yml | 20 -------- copier.yml | 9 ++++ example-answers.yml | 1 + template/Dockerfile.jinja | 2 + .../workflows/ci.yml.jinja" | 11 ++++- ...r_debug %}_debug_container.yml{% endif %}" | 49 +++++++++++++++++++ 6 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 "template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/{% if docker_debug %}_debug_container.yml{% endif %}" diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml index 706ffc81..c6cd4697 100644 --- a/.github/workflows/_container.yml +++ b/.github/workflows/_container.yml @@ -50,26 +50,6 @@ jobs: type=ref,event=tag type=raw,value=latest - - name: Create tags for publishing debug image - id: debug-meta - uses: docker/metadata-action@v5 - with: - images: ghcr.io/${{ github.repository }} - tags: | - type=ref,event=tag,suffix=-debug - type=raw,value=latest-debug - - - name: Build and publish debug image to container registry - if: github.ref_type == 'tag' - uses: docker/build-push-action@v6 - env: - DOCKER_BUILD_RECORD_UPLOAD: false - with: - context: . - push: true - target: debug - tags: ${{ steps.debug-meta.outputs.tags }} - - name: Push cached image to container registry if: inputs.publish && github.ref_type == 'tag' uses: docker/build-push-action@v6 diff --git a/copier.yml b/copier.yml index ac63bfda..6c414eb5 100644 --- a/copier.yml +++ b/copier.yml @@ -103,6 +103,15 @@ docker: Would you like to publish your project in a Docker container? You should select this if you are making a service. +docker_debug: + type: bool + when: "{{ docker }}" + help: | + Would you like to publish a debug image of your service? + This will increase the number of published images, but may + be useful if debugging the service inside of the cluster + infrastructure is required. + docs_type: type: str help: | diff --git a/example-answers.yml b/example-answers.yml index 291daedf..17869585 100644 --- a/example-answers.yml +++ b/example-answers.yml @@ -6,6 +6,7 @@ component_lifecycle: experimental description: An expanded https://github.com/DiamondLightSource/python-copier-template to illustrate how it looks with all the options enabled. distribution_name: dls-python-copier-template-example docker: true +docker_debug: true docs_type: sphinx git_platform: github.com github_org: DiamondLightSource diff --git a/template/Dockerfile.jinja b/template/Dockerfile.jinja index b6a4455f..121856a0 100644 --- a/template/Dockerfile.jinja +++ b/template/Dockerfile.jinja @@ -19,6 +19,7 @@ COPY --chmod=o+wrX . /workspaces/{{ repo_name }} WORKDIR /workspaces/{{ repo_name }} RUN touch dev-requirements.txt && pip install -c dev-requirements.txt . +{% if docker_debug %} FROM build AS debug {% if git_platform=="github.com" %} @@ -39,6 +40,7 @@ RUN pip install -e . ENTRYPOINT [ "/bin/bash", "-c", "--" ] CMD [ "while true; do sleep 30; done;" ] +{% endif %} # The runtime stage copies the built venv into a slim runtime container FROM python:${PYTHON_VERSION}-slim AS runtime # Add apt-get system dependecies for runtime here if needed diff --git "a/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/ci.yml.jinja" "b/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/ci.yml.jinja" index a6577629..a6b536aa 100644 --- "a/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/ci.yml.jinja" +++ "b/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/ci.yml.jinja" @@ -41,7 +41,16 @@ jobs: permissions: contents: read packages: write -{% endraw %}{% endif %}{% if sphinx %} +{% endraw %}{% if docker_debug %}{% raw %} + debug_container: + needs: [container, test] + uses: ./.github/workflows/_debug_container.yml + with: + publish: ${{ needs.test.result == 'success' }} + permissions: + contents: read + packages: write +{% endraw %}{% endif %}{% endif %}{% if sphinx %} docs: uses: ./.github/workflows/_docs.yml diff --git "a/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/{% if docker_debug %}_debug_container.yml{% endif %}" "b/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/{% if docker_debug %}_debug_container.yml{% endif %}" new file mode 100644 index 00000000..31206c1c --- /dev/null +++ "b/template/{% if git_platform==\"github.com\" %}.github{% endif %}/workflows/{% if docker_debug %}_debug_container.yml{% endif %}" @@ -0,0 +1,49 @@ +on: + workflow_call: + inputs: + publish: + type: boolean + description: If true, pushes image to container registry + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create tags for publishing debug image + id: debug-meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=tag,suffix=-debug + type=raw,value=latest-debug + + - name: Build and publish debug image to container registry + if: github.ref_type == 'tag' + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + push: true + target: debug + tags: ${{ steps.debug-meta.outputs.tags }} From 694c8f1775311cc6f1917e29a7a4ff8f16ce92fa Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Mon, 30 Jun 2025 10:37:56 +0100 Subject: [PATCH 9/9] Clarifying note to docs --- docs/how-to/debug-in-cluster.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/debug-in-cluster.md b/docs/how-to/debug-in-cluster.md index 037eeecd..4682781a 100644 --- a/docs/how-to/debug-in-cluster.md +++ b/docs/how-to/debug-in-cluster.md @@ -1,6 +1,6 @@ # Debugging containers -The container build also publishes a debug container for each tagged release of the container suffixed with `-debug`. This container contains an editable install of the workspace & debugpy and has an alternate entrypoint which allows the devcontainer to attach. +If the `docker_debug` option is chosen, the container build also publishes a debug container for each tagged release of the container suffixed with `-debug`. This container contains an editable install of the workspace & debugpy and has an alternate entrypoint which allows the devcontainer to attach. # Using Debug image in a Helm chart