diff --git a/.github/workflows/publish-chart.yaml b/.github/workflows/publish-chart.yaml new file mode 100644 index 00000000..d002fe20 --- /dev/null +++ b/.github/workflows/publish-chart.yaml @@ -0,0 +1,43 @@ +name: Publish Helm Chart to GHCR + +on: + push: + tags: + - '*-kpp*' + - 'v*-kpp*' + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Helm + uses: azure/setup-helm@v4 + + - name: Package and Push to GHCR + env: + HELM_EXPERIMENTAL_OCI: 1 + run: | + # GHCR requires repository and owner names to be strictly lowercase + OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + TAG="${{ github.ref_name }}" + + # Strip optional leading 'v' to get valid SemVer (e.g. v2.2.2-kpp-2023-08-24 -> 2.2.2-kpp-2023-08-24) + VERSION="${TAG#v}" + + echo "Packaging chart version: ${VERSION}" + mkdir -p dist + helm package . --version "${VERSION}" --destination dist/ + + # Log in to GHCR + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin + + # Push to GHCR + PKG=$(find dist -name "*.tgz" | head -n 1) + echo "Pushing ${PKG} to oci://ghcr.io/${OWNER}..." + helm push "${PKG}" "oci://ghcr.io/${OWNER}" diff --git a/README.md b/README.md index 45e25b52..8fdb9510 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This directory contains a Kubernetes chart to deploy a private Docker Registry. This chart will do the following: * Implement a Docker registry deployment +* Optionally deploy a DaemonSet to add registry service names to /etc/hosts on each node ## ⚠️ Repo Migration and Deprecation Notice @@ -52,6 +53,9 @@ their default values. | `image.repository` | Container image to use | `registry` | | `image.tag` | Container image tag to deploy | `2.8.1` | | `imagePullSecrets` | Specify image pull secrets | `nil` (does not add image pull secrets to deployed pods) | +| `daemonset.enabled` | Deploy a DaemonSet that adds registry service domain names to /etc/hosts on each node | `false` | +| `daemonset.priorityClassName` | Priority class for the hosts updater DaemonSet pods | `""` | +| `daemonset.annotations` | Annotations to add to the DaemonSet | `{}` | | `persistence.accessMode` | Access mode to use for PVC | `ReadWriteOnce` | | `persistence.enabled` | Whether to use a PVC for the Docker storage | `false` | | `persistence.deleteEnabled` | Enable the deletion of image blobs and manifests by digest | `nil` | diff --git a/files/host-setup.sh b/files/host-setup.sh new file mode 100644 index 00000000..ecddf961 --- /dev/null +++ b/files/host-setup.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env sh + +set -xeu + +CONFIG_FILE="/host/etc/containerd/config.toml" +BACKUP_FILE="/host/etc/containerd/config.toml.backup.$(date +%Y%m%d-%H%M%S)" +CONFIG_PATH="/etc/containerd/certs.d" + +# Check if config file exists +if [ ! -f "$CONFIG_FILE" ]; then + echo "Error: $CONFIG_FILE not found" + exit 1 +fi + +# Create backup +echo "Creating backup: $BACKUP_FILE" +cp "$CONFIG_FILE" "$BACKUP_FILE" + +restart_needed=1 +if grep -q "config_path.*=.*\"$CONFIG_PATH\"" "$CONFIG_FILE"; then + restart_needed=0 + echo "config_path is already set correctly in $CONFIG_FILE" +elif grep -q '^\[plugins\."io\.containerd\.grpc\.v1\.cri"\.registry\]' "$CONFIG_FILE"; then + echo "Registry section found, checking for config_path..." + + # Check if config_path exists but with wrong value + if grep -q "config_path.*=" "$CONFIG_FILE"; then + echo "Updating existing config_path..." + sed -i "s|config_path.*=.*|config_path = \"$CONFIG_PATH\"|" "$CONFIG_FILE" + else + echo "Adding config_path to existing registry section..." + # Add config_path after the registry section line + sed -i '/^\[plugins\."io\.containerd\.grpc\.v1\.cri"\.registry\]/a\ config_path = "'"$CONFIG_PATH"'"' "$CONFIG_FILE" + fi +else + echo "Registry section not found, adding complete section..." + # Add the entire registry section at the end + cat >> "$CONFIG_FILE" << EOF + +[plugins."io.containerd.grpc.v1.cri".registry] + config_path = "$CONFIG_PATH" +EOF +fi + +if [ "$restart_needed" -eq 1 ]; then + echo "Containerd configuration changed, restart may be required." +else + echo "No changes made to containerd configuration, restart not needed." +fi + +echo "Configuration updated successfully!" + +# Show the relevant section +echo "" +echo "Current registry configuration:" +grep -A 5 '^\[plugins\."io\.containerd\.grpc\.v1\.cri"\.registry\]' "$CONFIG_FILE" || echo "Section not found in output" + +# Verify required environment variables +if [ -z "$NAMESPACE" ]; then + echo "ERROR: NAMESPACE environment variable is not set or empty" + exit 1 +fi + +if [ -z "$SERVICE_NAME" ]; then + echo "ERROR: SERVICE_NAME environment variable is not set or empty" + exit 1 +fi + +if [ -z "$SERVICE_PORT" ]; then + echo "ERROR: SERVICE_PORT environment variable is not set or empty" + exit 1 +fi + +echo "Using NAMESPACE=$NAMESPACE, SERVICE_NAME=$SERVICE_NAME, SERVICE_PORT=$SERVICE_PORT" + +# Extract cluster domain from pod resolv.conf +cluster_domain="cluster.local" +if search_line=$(grep -E "^search|^domain" /etc/resolv.conf | head -1); then + if echo "$search_line" | grep -q "${NAMESPACE}.svc"; then + cluster_domain=$(echo "$search_line" | grep -o "${NAMESPACE}.svc.[^ ]*" | sed "s/${NAMESPACE}.svc.//") + fi +fi +echo "Detected cluster domain: ${cluster_domain}" + +prefixes="${SERVICE_NAME} ${SERVICE_NAME}.${NAMESPACE} ${SERVICE_NAME}.${NAMESPACE}.svc ${SERVICE_NAME}.${NAMESPACE}.svc.${cluster_domain}" + +hosts_entry="127.0.0.1 ${prefixes}" + +# Create a new hosts file without the old entries and with the new entry +grep -v "${SERVICE_NAME}" /host/etc/hosts > /tmp/hosts.new +echo "$hosts_entry" >> /tmp/hosts.new + +# Replace the hosts file with the new content +cat /tmp/hosts.new > /host/etc/hosts +rm /tmp/hosts.new + +echo "Added/Updated hosts entries for registry service: $hosts_entry" + +echo "Configuring containerd to allow insecure registries..." + +for prefix in $prefixes; do + cert_dir="/host/${CONFIG_PATH}/${prefix}:${SERVICE_PORT}" + echo "Creating directory: ${cert_dir}" + mkdir -p "${cert_dir}" + + echo "Writing hosts.toml for ${prefix}:${SERVICE_PORT}" + echo "[host.\"http://${prefix}:${SERVICE_PORT}\"]" > "${cert_dir}/hosts.toml" + echo "capabilities = [\"pull\", \"resolve\"]" >> "${cert_dir}/hosts.toml" + echo "plain-http = true" >> "${cert_dir}/hosts.toml" +done + +sleep infinity diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index af3d3dc6..02bb06e5 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -153,16 +153,20 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this {{- if .Values.proxy.enabled }} - name: REGISTRY_PROXY_REMOTEURL value: {{ required ".Values.proxy.remoteurl is required" .Values.proxy.remoteurl }} +{{- if .Values.proxy.username }} - name: REGISTRY_PROXY_USERNAME valueFrom: secretKeyRef: name: {{ if .Values.proxy.secretRef }}{{ .Values.proxy.secretRef }}{{ else }}{{ template "docker-registry.fullname" . }}-secret{{ end }} key: proxyUsername +{{- end }} +{{- if .Values.proxy.password }} - name: REGISTRY_PROXY_PASSWORD valueFrom: secretKeyRef: name: {{ if .Values.proxy.secretRef }}{{ .Values.proxy.secretRef }}{{ else }}{{ template "docker-registry.fullname" . }}-secret{{ end }} key: proxyPassword +{{- end }} {{- end -}} {{- if .Values.persistence.deleteEnabled }} diff --git a/templates/daemonset.yaml b/templates/daemonset.yaml new file mode 100644 index 00000000..07231c17 --- /dev/null +++ b/templates/daemonset.yaml @@ -0,0 +1,87 @@ +{{- if .Values.daemonset.enabled }} +{{- $hostSetupSh := .Files.Get "files/host-setup.sh" }} +{{- $cmData := dict "host-setup.sh" $hostSetupSh }} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ template "docker-registry.fullname" . }}-hostconfig + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + app: {{ template "docker-registry.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- if .Values.daemonset.annotations }} + annotations: + {{- toYaml .Values.daemonset.annotations | nindent 4 }} + {{- end }} +spec: + selector: + matchLabels: + app: {{ template "docker-registry.name" . }}-hostconfig + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "docker-registry.name" . }}-hostconfig + release: {{ .Release.Name }} + annotations: + configmap-hash: {{ $cmData | toYaml | sha256sum }} + spec: + hostNetwork: true + {{- if .Values.daemonset.priorityClassName }} + priorityClassName: "{{ .Values.daemonset.priorityClassName }}" + {{- end }} + containers: + - name: registry-host-config + image: busybox:latest + imagePullPolicy: IfNotPresent + command: + - sh + - /configmap/host-setup.sh + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: SERVICE_NAME + value: {{ template "docker-registry.fullname" . }} + - name: SERVICE_PORT + value: "{{ .Values.service.nodePort | default "" | required "service.nodePort required for daemonset to work" }}" + securityContext: + privileged: true + volumeMounts: + - name: etc + mountPath: /host/etc/hosts + subPath: hosts + - name: etc + mountPath: /host/etc/containerd + subPath: containerd + - name: configmap + mountPath: /configmap + readOnly: true + volumes: + - name: etc + hostPath: + path: /etc + - name: configmap + configMap: + name: {{ template "docker-registry.fullname" . }}-hostconfig + terminationGracePeriodSeconds: 5 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "docker-registry.fullname" . }}-hostconfig + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + app: {{ template "docker-registry.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + {{- if .Values.daemonset.annotations }} + annotations: + {{- toYaml .Values.daemonset.annotations | nindent 4 }} + {{- end }} +{{ dict "data" $cmData | toYaml }} +{{- end }} diff --git a/templates/localregistry.yaml b/templates/localregistry.yaml new file mode 100644 index 00000000..8954b7e6 --- /dev/null +++ b/templates/localregistry.yaml @@ -0,0 +1,15 @@ +{{- if .Values.localRegistryHosting.enabled }} +{{- $clusterHost := printf "%s.%s" .Values.service.name .Release.Namespace }} +{{- $clusterAddr := printf "%s:%d" $clusterHost (.Values.service.port | int) }} +{{- $nodeAddr := printf "%s:%d" $clusterHost (.Values.service.nodePort | default .Values.service.port | int) }} +{{- $externalAddr := .Values.localRegistryHosting.externalAddr | required "service.externalAddr required for local registry hosting" }} +{{- $data := dict "host" $externalAddr "hostFromContainerRuntime" $nodeAddr "hostFromClusterNetwork" $clusterAddr }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: local-registry-hosting + namespace: kube-public +data: + localRegistryHosting.v1: | + {{- $data | toYaml | nindent 4 }} +{{- end }} diff --git a/templates/secret.yaml b/templates/secret.yaml index 56bf7f84..9b0245f5 100644 --- a/templates/secret.yaml +++ b/templates/secret.yaml @@ -18,7 +18,7 @@ data: {{- else }} haSharedSecret: {{ randAlphaNum 16 | b64enc | quote }} {{- end }} - + {{- if eq .Values.storage "azure" }} {{- if and .Values.secrets.azure.accountName .Values.secrets.azure.accountKey .Values.secrets.azure.container }} azureAccountName: {{ .Values.secrets.azure.accountName | b64enc | quote }} @@ -41,3 +41,21 @@ data: {{- end }} proxyUsername: {{ .Values.proxy.username | default "" | b64enc | quote }} proxyPassword: {{ .Values.proxy.password | default "" | b64enc | quote }} +{{- if .Values.proxy.tls }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "docker-registry.fullname" . }}-proxy + namespace: {{ .Values.namespace | default .Release.Namespace }} + labels: + app: {{ template "docker-registry.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + heritage: {{ .Release.Service }} + release: {{ .Release.Name }} +type: kubernetes.io/tls +data: + ca.crt: {{ index .Values.proxy.tls "ca.crt" | b64enc | quote }} + tls.crt: {{ index .Values.proxy.tls "tls.crt" | b64enc | quote }} + tls.key: {{ index .Values.proxy.tls "tls.key" | b64enc | quote }} +{{- end }} diff --git a/values.yaml b/values.yaml index 09314874..5cf42765 100644 --- a/values.yaml +++ b/values.yaml @@ -12,6 +12,17 @@ updateStrategy: {} podAnnotations: {} podLabels: {} +localRegistryHosting: + enabled: false + # It's hard to infer an external address from helm. + externalAddr: ~ + +# DaemonSet for host configuration +daemonset: + enabled: false + priorityClassName: system-node-critical + annotations: {} + serviceAccount: create: false name: "" @@ -127,6 +138,10 @@ proxy: # the ref for a secret stored outside of this chart # Keys: proxyUsername, proxyPassword secretRef: "" + tls: {} + # ca.crt: "" + # tls.crt: "" + # tls.key: "" metrics: enabled: false