Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions deploy/charts/buzz-push-gateway/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1.0
appVersion: "0.1.0"
version: 0.2.0
appVersion: "0.2.0"
Comment thread
brow marked this conversation as resolved.
Resolved
type: application
3 changes: 3 additions & 0 deletions deploy/charts/buzz-push-gateway/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ spec:
template:
metadata:
labels: {{- include "push.runtimeLabels" . | nindent 8 }}
{{- with .Values.podAnnotations }}
annotations: {{- toYaml . | nindent 8 }}
{{- end }}
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 60
Expand Down
30 changes: 30 additions & 0 deletions deploy/charts/buzz-push-gateway/tests/datadog-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Render-only fixture proving Datadog Autodiscovery can scrape the private
# metrics listener without installing prometheus-operator CRDs. Deployment
# repositories must replace these illustrative selectors with their agent's
# actual namespace and pod labels.
podAnnotations:
ad.datadoghq.com/gateway.checks: |
{
"openmetrics": {
"init_config": {},
"instances": [
{
"openmetrics_endpoint": "http://%%host%%:8081/metrics",
"service": "buzz-push-gateway",
"namespace": "block.buzz_push_gateway",
"metrics": ["push_gateway_.*"],
"histogram_buckets_as_distributions": true,
"send_distribution_buckets": true,
"send_monotonic_counter": true,
"collect_counters_with_distributions": true
}
]
}
}
networkPolicy:
monitoring:
enabled: true
namespaceSelector:
kubernetes.io/metadata.name: datadog
podSelector:
app.kubernetes.io/name: datadog-agent
7 changes: 7 additions & 0 deletions deploy/charts/buzz-push-gateway/tests/release-contract.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ set -euo pipefail
env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml <<'RUBY'
auto_text = File.read('.github/workflows/auto-tag-on-release-pr-merge.yml')
publish_text = File.read('.github/workflows/push-gateway-helm-chart.yml')
chart = YAML.load_file('deploy/charts/buzz-push-gateway/Chart.yaml')
production = YAML.load_file('deploy/charts/buzz-push-gateway/values-production.yaml')
# Parse first, then pin the tag producer and consumer strings whose agreement
# makes this a reachable lane rather than an orphan publisher.
YAML.load(auto_text)
YAML.load(publish_text)
version = chart.fetch('version').to_s
raise "gateway chart version is not semver: #{version}" unless version.match?(/\A\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\z/)
raise "gateway chart version/appVersion drift" unless chart.fetch('appVersion').to_s == version
digest = production.dig('image', 'digest').to_s
raise "production image digest is not immutable: #{digest}" unless digest.match?(/\Asha256:[0-9a-f]{64}\z/)
[
'push-chart-release/*)',
'VERSION="${BRANCH#push-chart-release/}"',
Expand Down
85 changes: 64 additions & 21 deletions deploy/charts/buzz-push-gateway/tests/render.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
out=$(mktemp); production_out=$(mktemp)
trap 'rm -f "$out" "$production_out"' EXIT
out=$(mktemp); production_out=$(mktemp); route_out=$(mktemp); datadog_out=$(mktemp)
trap 'rm -f "$out" "$production_out" "$route_out" "$datadog_out" "${monitoring_out:-}"' EXIT

# Defaults must lint and render without parameter injection.
helm lint deploy/charts/buzz-push-gateway >/dev/null
helm template push deploy/charts/buzz-push-gateway >"$out"
# Production values must attach push.buzz.xyz to an explicit Gateway.
# Production values support a platform-owned ingress without rendering an
# HTTPRoute. The environment-owned inputs remain mandatory.
production_args=(
-f deploy/charts/buzz-push-gateway/values-production.yaml
--set 'image.digest=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
--set 'profiles.dogfood.appAttestAppId=REALTEAM.xyz.block.buzz.dogfood.mobile'
--set 'httpRoute.parentRefs[0].name=production-gateway'
--set 'httpRoute.parentRefs[0].namespace=gateway-system'
--set 'networkPolicy.postgresEgressCidrs[0]=10.42.0.0/16'
)
helm lint deploy/charts/buzz-push-gateway "${production_args[@]}" >/dev/null
helm template push deploy/charts/buzz-push-gateway "${production_args[@]}" >"$production_out"

# Gateway API remains an explicit supported ingress mode when an operator opts
# in and supplies the environment-owned parent.
helm template push deploy/charts/buzz-push-gateway \
--set httpRoute.enabled=true \
--set 'httpRoute.parentRefs[0].name=production-gateway' \
--set 'httpRoute.parentRefs[0].namespace=gateway-system' \
>"$route_out"

env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -ryaml -rset \
- "$out" "$production_out" <<'RUBY'
- "$out" "$production_out" "$route_out" <<'RUBY'
def assert!(condition, detail = "assertion failed")
raise detail unless condition
end
Expand All @@ -38,6 +44,7 @@ migration = runtime.merge("app.kubernetes.io/component" => "migration")
assert!(svc.dig("spec", "selector") == runtime)
assert!(d.dig("spec", "selector", "matchLabels") == runtime)
assert!(d.dig("spec", "template", "metadata", "labels") == runtime)
assert!(d.dig("spec", "template", "metadata", "annotations").nil?)
assert!(j.dig("spec", "template", "metadata", "labels") == migration)
assert!(svc.dig("spec", "selector") != j.dig("spec", "template", "metadata", "labels"))
jenv = j.dig("spec", "template", "spec", "containers", 0, "env").to_h { |entry| [entry["name"], entry] }
Expand Down Expand Up @@ -86,7 +93,14 @@ ingress_ports = np.dig("spec", "ingress")
.flat_map { |rule| rule.fetch("ports", []) }.map { |port| port["port"] }.to_set
assert!(ingress_ports == Set[8080], ingress_ports.inspect)
production = YAML.load_stream(File.read(ARGV[1])).compact
route = production.find { |x| x["kind"] == "HTTPRoute" }
assert!(!production.any? { |x| x["kind"] == "HTTPRoute" })
production_values = YAML.load_file("deploy/charts/buzz-push-gateway/values-production.yaml")
reviewed_digest = production_values.dig("image", "digest")
assert!(reviewed_digest.match?(/\Asha256:[0-9a-f]{64}\z/), reviewed_digest.inspect)
production_deployment = production.find { |x| x["kind"] == "Deployment" }
production_image = production_deployment.dig("spec", "template", "spec", "containers", 0, "image")
assert!(production_image == "ghcr.io/block/buzz-push-gateway@#{reviewed_digest}", production_image.inspect)
route = YAML.load_stream(File.read(ARGV[2])).compact.find { |x| x["kind"] == "HTTPRoute" }
assert!(!route.dig("spec", "parentRefs").empty?)
assert!(route.dig("spec", "hostnames").include?("push.buzz.xyz"))
RUBY
Expand All @@ -107,15 +121,15 @@ if helm template push deploy/charts/buzz-push-gateway --set httpRoute.enabled=tr
fi

# The checked-in production contract is intentionally undeployable until CI or
# the release system supplies an immutable digest and environment-owned values.
# the release system supplies its environment-owned values.
if helm template push deploy/charts/buzz-push-gateway -f deploy/charts/buzz-push-gateway/values-production.yaml >/dev/null 2>&1; then
echo 'expected uninjected production values to fail' >&2
exit 1
fi

# Enabling observability renders the scrape CRDs and adds a scoped 8081 ingress
# keyed to the named monitoring source — never a blanket 8081 rule.
monitoring_out=$(mktemp); trap 'rm -f "$out" "$production_out" "$monitoring_out"' EXIT
monitoring_out=$(mktemp)
helm template push deploy/charts/buzz-push-gateway \
--set podMonitor.enabled=true \
--set prometheusRule.enabled=true \
Expand Down Expand Up @@ -147,6 +161,44 @@ from = monitoring[0].fetch("from")[0]
assert!(!from.dig("namespaceSelector", "matchLabels").empty? && !from.dig("podSelector", "matchLabels").empty?, from.inspect)
RUBY

# Datadog discovers the same private endpoint from pod annotations and needs no
# prometheus-operator CRDs. Its agent ingress remains selector-scoped.
helm lint deploy/charts/buzz-push-gateway \
-f deploy/charts/buzz-push-gateway/tests/datadog-values.yaml >/dev/null
helm template push deploy/charts/buzz-push-gateway \
-f deploy/charts/buzz-push-gateway/tests/datadog-values.yaml \
>"$datadog_out"

env -u GEM_HOME -u GEM_PATH -u RUBYLIB -u RUBYOPT ruby -rjson -ryaml -rset \
- "$datadog_out" <<'RUBY'
def assert!(condition, detail = "assertion failed")
raise detail unless condition
end

xs = YAML.load_stream(File.read(ARGV[0])).compact
assert!(!xs.any? { |x| %w[PodMonitor PrometheusRule].include?(x["kind"]) })
deployment = xs.find { |x| x["kind"] == "Deployment" }
raw_check = deployment.dig(
"spec", "template", "metadata", "annotations",
"ad.datadoghq.com/gateway.checks",
)
check = JSON.parse(raw_check)
instance = check.dig("openmetrics", "instances", 0)
assert!(instance["openmetrics_endpoint"] == "http://%%host%%:8081/metrics", instance.inspect)
assert!(instance["metrics"] == ["push_gateway_.*"], instance.inspect)

np = xs.find do |x|
x["kind"] == "NetworkPolicy" && x.dig("metadata", "name") == "push-buzz-push-gateway"
end
monitoring = np.dig("spec", "ingress").select do |rule|
rule.fetch("ports", []).map { |port| port["port"] }.to_set == Set[8081]
end
assert!(monitoring.length == 1, "exactly one scoped Datadog 8081 ingress rule")
from = monitoring[0].fetch("from")[0]
assert!(!from.dig("namespaceSelector", "matchLabels").empty?, from.inspect)
assert!(!from.dig("podSelector", "matchLabels").empty?, from.inspect)
RUBY

# Negative: monitoring enabled with default empty selectors must fail (would
# otherwise render a blanket 8081 rule matching all namespaces/pods).
if helm template push deploy/charts/buzz-push-gateway \
Expand All @@ -156,9 +208,8 @@ if helm template push deploy/charts/buzz-push-gateway \
exit 1
fi

# Negative: scrape flags must be coupled. PodMonitor without ingress = an
# unreachable scraper; ingress without a PodMonitor = an open hole with no
# scraper. Both mismatches must fail schema validation.
# Negative: PodMonitor without ingress is an unreachable scraper and must fail.
# Scoped ingress without PodMonitor is valid for annotation-discovered agents.
if helm template push deploy/charts/buzz-push-gateway \
--set podMonitor.enabled=true \
--set 'networkPolicy.monitoring.namespaceSelector.kubernetes\.io/metadata\.name=monitoring' \
Expand All @@ -167,14 +218,6 @@ if helm template push deploy/charts/buzz-push-gateway \
echo 'expected podMonitor.enabled without monitoring ingress to fail' >&2
exit 1
fi
if helm template push deploy/charts/buzz-push-gateway \
--set networkPolicy.monitoring.enabled=true \
--set 'networkPolicy.monitoring.namespaceSelector.kubernetes\.io/metadata\.name=monitoring' \
--set 'networkPolicy.monitoring.podSelector.app\.kubernetes\.io/name=prometheus' \
>/dev/null 2>&1; then
echo 'expected monitoring ingress without podMonitor.enabled to fail' >&2
exit 1
fi

# Negative: retry-ratio threshold is a fraction; a value > 1 must fail schema.
if helm template push deploy/charts/buzz-push-gateway \
Expand Down
13 changes: 9 additions & 4 deletions deploy/charts/buzz-push-gateway/values-production.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# Required environment-owned values are deliberately invalid/empty here. A
# production renderer must inject all of them; CI proves omission fails.
# The immutable gateway image was reviewed from the successful multi-arch main
# build at source 4a9de1a3a121285ef475d630b2b5764044c02cde. Its SLSA
# provenance was verified against block/buzz/.github/workflows/docker.yml.
image:
tag: ""
digest: ""
digest: sha256:3936cad2d5cb0f6711c749024b96e56969d7e4506eb35d3519e3ef126fa6636d
# Environment-owned values remain deliberately invalid/empty. A production
# renderer must inject all of them; CI proves omission fails.
profiles:
dogfood:
appAttestAppId: ""
httpRoute:
enabled: true
# Keep disabled when the platform already routes push.buzz.xyz to this
# Service. Gateway API users enable it and inject an explicit parentRef.
enabled: false
parentRefs: []
hostnames:
- push.buzz.xyz
Expand Down
51 changes: 7 additions & 44 deletions deploy/charts/buzz-push-gateway/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
}
},
"apnsKey": false,
"podAnnotations": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"httpRoute": {
"type": "object",
"required": [
Expand Down Expand Up @@ -313,7 +319,7 @@
],
"allOf": [
{
"$comment": "Scraping opt-in is coupled: a PodMonitor and its scoped 8081 ingress must be enabled together, so we never render a scraper that cannot reach the port nor an ingress hole with no scraper.",
"$comment": "A PodMonitor requires scoped 8081 ingress. External scrapers such as Datadog may enable that ingress without rendering a PodMonitor.",
"if": {
"properties": {
"podMonitor": {
Expand Down Expand Up @@ -355,49 +361,6 @@
"networkPolicy"
]
}
},
{
"if": {
"properties": {
"networkPolicy": {
"properties": {
"monitoring": {
"properties": {
"enabled": {
"const": true
}
},
"required": [
"enabled"
]
}
},
"required": [
"monitoring"
]
}
},
"required": [
"networkPolicy"
]
},
"then": {
"properties": {
"podMonitor": {
"properties": {
"enabled": {
"const": true
}
},
"required": [
"enabled"
]
}
},
"required": [
"podMonitor"
]
}
}
]
}
9 changes: 6 additions & 3 deletions deploy/charts/buzz-push-gateway/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ appAttestRoot:
secretKey: app-attest-root.pem
service:
port: 8080
podAnnotations: {}
httpRoute:
# Disabled by default so a generic install cannot claim an unattached route.
# Production enables this with an explicit Gateway parentRef.
# Enable only when this chart owns a Gateway API route. Environments with an
# existing ingress or service mesh route should keep this disabled.
enabled: false
parentRefs: []
hostnames: [push.buzz.xyz]
Expand All @@ -60,8 +62,9 @@ networkPolicy:
podSelector:
k8s-app: kube-dns
# Scoped ingress to the private metrics port (8081). Off by default so 8081
# has no pod ingress at all; enable only alongside podMonitor and name the
# scraper's namespace/pod so reachability stays narrow.
# has no pod ingress at all; enable alongside podMonitor or an external
# annotation-discovered scraper and name its namespace/pod so reachability
# stays narrow.
monitoring:
enabled: false
namespaceSelector: {}
Expand Down
Loading
Loading