-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Watchtower charts #336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
2226284
7e89c8a
82dda3b
033a064
422b9c8
4e7e3fe
d33e2ac
b438112
0fdfd4e
9e63a33
85f14e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,11 @@ | ||
| # Image URL to use all building/pushing image targets | ||
| IMG ?= controller:latest | ||
|
|
||
| # Watchtower release whose binary is copied into the operator image as its second | ||
| # entrypoint. Must be a tag that exists in WATCHTOWER_IMAGE — the build pulls it. | ||
| WATCHTOWER_IMAGE ?= us-docker.pkg.dev/wandb-production/public/wandb/watchtower | ||
| WATCHTOWER_VERSION ?= 0.11.0 | ||
|
|
||
|
Comment on lines
+4
to
+8
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Makefile ---'
cat -n Makefile | sed -n '1,140p'
printf '%s\n' '--- Dockerfile ---'
cat -n Dockerfile | sed -n '1,100p'
printf '%s\n' '--- Watchtower references ---'
rg -n -C 3 'WATCHTOWER_(IMAGE|VERSION)|watchtower' --glob '!vendor/**' .
printf '%s\n' '--- Repository status ---'
git diff --statRepository: wandb/operator Length of output: 49148 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Build target and related configuration ---'
cat -n Makefile | sed -n '200,225p'
rg -n -C 4 'docker-build|docker build|WATCHTOWER_IMAGE|WATCHTOWER_VERSION|WATCHTOWER_REF' \
.github . --glob '!vendor/**' --glob '!docs/**' --glob '!.git/**' 2>/dev/null || true
printf '%s\n' '--- Deterministic reference-flow probe ---'
python3 - <<'PY'
from pathlib import Path
import re
makefile = Path("Makefile").read_text()
dockerfile = Path("Dockerfile").read_text()
make_image = re.search(r"^WATCHTOWER_IMAGE\s*\?=\s*(\S+)$", makefile, re.M)
make_version = re.search(r"^WATCHTOWER_VERSION\s*\?=\s*(\S+)$", makefile, re.M)
docker_image = re.search(r"^ARG WATCHTOWER_IMAGE=(\S+)$", dockerfile, re.M)
docker_version = re.search(r"^ARG WATCHTOWER_VERSION=(\S+)$", dockerfile, re.M)
from_expr = re.search(r"^FROM \$\{WATCHTOWER_IMAGE\}:\$\{WATCHTOWER_VERSION\}", dockerfile, re.M)
build_args = re.findall(r"--build-arg (WATCHTOWER_(?:IMAGE|VERSION))=\$\((WATCHTOWER_(?:IMAGE|VERSION))\)", makefile)
assert all((make_image, make_version, docker_image, docker_version, from_expr))
print("Makefile defaults:", make_image.group(1), make_version.group(1))
print("Dockerfile defaults:", docker_image.group(1), docker_version.group(1))
print("Defaults match:", (make_image.group(1), make_version.group(1)) ==
(docker_image.group(1), docker_version.group(1)))
print("Makefile forwards:", build_args)
def make_reference(image, version):
return f"{image}:{version}"
default_ref = make_reference(make_image.group(1), make_version.group(1))
override_ref = make_reference(
"us-docker.pkg.dev/wandb-production/public/wandb/watchtower",
"0.12.0",
)
print("Default resolved FROM:", default_ref)
print("Overridden resolved FROM:", override_ref)
print("Resolved references are tag-backed:", all("@" not in ref for ref in
(default_ref, override_ref)))
PYRepository: wandb/operator Length of output: 11914 Pin the Watchtower source by digest. 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) | ||
| ifeq (,$(shell go env GOBIN)) | ||
| GOBIN=$(shell go env GOPATH)/bin | ||
|
|
@@ -210,7 +215,10 @@ run: manifests generate fmt vet ## Run the manager from your host. | |
| # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ | ||
| .PHONY: docker-build | ||
| docker-build: ## Build controller docker image. | ||
| $(CONTAINER_TOOL) build --platform linux/amd64 -t ${IMG} -f Dockerfile . | ||
| $(CONTAINER_TOOL) build --platform linux/amd64 \ | ||
| --build-arg WATCHTOWER_IMAGE=$(WATCHTOWER_IMAGE) \ | ||
| --build-arg WATCHTOWER_VERSION=$(WATCHTOWER_VERSION) \ | ||
| -t ${IMG} -f Dockerfile . | ||
|
|
||
| .PHONY: docker-push | ||
| docker-push: | ||
|
|
||
|
jthakkar04 marked this conversation as resolved.
Outdated
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| apiVersion: v2 | ||
| name: watchtower | ||
| description: A Helm chart for the W&B Watchtower cluster administration UI | ||
| type: application | ||
| # Versioned in lockstep with the operator: Watchtower's binary ships inside the | ||
| # operator image, so the chart version doubles as the image tag it deploys. The | ||
| # release pipeline enforces chart version == appVersion == operator release. | ||
| version: 2.0.0-beta.3 | ||
| appVersion: "2.0.0-beta.3" | ||
| maintainers: | ||
| - name: wandb | ||
| email: support@wandb.com | ||
| url: https://wandb.com |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Watchtower is installed as {{ include "watchtower.fullname" . }} in namespace {{ .Release.Namespace }}. | ||
|
|
||
| It is published on a node port rather than through the W&B Ingress, so it is | ||
| reachable at: | ||
|
|
||
| {{- if eq .Values.service.type "NodePort" }} | ||
|
|
||
| http://<any-node-external-ip>:$(kubectl get svc -n {{ .Release.Namespace }} {{ include "watchtower.fullname" . }} -o jsonpath='{.spec.ports[0].nodePort}'){{ include "watchtower.basePath" . }}/ | ||
|
|
||
| Every node publishes that port. Reaching it from the public internet needs the | ||
| port open in the node firewall / security group; nothing in this chart opens it. | ||
| {{- else }} | ||
|
|
||
| a {{ .Values.service.type }} Service on port {{ .Values.service.port }}, path {{ include "watchtower.basePath" . }}/ | ||
| {{- end }} | ||
|
|
||
| {{ if eq .Values.mode "cluster" }} | ||
| Log in with the admin password: | ||
|
|
||
| kubectl get secret -n {{ .Release.Namespace }} {{ include "watchtower.authSecretName" . }} \ | ||
| -o jsonpath='{.data.{{ .Values.auth.secretKey }}}' | base64 -d | ||
|
|
||
| {{- else }} | ||
| WARNING: mode is {{ .Values.mode | quote }}, not "cluster" — the password gate is | ||
| OFF and anyone who can reach the port has full cluster administration. | ||
| {{- end }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| {{- define "watchtower.name" -}} | ||
| {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} | ||
| {{- end -}} | ||
|
|
||
| {{- define "watchtower.fullname" -}} | ||
| {{- if .Values.fullnameOverride -}} | ||
| {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} | ||
| {{- else -}} | ||
| {{- $name := default .Chart.Name .Values.nameOverride -}} | ||
| {{- if contains $name .Release.Name -}} | ||
| {{- .Release.Name | trunc 63 | trimSuffix "-" -}} | ||
| {{- else -}} | ||
| {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{- define "watchtower.labels" -}} | ||
| helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} | ||
| {{ include "watchtower.selectorLabels" . }} | ||
| app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} | ||
| app.kubernetes.io/managed-by: {{ .Release.Service }} | ||
| {{- end -}} | ||
|
|
||
| {{- define "watchtower.selectorLabels" -}} | ||
| app.kubernetes.io/name: {{ include "watchtower.name" . }} | ||
| app.kubernetes.io/instance: {{ .Release.Name }} | ||
| {{- end -}} | ||
|
|
||
| {{- define "watchtower.serviceAccountName" -}} | ||
| {{- if .Values.serviceAccount.create -}} | ||
| {{- default (include "watchtower.fullname" .) .Values.serviceAccount.name -}} | ||
| {{- else -}} | ||
| {{- default "default" .Values.serviceAccount.name -}} | ||
| {{- end -}} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Require a ServiceAccount name when creation is disabled. When 🤖 Prompt for AI Agents |
||
| {{- end -}} | ||
|
|
||
| {{/* | ||
| The image is the operator's, not Watchtower's. Falling back to .Chart.AppVersion | ||
| is safe because this chart is versioned in lockstep with the operator release, so | ||
| the two are the same string by construction. | ||
| */}} | ||
| {{- define "watchtower.image" -}} | ||
| {{- if .Values.image.digest -}} | ||
| {{- printf "%s@%s" .Values.image.repository .Values.image.digest -}} | ||
| {{- else -}} | ||
| {{- printf "%s:%s" .Values.image.repository (default .Chart.AppVersion .Values.image.tag) -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{/* | ||
| Normalizes basePath the same way the Watchtower binary does: "" or a "/"-prefixed | ||
| path with no trailing slash. Probe paths and the published URL are built from it, | ||
| so a values file writing "watchtower/" must not produce "//healthz". | ||
| */}} | ||
| {{- define "watchtower.basePath" -}} | ||
| {{- $path := default "" .Values.basePath -}} | ||
| {{- if $path -}} | ||
| {{- if not (hasPrefix "/" $path) -}}{{- $path = printf "/%s" $path -}}{{- end -}} | ||
| {{- trimSuffix "/" $path -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{- define "watchtower.wandbName" -}} | ||
| {{- default .Release.Name .Values.wandbName -}} | ||
| {{- end -}} | ||
|
|
||
| {{/* | ||
| ClusterRole/ClusterRoleBinding names are cluster-global, so two Watchtower | ||
| releases in different namespaces would otherwise fight over one object — the | ||
| second install silently adopting the first's rules and subject list. Qualify the | ||
| name with the namespace; namespaced Roles keep the plain fullname. | ||
| */}} | ||
| {{- define "watchtower.roleName" -}} | ||
| {{- if eq .Values.role.type "Role" -}} | ||
| {{- include "watchtower.fullname" . -}} | ||
| {{- else -}} | ||
| {{- printf "%s-%s" .Release.Namespace (include "watchtower.fullname" .) | trunc 63 | trimSuffix "-" -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
|
|
||
| {{/* | ||
| A user-managed Secret wins over the generated one; refusing to guess when neither | ||
| is available beats rendering a Deployment that CrashLoopBackOffs on a missing key. | ||
| */}} | ||
| {{- define "watchtower.authSecretName" -}} | ||
| {{- if .Values.auth.existingSecret -}} | ||
| {{- .Values.auth.existingSecret -}} | ||
| {{- else if .Values.auth.create -}} | ||
| {{- printf "%s-auth" (include "watchtower.fullname" .) -}} | ||
| {{- else -}} | ||
| {{- fail "watchtower: set auth.create=true to generate an admin password, or auth.existingSecret to supply one" -}} | ||
| {{- end -}} | ||
| {{- end -}} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| {{- $basePath := include "watchtower.basePath" . }} | ||
| apiVersion: apps/v1 | ||
| kind: Deployment | ||
| metadata: | ||
| name: {{ include "watchtower.fullname" . }} | ||
| namespace: {{ .Release.Namespace }} | ||
| labels: | ||
| {{- include "watchtower.labels" . | nindent 4 }} | ||
| spec: | ||
| # Deliberately not configurable: in-flight deploy jobs and their SSE streams | ||
| # live in the serving pod's memory, so a reconnect landing on a second pod | ||
| # would see no history. | ||
| replicas: 1 | ||
| selector: | ||
| matchLabels: | ||
| {{- include "watchtower.selectorLabels" . | nindent 6 }} | ||
| template: | ||
| metadata: | ||
| labels: | ||
| {{- include "watchtower.selectorLabels" . | nindent 8 }} | ||
| {{- with .Values.podLabels }} | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| {{- with .Values.podAnnotations }} | ||
| annotations: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| spec: | ||
| serviceAccountName: {{ include "watchtower.serviceAccountName" . }} | ||
| {{- with .Values.imagePullSecrets }} | ||
| imagePullSecrets: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| {{- with .Values.podSecurityContext }} | ||
| securityContext: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| containers: | ||
| - name: watchtower | ||
| image: {{ include "watchtower.image" . }} | ||
| imagePullPolicy: {{ .Values.image.pullPolicy }} | ||
| # The operator image entrypoint is /manager; this selects the Watchtower | ||
| # binary that ships alongside it. | ||
| command: | ||
| {{- toYaml .Values.command | nindent 12 }} | ||
| args: | ||
| - --port | ||
| - {{ .Values.containerPort | quote }} | ||
| env: | ||
| - name: WATCHTOWER_MODE | ||
| value: {{ .Values.mode | quote }} | ||
| - name: WATCHTOWER_BASE_PATH | ||
| value: {{ $basePath | quote }} | ||
| {{- if eq .Values.mode "cluster" }} | ||
| # Never inlined: an env value here would be readable from the pod spec | ||
| # by anyone who can `kubectl get deployment`. | ||
| - name: WATCHTOWER_PASSWORD | ||
| valueFrom: | ||
| secretKeyRef: | ||
| name: {{ include "watchtower.authSecretName" . }} | ||
| key: {{ .Values.auth.secretKey }} | ||
| {{- end }} | ||
| - name: WATCHTOWER_WANDB_NAME | ||
| value: {{ include "watchtower.wandbName" . | quote }} | ||
| - name: WATCHTOWER_NAMESPACE | ||
| valueFrom: | ||
| fieldRef: | ||
| fieldPath: metadata.namespace | ||
| {{- range $name, $value := .Values.env }} | ||
| - name: {{ $name }} | ||
| value: {{ $value | quote }} | ||
| {{- end }} | ||
| ports: | ||
| - name: http | ||
| containerPort: {{ .Values.containerPort }} | ||
| protocol: TCP | ||
| # Health routes sit outside the auth gate but inside the base path, so | ||
| # the probes have to carry the prefix too. | ||
| livenessProbe: | ||
| httpGet: | ||
| path: {{ $basePath }}/healthz | ||
| port: http | ||
| initialDelaySeconds: 15 | ||
| periodSeconds: 20 | ||
| readinessProbe: | ||
| httpGet: | ||
| path: {{ $basePath }}/ready | ||
| port: http | ||
| initialDelaySeconds: 5 | ||
| periodSeconds: 10 | ||
| {{- with .Values.securityContext }} | ||
| securityContext: | ||
| {{- toYaml . | nindent 12 }} | ||
| {{- end }} | ||
| {{- with .Values.resources }} | ||
| resources: | ||
| {{- toYaml . | nindent 12 }} | ||
| {{- end }} | ||
| # Watchtower drives Helm and writes the air-gapped dependency bundle | ||
| # relative to its working directory, neither of which the read-only | ||
| # root filesystem allows. | ||
| workingDir: /home/watchtower | ||
| volumeMounts: | ||
| - name: home | ||
| mountPath: /home/watchtower | ||
| - name: helm | ||
| mountPath: /helm | ||
| - name: tmp | ||
| mountPath: /tmp | ||
| {{- with .Values.extraVolumeMounts }} | ||
| {{- toYaml . | nindent 12 }} | ||
| {{- end }} | ||
| volumes: | ||
| - name: home | ||
| emptyDir: {} | ||
| - name: helm | ||
| emptyDir: {} | ||
| - name: tmp | ||
| emptyDir: {} | ||
| {{- with .Values.extraVolumes }} | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| {{- with .Values.nodeSelector }} | ||
| nodeSelector: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| {{- with .Values.tolerations }} | ||
| tolerations: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} | ||
| {{- with .Values.affinity }} | ||
| affinity: | ||
| {{- toYaml . | nindent 8 }} | ||
| {{- end }} |
Uh oh!
There was an error while loading. Please reload this page.