diff --git a/internal/infra/proxy.go b/internal/infra/proxy.go index 122e072..f64a305 100644 --- a/internal/infra/proxy.go +++ b/internal/infra/proxy.go @@ -45,7 +45,6 @@ func NewProxy(ctx context.Context, cli *client.Client, params *RunParams, nets * } hostCfg := &container.HostConfig{ - AutoRemove: true, ExtraHosts: []string{ "host.docker.internal:host-gateway", }, @@ -169,13 +168,26 @@ func (p *Proxy) TailLogs(ctx context.Context, cli *client.Client) { _, _ = stdcopy.StdCopy(w, w, out) } -func (p *Proxy) Close() error { +func (p *Proxy) Close() (err error) { + defer func() { + removeErr := p.cli.ContainerRemove(context.Background(), p.containerID, types.ContainerRemoveOptions{Force: true}) + if removeErr != nil { + err = fmt.Errorf("failed to remove proxy container: %w", removeErr) + } + }() + + // Check the error code if the container has already exited, so we can pass it along to the caller. If the proxy + //crashes we want the CLI to error out. Unlike the Updater it should never stop on its own. + containerInfo, inspectErr := p.cli.ContainerInspect(context.Background(), p.containerID) + if inspectErr != nil { + return fmt.Errorf("failed to inspect proxy container: %w", inspectErr) + } + if containerInfo.State.ExitCode != 0 { + return fmt.Errorf("proxy container exited with non-zero exit code: %d", containerInfo.State.ExitCode) + } + timeout := 5 _ = p.cli.ContainerStop(context.Background(), p.containerID, container.StopOptions{Timeout: &timeout}) - err := p.cli.ContainerRemove(context.Background(), p.containerID, types.ContainerRemoveOptions{Force: true}) - if err != nil { - return fmt.Errorf("failed to remove proxy container: %w", err) - } - return nil + return err } diff --git a/internal/infra/proxy_test.go b/internal/infra/proxy_test.go deleted file mode 100644 index 138fd68..0000000 --- a/internal/infra/proxy_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package infra - -import ( - "archive/tar" - "bytes" - "context" - "errors" - "io" - "net/http" - "os" - "testing" - "time" - - "github.com/docker/docker/api/types" - "github.com/moby/moby/client" -) - -// This tests the Proxy's ability to use a custom cert for outbound calls. -// It creates a custom proxy image to test with, passes it a cert, and uses it to -// communicate with a test server using the certs. -func TestNewProxy_customCert(t *testing.T) { - ctx := context.Background() - - CertSubject.CommonName = "host.docker.internal" - ca, err := GenerateCertificateAuthority() - if err != nil { - t.Fatal(err) - } - - cert, err := os.CreateTemp(os.TempDir(), "cert.pem") - key, err2 := os.CreateTemp(os.TempDir(), "key.pem") - if err != nil || err2 != nil { - t.Fatal(err, err2) - } - _, _ = cert.WriteString(ca.Cert) - _, _ = key.WriteString(ca.Key) - _ = cert.Close() - _ = key.Close() - - successChan := make(chan struct{}) - addr := "127.0.0.1:8765" - if os.Getenv("CI") != "" { - t.Log("detected running in actions") - addr = "0.0.0.0:8765" - } - testServer := &http.Server{ - ReadHeaderTimeout: time.Second, - Addr: addr, - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("SUCCESS")) - successChan <- struct{}{} - }), - } - defer func() { - _ = testServer.Shutdown(ctx) - }() - go func() { - t.Log("Starting HTTPS server") - if err = testServer.ListenAndServeTLS(cert.Name(), key.Name()); err != nil && !errors.Is(err, http.ErrServerClosed) { - panic(err) - } - }() - - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - t.Fatal(err) - } - - // build the test image - var buildContext bytes.Buffer - tw := tar.NewWriter(&buildContext) - _ = addFileToArchive(tw, "/Dockerfile", 0644, proxyTestDockerfile) - _ = tw.Close() - - proxyImageName := "curl-test" - resp, err := cli.ImageBuild(ctx, &buildContext, types.ImageBuildOptions{Tags: []string{proxyImageName}}) - if err != nil { - t.Fatal(err) - } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - - defer func() { - _, _ = cli.ImageRemove(ctx, proxyImageName, types.ImageRemoveOptions{}) - }() - - proxy, err := NewProxy(ctx, cli, &RunParams{ - ProxyCertPath: cert.Name(), - ProxyImage: proxyImageName, - }, nil) - if err != nil { - panic(err) - } - defer func() { - _ = proxy.Close() - }() - - t.Log("Starting proxy") - - go proxy.TailLogs(ctx, cli) - - select { - case <-successChan: - t.Log("Success!") - case <-time.After(5 * time.Second): - t.Errorf("Not able to contact the test server") - } -} - -const proxyTestDockerfile = ` -FROM ghcr.io/github/dependabot-update-job-proxy/dependabot-update-job-proxy:latest -RUN apk add --no-cache curl -RUN echo "#!/bin/sh" > /update-job-proxy -RUN echo "CURLing host.docker.internal" >> /update-job-proxy -RUN echo "curl -s https://host.docker.internal:8765" >> /update-job-proxy -RUN chmod +x /update-job-proxy -` diff --git a/internal/infra/run.go b/internal/infra/run.go index 6095f14..30772cb 100644 --- a/internal/infra/run.go +++ b/internal/infra/run.go @@ -15,14 +15,13 @@ import ( "syscall" "time" + "github.com/dependabot/cli/internal/model" + "github.com/dependabot/cli/internal/server" + "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/archive" "github.com/hexops/gotextdiff" "github.com/hexops/gotextdiff/myers" "github.com/hexops/gotextdiff/span" - - "github.com/dependabot/cli/internal/model" - "github.com/dependabot/cli/internal/server" - "github.com/docker/docker/api/types" "github.com/moby/moby/api/types/registry" "github.com/moby/moby/client" "gopkg.in/yaml.v3" @@ -330,8 +329,9 @@ func generateIgnoreConditions(params *RunParams, actual *model.Scenario) error { return nil } -func runContainers(ctx context.Context, params RunParams) error { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) +func runContainers(ctx context.Context, params RunParams) (err error) { + var cli *client.Client + cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("failed to create Docker client: %w", err) } @@ -365,7 +365,11 @@ func runContainers(ctx context.Context, params RunParams) error { if err != nil { return err } - defer prox.Close() + defer func() { + if proxyErr := prox.Close(); proxyErr != nil { + err = proxyErr + } + }() // proxy logs interfere with debugging output if !params.Debug { diff --git a/testdata/scripts/proxy.txt b/testdata/scripts/proxy.txt new file mode 100644 index 0000000..731e8ba --- /dev/null +++ b/testdata/scripts/proxy.txt @@ -0,0 +1,58 @@ +# Tests related to running the Proxy + +exec docker build -qt proxy-updater . +exec docker build -qt dummy-proxy -f Dockerfile.proxy . + +dependabot update go_modules dependabot/cli --updater-image proxy-updater --proxy-image dummy-proxy +stderr 'proxy \| Proxy is running' +stderr 'updater \| Updater is running' +! stderr 'proxy \| custom-ca-cert\.crt' + +dependabot update go_modules dependabot/cli --proxy-cert my-cert --updater-image proxy-updater --proxy-image dummy-proxy +stderr 'proxy \| custom-ca-cert\.crt' +stderr 'proxy \| I am a certificate' + +# Test that the CLI exits with non-zero if the proxy does too. +! dependabot update go_modules dependabot/cli --proxy-cert crash --updater-image proxy-updater --proxy-image dummy-proxy --proxy-username user --proxy-password pass + +exec docker rmi -f proxy-updater dummy-proxy + +-- crash -- +crash + +-- my-cert -- +I am a certificate + +-- Dockerfile.proxy -- +FROM ubuntu:22.04 + +COPY --chmod=755 update-ca-certificates /usr/bin/update-ca-certificates +COPY --chmod=755 update-job-proxy /update-job-proxy + +-- update-job-proxy -- +#!/usr/bin/env bash + +echo "Proxy is running" +echo "$(