Skip to content

Test dependencies

E2E tests run on real VMs and clusters. Anything a test fetches from the public internet at test time is a dependency on infrastructure we don't and can't control. This is one of the biggest source of unreliability in e2e tests: DockerHub will rate-limit us, distro package mirrors often go down down, and apt install <pkg> might quietly install a different version six months from now.

A major source of flaky / unrelated failures is reliance on external dependencies: anything outside the AWS/GCP/Azure account the test runs in and internal DD systems.

We will soon block all internet access from CI for security reasons, so prepare now.

Spotting a runtime dependency

Smell Example
A package manager on the host vm.Execute("sudo apt-get install -y jq"), yum install, zypper, choco
A download from a public host curl https://…, wget, Invoke-WebRequest, msiexec /i https://…
An image reference with no registry docker run busybox, FROM ubuntu:22.04, image: redis
A language package manager pip install, npm i, gem install, cargo install
A remotely-hosted manifest kubectl apply -f https://…, a Helm repository: URL, a remote kustomize base

Info

The rules above — what is allowed, in what order of preference, and the requirement to pin versions — are what review checks a PR against, straight from the guideline. This page is the how: the mechanisms behind each alternative, the commands, and the parts that bite.

Common ways to remove external deps

Type of dep Preferred mechanism
A public container image ECR pull-through cache
A CLI tool or system package on the VM Prebake it into the machine image
A third-party binary, installer, or tarball S3 artifact bucket

Warning

Most of these methods only work for tests that run on AWS - if your test runs on Azure or GCP, your options will be more limited.

ECR pull-through cache

Info

It is a cache, not a mirror. There is no "request that an image be added" process — the first pull of a tag populates it transparently.

Use the ECR pull-through cache set up in the datadog-agent-qa account (669783387624, us-east-1):

Upstream Prefix
DockerHub 669783387624.dkr.ecr.us-east-1.amazonaws.com/dockerhub/…
Public ECR 669783387624.dkr.ecr.us-east-1.amazonaws.com/ecr-public/…
Quay 669783387624.dkr.ecr.us-east-1.amazonaws.com/quay/…
  • DockerHub official images keep their library/ path: busybox:1.37.0 becomes …/dockerhub/library/busybox:1.37.0.
  • Only those three upstreams are supported. GHCR is not — if your image is only on GHCR, use an equivalent from a supported upstream if possible, otherwise leave unproxied.
  • Pulling from public.ecr.aws/… or mirror.gcr.io directly is not an approved alternative, even though neither is rate limited today. Route it through the prefixes above. Existing code that does otherwise is debt, not precedent.
  • There is no pull-through cache in GCP or Azure. For a test that only ever runs on GCP, the provider's registry or mirror.gcr.io is acceptable — do not import that habit into an AWS test. Ask #agent-devx-help before relying on any other public mirror.
Avoid hardcoding the registry adress

Don't hardcode the registry host. Read it from the runner parameter store, so the same test works in a different account:

reg, _ := runner.GetProfile().ParamStore().GetWithDefault(parameters.ImagePullRegistry, "")
if reg != "" {
    busyboxImage = strings.SplitN(reg, ",", 2)[0] + "/dockerhub/library/busybox:1.37.0"
}

The strings.SplitN(reg, ",", 2)[0] is not decoration: the parameter may hold a comma-separated list, and only the first entry is a usable host. Inside a Pulumi component the same value is on the environment: e.ImagePullRegistry().

Real examples to copy from:

  • test/new-e2e/tests/containers/docker_test.go — DockerHub, from a test
  • test/new-e2e/tests/agent-platform/tests/install_script_test.goecr-public
  • test/e2e-framework/components/datadog/apps/etcd/k8s.goquay, from a Pulumi component
  • test/new-e2e/tests/ndm/snmp/compose/snmpCompose.yaml — a compose file with a rewritten image:
  • test/new-e2e/tests/installer/host/host.go — the mirrored-with-upstream-fallback pair, for when you need both forms

Kubernetes and Kind

