Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
53 changes: 39 additions & 14 deletions hack/install-ate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -396,10 +396,34 @@ deploy_ate_apiserver() {

ensure_apiserver_prerequisites

run_ko apply -f manifests/ate-install/ate-api-server.yaml
component_manifests ate-api-server | run_ko resolve -f - | run_kubectl apply -f -
run_kubectl rollout status deployment/ate-api-server -n ate-system --timeout=120s
}

# Kind overlay vs base manifests is decided ONLY here; hack/verify/kind-overlays.sh
# sources this to assert kind deploys can't bypass the overlay and silently
# revert its env patches (e.g. pointing OTel exports at the GKE-only collector).
component_manifests() {
local component="$1"
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
kubectl kustomize "manifests/ate-install/kind/${component}" --load-restrictor LoadRestrictionsNone
return
fi
case "${component}" in
atenet)
cat manifests/ate-install/atenet-router.yaml
printf '\n---\n'
cat manifests/ate-install/atenet-dns.yaml
;;
ate-api-server) cat manifests/ate-install/ate-api-server.yaml ;;
atelet) cat manifests/ate-install/atelet.yaml ;;
*)
echo "component_manifests: unknown component ${component}" >&2
return 1
;;
esac
}

deploy_atelet() {
log_step "deploy_atelet"
ensure_crds
Expand All @@ -408,15 +432,7 @@ deploy_atelet() {
run_kubectl apply -f manifests/ate-install/ate-system-namespace.yaml \
&& run_kubectl wait --for=jsonpath='{.status.phase}'=Active namespace/ate-system --timeout=60s

local manifest=""
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
# Use Kustomize to build and resolve the atelet DaemonSet patch
manifest=$(kubectl kustomize manifests/ate-install/kind/atelet --load-restrictor LoadRestrictionsNone | run_ko resolve -f -)
else
# Use base manifest for GKE
manifest=$(run_ko resolve -f manifests/ate-install/atelet.yaml)
fi
echo "${manifest}" | run_kubectl apply -f -
component_manifests atelet | run_ko resolve -f - | run_kubectl apply -f -
run_kubectl rollout status daemonset/atelet -n ate-system --timeout=120s
}

Expand All @@ -428,11 +444,15 @@ deploy_atenet() {
run_kubectl apply -f manifests/ate-install/ate-system-namespace.yaml \
&& run_kubectl wait --for=jsonpath='{.status.phase}'=Active namespace/ate-system --timeout=60s

local router_manifest=""
router_manifest="$(render_atenet_router_manifest)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should render_atenet_router_manifest be made to understand the kind switch? It looks like other similar functions do:

render_ate_system_manifests() {
local client_auth=""
local router=""
client_auth="$(ateapi_client_auth)"
router="$(atenet_router)"
if [[ "${router}" == "agentgateway" ]]; then
local overlay="manifests/ate-install/agentgateway"
if [[ "${client_auth}" == "token" ]]; then
overlay="manifests/ate-install/agentgateway-token-client"
fi
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
overlay="manifests/ate-install/kind-agentgateway"
if [[ "${client_auth}" == "token" ]]; then
overlay="manifests/ate-install/kind-agentgateway-token-client"
fi
fi
kubectl kustomize "${overlay}" --load-restrictor LoadRestrictionsNone | run_ko resolve -f -
return
fi
if [[ "${client_auth}" == "token" ]]; then
local overlay="manifests/ate-install/token-client"
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
overlay="manifests/ate-install/kind-token-client"
fi
kubectl kustomize "${overlay}" --load-restrictor LoadRestrictionsNone | run_ko resolve -f -
return
fi
if [[ "${ATE_INSTALL_KIND:-false}" == "true" ]]; then
# Build everything resolved with Kustomize for Kind
kubectl kustomize manifests/ate-install/kind --load-restrictor LoadRestrictionsNone | run_ko resolve -f -
else
# Build everything resolved with base manifests for GKE
run_ko resolve -f manifests/ate-install
fi
}

echo "${router_manifest}" | run_kubectl apply -f -
if [[ "$(atenet_router)" == "agentgateway" ]]; then
local router_manifest=""
router_manifest="$(render_atenet_router_manifest)"
echo "${router_manifest}" | run_kubectl apply -f -

run_ko apply -f manifests/ate-install/atenet-dns.yaml
run_ko apply -f manifests/ate-install/atenet-dns.yaml
else
component_manifests atenet | run_ko resolve -f - | run_kubectl apply -f -
fi
run_kubectl rollout status deployment/atenet-router -n ate-system --timeout=120s
# The Deployment in atenet-dns.yaml is named "dns"; every other resource in
# that file is "atenet-dns". Waiting on the filename rather than the actual
Expand Down Expand Up @@ -577,6 +597,11 @@ delete_all() {
delete_ate_system
}

# Library mode: let hack/verify/kind-overlays.sh source the functions without dispatching.
if [[ -n "${ATE_INSTALL_LIB_ONLY:-}" ]]; then
return 0
fi

if [ "$#" -eq 0 ]; then
usage
exit 1
Expand Down
198 changes: 198 additions & 0 deletions hack/verify/kind-overlays.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/usr/bin/env bash

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Guards against partial-deploy drift: kind-mode component deploys must emit exactly
# the component overlay render, component and full-overlay renders must match, and
# each container must carry its OTel env — value-checked per container because a
# renamed container turns a strategic-merge patch into a silent ghost sibling.

set -o errexit -o nounset -o pipefail

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure we want to use hack/verify as a unit test for the deploy script.

Benjamin Elder (@BenTheElder) any opinions?


ROOT="$(git rev-parse --show-toplevel)"
cd "${ROOT}"

if ! command -v kubectl >/dev/null 2>&1; then
echo "FAIL: kubectl is required for kind overlay verification" >&2
exit 1
fi

OUT="$(mktemp -d)"
trap 'rm -rf "${OUT}"' EXIT

kubectl kustomize manifests/ate-install/kind --load-restrictor LoadRestrictionsNone > "${OUT}/root.yaml"
for c in atenet ate-api-server atelet; do
kubectl kustomize "manifests/ate-install/kind/${c}" --load-restrictor LoadRestrictionsNone > "${OUT}/${c}.yaml"
(
# shellcheck disable=SC2030 # subshell-local env is the point: lib-mode source must not leak
export ATE_INSTALL_LIB_ONLY=1 NO_DEV_ENV=true KUBECTL_CONTEXT=verify-only
# shellcheck source=/dev/null
source hack/install-ate.sh
ATE_INSTALL_KIND=true component_manifests "${c}"
) > "${OUT}/${c}.deploy.yaml"
if ! diff -u "${OUT}/${c}.yaml" "${OUT}/${c}.deploy.yaml" > "${OUT}/${c}.routing.diff"; then
echo "FAIL: install-ate.sh component_manifests ${c} (kind mode) does not emit the ${c} overlay render:" >&2
head -20 "${OUT}/${c}.routing.diff" >&2
exit 1
fi
done

# component_manifests being correct is not enough — a deploy function applying
# base manifests directly would still pass above. Stub side effects and assert
# each deploy function reaches component_manifests.
deploy_fn_for() {
case "$1" in
atenet) echo deploy_atenet ;;
ate-api-server) echo deploy_ate_apiserver ;;
atelet) echo deploy_atelet ;;
esac
}
for c in atenet ate-api-server atelet; do
fn="$(deploy_fn_for "${c}")"
(
# shellcheck disable=SC2030,SC2031 # subshell-local env is the point: lib-mode source must not leak
export ATE_INSTALL_LIB_ONLY=1 NO_DEV_ENV=true KUBECTL_CONTEXT=verify-only
# shellcheck source=/dev/null
source hack/install-ate.sh
# shellcheck disable=SC2317 # stubs are called indirectly through "${fn}"
{
log_step() { :; }
ensure_crds() { :; }
ensure_apiserver_prerequisites() { :; }
run_kubectl() { :; }
run_ko() { :; }
component_manifests() { echo "$1" >> "${OUT}/deploy-calls"; }
}
"${fn}"
)
if ! grep -qx "${c}" "${OUT}/deploy-calls" 2>/dev/null; then
echo "FAIL: ${fn} does not route its manifests through component_manifests ${c}" >&2
exit 1
fi
done

