Repository: jupyterhub/binderhub
Pull request #1531 (2022-09-16 14:59:57)

Podman in k8s
This pull request is a refactor that allows to use alternatives to Docker as image builder as requested in jupyterhub/binderhub#1513. In this case, support for Podman is implemented.

The PR implements a rewrite of the DaemonSet providing the docker image build service.

The new `imageBuilderType` parameter introduced will let users select which image builder to use:
- local: same as what setting dind.enabled to false used to doo
- dind: same as what setting dind.enabled to true used to do
- pink: new builder using Podman in place of Docker

The daemonset name pink is the acronym of "podman in kubernetes".

Configuration example:

```
service:
  type: ClusterIP

config:
  BinderHub:
    debug: true
    hub_url: "https://my.host.com/jupyter"

    use_registry: true
    image_prefix: "my.registry.com/my-project/my-image-"

    build_docker_host: "unix:///local/fake_var/run/pink/docker.sock"
    build_image: "docker.io/sgaist/repo2podman:latest"

imageBuilderType: pink

pink:
  hostStorageDir: /local/fake_var/lib/pink/storage/
  hostSocketDir: /local/fake_var/run/pink/

imageCleaner:
  host:
    dockerSocket: /local/fake_var/run/pink/docker.sock
    dockerLibDir: /local/fake_var/lib/pink/storage

ingress:
  enabled: true
  ingressClassName: nginx
  hosts:
    - my.host.com
```

One advantage of this new implementation is that it should allow the addition of new build services without requiring core modifications.

sgaist: Small note, as it is a preliminary work I have not written the documentation related to it yet.
sgaist: Following this month's JupyterHub's meeting, @manics and myself continued a bit on the podman front discussing the podman engine for repo2docker as well as this patch content. One thing that I was not aware of was that beside podman being a drop in replacement for docker in terms of command line interface, it now also implements a docker compatible API from a service point of view.
Therefore I ran a BinderHub deployment test with the pink daemonset enabled, the standard Build class, and the image cleaner configured to watch the storage folder of podman.

All these together worked quite nicely.

This means that the environment variable addition is not strictly required for the goal of using podman in place of docker for the pod building the images. However it does allow people to choose whether they want to go full podman, full docker, or a mix of both as it would be the default.

Patch:
diff --git a/.github/workflows/watch-dependencies.yaml b/.github/workflows/watch-dependencies.yaml
index 17839cd5..8af086c3 100644
--- a/.github/workflows/watch-dependencies.yaml
+++ b/.github/workflows/watch-dependencies.yaml
@@ -29,6 +29,10 @@ jobs:
             registry: registry.hub.docker.com
             repository: library/docker
             values_path: dind.daemonset.image.tag
+          - name: podman
+            registry: quay.io
+            repository: podman/stable
+            values_path: pink.daemonset.image.tag
 
           # FIXME: After docker-image-cleaner 1.0.0 is released, we can enable
           #        this. So far, there isn't any available stable release, and
@@ -59,7 +63,7 @@ jobs:
         run: |
           latest_tag=$(
               docker run --rm quay.io/skopeo/stable list-tags docker://${{ matrix.registry }}/${{ matrix.repository }} \
-            | jq -r '[.Tags[] | select(. | match("^\\d+\\.\\d+\\.\\d+$") | .string)] | sort_by(split(".") | map(tonumber)) | last'
+            | jq -r '[.Tags[] | select(. | match("^v?\\d+\\.\\d+\\.\\d+$") | .string)] | sort_by(split(".") | map(ltrimstr("v") | tonumber)) | last'
           )
           echo "tag=$latest_tag" >> $GITHUB_OUTPUT
 
diff --git a/binderhub/build.py b/binderhub/build.py
index 50d68379..17d96ca8 100644
--- a/binderhub/build.py
+++ b/binderhub/build.py
@@ -259,6 +259,12 @@ class KubernetesBuildExecutor(BuildExecutor):
         {}, help="Node selector for the kubernetes build pod.", config=True
     )
 
