feat(push): support platform-managed gateway runtime pods (#8113)

Platform-owned service meshes need runtime pod identity and a
replacement network policy without patching rendered gateway manifests.
Add runtime-only pod labels, service account and shutdown-grace
settings, plus an explicit named replacement-policy mode. Migration
isolation remains independent; the default render is unchanged.

Chart 0.3.4 publication and live deployment verification follow in the
separate release and deployment work.

### Validation

Added tests cover runtime-only settings, reserved selector labels,
minimum shutdown grace, replacement-policy acknowledgement and
conflicting settings. The complete chart render suite and
release-contract tests pass. Default rendering matches the current main
chart byte-for-byte. Repository-wide `just ci` reached unit tests but
failed in existing buzz-acp tests outside this chart change. Live mesh
behavior has not been exercised.

---------

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-06 10:48:39 -07:00
committed by GitHub
parent 001290a34d
commit 73a2a05c97
8 changed files with 533 additions and 10 deletions

View File

@@ -3,6 +3,6 @@ apiVersion: v2
# branches (see docs/push-gateway-deployment.md, "Gateway chart release").
name: buzz-push-gateway
description: Public capability-gated APNs last-hop gateway for Buzz
version: 0.3.3
version: 0.3.4
appVersion: "0.1.0"
type: application

View File

@@ -0,0 +1,47 @@
# Buzz push gateway chart
### Platform-managed runtime integration
`podLabels`, `serviceAccountName`, and `terminationGracePeriodSeconds` configure
only runtime pods. Defaults retain the existing pod specification; the grace
period cannot be shorter than 60 seconds. Additional labels cannot override the
chart's name, instance, or component selectors. Migration settings remain separate.
A parent chart that supplies its own runtime NetworkPolicy can set
`networkPolicy.enabled=false` and `networkPolicy.externalPolicyName` to that
policy's name. The name is an explicit acknowledgement, not a cluster existence
check: the parent must render and validate a policy selecting the runtime pods
before deployment. Supplying a replacement name while the upstream policy is
enabled is rejected. The migration NetworkPolicy remains enabled independently.
External-policy releases must run `tests/check-external-policy.rb` on the
**complete parent render including hooks**, before install or upgrade. Helm 3
excludes hooks from its post-renderer input, so `--post-renderer` alone is not a
sufficient gate. Render without `--no-hooks` or resource filtering, using the
same chart, release name, namespace, values and capabilities as the deployment:
```sh
set -o pipefail
helm template RELEASE PARENT_CHART --namespace NAMESPACE -f VALUES.yaml \
| ruby tests/check-external-policy.rb --expect DEPLOYMENT POLICY NAMESPACE > validated-release.yaml
```
`--expect` is mandatory for external-policy deployment and CI invocations.
Provide the intended Deployment name, replacement policy name and release namespace
from deployment configuration, not by extracting the annotations being checked.
Repeat it for each external-policy Deployment. Missing Deployments or removed or
overwritten markers then fail closed. No-argument mode is only for renders that
intentionally have no external-policy requirement, such as default-mode tests.
For upgrade preflight, include `--is-upgrade`. Deployment automation must stop if
this pipeline fails and must not change the chart or rendering inputs between
validation and deployment. Run the post-renderer additionally during Helm
install/upgrade to check ordinary resources, but retain the full-render preflight
to detect hook collisions. GitOps render pipelines must validate the complete
hook-inclusive output before submitting it to their reconciler.
The Deployment records the required policy name. The gate rejects a missing,
misspelled, duplicate, wrong-namespace or non-selecting replacement, and requires
both ingress and egress isolation. It emits no manifests on failure and preserves
successful output byte-for-byte. Parent CI must run this same gate on its complete
render; a subchart-only render intentionally fails in external-policy mode.
The deployment owner must separately validate the allowed traffic and rolling drain.

View File

@@ -3,6 +3,11 @@ kind: Deployment
metadata:
name: {{ include "push.name" . }}
labels: {{- include "push.runtimeLabels" . | nindent 4 }}
{{- if not .Values.networkPolicy.enabled }}
annotations:
buzz.block.xyz/release-namespace: {{ .Release.Namespace | quote }}
buzz.block.xyz/external-network-policy: {{ .Values.networkPolicy.externalPolicyName | quote }}
{{- end }}
spec:
replicas: {{ .Values.replicaCount }}
strategy: { type: RollingUpdate, rollingUpdate: { maxSurge: 1, maxUnavailable: 0 } }
@@ -11,12 +16,18 @@ spec:
template:
metadata:
labels: {{- include "push.runtimeLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podAnnotations }}
annotations: {{- toYaml . | nindent 8 }}
{{- end }}
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 60
{{- with .Values.serviceAccountName }}
serviceAccountName: {{ . | quote }}
{{- end }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
securityContext: { runAsNonRoot: true, runAsUser: 65532, runAsGroup: 65532, fsGroup: 65532, seccompProfile: { type: RuntimeDefault } }
{{- with .Values.image.pullSecrets }}
imagePullSecrets: {{ toYaml . | nindent 8 }}

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env ruby
# Validate the complete release; strict external callers supply --expect.
require 'yaml'
require 'date'
# Strict callers provide expectations independently of rendered annotations.
expectations = []
until ARGV.empty?
abort 'usage: check-external-policy.rb [--expect DEPLOYMENT POLICY NAMESPACE]...' unless ARGV.shift == '--expect' && ARGV.length >= 3
expectations << ARGV.shift(3)
end
input = STDIN.read
resources = YAML.parse_stream(input).children.map do |doc|
stream = Psych::Nodes::Stream.new
stream.children << doc
YAML.safe_load(stream.to_yaml, permitted_classes: [Date, Time], aliases: true)
end.compact
# Match Kubernetes Unstructured.IsList: an items array, including typed lists.
# https://github.com/kubernetes/apimachinery/blob/v0.34.1/pkg/apis/meta/v1/unstructured/unstructured.go
def flatten_resources(resource, enclosing_hooks = {})
annotations = resource.dig('metadata', 'annotations') || {}
hooks = enclosing_hooks.merge(annotations.select { |k, _| %w[helm.sh/hook argocd.argoproj.io/hook].include?(k) })
if resource['items'].is_a?(Array)
resource.fetch('items').flat_map { |item| flatten_resources(item, hooks) }
else
unless hooks.empty?
resource = Marshal.load(Marshal.dump(resource))
resource['metadata'] ||= {}
resource['metadata']['annotations'] = annotations.merge(hooks)
end
[resource]
end
end
resources = resources.flat_map { |resource| flatten_resources(resource) }
def label_value?(value)
value.is_a?(String) && value.length <= 63 && (value.empty? || /\A[A-Za-z0-9](?:[-_.A-Za-z0-9]*[A-Za-z0-9])?\z/.match?(value))
end
def label_key?(key)
return false unless key.is_a?(String)
parts = key.split('/', -1)
return false unless [1, 2].include?(parts.length) && !parts.last.empty? && label_value?(parts.last)
return true if parts.length == 1
prefix = parts.first
prefix.length <= 253 && /\A[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*\z/.match?(prefix)
end
annotation = 'buzz.block.xyz/external-network-policy'
expectations.each do |deployment_name, policy_name, namespace|
candidates = resources.select do |r|
r['kind'] == 'Deployment' && r.dig('metadata', 'name') == deployment_name &&
(r.dig('metadata', 'namespace').to_s.empty? ? namespace : r.dig('metadata', 'namespace')) == namespace
end
abort "external policy #{policy_name}: expected exactly one deployment #{deployment_name}" unless candidates.length == 1
annotations = candidates.first.dig('metadata', 'annotations') || {}
abort "external policy #{policy_name}: missing or overwritten ownership marker" unless annotations[annotation] == policy_name && annotations['buzz.block.xyz/release-namespace'] == namespace
end
resources.select { |r| r['kind'] == 'Deployment' }.each do |deployment|
name = deployment.dig('metadata', 'annotations', annotation)
next unless name
release_namespace = deployment.dig('metadata', 'annotations', 'buzz.block.xyz/release-namespace')
abort "external policy #{name}: missing release namespace" if !release_namespace.is_a?(String) || release_namespace.empty?
namespace = deployment.dig('metadata', 'namespace')
namespace = release_namespace if namespace.nil? || namespace.empty?
policies = resources.select do |r|
r['apiVersion'] == 'networking.k8s.io/v1' && r['kind'] == 'NetworkPolicy' &&
r.dig('metadata', 'name') == name &&
(r.dig('metadata', 'namespace').to_s.empty? ? release_namespace : r.dig('metadata', 'namespace')) == namespace
end
abort "external policy #{name}: expected exactly one replacement in #{namespace}" unless policies.length == 1
abort "external policy #{name}: replacement must not be a Helm hook" if policies.first.dig('metadata', 'annotations', 'helm.sh/hook')
abort "external policy #{name}: replacement must not be an Argo CD hook" if policies.first.dig('metadata', 'annotations', 'argocd.argoproj.io/hook')
spec = policies.first.fetch('spec')
labels = deployment.dig('spec', 'template', 'metadata', 'labels')
selector = spec.fetch('podSelector')
# Require the immutable runtime identity explicitly, not a namespace-wide policy.
identity = deployment.dig('spec', 'selector', 'matchLabels')
match = selector.fetch('matchLabels', {})
expressions = selector.fetch('matchExpressions', [])
identity_bound = identity.all? do |k, v|
match[k] == v || expressions.any? { |e| e['key'] == k && e['operator'] == 'In' && e['values'] == [v] }
end
abort "external policy #{name}: missing runtime identity selector" unless identity_bound
abort "external policy #{name}: selector does not match runtime" unless match.all? { |k, v| labels[k] == v }
selector.fetch('matchExpressions', []).each do |expression|
key = expression.fetch('key')
values = expression.fetch('values', [])
operator = expression.fetch('operator')
valid_values = values.is_a?(Array) && values.all? { |v| label_value?(v) }
valid_values &&= %w[In NotIn].include?(operator) ? !values.empty? : (%w[Exists DoesNotExist].include?(operator) && values.empty?)
abort "external policy #{name}: invalid selector expression" unless label_key?(key) && valid_values
matches = case operator
when 'In' then labels.key?(key) && values.include?(labels[key])
when 'NotIn' then !labels.key?(key) || !values.include?(labels[key])
when 'Exists' then labels.key?(key)
when 'DoesNotExist' then !labels.key?(key)
else false
end
abort "external policy #{name}: expression does not match runtime" unless matches
end
types = spec['policyTypes']
if types.nil? || types.empty?
types = ['Ingress']
types << 'Egress' unless spec.fetch('egress', []).empty?
end
abort "external policy #{name}: must isolate ingress and egress" unless %w[Ingress Egress].all? { |t| types.include?(t) }
end
# Preserve Helm output byte-for-byte only after every deployment passes.
STDOUT.write(input)

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
bash deploy/charts/buzz-push-gateway/tests/runtime-options.sh
bash deploy/charts/buzz-push-gateway/tests/grant-lifetime.sh
bash deploy/charts/buzz-push-gateway/tests/migration-annotations.sh
out=$(mktemp); production_out=$(mktemp); route_out=$(mktemp); datadog_out=$(mktemp)

View File

@@ -0,0 +1,257 @@
#!/usr/bin/env bash
set -euo pipefail
chart=deploy/charts/buzz-push-gateway
out=$(mktemp -d)
trap 'rm -rf "$out"' EXIT
helm template push "$chart" >"$out/default.yaml"
helm template push "$chart" \
--set-string 'podLabels.example\.com/runtime=gateway' \
--set serviceAccountName=mesh-gateway \
--set terminationGracePeriodSeconds=90 >"$out/runtime.yaml"
helm template push "$chart" --set networkPolicy.enabled=false \
--set networkPolicy.externalPolicyName=platform-runtime >"$out/external.yaml"
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml - "$out" <<'RUBY'
base, runtime, external = %w[default runtime external].map { |name| YAML.load_stream(File.read("#{ARGV[0]}/#{name}.yaml")).compact }
pod = runtime.find { |r| r['kind'] == 'Deployment' }.dig('spec', 'template')
raise 'runtime label missing' unless pod['metadata']['labels'].delete('example.com/runtime') == 'gateway'
raise 'service account missing' unless pod['spec'].delete('serviceAccountName') == 'mesh-gateway'
raise 'grace period missing' unless pod['spec']['terminationGracePeriodSeconds'] == 90
pod['spec']['terminationGracePeriodSeconds'] = 60
raise 'runtime options changed unrelated resources or selectors' unless runtime == base
expected = base.reject { |r| r['kind'] == 'NetworkPolicy' && r['metadata']['name'] == 'push-buzz-push-gateway' }
external.find { |r| r['kind'] == 'Deployment' }['metadata'].delete('annotations')
raise 'external policy mode changed migration isolation or other resources' unless external == expected
RUBY
reject() {
if helm template push "$chart" "$@" >"$out/invalid.log" 2>&1; then
echo "expected invalid runtime options to fail: $*" >&2
exit 1
fi
if ! grep -q 'schema' "$out/invalid.log"; then
cat "$out/invalid.log" >&2
exit 1
fi
}
reject --set networkPolicy.enabled=false
reject --set networkPolicy.enabled=false --set-string 'networkPolicy.externalPolicyName= '
reject --set networkPolicy.externalPolicyName=conflicting-policy
reject --set terminationGracePeriodSeconds=59
reject --set podLabels.invalid=true
for label in name instance component; do
reject --set-string "podLabels.app\\.kubernetes\\.io/$label=override"
done
# Kubernetes NetworkPolicy names are DNS subdomains, not single DNS labels.
long_name=$(printf '%0253d' 0)
for name in runtime.platform.example "$long_name"; do
helm template push "$chart" --set networkPolicy.enabled=false \
--set-string "networkPolicy.externalPolicyName=$name" >/dev/null
done
for name in "$long_name"x .runtime runtime. runtime..example Runtime runtime.-example; do
reject --set networkPolicy.enabled=false --set-string "networkPolicy.externalPolicyName=$name"
done
# Parent policy owns scrape access in external mode. Internal policy retains its guard.
helm template push "$chart" --set networkPolicy.enabled=false \
--set networkPolicy.externalPolicyName=platform-runtime \
--set podMonitor.enabled=true >"$out/external-monitor.yaml"
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml - "$out/external-monitor.yaml" <<'RUBY'
resources = YAML.load_stream(File.read(ARGV[0])).compact
raise 'PodMonitor missing in external mode' unless resources.any? { |r| r['kind'] == 'PodMonitor' }
policies = resources.select { |r| r['kind'] == 'NetworkPolicy' }
raise 'external mode must retain only migration policy' unless policies.map { |r| r['metadata']['name'] } == ['push-buzz-push-gateway-migration']
RUBY
reject --set podMonitor.enabled=true
reject --set podMonitor.enabled=true --set networkPolicy.monitoring.enabled=true
# External ownership replaces runtime egress only; migration still needs DB/DNS.
helm template push "$chart" --set networkPolicy.enabled=false \
--set networkPolicy.externalPolicyName=platform-runtime \
--set-json 'networkPolicy.apnsEgressCidrs=[]' >/dev/null
reject --set-json 'networkPolicy.apnsEgressCidrs=[]'
reject --set networkPolicy.enabled=false --set networkPolicy.externalPolicyName=platform-runtime \
--set-json 'networkPolicy.postgresEgressCidrs=[]'
reject --set networkPolicy.enabled=false --set networkPolicy.externalPolicyName=platform-runtime \
--set networkPolicy.dns=null
reject --set terminationGracePeriodSeconds=null
# Kubernetes DNS-subdomain service accounts and qualified label keys.
for name in '' runtime.platform.example "$long_name"; do
helm template push "$chart" --set-string "serviceAccountName=$name" >/dev/null
done
for name in INVALID_NAME ' ' "$long_name"x .runtime runtime. runtime..example; do
reject --set-string "serviceAccountName=$name"
done
long_label=$(printf '%063d' 0)
for key in 'example.com/Runtime_v1' "$long_label" "$long_name/$long_label"; do
helm template push "$chart" --set-json "podLabels={\"$key\":\"valid_value\"}" >/dev/null
done
for key in 'bad key' '/name' 'prefix/' 'UPPER.example/name' 'a/b/c' "$long_label"x "$long_name"x/name; do
reject --set-json "podLabels={\"$key\":\"value\"}"
done
reject --set-json 'podLabels={"valid":"bad value"}'
# Combined parent render gate: mutations must fail without emitting manifests.
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml - "$out" <<'RUBY'
resources = YAML.load_stream(File.read("#{ARGV[0]}/external.yaml")).compact
pod = resources.find { |r| r['kind'] == 'Deployment' }
policy = {'apiVersion' => 'networking.k8s.io/v1', 'kind' => 'NetworkPolicy',
'metadata' => {'name' => 'platform-runtime'},
'spec' => {'podSelector' => {'matchLabels' => pod.dig('spec', 'selector', 'matchLabels').dup},
'policyTypes' => %w[Ingress Egress], 'ingress' => [], 'egress' => []}}
File.write("#{ARGV[0]}/combined.yaml", (resources + [policy]).map(&:to_yaml).join)
list = {'apiVersion' => 'v1', 'kind' => 'List', 'items' => [policy]}
File.write("#{ARGV[0]}/list.yaml", (resources + [list]).map(&:to_yaml).join)
File.write("#{ARGV[0]}/duplicate-list.yaml", (resources + [policy, list]).map(&:to_yaml).join)
typed_list = {'apiVersion' => 'networking.k8s.io/v1', 'kind' => 'NetworkPolicyList', 'items' => [policy]}
File.write("#{ARGV[0]}/typed-list.yaml", (resources + [typed_list]).map(&:to_yaml).join)
File.write("#{ARGV[0]}/duplicate-typed-list.yaml", (resources + [policy, typed_list]).map(&:to_yaml).join)
expression_policy = Marshal.load(Marshal.dump(policy))
identity = expression_policy['spec']['podSelector'].delete('matchLabels')
expression_policy['spec']['podSelector']['matchExpressions'] = identity.map { |k, v| {'key' => k, 'operator' => 'In', 'values' => [v]} }
File.write("#{ARGV[0]}/expression-identity.yaml", (resources + [expression_policy]).map(&:to_yaml).join)
%w[helm.sh/hook argocd.argoproj.io/hook].each_with_index do |key, i|
wrapped = Marshal.load(Marshal.dump(typed_list))
wrapped['metadata'] = {'annotations' => {key => i == 0 ? 'pre-upgrade' : 'PreSync'}}
File.write("#{ARGV[0]}/wrapped-hook-#{i}.yaml", (resources + [wrapped]).map(&:to_yaml).join)
end
defaulted = Marshal.load(Marshal.dump(policy))
defaulted['spec'].delete('policyTypes')
defaulted['spec']['egress'] = [{'ports' => [{'protocol' => 'TCP', 'port' => 443}]}]
File.write("#{ARGV[0]}/defaulted-types.yaml", (resources + [defaulted]).map(&:to_yaml).join)
variants = {
'invalid-key' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'bad key', 'operator' => 'DoesNotExist'}] },
'invalid-value' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'absent', 'operator' => 'NotIn', 'values' => ['bad value']}] },
'numeric-value' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'absent', 'operator' => 'NotIn', 'values' => [123]}] },
'unknown-operator' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'absent', 'operator' => 'Unknown'}] },
'empty-notin' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'absent', 'operator' => 'NotIn', 'values' => []}] },
'valued-exists' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'app.kubernetes.io/name', 'operator' => 'Exists', 'values' => ['buzz-push-gateway']}] },
'argo-skip' => ->(p) { p['metadata']['annotations'] = {'argocd.argoproj.io/hook' => 'Skip'} },
'argo-presync' => ->(p) { p['metadata']['annotations'] = {'argocd.argoproj.io/hook' => 'PreSync', 'argocd.argoproj.io/hook-delete-policy' => 'HookSucceeded'} },
'hook-policy' => ->(p) { p['metadata']['annotations'] = {'helm.sh/hook' => 'pre-install', 'helm.sh/hook-delete-policy' => 'hook-succeeded'} },
'misspelled' => ->(p) { p['metadata']['name'] = 'typo' },
'wrong-selector' => ->(p) { p['spec']['podSelector']['matchLabels']['app.kubernetes.io/instance'] = 'other' },
'empty-selector' => ->(p) { p['spec']['podSelector'] = {} },
'wrong-namespace' => ->(p) { p['metadata']['namespace'] = 'other' },
'ingress-only' => ->(p) { p['spec']['policyTypes'] = ['Ingress'] },
'wrong-expression' => ->(p) { p['spec']['podSelector']['matchExpressions'] = [{'key' => 'absent', 'operator' => 'Exists'}] }
}
variants.each do |name, mutate|
changed = Marshal.load(Marshal.dump(policy))
mutate.call(changed)
File.write("#{ARGV[0]}/#{name}.yaml", (resources + [changed]).map(&:to_yaml).join)
end
RUBY
gate() { env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby "$chart/tests/check-external-policy.rb"; }
gate <"$out/default.yaml" >"$out/gated.yaml"
cmp "$out/default.yaml" "$out/gated.yaml"
gate <"$out/combined.yaml" >"$out/gated.yaml"
cmp "$out/combined.yaml" "$out/gated.yaml"
gate <"$out/list.yaml" >"$out/gated.yaml"
cmp "$out/list.yaml" "$out/gated.yaml"
for valid in defaulted-types typed-list expression-identity; do
gate <"$out/$valid.yaml" >"$out/gated.yaml"
cmp "$out/$valid.yaml" "$out/gated.yaml"
done
for mutation in wrapped-hook-0 wrapped-hook-1 duplicate-typed-list argo-skip argo-presync invalid-key invalid-value numeric-value unknown-operator empty-notin valued-exists duplicate-list external hook-policy misspelled wrong-selector empty-selector wrong-namespace ingress-only wrong-expression; do
if gate <"$out/$mutation.yaml" >"$out/gated.yaml" 2>"$out/gate-error"; then
echo "expected combined-render gate to reject $mutation" >&2
exit 1
fi
test ! -s "$out/gated.yaml"
grep -q 'external policy' "$out/gate-error"
done
# Helm assigns omitted namespaces to the actual release namespace.
helm template push "$chart" --namespace tenant-runtime --set networkPolicy.enabled=false \
--set networkPolicy.externalPolicyName=platform-runtime >"$out/tenant.yaml"
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml - "$out" <<'RUBY'
resources = YAML.load_stream(File.read("#{ARGV[0]}/tenant.yaml")).compact
policy = YAML.load_stream(File.read("#{ARGV[0]}/combined.yaml")).compact.last
{'omitted' => nil, 'empty' => '', 'explicit' => 'tenant-runtime', 'default' => 'default', 'wrong' => 'other'}.each do |variant, namespace|
candidate = Marshal.load(Marshal.dump(policy))
candidate['metadata']['namespace'] = namespace if namespace
File.write("#{ARGV[0]}/tenant-#{variant}.yaml", (resources + [candidate]).map(&:to_yaml).join)
end
# Missing release metadata must fail closed rather than assume default.
resources.find { |r| r['kind'] == 'Deployment' }['metadata']['annotations'].delete('buzz.block.xyz/release-namespace')
File.write("#{ARGV[0]}/tenant-missing-annotation.yaml", (resources + [policy]).map(&:to_yaml).join)
RUBY
for variant in omitted empty explicit; do
gate <"$out/tenant-$variant.yaml" >"$out/gated.yaml"
cmp "$out/tenant-$variant.yaml" "$out/gated.yaml"
done
for variant in default wrong missing-annotation; do
if gate <"$out/tenant-$variant.yaml" >"$out/gated.yaml" 2>"$out/gate-error"; then
echo "expected release-namespace gate to reject $variant" >&2
exit 1
fi
test ! -s "$out/gated.yaml"
grep -q 'external policy' "$out/gate-error"
done
# Valid parent YAML aliases in unrelated resources must pass through unchanged.
cat "$out/combined.yaml" >"$out/aliases.yaml"
cat >>"$out/aliases.yaml" <<'YAML'
---
apiVersion: v1
kind: ConfigMap
metadata:
name: unrelated
data:
first: &shared "value"
second: *shared
date: 2026-10-06
timestamp: 2026-10-06T16:00:00Z
YAML
gate <"$out/aliases.yaml" >"$out/gated.yaml"
cmp "$out/aliases.yaml" "$out/gated.yaml"
# Standard YAML scalars must not enable arbitrary Ruby object construction.
printf '%s\n' '--- !ruby/object:Object {}' >"$out/ruby-object.yaml"
if gate <"$out/ruby-object.yaml" >"$out/gated.yaml" 2>"$out/gate-error"; then
echo 'expected Ruby object tag to be rejected' >&2
exit 1
fi
test ! -s "$out/gated.yaml"
grep -q 'DisallowedClass' "$out/gate-error"
# Helm 3 excludes hooks from post-renderer stdin. The complete template stream
# must reject a hook colliding with an otherwise valid ordinary replacement.
mkdir -p "$out/parent/templates"
printf 'apiVersion: v2\nname: policy-hook-fixture\nversion: 0.1.0\n' >"$out/parent/Chart.yaml"
cp "$out/combined.yaml" "$out/parent/templates/runtime.yaml"
cat >"$out/parent/templates/hook.yaml" <<'YAML'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-runtime
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
YAML
if [[ $(helm version --short) == v3.* ]]; then
helm template push "$out/parent" --is-upgrade --post-renderer "$chart/tests/check-external-policy.rb" >"$out/post-rendered.yaml"
fi
if helm template push "$out/parent" --is-upgrade | gate >"$out/gated.yaml" 2>"$out/gate-error"; then
echo 'expected complete render to reject a hook collision' >&2
exit 1
fi
test ! -s "$out/gated.yaml"
grep -q 'external policy' "$out/gate-error"
# External-mode callers must bind expectations independently of the marker.
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml - "$out" <<'RUBY'
resources = YAML.load_stream(File.read("#{ARGV[0]}/combined.yaml")).compact
pod = resources.find { |r| r['kind'] == 'Deployment' }
pod['metadata'].delete('annotations')
File.write("#{ARGV[0]}/missing-marker.yaml", resources.map(&:to_yaml).join)
RUBY
if env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby "$chart/tests/check-external-policy.rb" --expect push-buzz-push-gateway platform-runtime default <"$out/missing-marker.yaml" >"$out/gated.yaml" 2>"$out/gate-error"; then
echo 'expected strict gate to reject removed marker' >&2
exit 1
fi
test ! -s "$out/gated.yaml"
grep -q 'missing or overwritten ownership marker' "$out/gate-error"

