Files
2026-10-07 13:23:42 +10:00

120 lines
3.7 KiB
Go

//go:build e2e
package harness
import (
"bytes"
"net"
"os"
"os/exec"
"strings"
"testing"
"time"
)
// ExternalCLI wraps an OCI client binary (docker/crane/skopeo) for the ECR
// data-plane subtests. Env is appended to the process environment (e.g.
// DOCKER_CONFIG to isolate the auth file).
type ExternalCLI struct {
Name string
Bin string
Env []string
}
// newExternalCLI locates bin on PATH (overridable via <NAME>_BIN, upper-cased)
// and SKIPS the calling test when absent — the data-plane subtests are optional
// on runners without the client installed.
func newExternalCLI(t *testing.T, name string, env []string) *ExternalCLI {
t.Helper()
bin := os.Getenv(strings.ToUpper(name) + "_BIN")
if bin == "" {
bin = name
}
resolved, err := exec.LookPath(bin)
if err != nil {
t.Skipf("%s not found on PATH (%v); skipping data-plane subtest", name, err)
}
return &ExternalCLI{Name: name, Bin: resolved, Env: env}
}
// NewDocker / NewCrane / NewSkopeo build wrappers for each client, skipping the
// test if the binary is missing. dockerConfig isolates the docker auth file so
// crane/skopeo can reuse it via DOCKER_CONFIG without touching ~/.docker.
func NewDocker(t *testing.T, dockerConfig string) *ExternalCLI {
return newExternalCLI(t, "docker", []string{"DOCKER_CONFIG=" + dockerConfig})
}
// NewCrane wraps the crane CLI, sharing the docker auth file through
// DOCKER_CONFIG. It skips the test if crane is not installed.
func NewCrane(t *testing.T, dockerConfig string) *ExternalCLI {
return newExternalCLI(t, "crane", []string{"DOCKER_CONFIG=" + dockerConfig})
}
// NewSkopeo wraps the skopeo CLI, pointing REGISTRY_AUTH_FILE at the docker
// auth file. It skips the test if skopeo is not installed.
func NewSkopeo(t *testing.T, dockerConfig string) *ExternalCLI {
return newExternalCLI(t, "skopeo", []string{"REGISTRY_AUTH_FILE=" + dockerConfig + "/config.json"})
}
// Run executes the client with args and returns combined output. Errors are
// returned (not fatal) so callers can assert on output or poll.
func (c *ExternalCLI) Run(timeout time.Duration, args ...string) (string, error) {
cmd := exec.Command(c.Bin, args...)
cmd.Env = append(os.Environ(), c.Env...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Start(); err != nil {
return "", err
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
return buf.String(), err
case <-time.After(timeout):
_ = cmd.Process.Kill()
<-done
return buf.String(), &timeoutError{}
}
}
// RunStdin is Run with data piped to the process stdin (docker login
// --password-stdin).
func (c *ExternalCLI) RunStdin(timeout time.Duration, stdin string, args ...string) (string, error) {
cmd := exec.Command(c.Bin, args...)
cmd.Env = append(os.Environ(), c.Env...)
cmd.Stdin = strings.NewReader(stdin)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Start(); err != nil {
return "", err
}
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
return buf.String(), err
case <-time.After(timeout):
_ = cmd.Process.Kill()
<-done
return buf.String(), &timeoutError{}
}
}
// RequireRegistryResolves SKIPS the calling test unless host resolves to an
// address. host may carry a :port suffix (stripped before lookup). The
// data-plane subtests need {account}.dkr.ecr.{region}.{suffix} to reach the
// awsgw bind IP (Route53 in prod, /etc/hosts in dev); without it the client
// cannot connect.
func RequireRegistryResolves(t *testing.T, host string) {
t.Helper()
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
if _, err := net.LookupHost(host); err != nil {
t.Skipf("registry host %q does not resolve (%v); seed /etc/hosts or DNS", host, err)
}
}