Skip to content

Commit c8817bf

Browse files
LawnGnomemrnugget
andauthored
campaigns: handle non-root volume use cases more gracefully (#434)
This reverts commit a35c2e7. Co-authored-by: Thorsten Ball <[email protected]>
1 parent 6792668 commit c8817bf

17 files changed

+984
-300
lines changed

internal/campaigns/action.go

Lines changed: 0 additions & 49 deletions
This file was deleted.

internal/campaigns/bind_workspace.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type dockerBindWorkspaceCreator struct {
2121

2222
var _ WorkspaceCreator = &dockerBindWorkspaceCreator{}
2323

24-
func (wc *dockerBindWorkspaceCreator) Create(ctx context.Context, repo *graphql.Repository, zip string) (Workspace, error) {
24+
func (wc *dockerBindWorkspaceCreator) Create(ctx context.Context, repo *graphql.Repository, steps []Step, zip string) (Workspace, error) {
2525
w, err := wc.unzipToWorkspace(ctx, repo, zip)
2626
if err != nil {
2727
return nil, errors.Wrap(err, "unzipping the repository")

internal/campaigns/bind_workspace_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func TestDockerBindWorkspaceCreator_Create(t *testing.T) {
6262
testTempDir := workspaceTmpDir(t)
6363

6464
creator := &dockerBindWorkspaceCreator{dir: testTempDir}
65-
workspace, err := creator.Create(context.Background(), repo, archivePath)
65+
workspace, err := creator.Create(context.Background(), repo, nil, archivePath)
6666
if err != nil {
6767
t.Fatalf("unexpected error: %s", err)
6868
}
@@ -90,7 +90,7 @@ func TestDockerBindWorkspaceCreator_Create(t *testing.T) {
9090
badZip.Close()
9191

9292
creator := &dockerBindWorkspaceCreator{dir: testTempDir}
93-
if _, err := creator.Create(context.Background(), repo, badZipFile); err == nil {
93+
if _, err := creator.Create(context.Background(), repo, nil, badZipFile); err == nil {
9494
t.Error("unexpected nil error")
9595
}
9696
})

internal/campaigns/campaign_spec.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/sourcegraph/campaignutils/env"
99
"github.com/sourcegraph/campaignutils/overridable"
1010
"github.com/sourcegraph/campaignutils/yaml"
11+
"github.com/sourcegraph/src-cli/internal/campaigns/docker"
1112
"github.com/sourcegraph/src-cli/schema"
1213
)
1314

@@ -73,7 +74,7 @@ type Step struct {
7374
Files map[string]string `json:"files,omitempty" yaml:"files,omitempty"`
7475
Outputs Outputs `json:"outputs,omitempty" yaml:"outputs,omitempty"`
7576

76-
image string
77+
image docker.Image
7778
}
7879

7980
type Outputs map[string]Output

internal/campaigns/docker/cache.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package docker
2+
3+
import "sync"
4+
5+
// ImageCache is a cache of metadata about Docker images, indexed by name.
6+
type ImageCache struct {
7+
images map[string]Image
8+
imagesMu sync.Mutex
9+
}
10+
11+
// NewImageCache creates a new image cache.
12+
func NewImageCache() *ImageCache {
13+
return &ImageCache{
14+
images: make(map[string]Image),
15+
}
16+
}
17+
18+
// Get returns the image cache entry for the given Docker image. The name may be
19+
// anything the Docker command line will accept as an image name: this will
20+
// generally be IMAGE or IMAGE:TAG.
21+
func (ic *ImageCache) Get(name string) Image {
22+
ic.imagesMu.Lock()
23+
defer ic.imagesMu.Unlock()
24+
25+
if image, ok := ic.images[name]; ok {
26+
return image
27+
}
28+
29+
image := &image{name: name}
30+
ic.images[name] = image
31+
return image
32+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package docker
2+
3+
import "testing"
4+
5+
func TestImageCache(t *testing.T) {
6+
cache := NewImageCache()
7+
if cache == nil {
8+
t.Error("unexpected nil cache")
9+
}
10+
11+
have := cache.Get("foo")
12+
if have == nil {
13+
t.Error("unexpected nil error")
14+
}
15+
if name := have.(*image).name; name != "foo" {
16+
t.Errorf("invalid name: have=%q want=%q", name, "foo")
17+
}
18+
19+
again := cache.Get("foo")
20+
if have != again {
21+
t.Errorf("invalid memoisation: first=%v second=%v", have, again)
22+
}
23+
}

internal/campaigns/docker/image.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package docker
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"strings"
8+
"sync"
9+
10+
"github.com/pkg/errors"
11+
12+
"github.com/sourcegraph/src-cli/internal/exec"
13+
)
14+
15+
// UIDGID represents a UID:GID pair.
16+
type UIDGID struct {
17+
UID int
18+
GID int
19+
}
20+
21+
func (ug UIDGID) String() string {
22+
return fmt.Sprintf("%d:%d", ug.UID, ug.GID)
23+
}
24+
25+
// Root is a root:root user.
26+
var Root = UIDGID{UID: 0, GID: 0}
27+
28+
// Image represents a Docker image, hopefully stored in the local cache.
29+
type Image interface {
30+
Digest(context.Context) (string, error)
31+
Ensure(context.Context) error
32+
UIDGID(context.Context) (UIDGID, error)
33+
}
34+
35+
type image struct {
36+
name string
37+
38+
// There are lots of once fields below: basically, we're going to try fairly
39+
// hard to prevent performing the same operations on the same image over and
40+
// over, since some of them are expensive.
41+
42+
digest string
43+
digestErr error
44+
digestOnce sync.Once
45+
46+
ensureErr error
47+
ensureOnce sync.Once
48+
49+
uidGid UIDGID
50+
uidGidErr error
51+
uidGidOnce sync.Once
52+
}
53+
54+
// Digest gets and returns the content digest for the image. Note that this is
55+
// different from the "distribution digest" (which is what you can use to
56+
// specify an image to `docker run`, as in `my/image@sha256:xxx`). We need to
57+
// use the content digest because the distribution digest is only computed for
58+
// images that have been pulled from or pushed to a registry. See
59+
// https://windsock.io/explaining-docker-image-ids/ under "A Final Twist" for a
60+
// good explanation.
61+
func (image *image) Digest(ctx context.Context) (string, error) {
62+
image.digestOnce.Do(func() {
63+
image.digest, image.digestErr = func() (string, error) {
64+
if err := image.Ensure(ctx); err != nil {
65+
return "", err
66+
}
67+
68+
// TODO!(sqs): is image id the right thing to use here? it is NOT
69+
// the digest. but the digest is not calculated for all images
70+
// (unless they are pulled/pushed from/to a registry), see
71+
// https://github.com/moby/moby/issues/32016.
72+
out, err := exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", "--", image.name).CombinedOutput()
73+
if err != nil {
74+
return "", errors.Wrapf(err, "inspecting docker image: %s", string(bytes.TrimSpace(out)))
75+
}
76+
id := string(bytes.TrimSpace(out))
77+
if id == "" {
78+
return "", errors.Errorf("unexpected empty docker image content ID for %q", image.name)
79+
}
80+
return id, nil
81+
}()
82+
})
83+
84+
return image.digest, image.digestErr
85+
}
86+
87+
// Ensure ensures that the image has been pulled by Docker. Note that it does
88+
// not attempt to pull a newer version of the image if it exists locally.
89+
func (image *image) Ensure(ctx context.Context) error {
90+
image.ensureOnce.Do(func() {
91+
image.ensureErr = func() error {
92+
// docker image inspect will return a non-zero exit code if the image and
93+
// tag don't exist locally, regardless of the format.
94+
if err := exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "1", image.name).Run(); err != nil {
95+
// Let's try pulling the image.
96+
if err := exec.CommandContext(ctx, "docker", "image", "pull", image.name).Run(); err != nil {
97+
return errors.Wrap(err, "pulling image")
98+
}
99+
}
100+
101+
return nil
102+
}()
103+
})
104+
105+
return image.ensureErr
106+
}
107+
108+
// UIDGID returns the user and group the container is configured to run as.
109+
func (image *image) UIDGID(ctx context.Context) (UIDGID, error) {
110+
image.uidGidOnce.Do(func() {
111+
image.uidGid, image.uidGidErr = func() (UIDGID, error) {
112+
stdout := new(bytes.Buffer)
113+
114+
// Digest also implicitly means Ensure has been called.
115+
digest, err := image.Digest(ctx)
116+
if err != nil {
117+
return UIDGID{}, errors.Wrap(err, "getting digest")
118+
}
119+
120+
args := []string{
121+
"run",
122+
"--rm",
123+
"--entrypoint", "/bin/sh",
124+
digest,
125+
"-c", "id -u; id -g",
126+
}
127+
cmd := exec.CommandContext(ctx, "docker", args...)
128+
cmd.Stdout = stdout
129+
130+
if err := cmd.Run(); err != nil {
131+
return UIDGID{}, errors.Wrap(err, "running id")
132+
}
133+
134+
// POSIX specifies the output of `id -u` as the effective UID,
135+
// terminated by a newline. `id -g` is the same, just for the GID.
136+
raw := strings.TrimSpace(stdout.String())
137+
var res UIDGID
138+
_, err = fmt.Sscanf(raw, "%d\n%d", &res.UID, &res.GID)
139+
if err != nil {
140+
return res, errors.Wrapf(err, "malformed uid/gid: %q", raw)
141+
}
142+
return res, nil
143+
}()
144+
})
145+
146+
return image.uidGid, image.uidGidErr
147+
}

0 commit comments

Comments
 (0)