618 lines
22 KiB
Go
618 lines
22 KiB
Go
package service
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
// DistributionBuilder executes a distribution build inside a platform-owned
|
|
// container. Builds are a platform responsibility: they must not depend on a
|
|
// machine-side run endpoint being registered and online, and the component auth
|
|
// key must never leave the platform.
|
|
type DistributionBuilder interface {
|
|
// Readiness reports whether the platform builder can execute a build. The
|
|
// reason must name the platform builder rather than a run endpoint
|
|
// capability, so an operator is not sent looking at the wrong subsystem.
|
|
Readiness() (bool, string)
|
|
// Build assembles the package described by input and returns its bytes.
|
|
Build(input domain.DistributionBuildInput) ([]byte, error)
|
|
}
|
|
|
|
type DistributionBuildProgress struct {
|
|
Percent int
|
|
Message string
|
|
}
|
|
|
|
type distributionBuilderWithProgress interface {
|
|
BuildWithProgress(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) ([]byte, error)
|
|
}
|
|
|
|
// DockerDistributionBuilderConfig configures a container-per-build builder.
|
|
type DockerDistributionBuilderConfig struct {
|
|
DockerBinary string
|
|
Image string
|
|
SourceDir string
|
|
SourceRepositoryURL string
|
|
SourceRevision string
|
|
WorkspaceDir string
|
|
CacheDir string
|
|
Timeout time.Duration
|
|
PlatformURL string
|
|
CommandRunner func(ctx context.Context, name string, args ...string) ([]byte, error)
|
|
CommandStream func(ctx context.Context, name string, args []string, onLine func(string)) ([]byte, error)
|
|
GitCommandRunner func(ctx context.Context, name string, args ...string) ([]byte, error)
|
|
}
|
|
|
|
// DockerDistributionBuilder runs each build in a container from a pinned image,
|
|
// with the run source mounted read-only and a per-job output directory mounted
|
|
// writable.
|
|
type DockerDistributionBuilder struct {
|
|
config DockerDistributionBuilderConfig
|
|
sourceMu sync.Mutex
|
|
}
|
|
|
|
func NewDockerDistributionBuilder(config DockerDistributionBuilderConfig) *DockerDistributionBuilder {
|
|
customRunner := config.CommandRunner != nil
|
|
if strings.TrimSpace(config.DockerBinary) == "" {
|
|
config.DockerBinary = "docker"
|
|
}
|
|
if config.Timeout <= 0 {
|
|
config.Timeout = 30 * time.Minute
|
|
}
|
|
if config.CommandRunner == nil {
|
|
config.CommandRunner = runCommandCombined
|
|
}
|
|
if config.GitCommandRunner == nil {
|
|
config.GitCommandRunner = runCommandCombined
|
|
}
|
|
if config.CommandStream == nil && !customRunner {
|
|
config.CommandStream = runCommandStreamCombined
|
|
}
|
|
return &DockerDistributionBuilder{config: config}
|
|
}
|
|
|
|
func runCommandCombined(ctx context.Context, name string, args ...string) ([]byte, error) {
|
|
return exec.CommandContext(ctx, name, args...).CombinedOutput()
|
|
}
|
|
|
|
func runCommandStreamCombined(ctx context.Context, name string, args []string, onLine func(string)) ([]byte, error) {
|
|
cmd := exec.CommandContext(ctx, name, args...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var mu sync.Mutex
|
|
var output bytes.Buffer
|
|
var wg sync.WaitGroup
|
|
capture := func(reader io.Reader) {
|
|
defer wg.Done()
|
|
scanner := bufio.NewScanner(reader)
|
|
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
mu.Lock()
|
|
output.WriteString(line)
|
|
output.WriteByte('\n')
|
|
mu.Unlock()
|
|
if onLine != nil {
|
|
onLine(line)
|
|
}
|
|
}
|
|
}
|
|
wg.Add(2)
|
|
go capture(stdout)
|
|
go capture(stderr)
|
|
err = cmd.Wait()
|
|
wg.Wait()
|
|
return output.Bytes(), err
|
|
}
|
|
|
|
func (builder *DockerDistributionBuilder) Readiness() (bool, string) {
|
|
if strings.TrimSpace(builder.config.Image) == "" {
|
|
return false, "platform builder image is not configured"
|
|
}
|
|
if !pinnedBuilderImage(builder.config.Image) {
|
|
return false, "platform builder image must be pinned to an explicit version or digest"
|
|
}
|
|
source := strings.TrimSpace(builder.config.SourceDir)
|
|
if source == "" {
|
|
return false, "platform builder run source directory is not configured"
|
|
}
|
|
source, err := filepath.Abs(strings.TrimSpace(builder.config.SourceDir))
|
|
if err != nil {
|
|
return false, "platform builder run source directory is invalid"
|
|
}
|
|
if _, err := os.Stat(source); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return false, "platform builder run source directory is not accessible"
|
|
}
|
|
if strings.TrimSpace(builder.config.SourceRepositoryURL) == "" {
|
|
if _, err := os.Stat(filepath.Join(source, "go.mod")); err != nil {
|
|
return false, "platform builder run source repository is not configured"
|
|
}
|
|
}
|
|
if strings.TrimSpace(builder.config.WorkspaceDir) == "" {
|
|
return false, "platform builder workspace directory is not configured"
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if _, err := builder.config.CommandRunner(ctx, builder.config.DockerBinary, "version", "--format", "{{.Server.Version}}"); err != nil {
|
|
return false, "platform builder container runtime is unavailable"
|
|
}
|
|
if _, err := builder.config.CommandRunner(ctx, builder.config.DockerBinary, "image", "inspect", builder.config.Image); err != nil {
|
|
return false, "platform builder image is unavailable"
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
// pinnedBuilderImage rejects floating references. An unpinned builder image
|
|
// silently changes what the platform ships.
|
|
func pinnedBuilderImage(image string) bool {
|
|
image = strings.TrimSpace(image)
|
|
if name, digest, found := strings.Cut(image, "@sha256:"); found {
|
|
if strings.TrimSpace(name) == "" || len(digest) != 64 {
|
|
return false
|
|
}
|
|
_, err := hex.DecodeString(digest)
|
|
return err == nil
|
|
}
|
|
reference := image
|
|
if slash := strings.LastIndex(image, "/"); slash >= 0 {
|
|
reference = image[slash+1:]
|
|
}
|
|
_, tag, found := strings.Cut(reference, ":")
|
|
if !found {
|
|
return false
|
|
}
|
|
tag = strings.TrimSpace(tag)
|
|
return tag != "" && tag != "latest"
|
|
}
|
|
|
|
func (builder *DockerDistributionBuilder) Build(input domain.DistributionBuildInput) ([]byte, error) {
|
|
return builder.BuildWithProgress(input, nil)
|
|
}
|
|
|
|
func (builder *DockerDistributionBuilder) BuildWithProgress(input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) ([]byte, error) {
|
|
if ready, reason := builder.Readiness(); !ready {
|
|
return nil, validationError(reason)
|
|
}
|
|
reportBuilderProgress(progress, 8, "env_check: platform builder readiness verified")
|
|
sourceDir, err := filepath.Abs(strings.TrimSpace(builder.config.SourceDir))
|
|
if err != nil {
|
|
return nil, validationError("platform builder run source directory is invalid")
|
|
}
|
|
workspaceDir, err := filepath.Abs(strings.TrimSpace(builder.config.WorkspaceDir))
|
|
if err != nil {
|
|
return nil, validationError("platform builder workspace directory is invalid")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), builder.config.Timeout)
|
|
defer cancel()
|
|
if input.ComponentKind == domain.DistributionComponentRun {
|
|
// Keep a shared checkout stable while Docker reads it. The checkout is
|
|
// refreshed and exported below for this build, rather than captured when
|
|
// the platform process starts.
|
|
builder.sourceMu.Lock()
|
|
defer builder.sourceMu.Unlock()
|
|
}
|
|
cacheDir, err := builder.cacheDir(workspaceDir)
|
|
if err != nil {
|
|
return nil, validationError("platform builder cache directory is invalid")
|
|
}
|
|
// Workspaces stay isolated per plugin and per job as required by
|
|
// run-build-download-flow.
|
|
jobDir := filepath.Join(workspaceDir, sanitizeIDPart(input.PluginID), sanitizeIDPart(input.JobID))
|
|
if err := os.RemoveAll(jobDir); err != nil {
|
|
return nil, err
|
|
}
|
|
outputDir := filepath.Join(jobDir, "output")
|
|
inputDir := filepath.Join(jobDir, "input")
|
|
buildDir := filepath.Join(jobDir, "build")
|
|
sourceMountDir := filepath.Join(jobDir, "source")
|
|
goBuildCacheDir := filepath.Join(cacheDir, "go-build")
|
|
goModCacheDir := filepath.Join(cacheDir, "go-mod")
|
|
for _, directory := range []string{outputDir, inputDir, buildDir, sourceMountDir, goBuildCacheDir, goModCacheDir} {
|
|
if err := os.MkdirAll(directory, 0o700); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
defer func() { _ = os.RemoveAll(jobDir) }()
|
|
reportBuilderProgress(progress, 14, "env_check: platform builder workspace prepared")
|
|
if input.ComponentKind == domain.DistributionComponentRun {
|
|
preparedSourceDir, err := builder.prepareRunSource(ctx, sourceDir, sourceMountDir, input, progress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sourceMountDir = preparedSourceDir
|
|
}
|
|
|
|
// The auth key reaches the container through a per-job input file, never
|
|
// through a job-channel response to a machine-side endpoint or a container
|
|
// command-line argument.
|
|
if strings.TrimSpace(input.AuthKey) == "" {
|
|
return nil, validationError("distribution build input is missing a component auth key")
|
|
}
|
|
if err := os.WriteFile(filepath.Join(inputDir, "auth-key"), []byte(input.AuthKey), 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
seedPayload := []byte("[]")
|
|
if strings.TrimSpace(input.WorkspaceSeed) != "" {
|
|
decoded, err := base64.StdEncoding.DecodeString(input.WorkspaceSeed)
|
|
if err != nil {
|
|
return nil, validationError("distribution build input has an invalid workspace seed")
|
|
}
|
|
seedPayload = decoded
|
|
}
|
|
if err := os.WriteFile(filepath.Join(inputDir, "workspace-seed.json"), seedPayload, 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
lifecyclePlanPayload := []byte("{}")
|
|
if input.AutonomousLifecycle != nil {
|
|
encoded, err := json.Marshal(input.AutonomousLifecycle)
|
|
if err != nil {
|
|
return nil, validationError("distribution build input has an invalid autonomous lifecycle plan")
|
|
}
|
|
lifecyclePlanPayload = encoded
|
|
}
|
|
if err := os.WriteFile(filepath.Join(inputDir, "autonomous-lifecycle-plan.json"), lifecyclePlanPayload, 0o600); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(inputDir, "build.sh"), []byte(distributionBuildScript), 0o500); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
outputName := strings.TrimSpace(input.OutputFilename)
|
|
if outputName == "" || filepath.Base(outputName) != outputName {
|
|
return nil, validationError("distribution build input has an invalid output filename")
|
|
}
|
|
args := builder.containerArgs(input, sourceMountDir, inputDir, buildDir, outputDir, goBuildCacheDir, goModCacheDir, outputName)
|
|
reportBuilderProgress(progress, 18, "git_sync: platform builder container starting")
|
|
output, err := builder.runBuildCommand(ctx, args, progress)
|
|
if err != nil {
|
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
return nil, validationError("platform builder timed out while building the distribution")
|
|
}
|
|
return nil, validationError("platform builder failed: " + safeBuilderFailureWithCommandError(output, err, input.AuthKey, builder.config.SourceDir, sourceDir, jobDir))
|
|
}
|
|
reportBuilderProgress(progress, 92, "package_finalize: reading platform builder output")
|
|
binary, err := os.ReadFile(filepath.Join(outputDir, outputName))
|
|
if err != nil {
|
|
return nil, validationError("platform builder did not produce a distribution executable")
|
|
}
|
|
if len(binary) == 0 {
|
|
return nil, validationError("platform builder produced an empty distribution executable")
|
|
}
|
|
return binary, nil
|
|
}
|
|
|
|
// prepareRunSource implements the source phase of a Jenkins-style build. A
|
|
// configured checkout is created when absent, fetched on every build, and
|
|
// exported at FETCH_HEAD into the per-job workspace. The archive keeps local
|
|
// uncommitted files and the checkout's .git directory out of the executable.
|
|
func (builder *DockerDistributionBuilder) prepareRunSource(ctx context.Context, sourceDir, destination string, input domain.DistributionBuildInput, progress func(DistributionBuildProgress)) (string, error) {
|
|
repository := strings.TrimSpace(input.RepositoryURL)
|
|
if repository == "" {
|
|
repository = strings.TrimSpace(builder.config.SourceRepositoryURL)
|
|
}
|
|
revision := strings.TrimSpace(input.SourceRevision)
|
|
if revision == "" {
|
|
revision = strings.TrimSpace(builder.config.SourceRevision)
|
|
}
|
|
if revision == "" {
|
|
revision = "main"
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(sourceDir, ".git")); errors.Is(err, os.ErrNotExist) {
|
|
if repository == "" {
|
|
if _, sourceErr := os.Stat(filepath.Join(sourceDir, "go.mod")); sourceErr != nil {
|
|
return "", validationError("platform builder has no Run source checkout or repository")
|
|
}
|
|
return sourceDir, nil
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(sourceDir), 0o700); err != nil {
|
|
return "", validationError("platform builder could not create the Run source parent")
|
|
}
|
|
reportBuilderProgress(progress, 20, "git_sync: creating Run source checkout")
|
|
if _, err := builder.config.GitCommandRunner(ctx, "git", "clone", "--no-checkout", repository, sourceDir); err != nil {
|
|
return "", validationError("platform builder could not clone the Run source")
|
|
}
|
|
} else if err != nil {
|
|
return "", validationError("platform builder could not inspect the Run source checkout")
|
|
}
|
|
|
|
reportBuilderProgress(progress, 26, "git_sync: fetching Run source revision")
|
|
if _, err := builder.config.GitCommandRunner(ctx, "git", "-C", sourceDir, "fetch", "--depth", "1", "origin", revision); err != nil {
|
|
return "", validationError("platform builder could not update the Run source revision")
|
|
}
|
|
archivePayload, err := builder.config.GitCommandRunner(ctx, "git", "-C", sourceDir, "archive", "--format=tar", "FETCH_HEAD")
|
|
if err != nil {
|
|
return "", validationError("platform builder could not export the Run source revision")
|
|
}
|
|
if err := extractRunSourceArchive(archivePayload, destination); err != nil {
|
|
return "", validationError("platform builder could not prepare the Run source workspace")
|
|
}
|
|
reportBuilderProgress(progress, 34, "git_sync: Run source revision prepared")
|
|
return destination, nil
|
|
}
|
|
|
|
func extractRunSourceArchive(payload []byte, destination string) error {
|
|
reader := tar.NewReader(bytes.NewReader(payload))
|
|
for {
|
|
header, err := reader.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := filepath.Clean(filepath.FromSlash(header.Name))
|
|
if name == "." || filepath.IsAbs(name) || name == ".." || strings.HasPrefix(name, ".."+string(os.PathSeparator)) {
|
|
return errors.New("Run source archive contains an unsafe path")
|
|
}
|
|
target := filepath.Join(destination, name)
|
|
if header.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(target, header.FileInfo().Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if !header.FileInfo().Mode().IsRegular() {
|
|
return errors.New("Run source archive contains an unsupported file")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
|
return err
|
|
}
|
|
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, header.FileInfo().Mode().Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, copyErr := io.Copy(file, reader)
|
|
closeErr := file.Close()
|
|
if copyErr != nil {
|
|
return copyErr
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
}
|
|
}
|
|
|
|
func (builder *DockerDistributionBuilder) cacheDir(workspaceDir string) (string, error) {
|
|
configured := strings.TrimSpace(builder.config.CacheDir)
|
|
if configured == "" {
|
|
configured = filepath.Join(workspaceDir, "_cache")
|
|
}
|
|
return filepath.Abs(configured)
|
|
}
|
|
|
|
func (builder *DockerDistributionBuilder) runBuildCommand(ctx context.Context, args []string, progress func(DistributionBuildProgress)) ([]byte, error) {
|
|
if builder.config.CommandStream == nil {
|
|
return builder.config.CommandRunner(ctx, builder.config.DockerBinary, args...)
|
|
}
|
|
return builder.config.CommandStream(ctx, builder.config.DockerBinary, args, func(line string) {
|
|
parsed, ok := parseBuilderProgressLine(line)
|
|
if ok {
|
|
reportBuilderProgress(progress, parsed.Percent, parsed.Message)
|
|
}
|
|
})
|
|
}
|
|
|
|
const builderProgressMarker = "__platform_builder_progress__|"
|
|
|
|
func parseBuilderProgressLine(line string) (DistributionBuildProgress, bool) {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.HasPrefix(line, builderProgressMarker) {
|
|
return DistributionBuildProgress{}, false
|
|
}
|
|
rest := strings.TrimPrefix(line, builderProgressMarker)
|
|
parts := strings.SplitN(rest, "|", 2)
|
|
if len(parts) != 2 {
|
|
return DistributionBuildProgress{}, false
|
|
}
|
|
percent, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
|
if err != nil {
|
|
return DistributionBuildProgress{}, false
|
|
}
|
|
message := strings.TrimSpace(parts[1])
|
|
if message == "" {
|
|
return DistributionBuildProgress{}, false
|
|
}
|
|
if percent < 1 {
|
|
percent = 1
|
|
}
|
|
if percent > 99 {
|
|
percent = 99
|
|
}
|
|
return DistributionBuildProgress{Percent: percent, Message: message}, true
|
|
}
|
|
|
|
func reportBuilderProgress(progress func(DistributionBuildProgress), percent int, message string) {
|
|
if progress == nil {
|
|
return
|
|
}
|
|
if percent < 1 {
|
|
percent = 1
|
|
}
|
|
if percent > 99 {
|
|
percent = 99
|
|
}
|
|
progress(DistributionBuildProgress{Percent: percent, Message: message})
|
|
}
|
|
|
|
const distributionBuildScript = `#!/bin/sh
|
|
set -eu
|
|
|
|
progress() {
|
|
printf '__platform_builder_progress__|%s|%s\n' "$1" "$2"
|
|
}
|
|
|
|
auth_key="$(cat /workspace/input/auth-key)"
|
|
if [ "$COMPONENT_KIND" = "run" ]; then
|
|
progress 24 'git_sync: preparing run source snapshot'
|
|
rm -rf /workspace/build/run-source
|
|
mkdir -p /workspace/build/run-source
|
|
find /workspace/source -mindepth 1 -maxdepth 1 ! -name pax_global_header -exec cp -R {} /workspace/build/run-source/ \;
|
|
run_command_dir="$(find /workspace/build/run-source -type d -path '*/cmd/run' -print -quit)"
|
|
run_module_dir="${run_command_dir%/cmd/run}"
|
|
if [ -z "$run_command_dir" ] || [ ! -f "$run_module_dir/go.mod" ]; then
|
|
printf 'Run source snapshot must contain a Go module with cmd/run\n' >&2
|
|
exit 2
|
|
fi
|
|
progress 34 'env_check: injecting run build metadata'
|
|
seed_b64="$(base64 /workspace/input/workspace-seed.json | tr -d '\n')"
|
|
mkdir -p "$run_module_dir/config"
|
|
cat > "$run_module_dir/config/workspace_seed_generated.go" <<EOF
|
|
package config
|
|
|
|
func init() { BuildWorkspaceSeed = "$seed_b64" }
|
|
EOF
|
|
cd "$run_module_dir"
|
|
ldflags="-s -w"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildMode=worker"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildPlatformURL=$PLATFORM_URL"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildRunEndpointID=$RUN_ENDPOINT_ID"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildDisplayName=Run-$SERVER_INSTANCE_ID"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildRegistrationToken=$auth_key"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildServerInstanceID=$SERVER_INSTANCE_ID"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildPluginID=$PLUGIN_ID"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildComponentKind=$COMPONENT_KIND"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildComponentKey=$PROFILE_KEY"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildKeyGeneration=$KEY_GENERATION"
|
|
ldflags="$ldflags -X browser.local/run/config.BuildVersion=$TARGET_RELEASE"
|
|
progress 48 'build_compile: downloading Go modules'
|
|
go mod download
|
|
progress 72 'build_compile: compiling run target executable'
|
|
go build -trimpath -ldflags "$ldflags" -o "/workspace/output/$OUTPUT_FILENAME" ./cmd/run
|
|
progress 88 'package_finalize: run executable written'
|
|
exit 0
|
|
fi
|
|
|
|
printf 'unsupported component kind\n' >&2
|
|
exit 2
|
|
`
|
|
|
|
func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir string, goBuildCacheDir string, goModCacheDir string, outputName string) []string {
|
|
platformURL := strings.TrimSpace(input.PlatformURL)
|
|
if platformURL == "" {
|
|
platformURL = strings.TrimSpace(builder.config.PlatformURL)
|
|
}
|
|
return []string{
|
|
"run", "--rm",
|
|
"--pull", "never",
|
|
"--read-only",
|
|
"--tmpfs", "/tmp:rw,nosuid,size=2147483648",
|
|
"-v", sourceDir + ":/workspace/source:ro",
|
|
"-v", inputDir + ":/workspace/input:ro",
|
|
"-v", buildDir + ":/workspace/build",
|
|
"-v", outputDir + ":/workspace/output",
|
|
"-v", goBuildCacheDir + ":/workspace/cache/go-build",
|
|
"-v", goModCacheDir + ":/workspace/cache/go-mod",
|
|
"-e", "CGO_ENABLED=0",
|
|
"-e", "GOOS=" + input.TargetOS,
|
|
"-e", "GOARCH=" + input.TargetArch,
|
|
"-e", "GOCACHE=/workspace/cache/go-build",
|
|
"-e", "GOMODCACHE=/workspace/cache/go-mod",
|
|
"-e", "COMPONENT_KIND=" + string(input.ComponentKind),
|
|
"-e", "SERVER_INSTANCE_ID=" + input.ServerInstanceID,
|
|
"-e", "PLUGIN_ID=" + input.PluginID,
|
|
"-e", "RUN_ENDPOINT_ID=" + input.RunEndpointID,
|
|
"-e", "PROFILE_KEY=" + input.ProfileKey,
|
|
"-e", "TARGET_RELEASE=" + input.TargetRelease,
|
|
"-e", "KEY_GENERATION=" + fmt.Sprint(input.KeyGeneration),
|
|
"-e", "PLATFORM_URL=" + platformURL,
|
|
"-e", "REPOSITORY_URL=" + input.RepositoryURL,
|
|
"-e", "SOURCE_REVISION=" + input.SourceRevision,
|
|
"-e", "OUTPUT_FILENAME=" + outputName,
|
|
builder.config.Image,
|
|
"/workspace/input/build.sh",
|
|
}
|
|
}
|
|
|
|
// safeBuilderFailure keeps host paths and secret values out of reported build
|
|
// failures.
|
|
func safeBuilderFailure(output []byte, sensitiveValues ...string) string {
|
|
text := strings.TrimSpace(string(output))
|
|
for _, sensitive := range sensitiveValues {
|
|
if strings.TrimSpace(sensitive) != "" {
|
|
text = strings.ReplaceAll(text, sensitive, "[redacted]")
|
|
}
|
|
}
|
|
if text == "" {
|
|
return "build command reported no diagnostic output"
|
|
}
|
|
lines := strings.Split(text, "\n")
|
|
kept := make([]string, 0, len(lines))
|
|
for index := len(lines) - 1; index >= 0 && len(kept) < 3; index-- {
|
|
line := strings.TrimSpace(lines[index])
|
|
if line == "" || strings.HasPrefix(line, builderProgressMarker) || strings.Contains(line, "/workspace/input") || strings.Contains(line, "auth-key") {
|
|
continue
|
|
}
|
|
line = redactBuilderHostPaths(line)
|
|
kept = append([]string{line}, kept...)
|
|
}
|
|
if len(kept) == 0 {
|
|
return "build command reported no shareable diagnostic output"
|
|
}
|
|
joined := strings.Join(kept, "; ")
|
|
if len(joined) > 400 {
|
|
joined = joined[:400]
|
|
}
|
|
return joined
|
|
}
|
|
|
|
func safeBuilderFailureWithCommandError(output []byte, commandErr error, sensitiveValues ...string) string {
|
|
message := safeBuilderFailure(output, sensitiveValues...)
|
|
if commandErr == nil || !strings.HasPrefix(message, "build command reported no ") {
|
|
return message
|
|
}
|
|
return safeBuilderFailure([]byte(commandErr.Error()), sensitiveValues...)
|
|
}
|
|
|
|
func redactBuilderHostPaths(line string) string {
|
|
fields := strings.Fields(line)
|
|
for index, field := range fields {
|
|
trimmed := strings.TrimLeft(field, "(\"'[")
|
|
if strings.HasPrefix(trimmed, "/") && !strings.HasPrefix(trimmed, "/workspace/") {
|
|
fields[index] = "[redacted-path]"
|
|
}
|
|
}
|
|
return strings.Join(fields, " ")
|
|
}
|
|
|
|
func builderJobFailureMessage(err error, sensitiveValues ...string) string {
|
|
if err == nil {
|
|
return "platform builder failed"
|
|
}
|
|
message := safeBuilderFailure([]byte(err.Error()), sensitiveValues...)
|
|
if message == "" {
|
|
return "platform builder failed"
|
|
}
|
|
if len(message) > 400 {
|
|
message = message[:400]
|
|
}
|
|
return message
|
|
}
|