python3 - "${OUT}" <<'EOF'
import re
import sys

out = sys.argv[1]
ENDPOINT = "http://opentelemetry-collector.otel-system.svc:4317"
INTERVAL = ("OTEL_METRIC_EXPORT_INTERVAL", "10000")
TIMEOUT = ("OTEL_METRIC_EXPORT_TIMEOUT", "10000")

def docs(path):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it is parsing the overlays and making sure all resources of a specific kind have the expected overlays, is that correct?

I like the idea of it, but why not parse the YAML directly? Maybe include this as a standalone Python script and avoid the bash validation?

Maintaining overlays seems like a more durable validation problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Python would need PyYAML though, which the repo doesn't declare — it'd pass only where the dev happens to have it installed. Would Go under tools/, like validate-image-cache, work instead? sigs.k8s.io/yaml is already vendored, so no new dependency.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is a fine place for it.

"""Split a kustomize render into per-resource docs keyed by (kind, name)."""
result = {}
for doc in open(path).read().split("\n---\n"):
kind = name = None
in_metadata = False
for line in doc.splitlines():
if line.startswith("kind: "):
kind = line.split(": ", 1)[1]
elif line == "metadata:":
in_metadata = True
elif in_metadata and line.startswith(" name: "):
name = line.split(": ", 1)[1]
in_metadata = False
elif in_metadata and not line.startswith(" "):
in_metadata = False
if kind and name:
result[(kind, name)] = doc.strip()
return result