+    extra_envs = Dict(
+        {},
+        help="Extra environment variables for the kubernetes build pod.",
+        config=True,
+    )
+
     log_tail_lines = Integer(
         100,
         help=(
@@ -380,7 +386,10 @@ class KubernetesBuildExecutor(BuildExecutor):
                 )
             )
 
-        env = []
+        env = [
+            client.V1EnvVar(name=key, value=value)
+            for key, value in self.extra_envs.items()
+        ]
         if self.git_credentials:
             env.append(
                 client.V1EnvVar(name="GIT_CREDENTIAL_ENV", value=self.git_credentials)
diff --git a/docs/source/zero-to-binderhub/setup-binderhub.rst b/docs/source/zero-to-binderhub/setup-binderhub.rst
index aa1681bd..e0cefd5b 100644
--- a/docs/source/zero-to-binderhub/setup-binderhub.rst
+++ b/docs/source/zero-to-binderhub/setup-binderhub.rst
@@ -355,7 +355,7 @@ You now have a functioning BinderHub at the above IP address.
 Customizing your Deployment
 ---------------------------
 
-The Helm chart used to install your BinderHub deployemnt exposes a number of
+The Helm chart used to install your BinderHub deployment exposes a number of
 optional features. Below we describe a few of the most common customizations
 and how you can configure them.
 
@@ -431,7 +431,7 @@ time at `the token administration page <https://github.com/settings/tokens>`_.
 GitLab
 ^^^^^^
 
-To access private GitLab repos, create an API token for your binderhub user
+To access private GitLab repos, create an API token for your BinderHub user
 under "User Settings" > "Access tokens". It at least needs the scopes "api" and
 "read_repository".
 
@@ -452,20 +452,20 @@ clone a repo.
 Use Docker-inside-Docker (DinD)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-By default, BinderHub will build pods with the host Docker installation.
+By default, BinderHub will build images with the host Docker installation.
 This often means you are stuck with whatever version of Docker provided by your
 cloud provider. BinderHub supports an alternative that uses `Docker-in-Docker
 (DinD) <https://hub.docker.com/_/docker>`_. To turn `dind` on, you'll need to set
 the following configuration in your ``config.yaml`` file::
 
+    imageBuilderType: dind
     dind:
-      enabled: true
       daemonset:
         image:
           name: docker
           tag: 18.09.2-dind
 
-If you plan to host multiple BinderHub deployments on the same kubernetes
+If you plan to host multiple BinderHub deployments on the same Kubernetes
 cluster, you'll also need to isolate the host socket and library directory
 for each DinD application::
 
@@ -474,4 +474,32 @@ for each DinD application::
       hostSocketDir: /var/run/dind/"<name of deployment, e.g. staging>"
 
 
+Use Podman-inside-Kubernetes (PinK)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In case Docker is not an option, Podman can be used as drop in replacement.
+Note that the implications about using a host installation of Podman are the same
+as with Docker. BinderHub supports an alternative that uses `Podman-in-Kubernetes
+(PinK) <https://www.redhat.com/sysadmin/podman-inside-kubernetes>`_. To turn
+`pink` on, you'll need to set the following configuration in your ``config.yaml`` file::
+
+    imageBuilderType: pink
+
+You can optionally override the default podman image:
+
+    pink:
+      daemonset:
+        image:
+          name: quay.io/podman/stable
+          tag: v4.2.0
+
+If you plan to host multiple BinderHub deployments on the same Kubernetes
+cluster, you'll also need to isolate the host socket and library directory
+for each DinD application::
+
+    pink:
+      hostStorageDir: /var/lib/pink/"<name of deployment, e.g. staging>"
+      hostSocketDir: /var/run/pink/"<name of deployment, e.g. staging>"
+
+
 For next steps, see :doc:`../debug` and :doc:`turn-off`.
diff --git a/helm-chart/binderhub/files/binderhub_config.py b/helm-chart/binderhub/files/binderhub_config.py
index ce270e4c..15d4408f 100644
--- a/helm-chart/binderhub/files/binderhub_config.py
+++ b/helm-chart/binderhub/files/binderhub_config.py
@@ -46,11 +46,12 @@ c.BinderHub.template_path = "/etc/binderhub/templates"
 for section, sub_cfg in get_value("config", {}).items():
     c[section].update(sub_cfg)
 
-if get_value("dind.enabled", False) and get_value("dind.hostSocketDir"):
-    c.BinderHub.build_docker_host = "unix://{}/docker.sock".format(
-        get_value("dind.hostSocketDir")
-    )
-
+imageBuilderType = get_value("imageBuilderType")
+if imageBuilderType in ["dind", "pink"]:
+    hostSocketDir = get_value(f"{imageBuilderType}.hostSocketDir")
+    if hostSocketDir:
+        socketname = "docker" if imageBuilderType == "dind" else "podman"
+        c.BinderHub.build_docker_host = f"unix://{hostSocketDir}/{socketname}.sock"
 
 if c.BinderHub.auth_enabled:
     hub_url = urlparse(c.BinderHub.hub_url)
diff --git a/helm-chart/binderhub/schema.yaml b/helm-chart/binderhub/schema.yaml
index a0b91a0e..5c38156a 100644
--- a/helm-chart/binderhub/schema.yaml
+++ b/helm-chart/binderhub/schema.yaml
@@ -28,6 +28,8 @@ required:
   - jupyterhub
   - deployment
   - dind
+  - pink
+  - imageBuilderType
   - imageCleaner
   - ingress
   - initContainers
@@ -349,6 +351,13 @@ properties:
           See the [Kubernetes docs](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
           to learn more about labels.
 
+  imageBuilderType:
+    type: string
+    enum: ["local", "dind", "pink"]
+    default: "local"
+    description: |
+      Selected image builder type
+
   dind:
     type: object
     additionalProperties: false
@@ -356,7 +365,7 @@ properties:
       enabled:
         type: boolean
         description: |
-          TODO
+          DEPRECATED: Use `imageBuilderType: dind`
       initContainers: &initContainers-spec
         type: array
         description: |
@@ -409,6 +418,29 @@ properties:
         description: |
           TODO
 
+  pink:
+    type: object
+    additionalProperties: false
+    properties:
+      initContainers: *initContainers-spec
+      daemonset:
+        type: object
+        additionalProperties: false
+        properties:
+          image: *image-spec
+          lifecycle: *lifecycle-spec
+          extraVolumes: *extraVolumes-spec
+          extraVolumeMounts: *extraVolumeMounts-spec
+      resources: *resources-spec
+      hostStorageDir:
+        type: string
+        description: |
+          Host path where the containers storage will be located
+      hostSocketDir:
+        type: string
+        description: |
+          Host path where the podman socket will be located
+
   imageCleaner:
     type: object
     additionalProperties: false
diff --git a/helm-chart/binderhub/templates/NOTES.txt b/helm-chart/binderhub/templates/NOTES.txt
index 58c74816..c468c0b5 100644
--- a/helm-chart/binderhub/templates/NOTES.txt
+++ b/helm-chart/binderhub/templates/NOTES.txt
@@ -182,6 +182,14 @@ config:
 {{- $breaking = print $breaking "\n\nRENAMED: jupyterhub.custom.cors.allowOrigin has been renamed to jupyterhub.hub.config.BinderSpawner.cors_allow_origin" }}
 {{- end }}
 
+{{- if hasKey .Values.dind "enabled" }}
+{{- $breaking = print $breaking "\n\nCHANGED:" }}
+{{- $breaking = print $breaking "\n\ndind:" }}
+{{- $breaking = print $breaking "\n  enabled: true" }}
+{{- $breaking = print $breaking "\n\nmust as of version 0.3.0 be replace by" }}
+{{- $breaking = print $breaking "\n\nimageBuilderType: dind" }}
+{{- end }}
+
 {{- if $breaking }}
 {{- fail (print $breaking_title $breaking) }}
 {{- end }}
diff --git a/helm-chart/binderhub/templates/container-builder/daemonset.yaml b/helm-chart/binderhub/templates/container-builder/daemonset.yaml
new file mode 100644
index 00000000..7a047108
--- /dev/null
+++ b/helm-chart/binderhub/templates/container-builder/daemonset.yaml
@@ -0,0 +1,124 @@
+{{- if ne .Values.imageBuilderType "local" -}}
+{{- $builderName := .Values.imageBuilderType -}}
+{{- $builder := index .Values $builderName -}}
+{{- $daemonset := $builder.daemonset -}}
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+  name: {{ .Release.Name }}-{{ $builderName }}
+spec:
+  updateStrategy:
+    type: RollingUpdate
+  selector:
+    matchLabels:
+      name:  {{ .Release.Name }}-{{ $builderName }}
+  template:
+    metadata:
+      labels:
+        name: {{ .Release.Name }}-{{ $builderName }}
+        app: binder
+        component: image-builder
+        release: {{ .Release.Name }}
+        heritage: {{ .Release.Service }}
+    spec:
+      {{- with include "jupyterhub.imagePullSecrets" (dict "root" . "image" $daemonset.image) }}
+      imagePullSecrets: {{ . }}
+      {{- end }}
+      tolerations:
+      - effect: NoSchedule
+        key: hub.jupyter.org/dedicated
+        operator: Equal
+        value: user
+      - effect: NoSchedule
+        key: hub.jupyter.org_dedicated
+        operator: Equal
+        value: user
+      nodeSelector: {{ .Values.config.BinderHub.build_node_selector | default dict | toJson }}
+
+      {{- with $builder.initContainers }}
+      initContainers:
+        {{- . | toYaml | nindent 8 }}
+      {{- end }}
+
+      containers:
+        - name: {{ $builderName }}
+          image: {{ $daemonset.image.name }}:{{ $daemonset.image.tag }}
+          {{- with $daemonset.image.pullPolicy }}
+          imagePullPolicy: {{ . }}
+          {{- end }}
+          {{- with $daemonset.resources }}
+          resources:
+            {{- $daemonset.resources | toYaml | nindent 12 }}
+          {{- end }}
+          {{- if eq $builderName "dind" }}
+          args:
+            - dockerd
+            - --storage-driver={{ $builder.storageDriver }}
+            - -H unix://{{ $builder.hostSocketDir }}/docker.sock
+            {{- with $daemonset.extraArgs }}
+            {{- . | toYaml | nindent 12 }}
+            {{- end }}
+          securityContext:
+            privileged: true
+          volumeMounts:
+          - name: dockerlib-dind
+            mountPath: /var/lib/docker
+          - name: run-dind
+            mountPath: {{ $builder.hostSocketDir }}
+          {{- end }}
+          {{- if eq $builderName "pink" }}
+          args:
+            - podman
+            - system
+            - service
+            - --time=0
+            - unix://{{ $builder.hostSocketDir }}/podman.sock
+          securityContext:
+            privileged: true
+            runAsUser: 0
+          volumeMounts:
+          - name: podman-containers
+            mountPath: /var/lib/containers/storage
+          - name: run-pink
+            mountPath: {{ $builder.hostSocketDir }}
+          {{- end }}
+
+          {{- with $daemonset.extraVolumeMounts }}
+          {{- . | toYaml | nindent 10 }}
+          {{- end }}
+
+          {{- with $daemonset.lifecycle }}
+          lifecycle:
+            {{- . | toYaml | nindent 12 }}
+          {{- end }}
+
+      volumes:
+        {{- if eq $builderName "dind" }}
+        - name: dockerlib-dind
+          hostPath:
+            path: {{ $builder.hostLibDir }}
+            type: DirectoryOrCreate
+        - name: run-dind
+          hostPath:
+            path: {{ $builder.hostSocketDir }}
+            type: DirectoryOrCreate
+        {{- with $daemonset.extraVolumes }}
+        {{- . | toYaml | nindent 8 }}
+        {{- end }}
+        {{- end }}
+        {{- if eq $builderName "pink" }}
+        - name: podman-containers
+          hostPath:
+            path: {{ $builder.hostStorageDir }}
+            type: DirectoryOrCreate
+        - name: run-pink
+          hostPath:
+            path: {{ $builder.hostSocketDir }}
+            type: DirectoryOrCreate
+        {{- with $daemonset.extraVolumes }}
+        {{- . | toYaml | nindent 8 }}
+        {{- end }}
+        {{- end }}
+
+      terminationGracePeriodSeconds: 30
+{{- end }}
diff --git a/helm-chart/binderhub/templates/dind/daemonset.yaml b/helm-chart/binderhub/templates/dind/daemonset.yaml
deleted file mode 100644
index 6893d5a7..00000000
--- a/helm-chart/binderhub/templates/dind/daemonset.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-{{- if .Values.dind.enabled -}}
-apiVersion: apps/v1
-kind: DaemonSet
-metadata:
-  name: {{ .Release.Name }}-dind
-spec:
-  updateStrategy:
-    type: RollingUpdate
-  selector:
-    matchLabels:
-      name:  {{ .Release.Name }}-dind
-  template:
-    metadata:
-      labels:
-        name: {{ .Release.Name }}-dind
-        app: binder
-        component: image-builder
-        release: {{ .Release.Name }}
-        heritage: {{ .Release.Service }}
-    spec:
-      {{- with include "jupyterhub.imagePullSecrets" (dict "root" . "image" .Values.dind.daemonset.image) }}
-      imagePullSecrets: {{ . }}
-      {{- end }}
-      tolerations:
-      - effect: NoSchedule
-        key: hub.jupyter.org/dedicated
-        operator: Equal
-        value: user
-      - effect: NoSchedule
-        key: hub.jupyter.org_dedicated
-        operator: Equal
-        value: user
-      nodeSelector: {{ .Values.config.BinderHub.build_node_selector | default dict | toJson }}
-      {{- with .Values.dind.initContainers }}
-      initContainers:
-        {{- . | toYaml | nindent 8 }}
-      {{- end }}
-      containers:
-      - name: dind
-        image: {{ .Values.dind.daemonset.image.name }}:{{ .Values.dind.daemonset.image.tag }}
-        {{- with .Values.dind.daemonset.image.pullPolicy }}
-        imagePullPolicy: {{ . }}
-        {{- end }}
-        resources:
-          {{- .Values.dind.resources | toYaml | nindent 10 }}
-        args:
-          - dockerd
-          - --storage-driver={{ .Values.dind.storageDriver }}
-          - -H unix://{{ .Values.dind.hostSocketDir }}/docker.sock
-          {{- with .Values.dind.daemonset.extraArgs }}
-          {{- . | toYaml | nindent 10 }}
-          {{- end }}
-        securityContext:
-          privileged: true
-        volumeMounts:
-        - name: dockerlib-dind
-          mountPath: /var/lib/docker
-        - name: rundind
-          mountPath: {{ .Values.dind.hostSocketDir }}
-        {{- with .Values.dind.daemonset.extraVolumeMounts }}
-        {{- . | toYaml | nindent 8 }}
-        {{- end }}
-        {{- with .Values.dind.daemonset.lifecycle }}
-        lifecycle:
-          {{- . | toYaml | nindent 10 }}
-        {{- end }}
-      terminationGracePeriodSeconds: 30
-      volumes:
-      - name: dockerlib-dind
-        hostPath:
-          path: {{ .Values.dind.hostLibDir }}
-          type: DirectoryOrCreate
-      - name: rundind
-        hostPath:
-          path: {{ .Values.dind.hostSocketDir }}
-          type: DirectoryOrCreate
-      {{- with .Values.dind.daemonset.extraVolumes }}
-      {{- . | toYaml | nindent 6 }}
-      {{- end }}
-{{- end }}
diff --git a/helm-chart/binderhub/templates/image-cleaner.yaml b/helm-chart/binderhub/templates/image-cleaner.yaml
index 8f86d0b2..4e815276 100644
--- a/helm-chart/binderhub/templates/image-cleaner.yaml
+++ b/helm-chart/binderhub/templates/image-cleaner.yaml
@@ -36,17 +36,17 @@ spec:
       serviceAccountName: {{ .Release.Name }}-image-cleaner
       {{- end }}
       containers:
-      {{- range $i, $kind := tuple "host" "dind" }}
-      {{- if or (and (eq $kind "dind") $Values.dind.enabled) (and (eq $kind "host") $Values.imageCleaner.host.enabled) }}
+      {{- range $i, $kind := tuple "host" "dind" "pink" }}
+      {{- if or (and (eq $kind "dind") (eq $Values.imageBuilderType "dind")) (and (eq $kind "pink") (eq $Values.imageBuilderType "pink")) (and (eq $kind "host") $Values.imageCleaner.host.enabled) }}
       - name: image-cleaner-{{ $kind }}
         image: {{ $Values.imageCleaner.image.name }}:{{ $Values.imageCleaner.image.tag }}
         {{- with $Values.imageCleaner.image.pullPolicy }}
         imagePullPolicy: {{ . }}
         {{- end }}
         volumeMounts:
-        - name: dockerlib-{{ $kind }}
-          mountPath: /var/lib/docker
-        - name: dockersocket-{{ $kind }}
+        - name: storage-{{ $kind }}
+          mountPath: /var/lib/{{ $kind }}
+        - name: socket-{{ $kind }}
           mountPath: /var/run/docker.sock
         env:
         - name: DOCKER_IMAGE_CLEANER_NODE_NAME
@@ -54,7 +54,7 @@ spec:
             fieldRef:
               fieldPath: spec.nodeName
         - name: DOCKER_IMAGE_CLEANER_PATH_TO_CHECK
-          value: /var/lib/docker
+          value: /var/lib/{{ $kind }}
         - name: DOCKER_IMAGE_CLEANER_DELAY_SECONDS
           value: {{ $Values.imageCleaner.delay | quote }}
         - name: DOCKER_IMAGE_CLEANER_THRESHOLD_TYPE
@@ -68,22 +68,33 @@ spec:
       terminationGracePeriodSeconds: 0
       volumes:
       {{- if .Values.imageCleaner.host.enabled }}
-      - name: dockerlib-host
+      - name: storage-host
         hostPath:
           path: {{ .Values.imageCleaner.host.dockerLibDir }}
-      - name: dockersocket-host
+      - name: socket-host
         hostPath:
           path: {{ .Values.imageCleaner.host.dockerSocket }}
           type: Socket
       {{- end }}
-      {{- if .Values.dind.enabled }}
-      - name: dockerlib-dind
+      {{- if eq $Values.imageBuilderType "dind" }}
+      - name: storage-dind
         hostPath:
           path: {{ .Values.dind.hostLibDir }}
           type: DirectoryOrCreate
-      - name: dockersocket-dind
+      - name: socket-dind
         hostPath:
           path: {{ .Values.dind.hostSocketDir }}/docker.sock
           type: Socket
       {{- end }}
+      {{- if eq $Values.imageBuilderType "pink" }}
+      - name: storage-pink
+        hostPath:
+          path: {{ .Values.pink.hostStorageDir }}
+          type: DirectoryOrCreate
+      - name: socket-pink
+        hostPath:
+          path: {{ .Values.pink.hostSocketDir }}/podman.sock
+          type: Socket
+      {{- end }}
+
 {{- end }}
diff --git a/helm-chart/binderhub/templates/secret.yaml b/helm-chart/binderhub/templates/secret.yaml
index bedffae2..e6ee24d5 100644
--- a/helm-chart/binderhub/templates/secret.yaml
+++ b/helm-chart/binderhub/templates/secret.yaml
@@ -16,7 +16,7 @@ stringData:
     in binderhub_config.py.
   */}}
   values.yaml: |
