Skip to content

Increase Envoy dataplane efficiency by adding egress policy cache #6780

Increase Envoy dataplane efficiency by adding egress policy cache

Increase Envoy dataplane efficiency by adding egress policy cache #6780

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
# 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
make test-dynamic-modules
# 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: |
make build-junittool
bin/junittool 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-matrix:
name: E2E (${{ matrix.dataplane }})
runs-on: ubuntu-latest
# Cluster bring-up, image builds and four sequential lanes; generous, but far
# short of the six-hour platform default a hung lane would otherwise consume.
timeout-minutes: 120
continue-on-error: ${{ matrix.experimental }} # TODO: Make AgentGateway required once tests show stability
strategy:
fail-fast: false
matrix:
# AgentGateway runs pre-submit only for release-* branches. Everywhere
# else it runs post-merge (push to main) and on the weekly schedule.
include: >-
${{ fromJSON(
(
(github.event_name != 'pull_request' && github.event_name != 'merge_group')
|| startsWith(github.base_ref, 'release-')
|| startsWith(github.event.merge_group.base_ref, 'refs/heads/release-')
)
&& '[
{"dataplane": "envoy", "experimental": false},
{"dataplane": "agentgateway", "experimental": true}
]'
|| '[
{"dataplane": "envoy", "experimental": false}
]'
) }}
env:
E2E_ATENET_DATAPLANE: ${{ matrix.dataplane }}
E2E_ENVOY_CONCURRENCY: "1"
# The installer requires a credential provider selection, and deploys the
# bundled Kubernetes Secrets provider with the control plane. Envoy only:
# it has no agentgateway credential-provider configuration yet. Job-wide
# because every ate-setup invocation reads it, including the control
# plane redeploy in run-microvm-demo-kind.sh.
ATE_CREDENTIAL_PROVIDER: ${{ matrix.dataplane == 'envoy' && '{"name":"k8s.io"}' || '{"enabled":false}' }}
# 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
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'
- 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.
env:
E2E_ENVOY_CONCURRENCY: "1"
run: hack/install-ate-kind.sh --deploy-ate-system --atenet-dataplane=${{ matrix.dataplane }}
- 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. The demo
# redeploys the control plane, so retain the selected dataplane.
env:
ATE_ATENET_DATAPLANE: ${{ matrix.dataplane }}
E2E_ENVOY_CONCURRENCY: "1"
run: hack/run-microvm-demo-kind.sh
- 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: Run E2E tests (gVisor)
env:
# Gates the injection suite. Injection happens inside the gateway, so
# the sandbox class does not matter and only this lane runs it.
E2E_EGRESS_CREDINJECT: ${{ matrix.dataplane == 'envoy' && '1' || '' }}
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-gvisor.xml
# The identity suite runs last and alone (see below), not here.
run: hack/run-e2e-kind.sh -skip '^TestActorIdentity' -v -args --no-color
- 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).
# Sequential with the gVisor lane above, not concurrent: a suite releases
# its namespace — and with it its worker pods — as each test passes, so the
# two runs do not contend for the one kind node.
env:
E2E_SANDBOX_CLASS: microvm
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-microvm.xml
run: hack/run-e2e-kind.sh -skip '^TestActorIdentity' -v -args --no-color
- name: Run E2E tests (identity, gVisor)
# The suite rotates the cluster-wide egress CA pool, and
# the gateway picks a rotation up only once kubelet has propagated the
# Secret into its pod, so until then it signs with a CA no actor trusts.
# Nothing that does TLS through the gateway can run alongside it.
env:
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-identity-gvisor.xml
run: hack/run-e2e-kind.sh ./internal/e2e/suites/identity -v -args --no-color
- name: Run E2E tests (identity, micro-VM)
env:
E2E_SANDBOX_CLASS: microvm
E2E_JUNIT_FILE: ${{ github.workspace }}/_artifacts/e2e-identity-microvm.xml
run: hack/run-e2e-kind.sh ./internal/e2e/suites/identity -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: |
make build-junittool
bin/junittool verify -manifest "${ARTIFACTS}/expected-junit.txt"
- 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
# Preserve the required-check name while the concrete Envoy and AgentGateway
# executions run as entries in the shared matrix above.
e2e-test:
name: e2e-test
needs: e2e-test-matrix
if: always()
runs-on: ubuntu-latest
steps:
- name: Require E2E matrix success
env:
MATRIX_RESULT: ${{ needs.e2e-test-matrix.result }}
run: test "$MATRIX_RESULT" = success