Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set an error exit code when the Updater exits with an error #214

Merged
merged 5 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Test
# -count=2 ensures that test fixtures cleanup after themselves
# because any leftover state will generally cause the second run to fail.
run: go test -shuffle=on -count=2 -race -cover -v -timeout=5m ./...
run: go test -shuffle=on -count=2 -race -cover -timeout=5m ./...

lint:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion cmd/dependabot/internal/cmd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func NewUpdateCommand() *cobra.Command {
if errors.Is(err, context.DeadlineExceeded) {
log.Fatalf("update timed out after %s", flags.timeout)
}
log.Fatalf("failed to run updater: %v", err)
log.Fatalf("updater failure: %v", err)
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion internal/infra/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (p *Proxy) Close() (err error) {
}()

// 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.
//crashes we want the CLI to error out.
containerInfo, inspectErr := p.cli.ContainerInspect(context.Background(), p.containerID)
if inspectErr != nil {
return fmt.Errorf("failed to inspect proxy container: %w", inspectErr)
Expand Down
10 changes: 9 additions & 1 deletion internal/infra/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,11 @@ func runContainers(ctx context.Context, params RunParams) (err error) {
if err != nil {
return err
}
defer updater.Close()
defer func() {
if updaterErr := updater.Close(); updaterErr != nil {
err = updaterErr
}
}()

// put the clone dir in the updater container to be used by during the update
if params.LocalDir != "" {
Expand All @@ -407,6 +411,10 @@ func runContainers(ctx context.Context, params RunParams) (err error) {
if err := updater.RunCmd(ctx, cmd, dependabot, userEnv(prox.url, params.ApiUrl)...); err != nil {
return err
}
// If the exit code is non-zero, error when using the `update` subcommand, but not the `test` subcommand.
if params.Expected == nil && *updater.ExitCode != 0 {
return fmt.Errorf("updater exited with code %d", *updater.ExitCode)
}
}

return nil
Expand Down
173 changes: 0 additions & 173 deletions internal/infra/run_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package infra

import (
"archive/tar"
"bytes"
"context"
"io"
"net/http"
"os"
"reflect"
Expand All @@ -13,11 +10,7 @@ import (

"github.com/dependabot/cli/internal/server"

"gopkg.in/yaml.v3"

"github.com/dependabot/cli/internal/model"
"github.com/docker/docker/api/types"
"github.com/moby/moby/client"
)

func Test_checkCredAccess(t *testing.T) {
Expand Down Expand Up @@ -166,169 +159,3 @@ func Test_generateIgnoreConditions(t *testing.T) {
}
})
}

func TestRun(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
t.Fatal(err)
}

ctx := context.Background()

var buildContext bytes.Buffer
tw := tar.NewWriter(&buildContext)
_ = addFileToArchive(tw, "/Dockerfile", 0644, dockerFile)
_ = addFileToArchive(tw, "/test_main.go", 0644, testMain)
_ = tw.Close()

UpdaterImageName := "test-updater"
resp, err := cli.ImageBuild(ctx, &buildContext, types.ImageBuildOptions{Tags: []string{UpdaterImageName}})
if err != nil {
t.Fatal(err)
}

_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()

defer func() {
_, _ = cli.ImageRemove(ctx, UpdaterImageName, types.ImageRemoveOptions{})
}()

cred := model.Credential{
"type": "git_source",
"host": "github.com",
"username": "x-access-token",
"password": "$LOCAL_GITHUB_ACCESS_TOKEN",
}

os.Setenv("LOCAL_GITHUB_ACCESS_TOKEN", "test-token")
err = Run(RunParams{
PullImages: true,
Job: &model.Job{
PackageManager: "ecosystem",
Source: model.Source{
Repo: "org/name",
},
},
Creds: []model.Credential{cred},
UpdaterImage: UpdaterImageName,
Output: "out.yaml",
})
if err != nil {
t.Error(err)
}

f, err := os.Open("out.yaml")
if err != nil {
t.Fatal(err)
}
defer f.Close()

var output model.Scenario
if err = yaml.NewDecoder(f).Decode(&output); err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(output.Input.Credentials, []model.Credential{cred}) {
t.Error("unexpected credentials", output.Input.Credentials)
}
if output.Input.Credentials[0]["password"] != "$LOCAL_GITHUB_ACCESS_TOKEN" {
t.Error("expected password to be masked")
}
}

const dockerFile = `
FROM golang:1.21

# needed to run update-ca-certificates
RUN apt-get update && apt-get install -y ca-certificates
# cli will try to start as dependabot
RUN useradd -ms /bin/bash dependabot

# need to be the user for permissions to work
USER dependabot
WORKDIR /home/dependabot
COPY *.go .
# cli runs bin/run (twice) to do an update, so put exe there
RUN go mod init cli_test && go mod tidy && go build -o bin/run
`

const testMain = `package main

import (
"bytes"
"context"
"encoding/xml"
"log"
"net"
"net/http"
"os"
"os/exec"
"strings"
"sync"
"time"
)

func main() {
if os.Args[1] == "update_files" {
return
}
var wg sync.WaitGroup

// print the line that fails
log.SetFlags(log.Lshortfile)

go connectivityCheck(&wg)
checkIfRoot()
proxyCheck()

wg.Wait()
}

func checkIfRoot() {
buf := &bytes.Buffer{}
cmd := exec.Command("id", "-u")
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
log.Fatalln(err)
}
userID := strings.TrimSpace(buf.String())
if userID == "0" {
log.Fatalln("User is root")
}
}

func connectivityCheck(wg *sync.WaitGroup) {
wg.Add(1)

var d net.Dialer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

_, err := d.DialContext(ctx, "tcp", "1.1.1.1:22")
if err != nil && err.Error() != "dial tcp 1.1.1.1:22: i/o timeout" {
log.Fatalln(err)
}
if err == nil {
log.Fatalln("a connection shouldn't be possible")
}

wg.Done()
}

func proxyCheck() {
res, err := http.Get("https://example.com")
if err != nil {
log.Fatalln(err)
}
defer res.Body.Close()
var v any
if err = xml.NewDecoder(res.Body).Decode(&v); err != nil {
log.Fatalln(err)
}
}
`
35 changes: 30 additions & 5 deletions internal/infra/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const (
type Updater struct {
cli *client.Client
containerID string

// ExitCode is set once an Updater command has completed.
ExitCode *int
}

const (
Expand Down Expand Up @@ -251,19 +254,27 @@ func (u *Updater) RunCmd(ctx context.Context, cmd, user string, env ...string) e
_, _ = io.Copy(os.Stderr, prefixer.New(r, "updater | "))
}()

// blocks until update is complete or ctl-c
ch := make(chan struct{})
go func() {
_, _ = stdcopy.StdCopy(w, w, execResp.Reader)
ch <- struct{}{}
}()

// blocks until update is complete or ctl-c
select {
case <-ctx.Done():
return ctx.Err()
case <-ch:
}

// check the exit code of the command
execInspect, err := u.cli.ContainerExecInspect(ctx, execCreate.ID)
if err != nil {
return fmt.Errorf("failed to inspect exec: %w", err)
}

u.ExitCode = &execInspect.ExitCode

return nil
}

Expand All @@ -282,10 +293,24 @@ func (u *Updater) Wait(ctx context.Context, condition container.WaitCondition) e
}

// Close kills and deletes the container and deletes updater mount paths related to the run.
func (u *Updater) Close() error {
return u.cli.ContainerRemove(context.Background(), u.containerID, types.ContainerRemoveOptions{
Force: true,
})
func (u *Updater) Close() (err error) {
defer func() {
removeErr := u.cli.ContainerRemove(context.Background(), u.containerID, types.ContainerRemoveOptions{Force: true})
if removeErr != nil {
err = fmt.Errorf("failed to remove proxy container: %w", removeErr)
}
}()

// Handle non-zero exit codes.
containerInfo, inspectErr := u.cli.ContainerInspect(context.Background(), u.containerID)
if inspectErr != nil {
return fmt.Errorf("failed to inspect proxy container: %w", inspectErr)
}
if containerInfo.State.ExitCode != 0 {
return fmt.Errorf("updater container exited with non-zero exit code: %d", containerInfo.State.ExitCode)
}

return
}

// JobFile is the payload passed to file updater containers.
Expand Down
37 changes: 37 additions & 0 deletions testdata/scripts/fail.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
exec docker build -t fail-updater .

! dependabot update go_modules dependabot/cli --updater-image fail-updater
stderr 'updater failure: updater exited with code 2'

# Assert that the test command doesn't fail if the updater fails
dependabot test -f input.yml --updater-image fail-updater

exec docker rmi -f fail-updater

-- Dockerfile --
FROM ubuntu:22.04

RUN useradd dependabot

COPY --chown=dependabot --chmod=755 update-ca-certificates /usr/bin/update-ca-certificates
COPY --chown=dependabot --chmod=755 run bin/run

-- update-ca-certificates --
#!/usr/bin/env bash

echo "Updating CA certificates..."

-- run --
#!/usr/bin/env bash

exit 2

-- input.yml --
input:
job:
package-manager: go_modules
source:
repo: dependabot/cli
directory: /
output:
-
2 changes: 1 addition & 1 deletion testdata/scripts/local.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
exec docker build -qt local-updater .

# The ls command in run will fail since this isn't a real updater
dependabot update go_modules dependabot-fixtures/go-modules-lib --updater-image local-updater
! dependabot update go_modules dependabot-fixtures/go-modules-lib --updater-image local-updater
stderr 'No such file or directory'

# The local flag should create the repo directory and put my-repo in it
Expand Down