def container_block(doc, container):
"""Return only the named container's lines: a doc-wide search would still find
the env on a ghost sibling left by a mistargeted patch. kustomize sorts map
keys, so "name:" sits inside the block, not necessarily on the dash line."""
lines = doc.splitlines()
for ci, line in enumerate(lines):
m = re.match(r"^(\s*)containers:$", line)
if not m:
continue
indent = m.group(1)
name_line = indent + " name: " + container
dash_name_line = indent + "- name: " + container
items, start = [], None
i = ci + 1
while i < len(lines):
ln = lines[i]
if ln.startswith(indent + "- "):
if start is not None:
items.append((start, i))
start = i
elif ln.strip() and len(ln) - len(ln.lstrip()) <= len(indent):
break
i += 1
if start is not None:
items.append((start, i))
for s, e in items:
block = lines[s:e]
if block[0] == dash_name_line or name_line in block:
return "\n".join(block)
return None

# (component overlay, workload kind, workload name, container, required env)
WORKLOADS = [
("atenet", "Deployment", "atenet-router", "atenet-router",
[("OTEL_EXPORTER_OTLP_ENDPOINT", ENDPOINT)]),
("atenet", "Deployment", "dns", None, []),
("ate-api-server", "Deployment", "ate-api-server", "ate-api-server",
[("OTEL_EXPORTER_OTLP_ENDPOINT", ENDPOINT), INTERVAL, TIMEOUT]),
("atelet", "DaemonSet", "atelet", "atelet",
[("OTEL_EXPORTER_OTLP_ENDPOINT", ENDPOINT), INTERVAL, TIMEOUT]),
]

root = docs(f"{out}/root.yaml")
failures = []
for component, kind, name, container, required_env in WORKLOADS:
comp = docs(f"{out}/{component}.yaml")
key = (kind, name)
if key not in comp:
failures.append(f"{component}: {kind} {name} missing from component overlay render")
continue
if key not in root:
failures.append(f"root overlay: {kind} {name} missing")
continue
if comp[key] != root[key]:
failures.append(
f"{component}: {kind} {name} renders differently in the component overlay "
f"and the full kind overlay — the same patch must exist in exactly one place"
)
if container is None:
continue
block = container_block(comp[key], container)
if block is None:
failures.append(f"{component}: {kind} {name} has no container {container!r}")
continue
for env_name, env_value in required_env:
# Name and value must match on the same env item: interval and timeout
# share "10000", so independent searches cross-match. kustomize quotes
# numeric-looking values.
item = re.compile(
r"- name: " + re.escape(env_name) + r"\n\s*value: \"?" + re.escape(env_value) + r"\"?(\n|$)"
)
if not item.search(block):
failures.append(
f"{component}: {kind} {name} container {container!r} lacks "
f"{env_name}={env_value} — the env patch no longer matches it"
)

if failures:
print("kind overlay verification FAILED:", file=sys.stderr)
for f in failures:
print(f" - {f}", file=sys.stderr)
sys.exit(1)
print(f"kind overlays OK: {len(WORKLOADS)} workloads consistent, install routing and OTel env verified")
EOF
40 changes: 40 additions & 0 deletions manifests/ate-install/kind/ate-api-server/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../ate-api-server.yaml

patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: ate-api-server
namespace: ate-system
spec:
template:
spec:
containers:
- name: ate-api-server
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://opentelemetry-collector.otel-system.svc:4317
- name: OTEL_METRIC_EXPORT_INTERVAL
value: "10000"
- name: OTEL_METRIC_EXPORT_TIMEOUT
value: "10000"
4 changes: 4 additions & 0 deletions manifests/ate-install/kind/atelet/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ patches:
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://opentelemetry-collector.otel-system.svc:4317
- name: OTEL_METRIC_EXPORT_INTERVAL
value: "10000"
- name: OTEL_METRIC_EXPORT_TIMEOUT
value: "10000"
- name: ATE_STORAGE_BACKEND
value: s3
- name: AWS_REGION
Expand Down
37 changes: 37 additions & 0 deletions manifests/ate-install/kind/atenet/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../../atenet-router.yaml
- ../../atenet-dns.yaml

patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: atenet-router
namespace: ate-system
spec:
template:
spec:
containers:
- name: atenet-router
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://opentelemetry-collector.otel-system.svc:4317
Loading
Loading