Two different pulls happen, with two different defaults:

  • The Kind node image (the VM image the cluster itself boots from) is proxied through the ECR cache by default. components/kubernetes/kind.go builds its reference from env.InternalDockerhubMirror(), which resolves to the cache on AWS and falls back to registry-1.docker.io elsewhere. Use that helper rather than writing the registry host yourself.
  • Images pulled by workloads inside the cluster (your test's pods) are not proxied by default. The containerd host config the framework installs (components/kubernetes/hosts.toml) only redirects the bare docker.io host to mirror.gcr.io — a public, unauthenticated mirror, not the ECR cache — because giving every pod containerd credentials for a private registry isn't solved cluster-wide.

A workload can pull through the authenticated ECR cache, but it takes two steps together, both opt-in: rewrite the pod's image reference to point at the cache prefix (same idiom as the parameter-store snippet above), and set the imagePullRegistry/imagePullUsername/imagePullPassword runner parameters so the framework attaches a matching ImagePullSecret to the pod (common/utils/kubernetes.go, NewImagePullSecret). Do one without the other and the pull fails: a rewritten reference with no secret can't authenticate, and a secret with no rewritten reference has nothing to authenticate against.

Example

type: info open: False test/new-e2e/tests/gpu/gpu_test.go, for example, does both:

var dockerRegistry = func() string {
    reg, _ := runner.GetProfile().ParamStore().GetWithDefault(parameters.ImagePullRegistry, "")
    if reg != "" {
        return strings.SplitN(reg, ",", 2)[0] + "/dockerhub"
    }
    return "docker.io"
}()

var cuda12DockerImage = fmt.Sprintf("%s/nvidia/cuda:%s-base-ubuntu22.04", dockerRegistry, defaultCudaVersion)

The ImagePullSecret half is handled separately, by the framework, once e.ImagePullRegistry() is non-empty — see the flags this test needs locally right below.

If your test doesn't opt in, its workload images still leave the VPC through mirror.gcr.io — say so in the test rather than assuming that path is fully internal.

Running a k8s-based test locally

Kubernetes workloads need credentials for the cache, which are not set by default:

dda inv new-e2e-tests.run --targets ./tests/gpu \
  -c ddagent:imagePullRegistry=669783387624.dkr.ecr.us-east-1.amazonaws.com \
  -c ddagent:imagePullUsername=AWS \
  -c ddagent:imagePullPassword=$(aws-vault exec sso-agent-qa-read-only -- aws ecr get-login-password)

If you see User: arn:aws:sts::… is not authorized to perform: ecr:BatchGetImage, you are authenticated against the wrong account — re-run aws-vault login sso-agent-sandbox-account-admin-8h. Note that those ddagent:imagePull* flags are for the in-cluster Agent; an EC2 host pulls with its own instance profile.

Prebake into the machine image

If a test needs a specific tool or system package in a VM, avoid installing it at test time at all costs. The better alternative is to:

  • Use a Docker image containing that tool that you can then pull the ECR pull-through cache.
  • "Bake" the dependency into the VM machine image (AMI).

We already provide -e2e AMI variants for most OSes for this purpose, that contain some common dependencies: docker itself, jq, python3 etc. These -e2e variants are already the default for tests running on AWS.

See Custom AMIs for more details on how to do this.

S3 artifact bucket

For a more miscellaneous third-party artifact, vendor it into s3://agent-e2e-s3-bucket and pull it from there. Hosts authenticate with their instance profile, so no credentials are involved in the test.

err := v.Env().RemoteHost.HostArtifactClient.Get("toto.txt", "toto.txt")

test/new-e2e/examples/vmenv_artifacts_test.go is the runnable example. Objects are namespaced by owning team — windows-products/xperf-5.0.8169.zip, processes/DiskSpd.zip — and the bucket URL is prepended for you, so the path in Get is the key, not a URL.

Tip

Pin the version in the key (xperf-5.0.8169.zip, not xperf.zip) so a re-upload can never change what an existing test downloads, and record where the artifact came from when you request the upload.

Some existing objects have no recorded upstream or version, which makes them impossible to reproduce or audit — this is a pattern to avoid.

Info

HostArtifactClient is implemented for AWS hosts only — the supported Linux flavors and Windows Server. On Azure and GCP it returns not implemented.

Warning

Uploading is not self-service: write access is held by agent-qa admins, because IAM cannot express "this team may write only under its own prefix" at the granularity we would want. Ask #agent-devx-help with the artifact, its upstream URL, its version, and the key you want it at. See E2E - Use a third party artifact in test on Confluence for the current inventory.

Azure and GCP

None of the three mechanisms above exist on Azure or GCP:

  • Container images: no pull-through cache (InternalDockerhubMirror() falls back to plain registry-1.docker.io). Use the most reliable mirror available instead — mirror.gcr.io on GCP; Azure has no blessed equivalent, ask #agent-devx-help.
  • Prebaked images: no -e2e AMI equivalent. docker.InstallDocker / docker.InstallCompose (test/e2e-framework/components/docker/install.go) install Docker at provision time as the accepted exception for these clouds.
  • S3 artifact bucket: HostArtifactClient returns not implemented (see above), with no fallback.