diff --git a/CHANGELOG.md b/CHANGELOG.md index 14165264..613e124b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Fix: do not inject file parameter as env vars - Public and private scopes now register DNS records in their correct Route53 hosted zone when using `DNS_TYPE=external_dns`, preventing cross-zone record leakage - Add configurable main HTTP port for k8s scopes (default 8080) and HTTP support for additional ports - Improve **wait deployment active** failure logging: consolidate repeated `Unhealthy` probe events per pod into a single human-readable line, emit a progress heartbeat every 10% of timeout, and surface a targeted suggested fix based on the probe failure mode (port not open / HTTP non-2xx / probe timeout) diff --git a/k8s/deployment/build_deployment b/k8s/deployment/build_deployment index a51bf971..6a333b8e 100755 --- a/k8s/deployment/build_deployment +++ b/k8s/deployment/build_deployment @@ -3,6 +3,7 @@ DEPLOYMENT_PATH="$OUTPUT_DIR/deployment-$SCOPE_ID-$DEPLOYMENT_ID.yaml" SECRET_PATH="$OUTPUT_DIR/secret-$SCOPE_ID-$DEPLOYMENT_ID.yaml" +SECRET_FILES_PATH="$OUTPUT_DIR/secret-files-$SCOPE_ID-$DEPLOYMENT_ID.yaml" SCALING_PATH="$OUTPUT_DIR/scaling-$SCOPE_ID-$DEPLOYMENT_ID.yaml" SERVICE_TEMPLATE_PATH="$OUTPUT_DIR/service-$SCOPE_ID-$DEPLOYMENT_ID.yaml" PDB_PATH="$OUTPUT_DIR/pdb-$SCOPE_ID-$DEPLOYMENT_ID.yaml" @@ -38,6 +39,18 @@ if [[ $TEMPLATE_GENERATION_STATUS -ne 0 ]]; then fi log info " ✅ Secret template: $SECRET_PATH" +gomplate -c .="$CONTEXT_PATH" \ + --file "$SECRET_FILES_TEMPLATE" \ + --out "$SECRET_FILES_PATH" + +TEMPLATE_GENERATION_STATUS=$? + +if [[ $TEMPLATE_GENERATION_STATUS -ne 0 ]]; then + log error " ❌ Failed to build secret-files template" + exit 1 +fi +log info " ✅ Secret-files template: $SECRET_FILES_PATH" + gomplate -c .="$CONTEXT_PATH" \ --file "$SCALING_TEMPLATE" \ --out "$SCALING_PATH" diff --git a/k8s/deployment/templates/deployment.yaml.tpl b/k8s/deployment/templates/deployment.yaml.tpl index 3552c483..e8313a92 100644 --- a/k8s/deployment/templates/deployment.yaml.tpl +++ b/k8s/deployment/templates/deployment.yaml.tpl @@ -295,6 +295,16 @@ spec: envFrom: - secretRef: name: s-{{ .scope.id }}-d-{{ .deployment.id }} + {{- if .parameters.results }} + env: + {{- range .parameters.results }} + {{- if and (eq .type "file") (gt (len .values) 0) }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "app-data-%s" $key }} + value: {{ .destination_path | quote }} + {{- end }} + {{- end }} + {{- end }} image: >- {{ .asset.url }} securityContext: @@ -355,9 +365,10 @@ spec: {{- range .parameters.results }} {{- if and (eq .type "file") }} {{- if gt (len .values) 0 }} - - name: {{ printf "file-%s" (filepath.Base .destination_path | strings.ReplaceAll "." "-" | strings.ReplaceAll "_" "-") }} - mountPath: {{ .destination_path }} - subPath: {{ filepath.Base .destination_path }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "file-%s" $key }} + mountPath: {{ .destination_path | quote }} + subPath: {{ filepath.Base .destination_path | quote }} readOnly: true {{- end }} {{- end }} @@ -373,12 +384,13 @@ spec: {{- range .parameters.results }} {{- if and (eq .type "file") }} {{- if gt (len .values) 0 }} - - name: {{ printf "file-%s" (filepath.Base .destination_path | strings.ReplaceAll "." "-" | strings.ReplaceAll "_" "-") }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "file-%s" $key }} secret: - secretName: s-{{ $.scope.id }}-d-{{ $.deployment.id }} + secretName: s-{{ $.scope.id }}-d-{{ $.deployment.id }}-files items: - - key: {{ printf "app-data-%s" (filepath.Base .destination_path) }} - path: {{ filepath.Base .destination_path }} + - key: {{ printf "app-file-%s" $key }} + path: {{ filepath.Base .destination_path | quote }} {{- end }} {{- end }} {{- end }} diff --git a/k8s/deployment/templates/secret-files.yaml.tpl b/k8s/deployment/templates/secret-files.yaml.tpl new file mode 100644 index 00000000..883a3f66 --- /dev/null +++ b/k8s/deployment/templates/secret-files.yaml.tpl @@ -0,0 +1,49 @@ +{{- $hasFile := false -}} +{{- if .parameters.results -}} + {{- range .parameters.results -}} + {{- if and (eq .type "file") (gt (len .values) 0) -}} + {{- $hasFile = true -}} + {{- end -}} + {{- end -}} +{{- end -}} +{{- if $hasFile -}} +apiVersion: v1 +kind: Secret +immutable: true +metadata: + name: s-{{ .scope.id }}-d-{{ .deployment.id }}-files + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + account: {{ .account.slug }} + account_id: "{{ .account.id }}" + namespace: {{ .namespace.slug }} + namespace_id: "{{ .namespace.id }}" + application: {{ .application.slug }} + application_id: "{{ .application.id }}" + scope: {{ .scope.slug }} + scope_id: "{{ .scope.id }}" + deployment_id: "{{ .deployment.id }}" +{{- $global := index .k8s_modifiers "global" }} +{{- if $global }} + {{- $labels := index $global "labels" }} + {{- if $labels }} +{{ data.ToYAML $labels | indent 4 }} + {{- end }} +{{- end }} +{{- $secret := index .k8s_modifiers "secret" }} +{{- if $secret }} + {{- $labels := index $secret "labels" }} + {{- if $labels }} +{{ data.ToYAML $labels | indent 4 }} + {{- end }} +{{- end }} +data: +{{- range .parameters.results }} + {{- if and (eq .type "file") (gt (len .values) 0) }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + {{ printf "app-file-%s" $key }}: {{ index .values 0 "value" | regexp.Replace "^data:[^;]+;base64," "" }} + {{- end }} +{{- end }} +type: Opaque +{{- end -}} diff --git a/k8s/deployment/templates/secret.yaml.tpl b/k8s/deployment/templates/secret.yaml.tpl index baa9564d..59028c66 100644 --- a/k8s/deployment/templates/secret.yaml.tpl +++ b/k8s/deployment/templates/secret.yaml.tpl @@ -37,11 +37,6 @@ data: {{ .variable }}: {{ index .values 0 "value" | base64.Encode }} {{- end }} {{- end }} - {{- if and (eq .type "file") }} - {{- if gt (len .values) 0 }} - {{ printf "app-data-%s" (filepath.Base .destination_path) }}: {{ index .values 0 "value" | regexp.Replace "^data:[^;]+;base64," "" }} - {{- end }} - {{- end }} {{- end }} {{- end }} NP_ACCOUNT: {{ .account.slug | base64.Encode }} diff --git a/k8s/deployment/tests/build_deployment.bats b/k8s/deployment/tests/build_deployment.bats index f010afce..41adfe2a 100644 --- a/k8s/deployment/tests/build_deployment.bats +++ b/k8s/deployment/tests/build_deployment.bats @@ -18,6 +18,7 @@ setup() { # Template paths export DEPLOYMENT_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/deployment.yaml.tpl" export SECRET_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/secret.yaml.tpl" + export SECRET_FILES_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/secret-files.yaml.tpl" export SCALING_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/scaling.yaml.tpl" export SERVICE_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/service.yaml.tpl" export PDB_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/pdb.yaml.tpl" @@ -143,6 +144,13 @@ teardown() { assert_file_exists "$OUTPUT_DIR/secret-scope-123-deploy-456.yaml" } +@test "build_deployment: creates secret-files file with correct name" { + run bash "$BATS_TEST_DIRNAME/../build_deployment" + + [ "$status" -eq 0 ] + assert_file_exists "$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" +} + @test "build_deployment: creates scaling file with correct name" { run bash "$BATS_TEST_DIRNAME/../build_deployment" @@ -170,3 +178,136 @@ teardown() { [ "$status" -eq 0 ] [ ! -f "$OUTPUT_DIR/context-scope-123.json" ] } + +# ============================================================================= +# Rendering Tests — real gomplate, assert on rendered output +# ============================================================================= +# These tests run the actual `gomplate` binary against the templates and +# verify the rendered Secret + Deployment YAML have the right shape. +# +# Regression guard for the file-type parameter bug: binary file content used +# to be stored under Secret key `app-data-` in the env-var Secret, +# which then leaked into the container env block via `envFrom`, which runc +# rejects with `invalid environment variable ... contains nul byte`. The fix +# splits the storage into two Secrets: +# - s--d- env-only, consumed via envFrom (safe) +# - s--d--files binary-only, consumed only by the volume mount +# Plus a plain `env:` entry on the application container that carries the +# file's destination path under name `app-data-`. + +# Minimal context that satisfies all five templates' required fields. +# Includes both an `environment` and a `file` parameter so we can assert on +# the file-specific keys without ignoring the rest of the Secret content. +_render_context() { + cat <<'JSON' +{ + "account": {"id": "acc1", "slug": "acct"}, + "namespace": {"id": "ns1", "slug": "nsps"}, + "application": {"id": "app1", "slug": "appslug"}, + "release": {"semver": "1.0.0"}, + "scope": { + "id": "scope-123", + "slug": "scopeslug", + "domain": "x.example.com", + "dimensions": {"env": "dev"}, + "capabilities": { + "cpu_millicores": 100, + "ram_memory": 128, + "additional_ports": [], + "scaling_type": "fixed", + "autoscaling": { + "min_replicas": 1, + "max_replicas": 3, + "target_cpu_utilization": 80, + "target_memory_enabled": false, + "target_memory_utilization": 80 + }, + "health_check": {"path": "/health", "timeout_seconds": 1, "period_seconds": 5, "initial_delay_seconds": 5} + } + }, + "deployment": {"id": "deploy-456"}, + "k8s_namespace": "ns-test", + "k8s_modifiers": {}, + "asset": {"url": "example.com/app:latest"}, + "main_http_port": 8080, + "traffic_image": "example.com/traffic:latest", + "container_cpu_in_millicores": 50, + "container_memory_in_memory": 64, + "pull_secrets": {"ENABLED": false, "SECRETS": []}, + "region": "us-east-1", + "component": "app", + "service_account_name": "", + "traffic_manager_config_map": "", + "pdb_enabled": "false", + "pdb_max_unavailable": "25%", + "parameters": { + "results": [ + {"type": "environment", "variable": "MY_VAR", "values": [{"value": "hello"}]}, + {"type": "file", "name": "API P12 Cert!", "destination_path": "/app-data/[2026-05-27] cert.p12", "values": [{"value": "data:application/x-pkcs12;base64,QUFBQkJC"}]} + ] + } +} +JSON +} + +@test "build_deployment: file-type parameter splits binary into a separate Secret" { + unset -f gomplate # use the real gomplate binary, not the setup mock + + export CONTEXT="$(_render_context)" + + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + + local secret_file="$OUTPUT_DIR/secret-scope-123-deploy-456.yaml" + local secret_files_file="$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" + local deploy_file="$OUTPUT_DIR/deployment-scope-123-deploy-456.yaml" + + assert_file_exists "$secret_file" + assert_file_exists "$secret_files_file" + assert_file_exists "$deploy_file" + + # The env-var Secret MUST NOT contain anything that pulls in binary content + # via envFrom. Both app-data-* and app-file-* keys are forbidden here. + ! grep -E 'app-(data|file)-' "$secret_file" + + # Param name "API P12 Cert!" sanitizes to api-p12-cert (lowercase, runs of + # non-alphanumeric collapse to '-', leading/trailing '-' trimmed). The same + # token is reused as env name suffix, Secret data key, and volume name. + assert_contains "$(cat "$secret_files_file")" "name: s-scope-123-d-deploy-456-files" + assert_contains "$(cat "$secret_files_file")" "app-file-api-p12-cert: QUFBQkJC" + ! grep -E 'app-data-' "$secret_files_file" + + # The deployment exposes the destination path to the app via a plain `env:` + # entry on the application container (not via any Secret) — no NUL bytes, + # and the env var name is derived from the parameter's display name. + assert_contains "$(cat "$deploy_file")" "- name: app-data-api-p12-cert" + # The path starts with `[`, which YAML parses as a flow sequence unless the + # value is quoted. mountPath, subPath, path, and the env value must all be + # quoted; otherwise the deployment agent fails with `did not find expected key`. + assert_contains "$(cat "$deploy_file")" 'value: "/app-data/[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'mountPath: "/app-data/[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'subPath: "[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'path: "[2026-05-27] cert.p12"' + + # The volume mount reads bytes from the files Secret, with key matching the + # one produced by secret-files.yaml.tpl. + assert_contains "$(cat "$deploy_file")" "secretName: s-scope-123-d-deploy-456-files" + assert_contains "$(cat "$deploy_file")" "key: app-file-api-p12-cert" +} + +@test "build_deployment: secret-files renders empty when no file params" { + unset -f gomplate + + # Same context as _render_context but with the file-type param removed. + export CONTEXT="$(_render_context | jq '.parameters.results |= map(select(.type != "file"))')" + + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + + # gomplate skips writing the output file when the template renders empty, + # which is the signal to apply_templates (which iterates the OUTPUT_DIR and + # skips zero-byte/missing files) to not create an empty files-Secret in the + # cluster. + local secret_files_file="$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" + [ ! -f "$secret_files_file" ] || [ ! -s "$secret_files_file" ] +} diff --git a/k8s/values.yaml b/k8s/values.yaml index d053bc0a..97c68ce6 100644 --- a/k8s/values.yaml +++ b/k8s/values.yaml @@ -22,6 +22,7 @@ configuration: DEPLOYMENT_MAX_WAIT_IN_SECONDS: 600 DEPLOYMENT_TEMPLATE: "$SERVICE_PATH/deployment/templates/deployment.yaml.tpl" SECRET_TEMPLATE: "$SERVICE_PATH/deployment/templates/secret.yaml.tpl" + SECRET_FILES_TEMPLATE: "$SERVICE_PATH/deployment/templates/secret-files.yaml.tpl" SCALING_TEMPLATE: "$SERVICE_PATH/deployment/templates/scaling.yaml.tpl" SERVICE_TEMPLATE: "$SERVICE_PATH/deployment/templates/service.yaml.tpl" PDB_TEMPLATE: "$SERVICE_PATH/deployment/templates/pdb.yaml.tpl" diff --git a/scheduled_task/deployment/build_deployment b/scheduled_task/deployment/build_deployment index a39f925e..c64f5780 100644 --- a/scheduled_task/deployment/build_deployment +++ b/scheduled_task/deployment/build_deployment @@ -2,6 +2,7 @@ DEPLOYMENT_PATH="$OUTPUT_DIR/deployment-$SCOPE_ID-$DEPLOYMENT_ID.yaml" SECRET_PATH="$OUTPUT_DIR/secret-$SCOPE_ID-$DEPLOYMENT_ID.yaml" +SECRET_FILES_PATH="$OUTPUT_DIR/secret-files-$SCOPE_ID-$DEPLOYMENT_ID.yaml" CONTEXT_PATH="$OUTPUT_DIR/context-$SCOPE_ID.json" echo "$CONTEXT" | jq --arg replicas "$REPLICAS" '. + {replicas: $replicas}' > "$CONTEXT_PATH" @@ -32,4 +33,17 @@ if [[ $TEMPLATE_GENERATION_STATUS -ne 0 ]]; then exit 1 fi +echo "Building Template: $SECRET_FILES_TEMPLATE to $SECRET_FILES_PATH" + +gomplate -c .="$CONTEXT_PATH" \ + --file "$SECRET_FILES_TEMPLATE" \ + --out "$SECRET_FILES_PATH" + +TEMPLATE_GENERATION_STATUS=$? + +if [[ $TEMPLATE_GENERATION_STATUS -ne 0 ]]; then + echo "Error building secret-files template" + exit 1 +fi + rm "$CONTEXT_PATH" diff --git a/scheduled_task/deployment/templates/deployment.yaml.tpl b/scheduled_task/deployment/templates/deployment.yaml.tpl index b5c677d6..a1d9f1f1 100644 --- a/scheduled_task/deployment/templates/deployment.yaml.tpl +++ b/scheduled_task/deployment/templates/deployment.yaml.tpl @@ -139,6 +139,16 @@ spec: envFrom: - secretRef: name: s-{{ .scope.id }}-d-{{ .deployment.id }} + {{- if .parameters.results }} + env: + {{- range .parameters.results }} + {{- if and (eq .type "file") (gt (len .values) 0) }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "app-data-%s" $key }} + value: {{ .destination_path | quote }} + {{- end }} + {{- end }} + {{- end }} image: {{ .asset.url }} resources: limits: @@ -153,9 +163,10 @@ spec: {{- range .parameters.results }} {{- if and (eq .type "file") }} {{- if gt (len .values) 0 }} - - name: {{ printf "file-%s" (filepath.Base .destination_path | strings.ReplaceAll "." "-") }} - mountPath: {{ .destination_path }} - subPath: {{ filepath.Base .destination_path }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "file-%s" $key }} + mountPath: {{ .destination_path | quote }} + subPath: {{ filepath.Base .destination_path | quote }} readOnly: true {{- end }} {{- end }} @@ -166,12 +177,13 @@ spec: {{- range .parameters.results }} {{- if and (eq .type "file") }} {{- if gt (len .values) 0 }} - - name: {{ printf "file-%s" (filepath.Base .destination_path | strings.ReplaceAll "." "-") }} + {{- $key := .name | strings.ToLower | regexp.Replace "[^a-z0-9]+" "-" | strings.Trim "-" }} + - name: {{ printf "file-%s" $key }} secret: - secretName: s-{{ $.scope.id }}-d-{{ $.deployment.id }} + secretName: s-{{ $.scope.id }}-d-{{ $.deployment.id }}-files items: - - key: {{ printf "app-data-%s" (filepath.Base .destination_path) }} - path: {{ filepath.Base .destination_path }} + - key: {{ printf "app-file-%s" $key }} + path: {{ filepath.Base .destination_path | quote }} {{- end }} {{- end }} {{- end }} diff --git a/scheduled_task/deployment/tests/build_deployment.bats b/scheduled_task/deployment/tests/build_deployment.bats new file mode 100644 index 00000000..aa29dd2b --- /dev/null +++ b/scheduled_task/deployment/tests/build_deployment.bats @@ -0,0 +1,166 @@ +#!/usr/bin/env bats +# ============================================================================= +# Tests for scheduled_task/deployment/build_deployment. +# +# Mirrors k8s/deployment/tests/build_deployment.bats with a scheduled_task +# context (CronJob instead of Deployment). The same file-parameter regressions +# apply because scheduled_task reuses the k8s secret templates and ships its +# own deployment template that follows the same two-Secret + sanitized-name +# pattern. +# ============================================================================= + +setup() { + export PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + source "$PROJECT_ROOT/testing/assertions.sh" + + export OUTPUT_DIR="$(mktemp -d)" + export SCOPE_ID="scope-123" + export DEPLOYMENT_ID="deploy-456" + export REPLICAS="1" + + # scheduled_task reuses the k8s secret templates and ships its own + # deployment template under scheduled_task/deployment/templates/. + export DEPLOYMENT_TEMPLATE="$PROJECT_ROOT/scheduled_task/deployment/templates/deployment.yaml.tpl" + export SECRET_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/secret.yaml.tpl" + export SECRET_FILES_TEMPLATE="$PROJECT_ROOT/k8s/deployment/templates/secret-files.yaml.tpl" + + export CONTEXT='{}' + + # Mock gomplate for orchestration tests (any test that doesn't `unset -f`). + gomplate() { + local out_file="" + while [[ $# -gt 0 ]]; do + case $1 in + --out) out_file="$2"; shift 2 ;; + *) shift ;; + esac + done + echo "apiVersion: v1" > "$out_file" + return 0 + } + export -f gomplate +} + +teardown() { + rm -rf "$OUTPUT_DIR" + unset -f gomplate +} + +# ============================================================================= +# File creation — confirms the script renders deployment + both Secrets +# ============================================================================= +@test "build_deployment: creates deployment file with correct name" { + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + assert_file_exists "$OUTPUT_DIR/deployment-scope-123-deploy-456.yaml" +} + +@test "build_deployment: creates secret file with correct name" { + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + assert_file_exists "$OUTPUT_DIR/secret-scope-123-deploy-456.yaml" +} + +@test "build_deployment: creates secret-files file with correct name" { + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + assert_file_exists "$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" +} + +# ============================================================================= +# Rendering tests — real gomplate, assert on rendered output +# ============================================================================= +# Minimal context that satisfies the scheduled_task deployment template plus +# the shared k8s secret + secret-files templates. Includes a file param with +# (a) a display name that needs sanitizing and (b) a destination_path with a +# leading `[` to lock in YAML quoting at every insertion point. +_render_context() { + cat <<'JSON' +{ + "account": {"id": "acc1", "slug": "acct"}, + "namespace": {"id": "ns1", "slug": "nsps"}, + "application": {"id": "app1", "slug": "appslug"}, + "release": {"semver": "1.0.0"}, + "scope": { + "id": "scope-123", + "slug": "scopeslug", + "domain": "x.example.com", + "dimensions": {"env": "dev"}, + "capabilities": { + "cpu_millicores": 100, + "ram_memory": 128, + "cron": "*/5 * * * *", + "concurrency_policy": "Forbid", + "history_limit": {"successful": 3, "failed": 1}, + "retries": 0 + } + }, + "deployment": {"id": "deploy-456"}, + "k8s_namespace": "ns-test", + "k8s_modifiers": {}, + "asset": {"url": "example.com/app:latest"}, + "component": "app", + "service_account_name": "", + "pull_secrets": {"ENABLED": false, "SECRETS": []}, + "parameters": { + "results": [ + {"type": "environment", "variable": "MY_VAR", "values": [{"value": "hello"}]}, + {"type": "file", "name": "API P12 Cert!", "destination_path": "/app-data/[2026-05-27] cert.p12", "values": [{"value": "data:application/x-pkcs12;base64,QUFBQkJC"}]} + ] + } +} +JSON +} + +@test "build_deployment: file-type parameter splits binary into a separate Secret" { + unset -f gomplate # use the real gomplate binary + + export CONTEXT="$(_render_context)" + + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + + local secret_file="$OUTPUT_DIR/secret-scope-123-deploy-456.yaml" + local secret_files_file="$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" + local deploy_file="$OUTPUT_DIR/deployment-scope-123-deploy-456.yaml" + + assert_file_exists "$secret_file" + assert_file_exists "$secret_files_file" + assert_file_exists "$deploy_file" + + # The envFrom Secret must not carry any file-related keys, otherwise the + # binary content would be injected as an env var and runc would reject it. + ! grep -E 'app-(data|file)-' "$secret_file" + + # The files Secret holds only the binary content under a sanitized key. + assert_contains "$(cat "$secret_files_file")" "name: s-scope-123-d-deploy-456-files" + assert_contains "$(cat "$secret_files_file")" "app-file-api-p12-cert: QUFBQkJC" + ! grep -E 'app-data-' "$secret_files_file" + + # The CronJob's application container gets a plain `env:` entry whose value + # is the destination path, plus a volume mount reading from the files Secret. + assert_contains "$(cat "$deploy_file")" "- name: app-data-api-p12-cert" + # Leading `[` in the path makes YAML parse the value as a flow sequence + # unless quoted — the four insertion points below all require quoting. + assert_contains "$(cat "$deploy_file")" 'value: "/app-data/[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'mountPath: "/app-data/[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'subPath: "[2026-05-27] cert.p12"' + assert_contains "$(cat "$deploy_file")" 'path: "[2026-05-27] cert.p12"' + + assert_contains "$(cat "$deploy_file")" "secretName: s-scope-123-d-deploy-456-files" + assert_contains "$(cat "$deploy_file")" "key: app-file-api-p12-cert" +} + +@test "build_deployment: secret-files renders empty when no file params" { + unset -f gomplate + + export CONTEXT="$(_render_context | jq '.parameters.results |= map(select(.type != "file"))')" + + run bash "$BATS_TEST_DIRNAME/../build_deployment" + [ "$status" -eq 0 ] + + # gomplate skips writing the output when the template renders empty; + # apply_templates handles missing/empty files gracefully. + local secret_files_file="$OUTPUT_DIR/secret-files-scope-123-deploy-456.yaml" + [ ! -f "$secret_files_file" ] || [ ! -s "$secret_files_file" ] +}