-    {{- pick .Values "config" "cors" "dind" "extraConfig" | toYaml | nindent 4 }}
+    {{- pick .Values "config" "imageBuilderType" "cors" "dind" "pink" "extraConfig" | toYaml | nindent 4 }}
 
   {{- /* Glob files to allow them to be mounted by the binderhub pod */}}
   {{- /* key=filename: value=content */}}
diff --git a/helm-chart/binderhub/values.yaml b/helm-chart/binderhub/values.yaml
index 3e3f8ba0..a912a145 100644
--- a/helm-chart/binderhub/values.yaml
+++ b/helm-chart/binderhub/values.yaml
@@ -247,8 +247,9 @@ deployment:
     timeoutSeconds: 10
   labels: {}
 
+imageBuilderType: "local"
+
 dind:
-  enabled: false
   initContainers: []
   daemonset:
     image:
@@ -266,6 +267,22 @@ dind:
   hostSocketDir: /var/run/dind
   hostLibDir: /var/lib/dind
 
+# Podman in Kubernetes
+pink:
+  initContainers: []
+  daemonset:
+    image:
+      name: quay.io/podman/stable
+      tag: v4.3.1
+      pullPolicy: ""
+      pullSecrets: []
+    lifecycle: {}
+    extraVolumes: []
+    extraVolumeMounts: []
+  resources: {}
+  hostStorageDir: /var/lib/pink/storage
+  hostSocketDir: /var/run/pink
+
 imageCleaner:
   enabled: true
   image:
diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml
index 362bc66d..8e569acc 100644
--- a/tools/templates/lint-and-validate-values.yaml
+++ b/tools/templates/lint-and-validate-values.yaml
@@ -86,8 +86,9 @@ deployment:
   livenessProbe: *probe
   labels: *labels
 
+imageBuilderType: local
+
 dind:
-  enabled: true
   initContainers: &initContainers
     - name: mock-init-container-name
       image: mock-init-container-image
@@ -108,6 +109,17 @@ dind:
   hostSocketDir: /var/run/dind
   hostLibDir: /var/lib/dind
 
+pink:
+  initContainers: *initContainers
+  daemonset:
+    image: *image
+    lifecycle: *lifecycle
+    extraVolumes: []
+    extraVolumeMounts: []
+  resources: *resources
+  hostStorageDir: /var/lib/pink
+  hostSocketDir: /var/run/pink
+
 imageCleaner:
   enabled: true
   image: *image
@@ -146,6 +158,7 @@ extraEnv: &extraEnv
     value: MOCK_ENV_VAR_VALUE1
   MOCK_ENV_VAR_NAME2:
     value: MOCK_ENV_VAR_VALUE2
+extraFiles: {}
 podAnnotations: *annotations
 
 global:


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 906e97aa..a8576884 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -161,6 +161,20 @@ jobs:
           helm template --validate binderhub-test helm-chart/binderhub \
               --values tools/templates/lint-and-validate-values.yaml
 