View File

@@ -81,17 +81,15 @@
"type": "object",
"required": [
"enabled",
"apnsEgressCidrs",
"postgresEgressCidrs",
"dns"
],
"properties": {
"enabled": {
"const": true
"type": "boolean"
},
"apnsEgressCidrs": {
"type": "array",
"minItems": 1
"type": "array"
},
"postgresEgressCidrs": {
"type": "array",
@@ -146,8 +144,51 @@
}
}
]
},
"externalPolicyName": {
"type": "string",
"description": "Name of the replacement runtime NetworkPolicy supplied by the parent chart. Required when the upstream policy is disabled.",
"pattern": "^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$",
"maxLength": 253
}
}
},
"allOf": [
{
"if": {
"properties": {
"enabled": {
"const": false
}
},
"required": [
"enabled"
]
},
"then": {
"required": [
"externalPolicyName"
],
"properties": {
"externalPolicyName": {
"minLength": 1
}
}
},
"else": {
"properties": {
"externalPolicyName": {
"maxLength": 0
},
"apnsEgressCidrs": {
"minItems": 1
}
},
"required": [
"apnsEgressCidrs"
]
}
}
]
},
"podMonitor": {
"type": "object",
@@ -247,6 +288,46 @@
}
}
}
},
"podLabels": {
"type": "object",
"propertyNames": {
"not": {
"enum": [
"app.kubernetes.io/name",
"app.kubernetes.io/instance",
"app.kubernetes.io/component"
]
},
"maxLength": 317,
"pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?[A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$",
"allOf": [
{
"anyOf": [
{
"pattern": "^[^/]+$"
},
{
"pattern": "^[^/]{1,253}/"
}
]
}
]
},
"additionalProperties": {
"type": "string",
"maxLength": 63,
"pattern": "^([A-Za-z0-9]([A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?)?$"
}
},
"serviceAccountName": {
"type": "string",
"maxLength": 253,
"pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)?$"
},
"terminationGracePeriodSeconds": {
"type": "integer",
"minimum": 60
}
},
"$defs": {
@@ -316,11 +397,12 @@
"profiles",
"httpRoute",
"image",
"migration"
"migration",
"terminationGracePeriodSeconds"
],
"allOf": [
{
"$comment": "A PodMonitor requires scoped 8081 ingress. External scrapers such as Datadog may enable that ingress without rendering a PodMonitor.",
"$comment": "A PodMonitor requires scoped 8081 ingress when this chart owns the runtime policy. In external-policy mode the named parent policy owns scrape access.",
"if": {
"properties": {
"podMonitor": {
@@ -332,10 +414,21 @@
"required": [
"enabled"
]
},
"networkPolicy": {
"properties": {
"enabled": {
"const": true
}
},
"required": [
"enabled"
]
}
},
"required": [
"podMonitor"
"podMonitor",
"networkPolicy"
]
},
"then": {

View File

@@ -51,6 +51,8 @@ podDisruptionBudget:
enabled: true
minAvailable: 1
networkPolicy:
# Parent chart must supply this named replacement when enabled is false.
externalPolicyName: ""
enabled: true
# Kubernetes NetworkPolicy cannot allow DNS names. Production operators must
# narrow these CIDRs to their PostgreSQL/NAT destinations where supported.
@@ -97,3 +99,8 @@ topologySpreadConstraints:
labelSelector:
matchLabels:
app.kubernetes.io/name: buzz-push-gateway
# Runtime-only mesh integration. Migration settings remain separate.
podLabels: {}
serviceAccountName: ""
terminationGracePeriodSeconds: 60