Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions k8s/deployment/build_deployment
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
26 changes: 19 additions & 7 deletions k8s/deployment/templates/deployment.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
49 changes: 49 additions & 0 deletions k8s/deployment/templates/secret-files.yaml.tpl
Original file line number Diff line number Diff line change
@@ -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 -}}
5 changes: 0 additions & 5 deletions k8s/deployment/templates/secret.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
141 changes: 141 additions & 0 deletions k8s/deployment/tests/build_deployment.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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-<filename>` 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-<scope>-d-<deploy> env-only, consumed via envFrom (safe)
# - s-<scope>-d-<deploy>-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-<filename>`.

# 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" ]
}
1 change: 1 addition & 0 deletions k8s/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions scheduled_task/deployment/build_deployment
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
26 changes: 19 additions & 7 deletions scheduled_task/deployment/templates/deployment.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}
Expand All @@ -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 }}
Expand Down
Loading
Loading