+      - name: "Helm template --validate for dind (with lint-and-validate-values.yaml)"
+        if: matrix.test == 'helm'
+        run: |
+          helm template --validate binderhub-test helm-chart/binderhub \
+              --values tools/templates/lint-and-validate-values.yaml \
+              --set imageBuilderType=dind
+
+      - name: "Helm template --validate for pink (with lint-and-validate-values.yaml)"
+        if: matrix.test == 'helm'
+        run: |
+          helm template --validate binderhub-test helm-chart/binderhub \
+              --values tools/templates/lint-and-validate-values.yaml \
+              --set imageBuilderType=pink
+
       - name: Validate the chart against the k8s API
         if: matrix.test == 'helm'
         run: |
diff --git a/binderhub/tests/test_build.py b/binderhub/tests/test_build.py
index ba13ab83..ee3a832c 100644
--- a/binderhub/tests/test_build.py
+++ b/binderhub/tests/test_build.py
@@ -13,7 +13,7 @@ from kubernetes import client
 from tornado.httputil import url_concat
 from tornado.queues import Queue
 
-from binderhub.build import Build, ProgressEvent
+from binderhub.build import Build, KubernetesBuildExecutor, ProgressEvent
 from binderhub.build_local import LocalRepo2dockerBuild, ProcessTerminated, _execute_cmd
 
 from .utils import async_requests
