Skip to content

Commit

Permalink
Resolves Docker images to their immutable identifiers.
Browse files Browse the repository at this point in the history
This automatically resolves Docker images to an immutable identifier
before saving the slug. This has numerous benefits:

1. For security, it prevents docker registry attack vectors (MiTM,
credential exposure).
2. For reliability, it prevents issues from someone accidentally
overwriting a tag.
  • Loading branch information
ejholmes committed Sep 7, 2017
1 parent 4879ff5 commit 25c12c3
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 11 deletions.
8 changes: 6 additions & 2 deletions docs/quickstart_using.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,23 @@ Good - we haven't deployed any apps, so we shouldn't see any. Lets deploy our fi

```console
$ emp deploy remind101/acme-inc:master
Pulling repository remind101/acme-inc
master: Pulling from remind101/acme-inc
345c7524bc96: Download complete
a1dd7097a8e8: Download complete
23debee88b99: Download complete
31862d352883: Download complete
c7388ff7ab91: Download complete
78fb106ed050: Download complete
133fcef559c4: Download complete
Digest: sha256:c6f77d2098bc0e32aef3102e71b51831a9083dd9356a0ccadca860596a1e9007
Status: Downloaded newer image for remind101/acme-inc:master
Status: Resolved remind101/acme-inc:master to remind101/acme-inc@sha256:c6f77d2098bc0e32aef3102e71b51831a9083dd9356a0ccadca860596a1e9007
Status: Extracted Procfile from "/go/src/github.com/remind101/acme-inc/Procfile"
Status: Created new release v1 for acme-inc
Status: Finished processing events for release v1 of acme-inc
```

So what just happened? We just told the Empire API to go out and get the 'master' tagged image from the remind101/acme-inc repository. The Empire daemon then pulled that image down from [hub.docker.com](http://hub.docker.com/), then extracted the *Procfile* from it to analyze what processes were available. Now lets see what apps we're running:
So what just happened? We just told the Empire API to go out and get the 'master' tagged image from the remind101/acme-inc repository. The Empire daemon then pulled that image down from [hub.docker.com](http://hub.docker.com/), resolved the image to it's content-adressable identifier, then extracted the *Procfile* from it to analyze what processes were available. Now lets see what apps we're running:

```console
$ emp apps
Expand Down
48 changes: 39 additions & 9 deletions registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import (
type dockerDaemon struct {
docker *dockerutil.Client
extractor empire.ProcfileExtractor

// can be set to false to disable Pull-before-Resolve.
noPull bool
}

// DockerDaemon returns an empire.ImageRegistry that uses a local Docker Daemon
Expand All @@ -43,18 +46,45 @@ func (r *dockerDaemon) ExtractProcfile(ctx context.Context, img image.Image, w *
}

func (r *dockerDaemon) Resolve(ctx context.Context, img image.Image, w *jsonmessage.Stream) (image.Image, error) {
if err := r.docker.PullImage(ctx, docker.PullImageOptions{
Registry: img.Registry,
Repository: img.Repository,
Tag: img.Tag,
OutputStream: w,
RawJSONStream: true,
}); err != nil {
if !r.noPull {
if err := r.docker.PullImage(ctx, docker.PullImageOptions{
Registry: img.Registry,
Repository: img.Repository,
Tag: img.Tag,
OutputStream: w,
RawJSONStream: true,
}); err != nil {
return img, err
}
}

// If the image already references an immutable identifier, there's
// nothing for us to do.
if img.Digest != "" {
return img, nil
}

i, err := r.docker.InspectImage(img.String())
if err != nil {
return img, err
}

// TODO
return img, nil
// If there are no repository digests (the case for Docker <= 1.11),
// then we just fallback to the original identifier.
if len(i.RepoDigests) <= 0 {
w.Encode(jsonmessage.JSONMessage{
Status: fmt.Sprintf("Status: Image has no repository digests. Using %s as image identifier", img),
})
return img, nil
}

digest := i.RepoDigests[0]

w.Encode(jsonmessage.JSONMessage{
Status: fmt.Sprintf("Status: Resolved %s to %s", img, digest),
})

return image.Decode(digest)
}

// cmdExtractor is an Extractor implementation that returns a Procfile based
Expand Down
70 changes: 70 additions & 0 deletions registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,76 @@ import (
"github.com/remind101/empire/pkg/jsonmessage"
)

func TestResolve(t *testing.T) {
api := httpmock.NewServeReplay(t).Add(httpmock.PathHandler(t,
"GET /version",
200, `{ "ApiVersion": "1.20" }`,
)).Add(httpmock.PathHandler(t,
"GET /images/remind101:acme-inc/json",
200, `{ "RepoDigests": [ "remind101/acme-inc@sha256:c6f77d2098bc0e32aef3102e71b51831a9083dd9356a0ccadca860596a1e9007" ] }`,
))

c, s := newTestDockerClient(t, api)
defer s.Close()

d := dockerDaemon{
docker: c,
noPull: true,
}

w := jsonmessage.NewStream(ioutil.Discard)
img, err := d.Resolve(nil, image.Image{
Tag: "acme-inc",
Repository: "remind101",
}, w)
if err != nil {
t.Fatal(err)
}

if got, want := img, "remind101/acme-inc@sha256:c6f77d2098bc0e32aef3102e71b51831a9083dd9356a0ccadca860596a1e9007"; got.String() != want {
t.Fatalf("Resolve() => %s; want %s", got, want)
}

img, err = d.Resolve(nil, img, w)
if err != nil {
t.Fatal(err)
}
if got, want := img, "remind101/acme-inc@sha256:c6f77d2098bc0e32aef3102e71b51831a9083dd9356a0ccadca860596a1e9007"; got.String() != want {
t.Fatalf("Resolve() => %s; want %s", got, want)
}
}

func TestResolve_NoDigests(t *testing.T) {
api := httpmock.NewServeReplay(t).Add(httpmock.PathHandler(t,
"GET /version",
200, `{ "ApiVersion": "1.20" }`,
)).Add(httpmock.PathHandler(t,
"GET /images/remind101:acme-inc/json",
200, `{ "RepoDigests": [] }`,
))

c, s := newTestDockerClient(t, api)
defer s.Close()

d := dockerDaemon{
docker: c,
noPull: true,
}

w := jsonmessage.NewStream(ioutil.Discard)
img, err := d.Resolve(nil, image.Image{
Tag: "acme-inc",
Repository: "remind101",
}, w)
if err != nil {
t.Fatal(err)
}

if got, want := img, "remind101:acme-inc"; got.String() != want {
t.Fatalf("Resolve() => %s; want %s", got, want)
}
}

func TestCMDExtractor(t *testing.T) {
api := httpmock.NewServeReplay(t).Add(httpmock.PathHandler(t,
"GET /version",
Expand Down

0 comments on commit 25c12c3

Please sign in to comment.