package service import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "context" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "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) } // DockerDistributionBuilderConfig configures a container-per-build builder. type DockerDistributionBuilderConfig struct { DockerBinary string Image string SourceDir string WorkspaceDir string Timeout time.Duration PlatformURL string CommandRunner 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 } func NewDockerDistributionBuilder(config DockerDistributionBuilderConfig) *DockerDistributionBuilder { if strings.TrimSpace(config.DockerBinary) == "" { config.DockerBinary = "docker" } if config.Timeout <= 0 { config.Timeout = 30 * time.Minute } if config.CommandRunner == nil { config.CommandRunner = runCommandCombined } return &DockerDistributionBuilder{config: config} } func runCommandCombined(ctx context.Context, name string, args ...string) ([]byte, error) { return exec.CommandContext(ctx, name, args...).CombinedOutput() } 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(filepath.Join(source, "go.mod")); err != nil { return false, "platform builder run source directory does not contain a run checkout" } 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) { if ready, reason := builder.Readiness(); !ready { return nil, validationError(reason) } 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") } // 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") for _, directory := range []string{outputDir, inputDir, buildDir} { if err := os.MkdirAll(directory, 0o700); err != nil { return nil, err } } defer func() { _ = os.RemoveAll(jobDir) }() // 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") } ctx, cancel := context.WithTimeout(context.Background(), builder.config.Timeout) defer cancel() args := builder.containerArgs(input, sourceDir, inputDir, buildDir, outputDir, outputName) if output, err := builder.config.CommandRunner(ctx, builder.config.DockerBinary, args...); 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: " + safeBuilderFailure(output, input.AuthKey, builder.config.SourceDir, sourceDir, jobDir)) } 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") } if input.ComponentKind == domain.DistributionComponentRun { return binary, nil } configPayload, err := os.ReadFile(filepath.Join(outputDir, "config.yaml")) if err != nil { return nil, validationError("platform builder did not produce client-manager configuration") } return packageClientManagerDistribution(input.PackageFormat, outputName, binary, configPayload) } const distributionBuildScript = `#!/bin/sh set -eu auth_key="$(cat /workspace/input/auth-key)" if [ "$COMPONENT_KIND" = "run" ]; then rm -rf /workspace/build/run-source mkdir -p /workspace/build/run-source cp -R /workspace/source/. /workspace/build/run-source/ seed_b64="$(base64 /workspace/input/workspace-seed.json | tr -d '\n')" cat > /workspace/build/run-source/config/workspace_seed_generated.go <&2 exit 2 fi case "$REPOSITORY_URL" in https://*) ;; *) printf 'client-manager repository must use https\n' >&2; exit 2 ;; esac cd /workspace/build git init --quiet git remote add origin "$REPOSITORY_URL" git fetch --quiet --depth 1 origin "$SOURCE_REVISION" git checkout --quiet --detach FETCH_HEAD { printf 'server_url: "%s"\n' "$PLATFORM_URL" printf 'server_instance_id: "%s"\n' "$SERVER_INSTANCE_ID" printf 'scum_client_credential: "%s"\n' "$auth_key" printf 'scum_client_name: "%s"\n' "$PROFILE_KEY" printf 'scum_client_version: "platform-build"\n' printf 'scum_client_machine_label: "managed-client"\n' printf 'ftp_provider: 3\n' } > config.yaml go mod download go build -trimpath -ldflags '-s -w' -o "/workspace/output/$OUTPUT_FILENAME" . cp config.yaml /workspace/output/config.yaml ` func (builder *DockerDistributionBuilder) containerArgs(input domain.DistributionBuildInput, sourceDir string, inputDir string, buildDir string, outputDir 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", "-e", "CGO_ENABLED=0", "-e", "GOOS=" + input.TargetOS, "-e", "GOARCH=" + input.TargetArch, "-e", "GOCACHE=/tmp/go-build", "-e", "GOMODCACHE=/tmp/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", } } func packageClientManagerDistribution(packageFormat string, outputName string, binary []byte, configPayload []byte) ([]byte, error) { switch packageFormat { case "zip": return zipDistributionFiles(outputName, binary, configPayload) case "tar.gz": return tarGzipDistributionFiles(outputName, binary, configPayload) default: return nil, validationError("platform builder received an unsupported client-manager package format") } } func zipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) { var buffer bytes.Buffer writer := zip.NewWriter(&buffer) files := []struct { name string mode os.FileMode payload []byte }{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}} for _, file := range files { header := &zip.FileHeader{Name: file.name, Method: zip.Deflate} header.SetMode(file.mode) header.SetModTime(time.Date(1980, time.January, 1, 0, 0, 0, 0, time.UTC)) entry, err := writer.CreateHeader(header) if err != nil { return nil, err } if _, err := entry.Write(file.payload); err != nil { return nil, err } } if err := writer.Close(); err != nil { return nil, err } return buffer.Bytes(), nil } func tarGzipDistributionFiles(outputName string, binary []byte, configPayload []byte) ([]byte, error) { var buffer bytes.Buffer gzipWriter := gzip.NewWriter(&buffer) gzipWriter.Header.ModTime = time.Unix(0, 0).UTC() tarWriter := tar.NewWriter(gzipWriter) files := []struct { name string mode int64 payload []byte }{{outputName, 0o755, binary}, {"config.yaml", 0o600, configPayload}} for _, file := range files { header := &tar.Header{Name: file.name, Mode: file.mode, Size: int64(len(file.payload)), ModTime: time.Unix(0, 0).UTC()} if err := tarWriter.WriteHeader(header); err != nil { return nil, err } if _, err := tarWriter.Write(file.payload); err != nil { return nil, err } } if err := tarWriter.Close(); err != nil { return nil, err } if err := gzipWriter.Close(); err != nil { return nil, err } return buffer.Bytes(), nil } // 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.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 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) string { if err == nil { return "platform builder failed" } message := strings.TrimSpace(err.Error()) if message == "" { return "platform builder failed" } if len(message) > 400 { message = message[:400] } return message }