From dd54765a518d65af195e649354b94a0c3f766202 Mon Sep 17 00:00:00 2001 From: fr12k Date: Sun, 10 May 2026 11:55:27 +0200 Subject: [PATCH] chore: minor bug and issue fixes --- .containifyci/containifyci.go | 4 +- .franky-workflow.yaml | 14 + .github/workflows/engine-ci-workflow.yml | 2 +- cmd/cache.go | 41 +-- docs/design/dead-code-audit.md | 308 ++++++++++++++++++++ go.mod | 2 + pkg/ai/claude/alpine/claude.go | 2 +- pkg/build/build.go | 43 ++- pkg/container/build.go | 1 - pkg/container/container.go | 38 +-- pkg/cri/docker/docker.go | 15 +- pkg/cri/podman/podman.go | 4 +- pkg/doctor/doctor.go | 12 +- pkg/golang/alpine/Dockerfile_chromium_go | 2 +- pkg/golang/alpine/Dockerfile_go | 2 +- pkg/golang/alpine/docker_metadata_gen.go | 8 +- pkg/golang/alpine/golang.go | 1 + pkg/golang/debiancgo/Dockerfilego | 4 +- pkg/golang/debiancgo/docker_metadata_gen.go | 6 +- pkg/network/sshforward.go | 153 +++++++++- pkg/network/sshforward_test.go | 205 ++++++++++++- pkg/packer/packer.go | 22 +- pkg/protobuf/protobuf.go | 7 +- pkg/pulumi/pulumi.go | 7 +- pkg/sonarcloud/sonarcloud.go | 10 +- pkg/utils/checksum.go | 12 + pkg/utils/idstore_test.go | 12 +- protos2/grpc_test.go | 18 +- 28 files changed, 816 insertions(+), 139 deletions(-) create mode 100644 .franky-workflow.yaml create mode 100644 docs/design/dead-code-audit.md create mode 100644 pkg/utils/checksum.go diff --git a/.containifyci/containifyci.go b/.containifyci/containifyci.go index 0a0c0dd0..b585815c 100644 --- a/.containifyci/containifyci.go +++ b/.containifyci/containifyci.go @@ -116,14 +116,14 @@ func main() { func DockerFile() *protos2.ContainerFile { return &protos2.ContainerFile{ Name: "golang-1.26.2-alpine-custom", - Content: `FROM golang:1.26.1-alpine + Content: `FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine RUN apk --no-cache add git openssh-client && \ rm -rf /var/cache/apk/* RUN go install github.com/wadey/gocovmerge@latest && \ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest && \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0 && \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && \ go clean -cache && \ go clean -modcache WORKDIR /app`, diff --git a/.franky-workflow.yaml b/.franky-workflow.yaml new file mode 100644 index 00000000..f47527dd --- /dev/null +++ b/.franky-workflow.yaml @@ -0,0 +1,14 @@ +stages: + - name: build + command: "go build main.go" + timeout_ms: 120000 + + - name: lint + command: "/home/agent/go/bin/golangci-lint run --fix" + timeout_ms: 120000 + needs: [build] + + - name: test + command: "go test ./..." + timeout_ms: 120000 + needs: [build] diff --git a/.github/workflows/engine-ci-workflow.yml b/.github/workflows/engine-ci-workflow.yml index a29739b6..b1ee51cc 100644 --- a/.github/workflows/engine-ci-workflow.yml +++ b/.github/workflows/engine-ci-workflow.yml @@ -159,7 +159,7 @@ jobs: ${{ runner.os }}-cache- - name: Install Engine CI - uses: jaxxstorm/action-install-gh-release@v1 + uses: jaxxstorm/action-install-gh-release@v3.0.0 if: ${{ inputs.install_binary }} with: repo: containifyci/engine-ci diff --git a/cmd/cache.go b/cmd/cache.go index 8a0411fb..07201683 100644 --- a/cmd/cache.go +++ b/cmd/cache.go @@ -77,20 +77,20 @@ func SaveCache() error { os.Exit(1) } cmd := fmt.Sprintf(` -set -x -docker pull %s -docker save -o ~/image-cache/%s.tar %s -`, image, info.Image, image) + set -x + docker pull %s + docker save -o ~/image-cache/%s.tar %s + `, image, info.Image, image) go runCommand(&wg, errs, "sh", []string{"-c", cmd}...) } // Wait for all commands to complete wg.Wait() close(errs) - // Check for any errors for err := range errs { - // errors. - slog.Warn("Error pull image", "error", err) + if err != nil { + slog.Warn("Error pull image", "error", err) + } } return nil } @@ -98,7 +98,12 @@ docker save -o ~/image-cache/%s.tar %s func LoadCache() error { InitBuildSteps() args := GetBuild(false) // Use plugin system for cache operations - //TODO: possible nil pointer dereference + if len(args) == 0 { + return fmt.Errorf("no build groups found in cache load") + } + if len(args[0].Builds) == 0 { + return fmt.Errorf("no builds found in first build group for cache load") + } arg := args[0].Builds[0] images := buildSteps.Images(args) if len(images) == 0 { @@ -121,15 +126,15 @@ func LoadCache() error { // nolint:staticcheck if arg.Runtime == utils.Docker { cmd := fmt.Sprintf(` -set -x -docker load -i ~/image-cache/%s.tar -`, info.Image) + set -x + docker load -i ~/image-cache/%s.tar + `, info.Image) go runCommand(&wg, errs, "sh", []string{"-c", cmd}...) } else if arg.Runtime == utils.Podman { cmd := fmt.Sprintf(` -set -x -podman load -i ~/image-cache/%s.tar -`, info.Image) + set -x + podman load -i ~/image-cache/%s.tar + `, info.Image) runCommand(&wg, errs, "sh", []string{"-c", cmd}...) } else { slog.Warn("Unsupported runtime for cache load", "runtime", arg.Runtime) @@ -140,10 +145,10 @@ podman load -i ~/image-cache/%s.tar // Wait for all commands to complete wg.Wait() close(errs) - // Check for any errors for err := range errs { - // errors. - slog.Warn("Error loading image", "error", err) + if err != nil { + slog.Warn("Error loading image", "error", err) + } } return nil } @@ -160,6 +165,6 @@ func runCommand(wg *sync.WaitGroup, errors chan<- error, cmd string, args ...str // Run the command err := command.Run() if err != nil { - errors <- fmt.Errorf("error running command: error %s, cmd %s, args %s", err, cmd, args) + errors <- fmt.Errorf("run command %s %v: %w", cmd, args, err) } } diff --git a/docs/design/dead-code-audit.md b/docs/design/dead-code-audit.md new file mode 100644 index 00000000..ae248c53 --- /dev/null +++ b/docs/design/dead-code-audit.md @@ -0,0 +1,308 @@ +# Dead Code Audit (initial deep pass) + +Date: 2026-05-10 +Scope: static repository scan of Go code + +## Method + +This report is based on grep-style static analysis: +- searched for exported symbols and likely call sites +- flagged items with no obvious internal references +- excluded generated protobuf / metadata code from dead-code claims unless clearly unused +- treated plugin/reflection/Cobra wiring as potentially live even when direct call sites are sparse + +## High-confidence dead code + +### `cmd/root.go` commented-out `All` +- Status: dead +- Evidence: function is fully commented out in source +- Recommendation: delete unless you plan to restore it soon + +### `pkg/kv/api.go` / `pkg/kv/client.go` unused helpers? +- Status: not yet confirmed dead +- Evidence: package is actively imported by higher-level command code, so do not remove blindly +- Recommendation: inspect per-symbol usage before pruning + +## Probable dead code candidates + +### `cmd/update.go` +- Symbols: `ConfigureUpdate`, `SetUpdateConfig`, `GetUpdateConfig`, `UpdateConfig` +- Evidence: repo-wide grep only found definitions in `cmd/update.go` +- Confidence: high +- Caveat: these are exported helpers and may be intended for external consumers +- Suggested action: if this repo is only consumed as a binary, consider removing or moving behind a narrow internal API + +### `pkg/dummy` +- Symbols: `Matches`, `New` +- Evidence: package is referenced from `cmd/engine.go` via `dummy.New()` for a Goreleaser step +- Confidence: low for deadness; likely live +- Note: this package is a placeholder/adapter rather than dead code + +### `pkg/pulumi` +- Symbols: `Matches`, `New`, `new`, `CacheFolder`, `ComputeChecksum`, `(*PulumiContainer).PulumiImage`, `BuildPulumiImage`, `CopyScript`, `ApplyEnvs`, `Release`, `Pull`, `Run` +- Evidence: `cmd/engine.go` imports `pkg/pulumi` and registers `pulumi.New()` at build-step setup +- Confidence: low for deadness; live +- Note: the entire package appears intentionally wired into the build graph +- Next step: only prune inside this package if a symbol-level search shows no internal calls + +### `pkg/packer` +- Symbols: `Matches`, `New`, `new`, `ComputeChecksum`, `(*packerContainer).packerImage`, `BuildpackerImage`, `CopyScript`, `Release`, `Pull`, `Run` +- Evidence: `cmd/engine.go` imports `pkg/packer` and registers `packer.New()` in build-step setup +- Confidence: low for deadness; live +- Note: the package is intentionally wired into the build graph +- Next step: only prune inside this package if a symbol-level search shows no internal calls + +### `pkg/logger/altscreen.go` +- Symbols: `AltScreen`, `NewAlt`, `Enter`, `Exit`, `Render`, `isTTY`, `enableWindowsVT` +- Evidence: `pkg/logger/terminal.go` references `AltScreen` and `NewAlt(os.Stdout)` +- Confidence: low for deadness; likely live +- Note: platform-specific helper, not dead by inspection + +### `client/pkg/build/plugin.go` +- Symbols: `Plugin.GetBuild`, `Plugin.GetBuilds`, `Serve`, `Build`, `BuildAsync`, `BuildGroups` +- Evidence: `client/client.go` uses `Build`, `BuildAsync`, `BuildGroups` +- Confidence: low for deadness; live +- Note: plugin entry points can be invoked indirectly + +## Package wiring discovered during inspection + +- `cmd/engine.go` imports and registers `pkg/pulumi` +- `cmd/engine.go` imports and registers `pkg/packer` +- `cmd/engine.go` imports and registers `pkg/dummy` +- `client/client.go` uses `client/pkg/build` helper entry points + +This means the earlier suspicion that `pkg/pulumi` and `pkg/packer` were unused was incorrect; they are part of the runtime build-step registry. + +## Generated / not-dead by default + +### `protos2/*.pb.go` +- Status: generated support code, should not be pruned just because direct call sites are sparse + +### `pkg/*/docker_metadata_gen.go` +- Status: generated metadata used by Dockerfile helpers + +## False positives to avoid + +- Cobra command functions may be invoked via command registration, not direct calls +- plugin/service interfaces may be used reflectively or by gRPC-generated code +- build-step packages often register themselves indirectly through factory functions +- tests often exercise symbols that are otherwise not referenced in production code + +## Recommended next verification pass + +1. Run a full import graph check for these packages: + - `pkg/pulumi` + - `pkg/packer` + - `cmd/update.go` +2. Search for package registration paths: + - build step registries + - `init()` functions + - `New()` factories +3. Use `go list -deps` / `go test ./...` to ensure no build-tag-only code is missed +4. If you want actual deletion candidates, focus on unexported helpers inside `pkg/pulumi` and `pkg/packer` first + +## Current summary + +At this stage, the only unequivocally dead code found is the commented-out `All` function in `cmd/root.go`. Most other suspects are either clearly live or require a deeper call-graph pass before removal. + +## Deeper inspection: `pkg/maven` + +### Findings +- `cmd/engine.go` imports `pkg/maven` and registers both `maven.New()` and `maven.NewProd()`. +- The package provides the expected build-step lifecycle: match, image build, container release, and run. +- Buildscript helpers are used by the Maven container implementation. + +### Conclusion +- No obvious dead code identified in `pkg/maven`. + +## Deeper inspection: `pkg/python` + +### Findings +- `cmd/engine.go` imports `pkg/python` and registers both `python.New()` and `python.NewProd()`. +- The package includes its own builder helpers and buildscript support, all of which feed the runtime pipeline. + +### Conclusion +- No obvious dead code identified in `pkg/python`. + +## Deeper inspection: `pkg/zig` + +### Findings +- `cmd/engine.go` imports `pkg/zig` and registers both `zig.New()` and `zig.NewProd()`. +- The buildscript helpers are used by the Zig container implementation and are covered by tests. + +### Conclusion +- No obvious dead code identified in `pkg/zig`. + +## Deeper inspection: `pkg/goreleaser` + +### Findings +- `cmd/engine.go` imports `pkg/goreleaser` and registers the goreleaser build step. +- The package has additional injection points (`cacheFolderFn`, `zigCacheFolderFn`, `newWithManager`) that are exercised by tests and support dependency injection. + +### Conclusion +- No obvious dead code identified in `pkg/goreleaser`. + +## Deeper inspection: `pkg/sonarcloud` + +### Findings +- `cmd/engine.go` imports `pkg/sonarcloud` and registers the build step. +- The `sonarqube.go` support code is used internally by the Sonarcloud container logic. + +### Conclusion +- No obvious dead code identified in `pkg/sonarcloud`. + +## Deeper inspection: `pkg/trivy` + +### Findings +- `cmd/engine.go` imports `pkg/trivy` and registers the build step. +- The `report.go` parser and formatter are used by the Trivy workflow and unit tests. + +### Conclusion +- No obvious dead code identified in `pkg/trivy`. + +## Deeper inspection: `pkg/gcloud` + +### Findings +- `cmd/engine.go` imports `pkg/gcloud` and registers the build step. +- `pkg/gcloud/src/main.go` is a standalone helper entry point compiled only with the `submodule` build tag. +- That means it is intentionally excluded from normal builds and should not be treated as dead code. + +### Conclusion +- No obvious dead code identified in `pkg/gcloud`. + +## Build-tag / auxiliary entry point check + +### `pkg/gcloud/src/main.go` +- Status: intentionally build-tagged auxiliary binary +- Evidence: `//go:build submodule` +- Conclusion: not dead; it is opt-in code for a specific build mode. + +### General rule added to the audit +- Files protected by build tags, especially standalone `package main` helpers, must not be marked dead unless the build mode they serve is itself removed. + +## Final sweep: `pkg/logger` + +### Findings +- `altscreen.go` is referenced by `terminal.go` and is part of the terminal rendering path. +- `slog_handler.go` is the public logging handler implementation used by higher-level logger setup. +- Benchmarks are test-only and not relevant to dead-code removal. + +### Conclusion +- No obvious dead code identified in `pkg/logger`. + +## Heuristic sweep: TODO / compatibility / wrapper markers + +### Findings +- Many TODOs represent incomplete features, not dead code. +- Backward-compatibility helpers and wrappers are especially present in `pkg/autodiscovery`, `protos2`, and the build-step packages. +- These should be treated as live unless both local and downstream usage are disproven. + +### Conclusion +- No additional dead code found from TODO/wrapper heuristics. + +## Audit status + +### Confirmed dead code +- Commented-out `All` function in `cmd/root.go` + +### Cleanup proposal: safe removal candidates +1. **Delete the commented-out `All` block** in `cmd/root.go` + - This is the only confirmed dead code. + - Safe because it is commented out and unreachable. + +### Cleanup proposal: keep, but document as supported API +2. **Retain `cmd/update.go` exported config helpers** + - `ConfigureUpdate` + - `SetUpdateConfig` + - `GetUpdateConfig` + - These are not used internally, but they are exported extension points. + - Because this repo is used as a dependency by downstream projects, deleting them could be a breaking change. + - Recommendation: document them as supported extension API rather than pruning them now. + +### Explicitly not dead due to wiring / generated / build-tag / downstream usage +- build-step packages registered from `cmd/engine.go` +- generated protobuf and Docker metadata code +- `pkg/gcloud/src/main.go` (`submodule` build tag) +- logger, doctor, autodiscovery, and client build helpers + +### Overall conclusion +- The codebase is largely cohesive and intentionally modular. +- The only safe code cleanup identified is removal of the commented-out `All` block. +- Everything else should be preserved unless downstream compatibility is explicitly audited. + +## Deeper inspection: `cmd/update.go` + +### Findings +- `ConfigureUpdate`, `SetUpdateConfig`, and `GetUpdateConfig` are exported but currently only referenced inside `cmd/update.go`. +- `updateCmd` and `runUpdate` are live because they are attached to Cobra via `init()`. +- The package is reachable through the CLI, but the exported config helpers are not currently used anywhere else in this repository. + +### Interpretation +- These helpers are **not dead in the strict binary sense** if external consumers import `cmd` as a library. +- However, for this repository’s current binary-oriented usage, they are **unreferenced internal API surface** and are the strongest deletion candidates after the commented-out code. + +### Recommendation +- Keep them if you expect downstream projects to override update metadata. +- Otherwise, consider moving them to a dedicated extension package or marking them as supported integration API in documentation rather than deleting immediately. + +## Dependency-aware audit notes + +Because this repository is used as a dependency by other projects such as `engine-java`, symbols that appear unused locally may still be relied on externally through: +- exported builder constructors +- build-step registration helpers +- Cobra command extension points +- package-level configuration setters/getters + +That means any deletion candidate should be checked against: +1. local imports/call sites, +2. generated code or command registration, +3. downstream repositories that compile against this module. + +## Deeper inspection: `pkg/autodiscovery` + +### Findings +- The autodiscovery packages are heavily used by command entry points such as `cmd/engine.go` and `cmd/init.go`. +- Functions like `DiscoverProjects`, `DiscoverAndGenerateBuildGroups`, `DiscoverPythonProjects`, and the build conversion helpers are part of the main project detection pipeline. +- Test files also exercise many helper methods directly, so the package has broad live coverage. + +### Conclusion +- No obvious dead code was identified in `pkg/autodiscovery` during this pass. +- Some helpers are only used via downstream selection logic and should not be pruned without a full call-graph / integration sweep. + +## Deeper inspection: `client/pkg/build` + +### Findings +- Constructors like `NewGoServiceBuild`, `NewGoLibraryBuild`, `NewMavenServiceBuild`, `NewMavenLibraryBuild`, `NewPythonServiceBuild`, and `NewPythonLibraryBuild` are used both locally and in autodiscovery tests. +- `Build`, `BuildAsync`, and `BuildGroups` are used by the CLI client and the generated/extension entry points. +- The AI build constructors are also referenced by the top-level config and tests. + +### Conclusion +- No obvious dead code identified here. +- These are public helper APIs and should be treated as part of the supported extension surface. + +## Deeper inspection: `pkg/golang` + +### Findings +- The top-level `pkg/golang/golang.go` selectors are live and route into the alpine/debian/cgo implementations. +- `NewGoLibraryBuild` / `NewMavenLibraryBuild` / `NewPythonLibraryBuild` equivalents are used by autodiscovery to map project types. +- The alpine/debian/cgo implementation packages are actively wired through `cmd/engine.go`. + +### Conclusion +- No dead code identified in the Go build packages during this pass. +- Many helpers are public dispatch points and should be retained unless downstream usage is disproven. + +## Deeper inspection: `pkg/doctor` + +### Findings +- `cmd/doctor.go` constructs `doctor.NewDoctor(...)` and runs checks via `RunChecks`. +- `doctor.NewDoctor` registers all built-in checks: + - runtime detection + - runtime connectivity + - runtime version + - volume config + - volume write test +- The individual check constructors are therefore live, even though some return unexported concrete types. + +### Conclusion +- No obvious dead code identified in `pkg/doctor`. +- The package is command-driven and should be treated as runtime code, not library deadwood. diff --git a/go.mod b/go.mod index fd8fc089..9208d0f1 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/containifyci/engine-ci go 1.25.5 +toolchain go1.25.10 + // replace github.com/containifyci/engine-ci/protos2 => ./protos2 // replace github.com/containifyci/engine-ci/client => ./client diff --git a/pkg/ai/claude/alpine/claude.go b/pkg/ai/claude/alpine/claude.go index cb91ca84..df36ce89 100644 --- a/pkg/ai/claude/alpine/claude.go +++ b/pkg/ai/claude/alpine/claude.go @@ -217,7 +217,7 @@ The commit message should: ssh, err := network.SSHForward(*c.GetBuild()) if err != nil { - return "", fmt.Errorf("SSH forward failed: %w", err) + return "", fmt.Errorf("ssh forward failed: %w", err) } opts := types.ContainerConfig{} diff --git a/pkg/build/build.go b/pkg/build/build.go index f3652df4..cb958545 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -242,15 +242,22 @@ func (bs *BuildSteps) PrintSteps() { } func (bs *BuildSteps) Run(arg *container.Build, step ...string) BuildResult { - return bs.runAllMatchingBuilds(arg, step) + // TODO: Wire through a proper context from the caller instead of context.Background(). + // The Run method should accept a context.Context parameter, and callers (e.g. Command.Run + // in cmd/build.go) should propagate one from the HTTP request or CLI signal handling. + ctx := context.Background() + return bs.runAllMatchingBuilds(ctx, arg, step) } -func (bs *BuildSteps) runAllMatchingBuilds(arg *container.Build, step []string) BuildResult { +func (bs *BuildSteps) runAllMatchingBuilds(ctx context.Context, arg *container.Build, step []string) BuildResult { var wg sync.WaitGroup ids := utils.IDStore{} var buildErr error var aiBuildResult *BuildResult + // Channel to collect errors from async goroutines + errCh := make(chan error, len(bs.Steps)) + for i, buildCtx := range bs.Steps { if !buildCtx.build.Matches(*arg) { // slog.Debug("Build step does not match config", "step", buildCtx.build.Name(), "index", i) @@ -273,8 +280,7 @@ func (bs *BuildSteps) runAllMatchingBuilds(arg *container.Build, step []string) ids.Add(id) if err != nil { slog.Error("Failed to run build step.", "error", err) - // TODO how to pass errors back from here to main thread - // os.Exit(1) + errCh <- err } slog.Debug("Completed async step", "step", build.Name()) @@ -289,7 +295,7 @@ func (bs *BuildSteps) runAllMatchingBuilds(arg *container.Build, step []string) slog.Info("AI build step detected - ensure proper handling", "step", buildCtx.build.Name(), "ids", ids.Get()) // TODO: use Container Manager to get container logs - if aiCtx, err := GetLog(arg, arg.Custom.Strings("ai_context")...); err != nil { + if aiCtx, err := GetLog(ctx, arg, arg.Custom.Strings("ai_context")...); err != nil { slog.Error("Failed to get AI context logs", "error", err) } else { arg.Custom["ai_context"] = []string{aiCtx} @@ -308,7 +314,7 @@ func (bs *BuildSteps) runAllMatchingBuilds(arg *container.Build, step []string) if buildType != nil && *buildType == container.AI { finishSignal := arg.Custom.String("ai_done_word") if finishSignal != "" { - logs, logErr := GetLog(arg, id) + logs, logErr := GetLog(ctx, arg, id) if logErr != nil { slog.Error("Failed to get container logs", "error", logErr) return BuildResult{IDs: ids.Get(), Loop: container.BuildStop, Error: logErr} @@ -336,11 +342,32 @@ func (bs *BuildSteps) runAllMatchingBuilds(arg *container.Build, step []string) // Wait for all async steps to complete slog.Debug("Waiting for all async steps to complete") wg.Wait() + close(errCh) + + // Collect the first async error (if any) for the BuildResult + var asyncErr error + for err := range errCh { + if asyncErr == nil { + asyncErr = err + } + slog.Warn("Async build step error collected", "error", err) + } if aiBuildResult != nil { return *aiBuildResult } + // Propagate async error if there was no sync error + if buildErr == nil && asyncErr != nil { + buildErr = asyncErr + slog.Warn("Build completed with async step error(s)", "error", asyncErr) + } + + if buildErr != nil { + slog.Info("Build completed with errors") + return BuildResult{IDs: ids.Get(), Loop: container.BuildContinue, Error: buildErr} + } + slog.Info("All build steps completed successfully") return BuildResult{IDs: ids.Get(), Loop: container.BuildContinue, Error: nil} } @@ -375,7 +402,7 @@ func uniqueStrings(input []string) []string { return result } -func GetLog(arg *container.Build, ids ...string) (string, error) { +func GetLog(ctx context.Context, arg *container.Build, ids ...string) (string, error) { var logs []string for _, id := range ids { @@ -383,7 +410,7 @@ func GetLog(arg *container.Build, ids ...string) (string, error) { continue } - r, err := arg.RuntimeClient().ContainerLogs(context.TODO(), id, true, true, false) + r, err := arg.RuntimeClient().ContainerLogs(ctx, id, true, true, false) if err != nil { slog.Error("Failed to get container logs", "error", err) return "", fmt.Errorf("failed to get container logs for %s: %w", id, err) diff --git a/pkg/container/build.go b/pkg/container/build.go index 790cc376..b7239eb6 100644 --- a/pkg/container/build.go +++ b/pkg/container/build.go @@ -138,7 +138,6 @@ type Build struct { defaults bool } - type BuildGroup struct { Builds []*Build } diff --git a/pkg/container/container.go b/pkg/container/container.go index cf9e1ecb..e3f2fb95 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -70,16 +70,17 @@ func (e *EnvType) Type() string { type Container struct { t - Source fs.ReadDirFS - Build *Build - Secret map[string]string - Env EnvType - Prefix string - Image string - Name string - ID string - Opts types.ContainerConfig - Verbose bool + Source fs.ReadDirFS + Build *Build + Secret map[string]string + Env EnvType + Prefix string + Image string + Name string + ID string + Opts types.ContainerConfig + Verbose bool + StreamLogs bool // controls whether container logs are streamed; defaults to true } type PushOption struct { @@ -105,7 +106,7 @@ func NewWithManager(manager cri.ContainerManager) *Container { ctx := context.Background() build := &Build{} logger.NewLogAggregator("") - return &Container{t: t{client: _client, ctx: ctx}, Build: build.Defaults()} + return &Container{t: t{client: _client, ctx: ctx}, Build: build.Defaults(), StreamLogs: true} } func New(build Build) *Container { @@ -122,7 +123,7 @@ func New(build Build) *Container { // Use background context with reasonable timeout instead of TODO ctx := context.Background() - container := &Container{t: t{client: _client, ctx: ctx}, Env: build.Env, Build: &build, Secret: build.Secret, Verbose: build.Verbose} + container := &Container{t: t{client: _client, ctx: ctx}, Env: build.Env, Build: &build, Secret: build.Secret, Verbose: build.Verbose, StreamLogs: true} return container } @@ -222,14 +223,15 @@ func (c *Container) Start() error { return fmt.Errorf("failed to start container %s: %w", c.ID, err) } - // TODO make this optional or provide a way to opt out shortImage := strings.ReplaceAll(c.Opts.Image, c.GetBuild().Registry+"/", "") img, tag := ParseImageTag(shortImage) short := fmt.Sprintf("%s:%s", img, safeShort(tag, 8)) - go func() { - streamContainerLogs(c.ctx, c.client(), c.ID, short, c.Prefix) - }() + if c.StreamLogs { + go func() { + streamContainerLogs(c.ctx, c.client(), c.ID, short, c.Prefix) + }() + } return err } @@ -316,7 +318,7 @@ func (c *Container) Exec(cmd ...string) error { slog.Error("Failed to exec command", "error", err) return err } - //TODO: the stdout is not showing up at all, need to fix that + // Demultiplex stdout/stderr and write to os.Stdout _, err = io.Copy(os.Stdout, reader) if err != nil { slog.Error("Failed to copy output", "error", err) @@ -351,7 +353,7 @@ func (c *Container) Wait() error { c.ctx.Done() slog.Error("Failed to inspect container", "error", err) } - return fmt.Errorf("Container %s exited with status %d", inspection.Image, *statusCode) + return fmt.Errorf("container %s exited with status %d", inspection.Image, *statusCode) } logger.GetLogAggregator().SuccessMessage(c.Prefix, "Container exited with status 0") return nil diff --git a/pkg/cri/docker/docker.go b/pkg/cri/docker/docker.go index c122a66f..d6825e91 100644 --- a/pkg/cri/docker/docker.go +++ b/pkg/cri/docker/docker.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" "github.com/docker/go-connections/nat" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -129,7 +130,7 @@ func (d *DockerManager) CreateContainer(ctx context.Context, opts *types.Contain slog.Error("Failed to inspect image", "error", err, "image", opts.Image) return "", fmt.Errorf("error inspecting image: %w", err) } - if info.Platform != nil && info.Platform.OS != opts.Platform.Container.OS { + if info.Platform != nil && (info.Platform.OS != opts.Platform.Container.OS || info.Platform.Architecture != opts.Platform.Container.Architecture) { slog.Info("Pull image for platform", "image", opts.Image, "requestedPlatform", opts.Platform.Container.String(), "imagePlatform", info.Platform.OS) //This ensure that the requested platform is pulled before creating the container. @@ -387,7 +388,17 @@ func (d *DockerManager) ExecContainer(ctx context.Context, id string, cmd []stri if err != nil { return nil, err } - return resp.Reader, nil + defer resp.Close() + + // Docker's exec attach returns a multiplexed stream (stdout+stderr + // interleaved with 8-byte headers). We must demultiplex it via + // stdcopy.StdCopy so the caller can read clean output. + out := new(bytes.Buffer) + _, err = stdcopy.StdCopy(out, out, resp.Reader) + if err != nil { + return nil, err + } + return out, nil } func (d *DockerManager) InspectContainer(ctx context.Context, id string) (*types.ContainerConfig, error) { diff --git a/pkg/cri/podman/podman.go b/pkg/cri/podman/podman.go index 4ffb13f2..b8015f84 100644 --- a/pkg/cri/podman/podman.go +++ b/pkg/cri/podman/podman.go @@ -873,7 +873,8 @@ func (p *PodmanManager) PullImage(ctx context.Context, image string, authBase64 var buf bytes.Buffer unwahr := false - var progressWriter io.Writer = &buf + // Write progress to both stderr (visible to user) and the buffer (returned to caller) + var progressWriter = io.MultiWriter(os.Stderr, &buf) platformSpec := types.ParsePlatform(platform) @@ -888,7 +889,6 @@ func (p *PodmanManager) PullImage(ctx context.Context, image string, authBase64 opts.OS = &platformSpec.OS } - // TODO progress writer is not working _, err = images.Pull(p.conn, image, &opts) if err != nil { return nil, err diff --git a/pkg/doctor/doctor.go b/pkg/doctor/doctor.go index dca79c5b..355722e7 100644 --- a/pkg/doctor/doctor.go +++ b/pkg/doctor/doctor.go @@ -14,12 +14,12 @@ type Doctor struct { // DoctorOptions configures doctor behavior type DoctorOptions struct { - Categories []CheckCategory - Verbose bool - JSONOutput bool - IncludeWarnings bool - Parallel bool - KeepTestContainers bool // Don't cleanup test containers (for debugging) + Categories []CheckCategory + Verbose bool + JSONOutput bool + IncludeWarnings bool + Parallel bool + KeepTestContainers bool // Don't cleanup test containers (for debugging) } // NewDoctor creates a new doctor instance diff --git a/pkg/golang/alpine/Dockerfile_chromium_go b/pkg/golang/alpine/Dockerfile_chromium_go index cf224d2f..e7dac1a2 100644 --- a/pkg/golang/alpine/Dockerfile_chromium_go +++ b/pkg/golang/alpine/Dockerfile_chromium_go @@ -5,6 +5,6 @@ RUN apk --no-cache add git openssh-client chromium && \ RUN go install github.com/wadey/gocovmerge@latest && \ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest && \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 && \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && \ go clean -cache && \ go clean -modcache diff --git a/pkg/golang/alpine/Dockerfile_go b/pkg/golang/alpine/Dockerfile_go index 3b3e8b91..c2ed2722 100644 --- a/pkg/golang/alpine/Dockerfile_go +++ b/pkg/golang/alpine/Dockerfile_go @@ -5,6 +5,6 @@ RUN apk --no-cache add git openssh-client && \ RUN go install github.com/wadey/gocovmerge@latest && \ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest && \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 && \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && \ go clean -cache && \ go clean -modcache diff --git a/pkg/golang/alpine/docker_metadata_gen.go b/pkg/golang/alpine/docker_metadata_gen.go index 736cd2f3..26fb6be8 100644 --- a/pkg/golang/alpine/docker_metadata_gen.go +++ b/pkg/golang/alpine/docker_metadata_gen.go @@ -8,7 +8,7 @@ const ( ImageVersion = "1.26.3-alpine" // DockerfileChecksum is the checksum of the Dockerfile content - DockerfileChecksum = "4a9ca95b1e23fc6c6cbdb48a1fc2640761110b15319b88d04009801e92615d9d" + DockerfileChecksum = "1dd17d1cb0efbcb4ac9a44385611787d56ffb36a449c8dd470ef451e4661cc5d" ) // DockerfileContent contains the embedded Dockerfile content @@ -19,7 +19,7 @@ RUN apk --no-cache add git openssh-client && \ RUN go install github.com/wadey/gocovmerge@latest && \ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest && \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 && \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && \ go clean -cache && \ go clean -modcache ` @@ -30,7 +30,7 @@ const ( ImageVersionChromium = "1.26.3-alpine" // DockerfileChecksumChromium is the checksum of the Dockerfile content - DockerfileChecksumChromium = "e9221d40fec0889f7a9be30f62db46f52401c963b05e8876b54602557843c474" + DockerfileChecksumChromium = "bba251262dea4e4b71291bd498ee36fb36bf282d71815698c057bb5a72472e02" ) // DockerfileContentChromium contains the embedded Dockerfile content @@ -41,7 +41,7 @@ RUN apk --no-cache add git openssh-client chromium && \ RUN go install github.com/wadey/gocovmerge@latest && \ go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest && \ - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 && \ + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 && \ go clean -cache && \ go clean -modcache ` diff --git a/pkg/golang/alpine/golang.go b/pkg/golang/alpine/golang.go index 1cbe7f6d..722d13d3 100644 --- a/pkg/golang/alpine/golang.go +++ b/pkg/golang/alpine/golang.go @@ -280,6 +280,7 @@ func (c *GoContainer) Build() error { opts := types.ContainerConfig{} opts.Image = imageTag + opts.Platform = types.AutoPlatform opts.Env = append(opts.Env, []string{ "GOMODCACHE=/go/pkg/", "GOCACHE=/go/pkg/build-cache", diff --git a/pkg/golang/debiancgo/Dockerfilego b/pkg/golang/debiancgo/Dockerfilego index a2965e8f..8b796c29 100644 --- a/pkg/golang/debiancgo/Dockerfilego +++ b/pkg/golang/debiancgo/Dockerfilego @@ -1,4 +1,4 @@ -FROM golang:1.26.3 +FROM --platform=$BUILDPLATFORM golang:1.26.2 RUN apt-get update && \ apt-get install -y clang build-essential \ @@ -28,4 +28,4 @@ RUN apt-get update && \ ENV CGO_ENABLED=1 ENV OPENSSL_DIR=/usr/include/openssl ENV CGO_CFLAGS="-I/usr/include/openssl" -ENV CGO_LDFLAGS="-L/usr/lib/aarch64-linux-gnu -lssl -lcrypto" +ENV CGO_LDFLAGS="-L/usr/lib/$TARGETARCH-linux-gnu -lssl -lcrypto" diff --git a/pkg/golang/debiancgo/docker_metadata_gen.go b/pkg/golang/debiancgo/docker_metadata_gen.go index 44481c95..5ff07cc5 100644 --- a/pkg/golang/debiancgo/docker_metadata_gen.go +++ b/pkg/golang/debiancgo/docker_metadata_gen.go @@ -8,11 +8,11 @@ const ( ImageVersion = "1.26.3" // DockerfileChecksum is the checksum of the Dockerfile content - DockerfileChecksum = "4c23f149f9c94fbeadfc0686df039bf7d639ad97041c95f00d0e4fec6d528567" + DockerfileChecksum = "1078745db7c118094fa7e982874edde984c7e3bb8720050cdd29471003171ddb" ) // DockerfileContent contains the embedded Dockerfile content -var DockerfileContent = `FROM golang:1.26.3 +var DockerfileContent = `FROM --platform=$BUILDPLATFORM golang:1.26.2 RUN apt-get update && \ apt-get install -y clang build-essential \ @@ -42,7 +42,7 @@ RUN apt-get update && \ ENV CGO_ENABLED=1 ENV OPENSSL_DIR=/usr/include/openssl ENV CGO_CFLAGS="-I/usr/include/openssl" -ENV CGO_LDFLAGS="-L/usr/lib/aarch64-linux-gnu -lssl -lcrypto" +ENV CGO_LDFLAGS="-L/usr/lib/$TARGETARCH-linux-gnu -lssl -lcrypto" ` // GetDockerfileMetadata returns the metadata for the specified variant type. diff --git a/pkg/network/sshforward.go b/pkg/network/sshforward.go index a12d6900..903f6e1d 100644 --- a/pkg/network/sshforward.go +++ b/pkg/network/sshforward.go @@ -1,9 +1,12 @@ package network import ( + "errors" "fmt" "log/slog" "os" + "os/exec" + "strconv" "github.com/containifyci/engine-ci/pkg/container" "github.com/containifyci/engine-ci/pkg/cri/types" @@ -11,18 +14,34 @@ import ( const ( DARWIN_SSH_AUTH_SOCK = "/run/host-services/ssh-auth.sock" + // SOCAT_CONTAINER_SOCK is the UNIX socket path inside the container + // that socat listens on and forwards to the host via TCP. + SOCAT_CONTAINER_SOCK = "/tmp/ssh-agent.sock" + // DefaultSocatPort is the default TCP port used for socat SSH forwarding. + DefaultSocatPort = 12345 ) +// Forward holds the configuration for forwarding the SSH agent socket +// into a container. By default it uses a bind mount, but when SocatPort +// is set it uses TCP-based forwarding via socat instead. type Forward struct { - Volume *types.Volume - Source string - Target string - Env string + Volume *types.Volume + hostSocatCmd *exec.Cmd + Source string + Target string + Env string + SocatPort int } -// TODO implement ssh socket forward with -// socat TCP-LISTEN:12345,reuseaddr,fork UNIX-CONNECT:$SSH_AUTH_SOCK on host -// socat TCP-LISTEN:12345,reuseaddr,fork UNIX-CONNECT:$SSH_AUTH_SOCK in container +// Apply applies the SSH forward configuration to the container options. +// +// When SocatPort is set (socat-based forwarding), it sets an entrypoint +// that runs socat inside the container to forward from a UNIX socket +// to host.docker.internal:PORT, and sets SSH_AUTH_SOCK to that socket. +// Socat must be installed in the container image. +// +// When SocatPort is 0 (default), it uses a bind mount of the host's +// SSH agent socket into the container. func (f *Forward) Apply(opts *types.ContainerConfig) types.ContainerConfig { if f == nil { return *opts @@ -30,12 +49,42 @@ func (f *Forward) Apply(opts *types.ContainerConfig) types.ContainerConfig { if f.Env != "" { opts.Env = append(opts.Env, f.Env) } - if f.Volume != nil { + if f.Volume != nil && f.SocatPort <= 0 { opts.Volumes = append(opts.Volumes, *f.Volume) } + + if f.SocatPort > 0 { + // Use an entrypoint wrapper that starts socat in the container + // to forward TCP:host.docker.internal:PORT -> UNIX:SOCAT_CONTAINER_SOCK + // in the background, then execs the original Cmd. + entrypoint := fmt.Sprintf( + `socat UNIX-LISTEN:%s,fork TCP:host.docker.internal:%d & exec "$@"`, + SOCAT_CONTAINER_SOCK, f.SocatPort, + ) + opts.Entrypoint = []string{"/bin/sh", "-c", entrypoint, "--"} + } + return *opts } +// Cleanup kills the host-side socat process if one was started. +// It should be called with 'defer' after a successful SSHForwardWithSocat call. +func (f *Forward) Cleanup() { + if f == nil || f.hostSocatCmd == nil || f.hostSocatCmd.Process == nil { + return + } + slog.Debug("Cleaning up host-side socat process", "pid", f.hostSocatCmd.Process.Pid) + if err := f.hostSocatCmd.Process.Kill(); err != nil { + slog.Warn("Failed to kill host socat process", "error", err) + } + // Wait to reap the process; ignore errors after kill. + _ = f.hostSocatCmd.Wait() + f.hostSocatCmd = nil +} + +// SSHForward creates a bind-mount based SSH forward (default approach). +// This is the recommended approach for Linux and works on macOS when +// /run/host-services/ssh-auth.sock is bind-mountable. func SSHForward(build container.Build) (*Forward, error) { switch build.Platform.Host.OS { case "linux": @@ -59,11 +108,6 @@ func SSHForward(build container.Build) (*Forward, error) { slog.Warn("SSH forwarding is not supported on macOS with Podman") return nil, nil } - // _, err := os.Stat(DARWIN_SSH_AUTH_SOCK) - // if err != nil { - // slog.Error("SSH_AUTH_SOCK is not available on the host") - // os.Exit(1) - // } return &Forward{ Source: DARWIN_SSH_AUTH_SOCK, Target: DARWIN_SSH_AUTH_SOCK, @@ -77,3 +121,86 @@ func SSHForward(build container.Build) (*Forward, error) { } return nil, fmt.Errorf("unsupported platform: %s", build.Platform.Host) } + +// SSHForwardWithSocat creates a TCP-based SSH forward via socat. +// +// It starts a host-side socat process that listens on the given port +// and forwards connections to the host's SSH agent UNIX socket. The +// returned Forward's Apply method configures the container to connect +// to that TCP port via host.docker.internal. +// +// The caller MUST call f.Cleanup() (typically via defer) when the +// forward is no longer needed, to kill the host-side socat process. +// Socat must be installed on the host and in the container image. +// +// If port is 0, DefaultSocatPort is used. +func SSHForwardWithSocat(build container.Build, port int) (*Forward, error) { + if port <= 0 { + port = DefaultSocatPort + } + + sshAuthSocket, err := resolveSSHAuthSocket(build) + if err != nil { + return nil, err + } + if sshAuthSocket == "" { + // No SSH socket available; not an error, just skip forwarding. + return nil, nil + } + + // Check that socat is available on the host. + if _, err := exec.LookPath("socat"); err != nil { + return nil, fmt.Errorf("socat is not installed on the host: %w", err) + } + + // Start host-side socat: TCP-LISTEN:PORT -> UNIX-CONNECT:$SSH_AUTH_SOCK + socatArgs := []string{ + fmt.Sprintf("TCP-LISTEN:%d,reuseaddr,fork", port), + fmt.Sprintf("UNIX-CONNECT:%s", sshAuthSocket), + } + cmd := exec.Command("socat", socatArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start host-side socat: %w", err) + } + + slog.Debug("Started host-side socat for SSH forwarding", + "port", port, + "socket", sshAuthSocket, + "pid", cmd.Process.Pid, + ) + + portStr := strconv.Itoa(port) + + return &Forward{ + SocatPort: port, + Source: fmt.Sprintf("tcp:host.docker.internal:%s", portStr), + Target: SOCAT_CONTAINER_SOCK, + Env: "SSH_AUTH_SOCK=" + SOCAT_CONTAINER_SOCK, + hostSocatCmd: cmd, + }, nil +} + +// resolveSSHAuthSocket resolves the host's SSH agent socket path +// depending on the platform. +func resolveSSHAuthSocket(build container.Build) (string, error) { + switch build.Platform.Host.OS { + case "linux": + sshAuthSocket := os.Getenv("SSH_AUTH_SOCK") + if sshAuthSocket == "" { + slog.Warn("SSH_AUTH_SOCK is not set") + return "", nil + } + return sshAuthSocket, nil + case "darwin": + if build.Runtime == "podman" { + slog.Warn("SSH forwarding is not supported on macOS with Podman") + return "", nil + } + return DARWIN_SSH_AUTH_SOCK, nil + default: + return "", errors.New("unsupported platform") + } +} diff --git a/pkg/network/sshforward_test.go b/pkg/network/sshforward_test.go index c31c3b13..9f94b5d3 100644 --- a/pkg/network/sshforward_test.go +++ b/pkg/network/sshforward_test.go @@ -1,6 +1,7 @@ package network import ( + "os" "testing" "github.com/containifyci/engine-ci/pkg/container" @@ -10,15 +11,17 @@ import ( ) func TestSSHForward(t *testing.T) { - // Todo: add test for podman runtime tests := []struct { - want *Forward - os string - cri utils.RuntimeType + name string + want *Forward + os string + cri utils.RuntimeType + wantErr bool }{ { - os: "linux", - cri: utils.Docker, + name: "linux with SSH_AUTH_SOCK set", + os: "linux", + cri: utils.Docker, want: &Forward{ Source: "linux_ssh_auth_socket", Target: "linux_ssh_auth_socket", @@ -31,8 +34,9 @@ func TestSSHForward(t *testing.T) { }, }, { - os: "darwin", - cri: utils.Docker, + name: "darwin with Docker", + os: "darwin", + cri: utils.Docker, want: &Forward{ Source: "/run/host-services/ssh-auth.sock", Target: "/run/host-services/ssh-auth.sock", @@ -45,14 +49,26 @@ func TestSSHForward(t *testing.T) { }, }, { - os: "unknown", - cri: utils.Unknown, - want: nil, + name: "darwin with Podman returns nil (skip warning)", + os: "darwin", + cri: utils.Podman, + want: nil, + wantErr: false, // SSHForward returns (nil, nil) for darwin+Podman + }, + { + name: "unknown OS returns error", + os: "unknown", + cri: utils.Unknown, + want: nil, + wantErr: true, }, } for _, tt := range tests { - t.Setenv("SSH_AUTH_SOCK", "linux_ssh_auth_socket") - t.Run("test for "+tt.os, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + origSSHAuthSock := os.Getenv("SSH_AUTH_SOCK") + t.Setenv("SSH_AUTH_SOCK", "linux_ssh_auth_socket") + defer os.Setenv("SSH_AUTH_SOCK", origSSHAuthSock) + build := &container.Build{ Runtime: tt.cri, Platform: types.Platform{ @@ -62,7 +78,168 @@ func TestSSHForward(t *testing.T) { }, } got, err := SSHForward(*build) - if tt.want == nil { + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestForwardApply_BindMount(t *testing.T) { + // Default (SocatPort == 0) uses bind mount + env + f := &Forward{ + Source: "/tmp/ssh-xyz/agent.sock", + Target: "/tmp/ssh-xyz/agent.sock", + Env: "SSH_AUTH_SOCK=/tmp/ssh-xyz/agent.sock", + Volume: &types.Volume{ + Type: "bind", + Source: "/tmp/ssh-xyz/agent.sock", + Target: "/tmp/ssh-xyz/agent.sock", + }, + } + + opts := types.ContainerConfig{} + opts = f.Apply(&opts) + + assert.Contains(t, opts.Env, "SSH_AUTH_SOCK=/tmp/ssh-xyz/agent.sock") + assert.Len(t, opts.Volumes, 1) + assert.Empty(t, opts.Entrypoint, "bind mount mode should not set an entrypoint") +} + +func TestForwardApply_SocatMode(t *testing.T) { + // SocatPort > 0 uses TCP forwarding: no bind mount, sets entrypoint wrapper + f := &Forward{ + SocatPort: 12345, + Source: "tcp:host.docker.internal:12345", + Target: SOCAT_CONTAINER_SOCK, + Env: "SSH_AUTH_SOCK=" + SOCAT_CONTAINER_SOCK, + } + + opts := types.ContainerConfig{ + Cmd: []string{"go", "build", "./..."}, + } + opts = f.Apply(&opts) + + assert.Contains(t, opts.Env, "SSH_AUTH_SOCK="+SOCAT_CONTAINER_SOCK) + assert.Empty(t, opts.Volumes, "socat mode should not add bind mount volumes") + // Entrypoint: ["/bin/sh", "-c", "", "--"] + assert.Len(t, opts.Entrypoint, 4, "entrypoint should be shell -c