@@ -241,6 +241,47 @@ def test_git_credentials_passed_to_podspec_upon_submit():
     assert env["GIT_CREDENTIAL_ENV"] == git_credentials
 
 
+def test_extra_environment_variables_passed_to_podspec_upon_submit():
+    extra_environments = {
+        "CONTAINER_HOST": "unix:///var/run/docker.sock",
+        "REGISTRY_AUTH_FILE": "/root/.docker/config.json",
+    }
+
+    mock_k8s_api = _list_image_builder_pods_mock()
+
+    class EnvBuild(KubernetesBuildExecutor):
+        q = mock.MagicMock()
+        api = mock_k8s_api
+        name = "test_build"
+        repo_url = "repo"
+        ref = "ref"
+        image_name = "name"
+        extra_envs = extra_environments
+        namespace = "build_namespace"
+        push_secret = ""
+        build_image = "image"
+        memory_limit = 0
+        docker_host = "http://mydockerregistry.local"
+        node_selector = {}
+
+    build = EnvBuild()
+
+    with mock.patch.object(build.stop_event, "is_set", return_value=True):
+        build.submit()
+
+    call_args_list = mock_k8s_api.create_namespaced_pod.call_args_list
+    assert len(call_args_list) == 1
+
+    args = call_args_list[0][0]
+    pod = args[1]
+
+    assert len(pod.spec.containers) == 1
+
+    env = {env_var.name: env_var.value for env_var in pod.spec.containers[0].env}
+
+    assert env == extra_environments
+
+
 async def test_local_repo2docker_build():
     q = Queue()
     repo_url = "https://github.com/binderhub-ci-repos/cached-minimal-dockerfile"
diff --git a/testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml b/testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml
index e9d29bfd..19027d86 100644
--- a/testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml
+++ b/testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml
@@ -33,13 +33,12 @@ ingress:
   # used actively in our CI system.
   enabled: true
 
-dind:
-  # Not enabled to test the creation/update of the image-cleaner DaemonSet
-  # resource because it also requires us to setup a container registry to test
-  # against which we haven't. We currently only test this via
-  # lint-and-validate-values.yaml that makes sure our rendered templates are
-  # valid against a k8s api-server.
-  enabled: false
+# No in cluster builder to test the creation/update of the image-cleaner DaemonSet
+# resource because it also requires us to setup a container registry to test
+# against which we haven't. We currently only test this through the use of
+# lint-and-validate-values.yaml and setting this value explicitly to make sure
+# our rendered templates are valid against a k8s api-server.
+imageBuilderType: "local"
 
 # NOTE: This is a mirror of the jupyterhub section in
 #       jupyterhub-chart-config.yaml in testing/local-binder-k8s-hub, keep these