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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ jobs:

- name: Run tests
run: make test
env:
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
33 changes: 3 additions & 30 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,42 +135,15 @@ build-all: build build-exec
dev: $(AIR)
$(AIR) -c .air.toml

# Run tests
# Compile test binaries and grant network capabilities (runs as user, not root)
# Run tests (as root for network capabilities, enables caching and parallelism)
# Usage: make test - runs all tests
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
test: ensure-ch-binaries ensure-envoy-binaries lib/system/exec_agent/exec-agent
@echo "Building test binaries..."
@mkdir -p $(BIN_DIR)/tests
@for pkg in $$(go list -tags containers_image_openpgp ./...); do \
pkg_name=$$(basename $$pkg); \
go test -c -tags containers_image_openpgp -o $(BIN_DIR)/tests/$$pkg_name.test $$pkg 2>/dev/null || true; \
done
@echo "Granting capabilities to test binaries..."
@for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' $$test 2>/dev/null || true; \
fi; \
done
@echo "Running tests as current user with capabilities..."
@if [ -n "$(TEST)" ]; then \
echo "Running specific test: $(TEST)"; \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Checking $$(basename $$test) for $(TEST)..."; \
$$test -test.run=$(TEST) -test.v -test.timeout=60s 2>&1 | grep -q "PASS\|FAIL" && \
$$test -test.run=$(TEST) -test.v -test.timeout=60s || true; \
fi; \
done; \
sudo env "PATH=$$PATH" "DOCKER_USERNAME=$$DOCKER_USERNAME" "DOCKER_PASSWORD=$$DOCKER_PASSWORD" go test -tags containers_image_openpgp -run=$(TEST) -v -timeout=180s ./...; \
else \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Running $$(basename $$test)..."; \
$$test -test.v -test.parallel=10 -test.timeout=60s || exit 1; \
fi; \
done; \
sudo env "PATH=$$PATH" "DOCKER_USERNAME=$$DOCKER_USERNAME" "DOCKER_PASSWORD=$$DOCKER_PASSWORD" go test -tags containers_image_openpgp -v -timeout=180s ./...; \
fi

# Generate JWT token for testing
Expand Down
16 changes: 8 additions & 8 deletions cmd/api/api/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import (
"time"

"github.com/go-chi/chi/v5"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/onkernel/hypeman/lib/images"
"github.com/onkernel/hypeman/lib/oapi"
"github.com/onkernel/hypeman/lib/paths"
"github.com/onkernel/hypeman/lib/registry"
Expand Down Expand Up @@ -54,7 +54,7 @@ func TestRegistryPushAndConvert(t *testing.T) {
srcRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)

img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

digest, err := img.Digest()
Expand Down Expand Up @@ -109,7 +109,7 @@ func TestRegistryPushAndCreateInstance(t *testing.T) {
srcRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)

img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

digest, err := img.Digest()
Expand Down Expand Up @@ -177,7 +177,7 @@ func TestRegistryLayerCaching(t *testing.T) {
srcRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)

img, err := remote.Image(srcRef)
img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

digest, err := img.Digest()
Expand Down Expand Up @@ -259,7 +259,7 @@ func TestRegistrySharedLayerCaching(t *testing.T) {
t.Log("Pulling alpine:latest...")
alpineRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)
alpineImg, err := remote.Image(alpineRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
alpineImg, err := remote.Image(alpineRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

// Get alpine layers for comparison
Expand Down Expand Up @@ -291,7 +291,7 @@ func TestRegistrySharedLayerCaching(t *testing.T) {
t.Log("Pulling alpine:3.18 (shares base layer)...")
alpine318Ref, err := name.ParseReference("docker.io/library/alpine:3.18")
require.NoError(t, err)
alpine318Img, err := remote.Image(alpine318Ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
alpine318Img, err := remote.Image(alpine318Ref, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

alpine318Digest, _ := alpine318Img.Digest()
Expand Down Expand Up @@ -341,7 +341,7 @@ func TestRegistryTagPush(t *testing.T) {
srcRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)

img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

digest, err := img.Digest()
Expand Down Expand Up @@ -393,7 +393,7 @@ func TestRegistryDockerV2ManifestConversion(t *testing.T) {
srcRef, err := name.ParseReference("docker.io/library/alpine:latest")
require.NoError(t, err)

img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(authn.DefaultKeychain))
img, err := remote.Image(srcRef, remote.WithAuthFromKeychain(images.GetKeychain()))
require.NoError(t, err)

// Wrap the image to simulate Docker v2 format (Docker daemon returns this format)
Expand Down
40 changes: 36 additions & 4 deletions lib/images/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@ type ociClient struct {
cacheDir string
}

// envKeychain authenticates to Docker Hub using environment variables.
// This is useful in CI environments where credentials are passed via env vars
// rather than stored in ~/.docker/config.json.
type envKeychain struct {
username, password string
}

func (k *envKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) {
// Only authenticate for Docker Hub
registry := resource.RegistryStr()
if registry == "index.docker.io" || registry == "registry-1.docker.io" {
return authn.FromConfig(authn.AuthConfig{
Username: k.username,
Password: k.password,
}), nil
}
// Fall back to anonymous for other registries
return authn.Anonymous, nil
}

// GetKeychain returns a keychain that tries env vars first, then default keychain.
// Set DOCKER_USERNAME and DOCKER_PASSWORD environment variables for Docker Hub auth.
func GetKeychain() authn.Keychain {
username := os.Getenv("DOCKER_USERNAME")
password := os.Getenv("DOCKER_PASSWORD")

if username != "" && password != "" {
return &envKeychain{username: username, password: password}
}
return authn.DefaultKeychain
}

// digestToLayoutTag converts a digest to a valid OCI layout tag.
// Uses just the hex portion without the algorithm prefix.
// Example: "sha256:abc123..." -> "abc123..."
Expand Down Expand Up @@ -68,11 +100,11 @@ func (c *ociClient) inspectManifest(ctx context.Context, imageRef string) (strin
return "", fmt.Errorf("parse image reference: %w", err)
}

// Use system authentication (reads from ~/.docker/config.json, etc.)
// Use env var authentication if available, otherwise fall back to ~/.docker/config.json
// Default retry: only on network errors, max ~1.3s total
descriptor, err := remote.Head(ref,
remote.WithContext(ctx),
remote.WithAuthFromKeychain(authn.DefaultKeychain))
remote.WithAuthFromKeychain(GetKeychain()))
if err != nil {
return "", fmt.Errorf("fetch manifest: %w", wrapRegistryError(err))
}
Expand Down Expand Up @@ -124,11 +156,11 @@ func (c *ociClient) pullToOCILayout(ctx context.Context, imageRef, layoutTag str
return fmt.Errorf("parse image reference: %w", err)
}

// Use system authentication (reads from ~/.docker/config.json, etc.)
// Use env var authentication if available, otherwise fall back to ~/.docker/config.json
// Default retry: only on network errors, max ~1.3s total
img, err := remote.Image(ref,
remote.WithContext(ctx),
remote.WithAuthFromKeychain(authn.DefaultKeychain))
remote.WithAuthFromKeychain(GetKeychain()))
if err != nil {
// Rate limits fail here immediately (429 is not retried by default)
return fmt.Errorf("fetch image manifest: %w", wrapRegistryError(err))
Expand Down
Loading