Skip to content

Sync upstream sandbox, snapshot, and HTTPS egress updates #357

Sync upstream sandbox, snapshot, and HTTPS egress updates

Sync upstream sandbox, snapshot, and HTTPS egress updates #357

Workflow file for this run

# 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.
name: pr-workflow
on:
pull_request:
merge_group:
push:
branches: [main]
schedule:
# Weekly run on the default branch keeps the micro-VM asset cache warm (GitHub
# evicts caches idle for 7 days). The push-to-main run populates the cache that
# PRs inherit; this refreshes it so PRs keep hitting it (no asset re-download).
- cron: '37 4 * * 1'
# Nothing here writes to the repository: the jobs check the tree out, build it,
# and run it in a throwaway cluster.
permissions:
contents: read
# Superseded pushes to a pull request are cancelled; every commit that lands on
# main still gets its own full run.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
run-tests:
runs-on: ubuntu-latest
# A bound well clear of the observed runtime. Without one the platform
# default applies, and a wedged step burns a free-tier slot for six hours.
timeout-minutes: 45
env:
# Where each runner writes its JUnit. Every invocation needs its own file;
# a shared path means the last writer wins.
ARTIFACTS: ${{ github.workspace }}/_artifacts
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: 'go.mod'
# Unconditional: a released migration becoming permanent is exactly what
# this guards, and that happens on the push to main, not on the PR.
- name: Verify immutable PostgreSQL migrations
run: hack/verify/postgresql-migrations.sh
# we only test on amd64, make sure everything still builds for arm64
- name: build for arm64
run: GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build ./...
# gotestsum wraps go test to emit JUnit; the go test arguments are unchanged.
- name: unit tests
run: hack/run-tool.sh gotestsum --junitfile "${ARTIFACTS}/unit.xml"
--jsonfile "${ARTIFACTS}/unit.json" --format standard-verbose
-- -race -v ./...
# tools/apitool has its own module so we need to run it explicitly.
# run-tool.sh resolves the repo root itself, so it works from the subdir.
- name: apitool tests
working-directory: tools/apitool
run: ../../hack/run-tool.sh gotestsum --junitfile "${ARTIFACTS}/apitool.xml"
--jsonfile "${ARTIFACTS}/apitool.json" --format standard-verbose
-- -race -v ./...
# Kept apart from the root module so it can move to its own repository.
- name: gcp-secret-manager credential provider tests
working-directory: internal/plugins/gcp-secret-manager
run: ../../../hack/run-tool.sh gotestsum --junitfile "${ARTIFACTS}/gcp-secret-manager.xml"
--jsonfile "${ARTIFACTS}/gcp-secret-manager.json" --format standard-verbose
-- -race -v ./...
# The root verifiers stop at the module's go.mod, so it gets its own run:
# gofmt, go vet and the tidy check, then golangci-lint with the root config.
- name: gcp-secret-manager credential provider verify
working-directory: internal/plugins/gcp-secret-manager
run: |
make verify
../../../hack/run-tool.sh golangci-lint run --config ../../../.golangci.yaml ./...
- name: Cache cargo
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
cmd/dataplane/envoy/dynamic-modules/*/target
key: cargo-${{ runner.os }}-${{ hashFiles('cmd/dataplane/envoy/dynamic-modules/*/Cargo.lock') }}
restore-keys: cargo-${{ runner.os }}-
- name: dynamic module tests
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq --no-install-recommends clang libclang-dev
hack/test-dynamic-modules.sh
# Root-gated tests (overlay mounts, whiteout mknod, trusted.* xattrs, ...)
# skip for the unprivileged runner user above; rerun the packages that
# contain them (any test importing internal/roottest) under sudo.
- name: root-gated tests
env:
ROOT_JUNIT_FILE: ${{ github.workspace }}/_artifacts/root.xml
run: hack/run-root-tests.sh -race -v
# An exit code cannot distinguish "all passed" from "none ran". A registered
# file that was never written fails too.
- name: Require tests to have run
if: always()
run: go -C tools/junittool run . verify -manifest "${ARTIFACTS}/expected-junit.txt"
- name: Upload test results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-results-unit
path: ${{ env.ARTIFACTS }}
if-no-files-found: error
- name: verify
run: hack/verify-all.sh
# One kind cluster exercises BOTH runtimes. Free x86-64 ubuntu-latest runners
# expose /dev/kvm (with a udev rule), so create-kind-cluster.sh mounts it and the
# micro-VM (kata + cloud-hypervisor) sandbox class works alongside gVisor.
e2e-test:
name: e2e-test
runs-on: ubuntu-latest
# Cluster bring-up, image builds and two sequential lanes; generous, but far
# short of the six-hour platform default a hung lane would otherwise consume.
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- dataplane: agentgateway
env:
E2E_ATENET_DATAPLANE: ${{ matrix.dataplane }}
# The installer requires a credential provider selection, and deploys the
# bundled Kubernetes Secrets provider with the control plane. Job-wide
# because every ate-setup invocation reads it, including the control
# plane redeploy in run-microvm-demo-kind.sh.
ATE_CREDENTIAL_PROVIDER: '{"name":"k8s.io"}'
# E2E_JUNIT_FILE is set per step, not here: the lanes below share this
# job, so one job-level path would have each overwrite the last.
ARTIFACTS: ${{ github.workspace }}/_artifacts
# Cache the envoy-dataplane Rust build. Only main writes it, so PRs
# share one copy instead of each storing their own.
DOCKER_BUILD_FLAGS: >-
--cache-from type=gha,scope=envoy-dataplane
${{ github.ref == 'refs/heads/main' && matrix.dataplane == 'envoy'
&& '--cache-to type=gha,mode=max,scope=envoy-dataplane,ignore-error=true' || '' }}
steps:
- name: Checkout
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Setup Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version-file: 'go.mod'
# Use the e2e cache below instead. setup-go's cache key is shared with
# run-tests, whose unit-test builds don't match ko's flags
# (CGO_ENABLED=0, -trimpath), so ko got no cache hits from it.
cache: false
- name: Restore Go build cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: e2e-go-${{ runner.os }}-${{ github.sha }}
restore-keys: e2e-go-${{ runner.os }}-
- name: Set up Docker Buildx
# The docker-container driver is what can use the GitHub Actions cache.
# Host networking and plain HTTP let it push to the kind registry.
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
with:
driver-opts: network=host
buildkitd-config-inline: |
[registry."localhost:5001"]
http = true
- name: Expose GitHub runtime to buildx
# buildx's gha cache backend reads these from the environment, but the
# runner only passes them to actions, not to run steps.
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith('ACTIONS_')) core.exportVariable(key, value);
}
- name: Cache micro-VM assets
id: microvm-assets
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
# Assembling the assets (download kata-static + cloud-hypervisor + the
# prebuilt virtiofsd — all downloads on amd64) is fully pinned by assemble.sh,
# so key the cache on its hash. The push-to-main run populates the
# default-branch cache PRs inherit; the weekly schedule refreshes it.
# run-microvm-demo-kind.sh skips assembling when these are present.
path: bin/microvm-assets/amd64
key: microvm-assets-amd64-${{ hashFiles('hack/microvm-assets/assemble.sh') }}
- name: Enable KVM
# Grant the runner access to /dev/kvm so create-kind-cluster.sh mounts it into
# the node and labels it for the micro-VM sandbox class.
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Create cluster
run: hack/create-kind-cluster.sh
- name: Install Agent Substrate (${{ matrix.dataplane }})
# The dataplane selection applies to both the ingress router and egress
# gateway.
run: hack/install-ate-kind.sh --deploy-ate-system --atenet-dataplane=${{ matrix.dataplane }} --rollout-timeout=300s
- name: Enable NFS
# Load NFS kernel modules so in-cluster NFS server and CSI driver can run.
run: |
sudo modprobe nfs || true
sudo modprobe nfsd || true
- name: Install CSI NFS driver
run: hack/install-ate-kind.sh --setup-csi=nfs
- name: Deploy micro-VM counter demo
# Stages the (cached) assets into the cluster's rustfs and deploys the
# counter-microvm demo onto the control plane installed above.
run: hack/run-microvm-demo-kind.sh --skip-control-plane
- name: Deploy gVisor counter demo
run: hack/install-ate-kind.sh --deploy-demo-counter
- name: Deploy egress demos
# TestActorEgress in the networking suite builds its Actor from the egress
# ActorTemplate for the class under test, so both fixtures have to exist
# before their respective lane runs. The demo's actors trust the CA the
# gateway mints its leaves from.
run: |
hack/install-ate-kind.sh --deploy-demo-egress
hack/install-ate-kind.sh --deploy-demo-egress-microvm
- name: Trust local TLS origins in egress gateway
run: hack/setup-e2e-egress-tls-kind.sh
# scale the idle sandbox class's demo pools to zero so each lane has the node
- name: Focus the node on gVisor
run: hack/e2e-focus-sandbox-class.sh gvisor
- name: Run E2E tests (gVisor)
env:
E2E_PARALLELISM: 8
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-gvisor.xml
run: hack/run-e2e-kind.sh -v -args --no-color
- name: Focus the node on micro-VM
run: hack/e2e-focus-sandbox-class.sh microvm
- name: Run E2E tests (micro-VM)
# The same suites again, with every fixture repointed at its micro-VM
# variant by the single E2E_SANDBOX_CLASS knob (see internal/e2e/sandbox.go).
env:
E2E_SANDBOX_CLASS: microvm
E2E_PARALLELISM: 8
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-microvm.xml
run: hack/run-e2e-kind.sh -v -args --no-color
# Every lane must have reported tests; a `-run` filter that stops matching
# otherwise leaves its lane green and empty. The set comes from the manifest
# each lane registers itself in, so adding a lane needs no change here, and
# run-e2e.sh refuses to run in CI without E2E_JUNIT_FILE.
- name: Require every lane to have run tests
if: always()
run: go -C tools/junittool run . verify -manifest "${ARTIFACTS}/expected-junit.txt"
- name: Save Go build cache
# Main only, so PRs restore the latest main build rather than each
# storing its own copy.
if: ${{ !cancelled() && github.ref == 'refs/heads/main' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: e2e-go-${{ runner.os }}-${{ github.sha }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-results-e2e-${{ matrix.dataplane }}
path: ${{ env.ARTIFACTS }}
if-no-files-found: error
- name: Dump diagnostics on failure
if: failure()
run: |
kubectl --context kind-kind get workerpool,pods -A -o wide || true
dump() {
echo "=== logs: $1/$2 ==="
kubectl --context kind-kind logs -n "$1" "$2" --all-containers --tail=300 2>/dev/null || true
}
for p in $(kubectl --context kind-kind get pods -n ate-system -o name 2>/dev/null); do
dump ate-system "$p"
done
# Every worker pod in any namespace: the demo pools plus the e2e suites'
# randomly-named per-test namespaces, which the suites keep on failure.
# The failing actor runs in one of these, so this is where its ateom logs
# (and, for a micro-VM worker, the guest console tail) live.
kubectl --context kind-kind get pods -A -l ate.dev/worker-pool \
-o 'custom-columns=:.metadata.namespace,:.metadata.name' --no-headers 2>/dev/null \
| while read -r ns name; do dump "$ns" "$name"; done