feat: move distribution builds to platform Docker builder
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestPinnedBuilderImageRequiresExplicitTagOrDigest(t *testing.T) {
|
||||
validDigest := strings.Repeat("a", 64)
|
||||
for _, test := range []struct {
|
||||
image string
|
||||
want bool
|
||||
}{
|
||||
{image: "browser-platform-distribution-builder:1.0.0", want: true},
|
||||
{image: "registry.example.test:5000/builders/distribution:v1", want: true},
|
||||
{image: "registry.example.test/builders/distribution@sha256:" + validDigest, want: true},
|
||||
{image: "browser-platform-distribution-builder", want: false},
|
||||
{image: "browser-platform-distribution-builder:latest", want: false},
|
||||
{image: "registry.example.test/builders/distribution@sha256:", want: false},
|
||||
{image: "registry.example.test/builders/distribution@sha256:not-a-digest", want: false},
|
||||
} {
|
||||
t.Run(test.image, func(t *testing.T) {
|
||||
if got := pinnedBuilderImage(test.image); got != test.want {
|
||||
t.Fatalf("pinnedBuilderImage(%q) = %t, want %t", test.image, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDistributionBuilderReadinessNamesPlatformBuilderFailures(t *testing.T) {
|
||||
sourceDir := createBuilderSource(t)
|
||||
workspaceDir := t.TempDir()
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
config DockerDistributionBuilderConfig
|
||||
reason string
|
||||
}{
|
||||
{
|
||||
name: "floating image",
|
||||
config: DockerDistributionBuilderConfig{Image: "golang:latest", SourceDir: sourceDir, WorkspaceDir: workspaceDir},
|
||||
reason: "platform builder image must be pinned",
|
||||
},
|
||||
{
|
||||
name: "missing source",
|
||||
config: DockerDistributionBuilderConfig{Image: "builder:1.0.0", WorkspaceDir: workspaceDir},
|
||||
reason: "platform builder run source directory is not configured",
|
||||
},
|
||||
{
|
||||
name: "image unavailable",
|
||||
config: DockerDistributionBuilderConfig{
|
||||
Image: "builder:1.0.0",
|
||||
SourceDir: sourceDir,
|
||||
WorkspaceDir: workspaceDir,
|
||||
CommandRunner: func(_ context.Context, _ string, args ...string) ([]byte, error) {
|
||||
if len(args) > 0 && args[0] == "version" {
|
||||
return []byte("27.0.0"), nil
|
||||
}
|
||||
return nil, errors.New("image missing")
|
||||
},
|
||||
},
|
||||
reason: "platform builder image is unavailable",
|
||||
},
|
||||
{
|
||||
name: "container runtime unavailable",
|
||||
config: DockerDistributionBuilderConfig{
|
||||
Image: "builder:1.0.0",
|
||||
SourceDir: sourceDir,
|
||||
WorkspaceDir: workspaceDir,
|
||||
CommandRunner: func(context.Context, string, ...string) ([]byte, error) {
|
||||
return nil, errors.New("docker unavailable")
|
||||
},
|
||||
},
|
||||
reason: "platform builder container runtime is unavailable",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ready, reason := NewDockerDistributionBuilder(test.config).Readiness()
|
||||
if ready || !strings.Contains(reason, test.reason) || !strings.Contains(reason, "platform builder") {
|
||||
t.Fatalf("expected platform builder readiness failure %q, ready=%t reason=%q", test.reason, ready, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDistributionBuilderKeepsSecretInIsolatedInput(t *testing.T) {
|
||||
sourceDir := createBuilderSource(t)
|
||||
workspaceDir := t.TempDir()
|
||||
secret := "component-auth-key-that-must-not-leave-input"
|
||||
var dockerArgs []string
|
||||
builder := NewDockerDistributionBuilder(DockerDistributionBuilderConfig{
|
||||
DockerBinary: "docker-test",
|
||||
Image: "browser-platform-distribution-builder:1.0.0",
|
||||
SourceDir: sourceDir,
|
||||
WorkspaceDir: workspaceDir,
|
||||
CommandRunner: func(_ context.Context, name string, args ...string) ([]byte, error) {
|
||||
if name != "docker-test" {
|
||||
t.Fatalf("unexpected container runtime %q", name)
|
||||
}
|
||||
if len(args) > 0 && (args[0] == "version" || args[0] == "image") {
|
||||
return []byte("27.0.0"), nil
|
||||
}
|
||||
dockerArgs = append([]string(nil), args...)
|
||||
inputDir := builderMountHostPath(t, args, "/workspace/input:ro")
|
||||
outputDir := builderMountHostPath(t, args, "/workspace/output")
|
||||
authPath := filepath.Join(inputDir, "auth-key")
|
||||
payload, err := os.ReadFile(authPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read per-job auth input: %v", err)
|
||||
}
|
||||
if string(payload) != secret {
|
||||
t.Fatalf("unexpected per-job auth input %q", payload)
|
||||
}
|
||||
info, err := os.Stat(authPath)
|
||||
if err != nil {
|
||||
t.Fatalf("stat per-job auth input: %v", err)
|
||||
}
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Fatalf("auth input mode = %o, want 600", info.Mode().Perm())
|
||||
}
|
||||
script, err := os.ReadFile(filepath.Join(inputDir, "build.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read build script: %v", err)
|
||||
}
|
||||
if bytes.Contains(script, []byte(secret)) {
|
||||
t.Fatal("build script must not embed the component auth key")
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(outputDir, "run.exe"), []byte("compiled-run"), 0o700); err != nil {
|
||||
t.Fatalf("write fake build output: %v", err)
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
input := domain.DistributionBuildInput{
|
||||
JobID: "job/build:one",
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
ServerInstanceID: "server-one",
|
||||
PluginID: "game.scum",
|
||||
RunEndpointID: "server-run-server-one",
|
||||
TargetOS: "windows",
|
||||
TargetArch: "amd64",
|
||||
TargetRelease: "release-one",
|
||||
PlatformURL: "https://platform.example.test",
|
||||
OutputFilename: "run.exe",
|
||||
KeyGeneration: 1,
|
||||
AuthKey: secret,
|
||||
}
|
||||
payload, err := builder.Build(input)
|
||||
if err != nil {
|
||||
t.Fatalf("build Run distribution: %v", err)
|
||||
}
|
||||
if string(payload) != "compiled-run" {
|
||||
t.Fatalf("unexpected built payload %q", payload)
|
||||
}
|
||||
joinedArgs := strings.Join(dockerArgs, "\x00")
|
||||
if strings.Contains(joinedArgs, secret) {
|
||||
t.Fatal("component auth key leaked into Docker arguments or environment")
|
||||
}
|
||||
for _, required := range []string{"--read-only", "--pull\x00never", sourceDir + ":/workspace/source:ro", ":/workspace/input:ro", ":/workspace/output"} {
|
||||
if !strings.Contains(joinedArgs, required) {
|
||||
t.Fatalf("Docker arguments do not contain required isolation %q: %q", required, joinedArgs)
|
||||
}
|
||||
}
|
||||
jobDir := filepath.Join(workspaceDir, "game.scum", "job-build-one")
|
||||
if _, err := os.Stat(jobDir); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("per-job workspace was not removed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerDistributionBuilderRedactsFailureAndTimeout(t *testing.T) {
|
||||
sourceDir := createBuilderSource(t)
|
||||
workspaceDir := t.TempDir()
|
||||
secret := "sensitive-component-key"
|
||||
input := domain.DistributionBuildInput{
|
||||
JobID: "job-redaction",
|
||||
ComponentKind: domain.DistributionComponentRun,
|
||||
PluginID: "game.scum",
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
OutputFilename: "run",
|
||||
AuthKey: secret,
|
||||
}
|
||||
builder := NewDockerDistributionBuilder(DockerDistributionBuilderConfig{
|
||||
Image: "builder:1.0.0",
|
||||
SourceDir: sourceDir,
|
||||
WorkspaceDir: workspaceDir,
|
||||
CommandRunner: func(_ context.Context, _ string, args ...string) ([]byte, error) {
|
||||
if len(args) > 0 && (args[0] == "version" || args[0] == "image") {
|
||||
return []byte("27.0.0"), nil
|
||||
}
|
||||
jobDir := filepath.Join(workspaceDir, "game.scum", "job-redaction")
|
||||
return []byte(secret + "\n" + sourceDir + "/go.mod: build failed\n" + jobDir + "/input/auth-key"), errors.New("exit 1")
|
||||
},
|
||||
})
|
||||
_, err := builder.Build(input)
|
||||
if err == nil {
|
||||
t.Fatal("expected failed builder command")
|
||||
}
|
||||
if strings.Contains(err.Error(), secret) || strings.Contains(err.Error(), sourceDir) || strings.Contains(err.Error(), workspaceDir) || strings.Contains(err.Error(), "auth-key") {
|
||||
t.Fatalf("builder failure leaked secret or host path: %v", err)
|
||||
}
|
||||
|
||||
timeoutBuilder := NewDockerDistributionBuilder(DockerDistributionBuilderConfig{
|
||||
Image: "builder:1.0.0",
|
||||
SourceDir: sourceDir,
|
||||
WorkspaceDir: workspaceDir,
|
||||
Timeout: 5 * time.Millisecond,
|
||||
CommandRunner: func(ctx context.Context, _ string, args ...string) ([]byte, error) {
|
||||
if len(args) > 0 && (args[0] == "version" || args[0] == "image") {
|
||||
return []byte("27.0.0"), nil
|
||||
}
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
})
|
||||
_, err = timeoutBuilder.Build(input)
|
||||
if err == nil || !strings.Contains(err.Error(), "platform builder timed out") {
|
||||
t.Fatalf("expected bounded platform builder timeout, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackageClientManagerDistributionProducesProtectedArchives(t *testing.T) {
|
||||
for _, packageFormat := range []string{"zip", "tar.gz"} {
|
||||
t.Run(packageFormat, func(t *testing.T) {
|
||||
payload, err := packageClientManagerDistribution(packageFormat, "client-manager.exe", []byte("binary"), []byte("credential: protected"))
|
||||
if err != nil {
|
||||
t.Fatalf("package client manager: %v", err)
|
||||
}
|
||||
files := readDistributionArchive(t, packageFormat, payload)
|
||||
if string(files["client-manager.exe"].payload) != "binary" || files["client-manager.exe"].mode.Perm() != 0o755 {
|
||||
t.Fatalf("unexpected executable archive entry: %+v", files["client-manager.exe"])
|
||||
}
|
||||
if string(files["config.yaml"].payload) != "credential: protected" || files["config.yaml"].mode.Perm() != 0o600 {
|
||||
t.Fatalf("unexpected config archive entry: %+v", files["config.yaml"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type distributionArchiveFile struct {
|
||||
mode os.FileMode
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func createBuilderSource(t *testing.T) string {
|
||||
t.Helper()
|
||||
sourceDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "go.mod"), []byte("module browser.local/run\n"), 0o600); err != nil {
|
||||
t.Fatalf("write run source go.mod: %v", err)
|
||||
}
|
||||
return sourceDir
|
||||
}
|
||||
|
||||
func builderMountHostPath(t *testing.T, args []string, containerSuffix string) string {
|
||||
t.Helper()
|
||||
for index := 0; index+1 < len(args); index++ {
|
||||
if args[index] != "-v" || !strings.HasSuffix(args[index+1], ":"+containerSuffix) {
|
||||
continue
|
||||
}
|
||||
return strings.TrimSuffix(args[index+1], ":"+containerSuffix)
|
||||
}
|
||||
t.Fatalf("Docker arguments do not mount %s: %+v", containerSuffix, args)
|
||||
return ""
|
||||
}
|
||||
|
||||
func readDistributionArchive(t *testing.T, packageFormat string, payload []byte) map[string]distributionArchiveFile {
|
||||
t.Helper()
|
||||
files := map[string]distributionArchiveFile{}
|
||||
switch packageFormat {
|
||||
case "zip":
|
||||
reader, err := zip.NewReader(bytes.NewReader(payload), int64(len(payload)))
|
||||
if err != nil {
|
||||
t.Fatalf("open zip: %v", err)
|
||||
}
|
||||
for _, entry := range reader.File {
|
||||
body, err := entry.Open()
|
||||
if err != nil {
|
||||
t.Fatalf("open zip entry %s: %v", entry.Name, err)
|
||||
}
|
||||
content, err := io.ReadAll(body)
|
||||
if closeErr := body.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("read zip entry %s: %v", entry.Name, err)
|
||||
}
|
||||
files[entry.Name] = distributionArchiveFile{mode: entry.Mode(), payload: content}
|
||||
}
|
||||
case "tar.gz":
|
||||
gzipReader, err := gzip.NewReader(bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
t.Fatalf("open gzip: %v", err)
|
||||
}
|
||||
tarReader := tar.NewReader(gzipReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("read tar header: %v", err)
|
||||
}
|
||||
content, err := io.ReadAll(tarReader)
|
||||
if err != nil {
|
||||
t.Fatalf("read tar entry %s: %v", header.Name, err)
|
||||
}
|
||||
files[header.Name] = distributionArchiveFile{mode: os.FileMode(header.Mode), payload: content}
|
||||
}
|
||||
if err := gzipReader.Close(); err != nil {
|
||||
t.Fatalf("close gzip: %v", err)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unsupported archive format %q", packageFormat)
|
||||
}
|
||||
return files
|
||||
}
|
||||
Reference in New Issue
Block a user