diff --git a/charts/openhands/charts/automation/templates/deployment.yaml b/charts/openhands/charts/automation/templates/deployment.yaml index 513565478..c0d3bedcb 100644 --- a/charts/openhands/charts/automation/templates/deployment.yaml +++ b/charts/openhands/charts/automation/templates/deployment.yaml @@ -47,7 +47,7 @@ spec: {{- if .Values.database.createDatabaseUser }} # Create automation database and user in an existing PostgreSQL instance - name: create-db-user - image: postgres:14 + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" env: - name: PGPASSWORD valueFrom: @@ -124,7 +124,7 @@ spec: {{- else if .Values.postgresql.enabled }} # Wait for the automation's own PostgreSQL subchart to be ready - name: wait-for-postgres - image: bitnamilegacy/postgresql:latest + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | diff --git a/charts/openhands/charts/automation/values.yaml b/charts/openhands/charts/automation/values.yaml index a23f7f845..17e61e8d9 100644 --- a/charts/openhands/charts/automation/values.yaml +++ b/charts/openhands/charts/automation/values.yaml @@ -230,6 +230,11 @@ postgresql: enabled: false global: + # psql/pg_isready image for the database init containers and jobs. The openhands + # umbrella's global wins; this default only applies when rendering the subchart alone. + postgresClientImage: + repository: docker.io/alpine/psql + tag: 18.6@sha256:08f354a83552fcefa508116268463b371717577fe49d3fcb7b82cf8d2be464b8 security: # This allows using the bitnamilegacy image repo allowInsecureImages: true diff --git a/charts/openhands/charts/integrations-hub/templates/deployment.yaml b/charts/openhands/charts/integrations-hub/templates/deployment.yaml index 15a510ff1..a2f4d91d3 100644 --- a/charts/openhands/charts/integrations-hub/templates/deployment.yaml +++ b/charts/openhands/charts/integrations-hub/templates/deployment.yaml @@ -37,7 +37,7 @@ spec: # Create database and user in PostgreSQL. Disable this when the database # and user are provisioned outside the chart. - name: create-db-user - image: postgres:14 + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" env: - name: PGPASSWORD valueFrom: @@ -109,7 +109,7 @@ spec: {{- else if .Values.postgresql.enabled }} # Wait for the service's own PostgreSQL subchart to be ready - name: wait-for-postgres - image: bitnamilegacy/postgresql:latest + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | diff --git a/charts/openhands/charts/integrations-hub/values.yaml b/charts/openhands/charts/integrations-hub/values.yaml index d248b13a9..0d872f013 100644 --- a/charts/openhands/charts/integrations-hub/values.yaml +++ b/charts/openhands/charts/integrations-hub/values.yaml @@ -171,6 +171,11 @@ postgresql: enabled: false global: + # psql/pg_isready image for the database init containers and jobs. The openhands + # umbrella's global wins; this default only applies when rendering the subchart alone. + postgresClientImage: + repository: docker.io/alpine/psql + tag: 18.6@sha256:08f354a83552fcefa508116268463b371717577fe49d3fcb7b82cf8d2be464b8 security: # This allows using the bitnamilegacy image repo allowInsecureImages: true diff --git a/charts/openhands/charts/plugin-directory/templates/_init-containers.yaml b/charts/openhands/charts/plugin-directory/templates/_init-containers.yaml index ba5a108ee..facb0911a 100644 --- a/charts/openhands/charts/plugin-directory/templates/_init-containers.yaml +++ b/charts/openhands/charts/plugin-directory/templates/_init-containers.yaml @@ -1,7 +1,7 @@ {{- define "plugin-directory.dbInitContainers" }} {{- if .Values.databaseMigrations.waitForDatabase }} - name: wait-for-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | @@ -16,7 +16,7 @@ {{- end }} {{- if .Values.databaseMigrations.createDatabases }} - name: create-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | diff --git a/charts/openhands/charts/plugin-directory/values.yaml b/charts/openhands/charts/plugin-directory/values.yaml index 8caf588da..d282396df 100644 --- a/charts/openhands/charts/plugin-directory/values.yaml +++ b/charts/openhands/charts/plugin-directory/values.yaml @@ -160,6 +160,11 @@ datadog: env: {} global: + # psql/pg_isready image for the database init containers and jobs. The openhands + # umbrella's global wins; this default only applies when rendering the subchart alone. + postgresClientImage: + repository: docker.io/alpine/psql + tag: 18.6@sha256:08f354a83552fcefa508116268463b371717577fe49d3fcb7b82cf8d2be464b8 scheduling: # Affinity applied to this chart's pods when `affinity` above is empty. The # openhands umbrella sets this once for every chart it owns; this default diff --git a/charts/openhands/charts/runtime-api/templates/_init-containers.yaml b/charts/openhands/charts/runtime-api/templates/_init-containers.yaml index b0354175d..ccbbb17f2 100644 --- a/charts/openhands/charts/runtime-api/templates/_init-containers.yaml +++ b/charts/openhands/charts/runtime-api/templates/_init-containers.yaml @@ -1,7 +1,7 @@ {{- define "runtime-api.dbInitContainers" }} {{- if .Values.databaseMigrations.waitForDatabase }} - name: wait-for-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | @@ -18,7 +18,7 @@ {{- end }} {{- if .Values.databaseMigrations.createDatabases }} - name: create-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | diff --git a/charts/openhands/charts/runtime-api/templates/create-db-user-job.yaml b/charts/openhands/charts/runtime-api/templates/create-db-user-job.yaml index 46d90e560..dc888f53f 100644 --- a/charts/openhands/charts/runtime-api/templates/create-db-user-job.yaml +++ b/charts/openhands/charts/runtime-api/templates/create-db-user-job.yaml @@ -22,7 +22,7 @@ spec: {{- end }} containers: - name: create-user - image: postgres:14 + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" env: - name: PGPASSWORD valueFrom: diff --git a/charts/openhands/charts/runtime-api/values.yaml b/charts/openhands/charts/runtime-api/values.yaml index 022197360..03301553c 100644 --- a/charts/openhands/charts/runtime-api/values.yaml +++ b/charts/openhands/charts/runtime-api/values.yaml @@ -370,6 +370,11 @@ replicated: enabled: false global: + # psql/pg_isready image for the database init containers and jobs. The openhands + # umbrella's global wins; this default only applies when rendering the subchart alone. + postgresClientImage: + repository: docker.io/alpine/psql + tag: 18.6@sha256:08f354a83552fcefa508116268463b371717577fe49d3fcb7b82cf8d2be464b8 # Canonical agent-server image. Defaults any warm-runtime configsByName entry # that omits its own `image`. In the openhands umbrella the parent chart's # global wins; this default only applies when rendering the subchart alone. diff --git a/charts/openhands/templates/_init-containers.yaml b/charts/openhands/templates/_init-containers.yaml index fcb308a26..a8fe51c93 100644 --- a/charts/openhands/templates/_init-containers.yaml +++ b/charts/openhands/templates/_init-containers.yaml @@ -1,7 +1,7 @@ {{- define "openhands.dbInitContainers" }} {{- if .Values.databaseMigrations.waitForDatabase }} - name: wait-for-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | @@ -18,7 +18,7 @@ {{- end }} {{- if .Values.databaseMigrations.createDatabases }} - name: create-db - image: "bitnamilegacy/postgresql:latest" + image: "{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}" command: ['sh', '-c'] args: - | diff --git a/charts/openhands/values.yaml b/charts/openhands/values.yaml index 536e1d8b5..57b1da545 100644 --- a/charts/openhands/values.yaml +++ b/charts/openhands/values.yaml @@ -538,15 +538,14 @@ keycloak: # this is evaluated in the Keycloak subchart context at render time. - name: KC_DB_URL_PROPERTIES value: 'sslmode={{ .Values.externalDatabase.sslMode | default "prefer" }}' - waitForDb: - image: "bitnamilegacy/postgresql:latest" initContainers: - name: wait-for-db - # The Bitnami Keycloak subchart renders initContainers through common.tplvalues.render, - # so this template expression is evaluated at deploy time rather than being treated as - # a literal string. This lets us override just the image (e.g. for Replicated proxy) - # without duplicating the entire init container. - image: '{{ .Values.waitForDb.image }}' + # Rendered through common.tplvalues.render, so this resolves against the shared global. + image: '{{ .Values.global.postgresClientImage.repository }}:{{ .Values.global.postgresClientImage.tag }}' + # The client image defaults to root; keep the uid the Bitnami image ran this as. + securityContext: + runAsUser: 1001 + runAsNonRoot: true command: ['sh', '-c'] args: - | @@ -1445,6 +1444,10 @@ replicated: postgresDatabase: "" global: + # psql/pg_isready image for the database init containers and jobs. + postgresClientImage: + repository: docker.io/alpine/psql + tag: 18.6@sha256:08f354a83552fcefa508116268463b371717577fe49d3fcb7b82cf8d2be464b8 # Canonical agent-server image. Defaults the runtime image (runtime.image) and # any warm-runtime configsByName entry that omits its own `image`. Override # either of those for per-use exceptions; set it here to move them together. diff --git a/replicated/openhands.yaml b/replicated/openhands.yaml index 8089a4f19..16b2ee513 100644 --- a/replicated/openhands.yaml +++ b/replicated/openhands.yaml @@ -21,6 +21,9 @@ spec: # global.agentServerImage. agentServerImage: repository: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/ghcr.io/openhands/agent-server' + # Tag pinned in the chart. + postgresClientImage: + repository: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/docker.io/alpine/psql' # Runtime (agent-server sandbox) env vars. The chart serialises this map # into OH_AGENT_SERVER_ENV and mirrors it into every warm-runtime entry's # env (runtime-api claims warm pods by exact env match, so a key here @@ -191,8 +194,6 @@ spec: keycloakConfigCli: image: repository: 'proxy/{{repl LicenseFieldValue "appSlug"}}/docker.io/bitnamilegacy/keycloak-config-cli' - waitForDb: - image: 'images.r9.all-hands.dev/proxy/{{repl LicenseFieldValue "appSlug"}}/docker.io/bitnamilegacy/postgresql:latest' # caBundle wiring: mount the trust-manager Bundle ConfigMap and point # Keycloak's truststore at the merged PEM. Repeats the chart's default # KC_* env vars because helm value merging replaces arrays — adding @@ -816,6 +817,8 @@ spec: # warmRuntimes default entry both inherit this. Tag pinned in the chart. agentServerImage: repository: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/agent-server' + postgresClientImage: + repository: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/psql' image: repository: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/deploy' keycloak: @@ -824,8 +827,6 @@ spec: keycloakConfigCli: image: repository: '{{repl LocalRegistryNamespace }}/keycloak-config-cli' - waitForDb: - image: '{{repl LocalRegistryHost }}/{{repl LocalRegistryNamespace }}/postgresql:latest' postgresql: image: repository: '{{repl LocalRegistryNamespace }}/postgresql' diff --git a/scripts/test_postgres_client_image.py b/scripts/test_postgres_client_image.py new file mode 100644 index 000000000..4a9d59037 --- /dev/null +++ b/scripts/test_postgres_client_image.py @@ -0,0 +1,90 @@ +"""Every psql/pg_isready container must use the pinned global.postgresClientImage, mirrored in each mode.""" + +import subprocess +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +CHART = ROOT / "charts/openhands" +APP, HOST, NS = "test-app", "registry.test", "test-ns" +# Turn on every site that runs the client, including the non-default branches. +SITE_VALUES = [ + { + "databaseMigrations": {"createDatabases": True}, + "keycloak": {"enabled": True}, + "runtime-api": { + "databaseMigrations": {"createDatabases": True}, + "database": {"create": True, "host": "db", "user": "postgres", "name": "rt", "new_user": "rt_user"}, + }, + "plugin-directory": {"enabled": True, "databaseMigrations": {"createDatabases": True}}, + "automation": {"enabled": True, "database": {"createDatabaseUser": True}}, + "integrations-hub": {"enabled": True, "database": {"host": "db", "createDatabaseUser": True}}, + }, + { + "automation": {"enabled": True, "database": {"createDatabaseUser": False}, "postgresql": {"enabled": True}}, + "integrations-hub": {"enabled": True, "database": {"host": "db", "createDatabaseUser": False}, "postgresql": {"enabled": True}}, + }, +] + + +def chart_default(): + return yaml.safe_load((CHART / "values.yaml").read_text())["global"]["postgresClientImage"] + + +def replicated_repository(mode): + spec = yaml.safe_load((ROOT / "replicated/openhands.yaml").read_text())["spec"] + if mode == "online": + values = spec["values"] + else: + values = next(b for b in spec["optionalValues"] if "HasLocalRegistry" in b["when"])["values"] + repo = values["global"]["postgresClientImage"]["repository"] + for source, target in { + '{{repl LicenseFieldValue "appSlug"}}': APP, + "{{repl LocalRegistryHost }}": HOST, + "{{repl LocalRegistryNamespace }}": NS, + }.items(): + repo = repo.replace(source, target) + return repo + + +def client_images(rendered): + images = [] + for doc in yaml.safe_load_all(rendered): + if not doc: + continue + spec = doc.get("spec", {}) + pod = spec.get("template", {}).get("spec") or spec.get("jobTemplate", {}).get("spec", {}).get("template", {}).get("spec") + for c in (pod or {}).get("initContainers", []) + (pod or {}).get("containers", []): + script = " ".join((c.get("command") or []) + (c.get("args") or [])) + if "psql" in script or "pg_isready" in script: + images.append((doc["metadata"]["name"], c["name"], c["image"])) + return images + + +def test_chart_default_is_digest_pinned(): + assert "@sha256:" in chart_default()["tag"] + + +@pytest.mark.parametrize("mode", ["online", "airgap"]) +@pytest.mark.parametrize("sites", range(len(SITE_VALUES))) +def test_every_client_container_uses_the_mirrored_image(mode, sites, tmp_path): + default = chart_default() + expected_repo = { + "online": f"images.r9.all-hands.dev/proxy/{APP}/{default['repository']}", + "airgap": f"{HOST}/{NS}/{default['repository'].rsplit('/', 1)[-1]}", + }[mode] + assert replicated_repository(mode) == expected_repo + values = dict(SITE_VALUES[sites], **{"global": {"postgresClientImage": {"repository": expected_repo}}}) + values_path = tmp_path / "values.yaml" + values_path.write_text(yaml.safe_dump(values)) + rendered = subprocess.run( + ["helm", "template", "openhands", str(CHART), "-f", str(values_path)], + check=True, + capture_output=True, + text=True, + ).stdout + images = client_images(rendered) + assert len(images) >= 5 + assert all(image == f"{expected_repo}:{default['tag']}" for _, _, image in images), images