Skip to content

Commit

Permalink
feat(*): add digest to invocation image on build
Browse files Browse the repository at this point in the history
resolves cnabio#690
  • Loading branch information
Michelle Noorali committed Apr 1, 2019
1 parent 9551a6d commit b2f3aee
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 68 deletions.
2 changes: 1 addition & 1 deletion cmd/duffle/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (b *buildCmd) run() (err error) {
return fmt.Errorf("cannot prepare build: %v", err)
}

if err := bldr.Build(ctx, app); err != nil {
if err := bldr.Build(ctx, app, bf); err != nil {
return err
}

Expand Down
1 change: 1 addition & 0 deletions cmd/duffle/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type exportCmd struct {
verbose bool
insecure bool
bundleIsFile bool
signer string
}

func newExportCmd(w io.Writer) *cobra.Command {
Expand Down
53 changes: 14 additions & 39 deletions pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/Masterminds/semver"
"github.com/pkg/errors"
Expand Down Expand Up @@ -87,11 +85,6 @@ func (b *Builder) PrepareBuild(bldr *Builder, mfst *manifest.Manifest, appDir st
return nil, nil, err
}

ii := bundle.InvocationImage{}
ii.Image = imb.URI()
ii.ImageType = imb.Type()
bf.InvocationImages = append(bf.InvocationImages, ii)

baseVersion := mfst.Version
if baseVersion == "" {
baseVersion = "0.1.0"
Expand Down Expand Up @@ -131,45 +124,27 @@ func (b *Builder) version(baseVersion, sha string) (string, error) {
}

// Build passes the context of each component to its respective builder
func (b *Builder) Build(ctx context.Context, app *AppContext) error {
if err := buildInvocationImages(ctx, b.ImageBuilders, app); err != nil {
func (b *Builder) Build(ctx context.Context, app *AppContext, bf *bundle.Bundle) error {
if err := b.buildInvocationImages(ctx, app, bf); err != nil {
return fmt.Errorf("error building image: %v", err)
}
return nil
}

func buildInvocationImages(ctx context.Context, imageBuilders []imagebuilder.ImageBuilder, app *AppContext) (err error) {
errc := make(chan error)

go func() {
defer close(errc)
var wg sync.WaitGroup
wg.Add(len(imageBuilders))

for _, c := range imageBuilders {
go func(c imagebuilder.ImageBuilder) {
defer wg.Done()
err = c.Build(ctx, app.Log)
if err != nil {
errc <- fmt.Errorf("error building image %v: %v", c.Name(), err)
}
}(c)
}

wg.Wait()
}()

for errc != nil {
select {
case err, ok := <-errc:
if !ok {
errc = nil
continue
}
func (b *Builder) buildInvocationImages(ctx context.Context, app *AppContext, bf *bundle.Bundle) (err error) {
built := []bundle.InvocationImage{}
for _, c := range b.ImageBuilders {
digest, err := c.Build(ctx, app.Log)
if err != nil {
return err
default:
time.Sleep(time.Second)
}
ii := bundle.InvocationImage{}
ii.Image = c.URI()
ii.ImageType = c.Type()
ii.Digest = digest
built = append(built, ii)
}
bf.InvocationImages = built

return nil
}
39 changes: 20 additions & 19 deletions pkg/imagebuilder/docker/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import (
"os"
"path"
"path/filepath"
"strings"

"github.com/deislabs/duffle/pkg/duffle/manifest"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
Expand All @@ -22,10 +19,11 @@ import (
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"

"github.com/sirupsen/logrus"

"golang.org/x/net/context"

"github.com/deislabs/duffle/pkg/duffle/manifest"
"github.com/deislabs/duffle/pkg/imagebuilder/docker/digester"
)

const (
Expand Down Expand Up @@ -63,13 +61,6 @@ func (db Builder) URI() string {
return db.Image
}

// Digest returns the name of a Docker Builder, which will give the image name
//
// TODO - return the actual digest
func (db Builder) Digest() string {
return strings.Split(db.Image, ":")[1]
}

// NewBuilder returns a new Docker builder based on the manifest
func NewBuilder(c *manifest.InvocationImage, cli *command.DockerCli) *Builder {
return &Builder{
Expand Down Expand Up @@ -109,7 +100,7 @@ func (db *Builder) PrepareBuild(appDir, registry, name string) error {
}

// Build builds the docker images.
func (db Builder) Build(ctx context.Context, log io.WriteCloser) error {
func (db *Builder) Build(ctx context.Context, log io.WriteCloser) (string, error) {
defer db.BuildContext.Close()
buildOpts := types.ImageBuildOptions{
Tags: []string{db.Image},
Expand All @@ -118,23 +109,33 @@ func (db Builder) Build(ctx context.Context, log io.WriteCloser) error {

resp, err := db.dockerBuilder.DockerClient.Client().ImageBuild(ctx, db.BuildContext, buildOpts)
if err != nil {
return fmt.Errorf("error building image builder %v with builder %v: %v", db.Name(), db.Type(), err)
return "", fmt.Errorf("error building image builder %v with builder %v: %v", db.Name(), db.Type(), err)
}

defer resp.Body.Close()

outFd, isTerm := term.GetFdInfo(db.BuildContext)
if err := jsonmessage.DisplayJSONMessagesStream(resp.Body, log, outFd, isTerm, nil); err != nil {
return fmt.Errorf("error streaming messages for image builder %v with builder %v: %v", db.Name(), db.Type(), err)
return "", fmt.Errorf("error streaming messages for image builder %v with builder %v: %v", db.Name(), db.Type(), err)
}

if _, _, err = db.dockerBuilder.DockerClient.Client().ImageInspectWithRaw(ctx, db.Image); err != nil {
if dockerclient.IsErrNotFound(err) {
return fmt.Errorf("could not locate image for %s: %v", db.Name(), err)
return "", fmt.Errorf("could not locate image for %s: %v", db.Name(), err)
}
return fmt.Errorf("imageInspectWithRaw error for image builder %v: %v", db.Name(), err)
return "", fmt.Errorf("imageInspectWithRaw error for image builder %v: %v", db.Name(), err)
}

return nil
d := digester.NewDigester(
db.dockerBuilder.DockerClient.Client(),
db.Image,
ctx,
)
digestStr, err := d.Digest()
if err != nil {
return "", fmt.Errorf("Failed to calculate digest for image: %s", err)
}

return digestStr, nil
}

func archiveSrc(contextPath string, b *Builder) error {
Expand Down
39 changes: 39 additions & 0 deletions pkg/imagebuilder/docker/digester/digester.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package digester

import (
"golang.org/x/net/context"

"github.com/docker/docker/client"
"github.com/opencontainers/go-digest"
)

type Digester struct {
Client client.ImageAPIClient
Image string
Context context.Context
}

// NewDigester returns a Digester given the args client, image, ctx
//
// client allows us to talk to the Docker client
// image is a string identifier of the image we want to compute digest of
// ctx is context to use and pass to Docker client
func NewDigester(client client.ImageAPIClient, image string, ctx context.Context) *Digester {
return &Digester{
Client: client,
Image: image,
Context: ctx,
}
}

// Digest returns the digest of the image tar
func (d *Digester) Digest() (string, error) {
reader, err := d.Client.ImageSave(d.Context, []string{d.Image})
computedDigest, err := digest.Canonical.FromReader(reader)
if err != nil {
return "", err
}
defer reader.Close()

return computedDigest.String(), nil
}
83 changes: 83 additions & 0 deletions pkg/imagebuilder/docker/digester/digester_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package digester

import (
"bytes"
"context"
"io"
"io/ioutil"
"testing"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
)

func TestDigest(t *testing.T) {
d := Digester{
Client: &mockDockerClient{},
Image: "mock-image",
}
digestStr, err := d.Digest()
if err != nil {
t.Errorf("Not expecting error computing digest but got error: %s", err)
}
expectedDigestStr := "sha256:5dffd8ab8b1b8db6fbb2a62f5059d930fe79f3e3d7b2e65d331af5b8de03c93c"
if digestStr != expectedDigestStr {
t.Errorf("Expected digest %s, got %s", expectedDigestStr, digestStr)
}
}

type mockDockerClient struct{}

func (m *mockDockerClient) ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader([]byte("mock-context-for-digest"))), nil
}

func (m *mockDockerClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
return types.ImageBuildResponse{}, nil
}
func (m *mockDockerClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
return nil, nil
}
func (m *mockDockerClient) BuildCancel(ctx context.Context, id string) error {
return nil
}

func (m *mockDockerClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return nil, nil
}

func (m *mockDockerClient) ImageHistory(ctx context.Context, image string) ([]image.HistoryResponseItem, error) {
return nil, nil
}
func (m *mockDockerClient) ImageImport(ctx context.Context, source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
return nil, nil
}
func (m *mockDockerClient) ImageInspectWithRaw(ctx context.Context, image string) (types.ImageInspect, []byte, error) {
return types.ImageInspect{}, nil, nil
}
func (m *mockDockerClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) {
return nil, nil
}
func (m *mockDockerClient) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
return types.ImageLoadResponse{}, nil
}
func (m *mockDockerClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
return nil, nil
}
func (m *mockDockerClient) ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return nil, nil
}
func (m *mockDockerClient) ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
return nil, nil
}
func (m *mockDockerClient) ImageSearch(ctx context.Context, term string, options types.ImageSearchOptions) ([]registry.SearchResult, error) {
return nil, nil
}
func (m *mockDockerClient) ImageTag(ctx context.Context, image, ref string) error {
return nil
}
func (m *mockDockerClient) ImagesPrune(ctx context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) {
return types.ImagesPruneReport{}, nil
}
3 changes: 1 addition & 2 deletions pkg/imagebuilder/imagebuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ type ImageBuilder interface {
Name() string
Type() string
URI() string
Digest() string

PrepareBuild(string, string, string) error
Build(context.Context, io.WriteCloser) error
Build(context.Context, io.WriteCloser) (string, error)
}
9 changes: 2 additions & 7 deletions pkg/imagebuilder/mock/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,6 @@ func (b Builder) URI() string {
return "mock-uri:1.0.0"
}

// Digest represents the digest of a mock builder
func (b Builder) Digest() string {
return "mock-digest"
}

// NewBuilder returns a new mock builder
func NewBuilder(c *manifest.InvocationImage) *Builder {
return &Builder{}
Expand All @@ -42,6 +37,6 @@ func (b *Builder) PrepareBuild(appDir, registry, name string) error {
}

// Build is no-op for a mock builder
func (b Builder) Build(ctx context.Context, log io.WriteCloser) error {
return nil
func (b Builder) Build(ctx context.Context, log io.WriteCloser) (string, error) {
return "mock-digest", nil
}

0 comments on commit b2f3aee

Please sign in to comment.