From e1d7666029c1365d387ea9ddd7d7697608324636 Mon Sep 17 00:00:00 2001 From: Emily Casey Date: Tue, 18 Jun 2019 10:55:42 -0400 Subject: [PATCH] Allow additional image names (tags) to be saved Signed-off-by: Andrew Meyer Signed-off-by: Javier Romero --- README.md | 6 + fakes/image.go | 51 +++++- fakes/image_test.go | 67 ++++++++ go.mod | 2 +- image.go | 24 ++- local.go | 338 +++++++++++++++++++++++-------------- local_test.go | 308 ++++++++++++++++++++++++--------- remote.go | 163 +++++++++++------- remote_test.go | 276 +++++++++++++++++++++--------- testhelpers/testhelpers.go | 26 ++- 10 files changed, 898 insertions(+), 363 deletions(-) create mode 100644 fakes/image_test.go diff --git a/README.md b/README.md index 8157198b..22c4cccd 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Helpful utilities for working with images ## Development +To format: + +```bash +$ ./bin/format +``` + To run tests: ```bash diff --git a/fakes/image.go b/fakes/image.go index 54ba1ffb..c51f0696 100644 --- a/fakes/image.go +++ b/fakes/image.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" "time" + "unicode" "github.com/pkg/errors" @@ -19,7 +20,6 @@ import ( func NewImage(name, topLayerSha, digest string) *Image { return &Image{ - alreadySaved: false, labels: map[string]string{}, env: map[string]string{}, topLayerSha: topLayerSha, @@ -29,11 +29,11 @@ func NewImage(name, topLayerSha, digest string) *Image { layersMap: map[string]string{}, prevLayersMap: map[string]string{}, createdAt: time.Now(), + savedNames: map[string]bool{}, } } type Image struct { - alreadySaved bool deleted bool layers []string layersMap map[string]string @@ -50,6 +50,7 @@ type Image struct { createdAt time.Time layerDir string workingDir string + savedNames map[string]bool } func (f *Image) CreatedAt() (time.Time, error) { @@ -154,13 +155,14 @@ func (f *Image) ReuseLayer(sha string) error { return nil } -func (f *Image) Save() (string, error) { - f.alreadySaved = true - +func (f *Image) Save(additionalNames ...string) imgutil.SaveResult { var err error f.layerDir, err = ioutil.TempDir("", "fake-image") if err != nil { - return "", errors.Wrap(err, "failed to create tmpDir") + return imgutil.NewFailedResult( + append([]string{f.name}, additionalNames...), + errors.Wrap(err, "failed to create tmpDir"), + ) } for sha, path := range f.layersMap { @@ -174,7 +176,31 @@ func (f *Image) Save() (string, error) { f.layers[i] = filepath.Join(f.layerDir, filepath.Base(layerPath)) } - return "saved-digest-from-fake-run-image", nil + allNames := append([]string{f.name}, additionalNames...) + + errs := map[string]error{} + for _, n := range allNames { + if !isASCII(n) { + errs[n] = errors.New("could not parse reference") + } else { + errs[n] = nil + f.savedNames[n] = true + } + } + + return imgutil.SaveResult{ + Outcomes: errs, + Digest: "saved-digest-from-fake-run-image", + } +} + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > unicode.MaxASCII { + return false + } + } + return true } func (f *Image) copyLayer(path, newPath string) error { @@ -294,9 +320,18 @@ func (f *Image) NumberOfAddedLayers() int { } func (f *Image) IsSaved() bool { - return f.alreadySaved + return len(f.savedNames) > 0 } func (f *Image) Base() string { return f.base } + +func (f *Image) SavedNames() []string { + var names []string + for k := range f.savedNames { + names = append(names, k) + } + + return names +} diff --git a/fakes/image_test.go b/fakes/image_test.go new file mode 100644 index 00000000..cbe8105b --- /dev/null +++ b/fakes/image_test.go @@ -0,0 +1,67 @@ +package fakes_test + +import ( + "github.com/buildpack/imgutil/fakes" + "math/rand" + "testing" + "time" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + h "github.com/buildpack/imgutil/testhelpers" +) + +var localTestRegistry *h.DockerRegistry + +func newRepoName() string { + return "test-image-" + h.RandString(10) +} + +func TestFake(t *testing.T) { + rand.Seed(time.Now().UTC().UnixNano()) + + localTestRegistry = h.NewDockerRegistry() + localTestRegistry.Start(t) + defer localTestRegistry.Stop(t) + + spec.Run(t, "FakeImage", testFake, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func testFake(t *testing.T, when spec.G, it spec.S) { + when("#SavedNames", func() { + when("additional names are provided during save", func() { + var ( + repoName = newRepoName() + additionalNames = []string{ + newRepoName(), + newRepoName(), + } + ) + + it("returns list of saved names", func() { + image := fakes.NewImage(repoName, "", "") + + _ = image.Save(additionalNames...) + + names := image.SavedNames() + h.AssertContains(t, names, append(additionalNames, repoName)...) + }) + + when("an image name is not valid", func() { + it("returns a list of image names with errors", func() { + badImageName := repoName + ":🧨" + + image := fakes.NewImage(repoName, "", "") + + result := image.Save(append([]string{badImageName}, additionalNames...)...) + h.AssertError(t, result.Outcomes[badImageName], "could not parse reference") + + names := image.SavedNames() + h.AssertContains(t, names, append(additionalNames, repoName)...) + h.AssertDoesNotContain(t, names, badImageName) + }) + }) + }) + }) +} diff --git a/go.mod b/go.mod index db797e54..8d65675b 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/sirupsen/logrus v1.4.1 // indirect github.com/stretchr/testify v1.3.0 // indirect golang.org/x/net v0.0.0-20190415214537-1da14a5a36f2 // indirect - golang.org/x/sync v0.0.0-20190412183630-56d357773e84 // indirect + golang.org/x/sync v0.0.0-20190412183630-56d357773e84 golang.org/x/sys v0.0.0-20190416152802-12500544f89f // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect google.golang.org/grpc v1.20.0 // indirect diff --git a/image.go b/image.go index f561c0a2..7a8e3036 100644 --- a/image.go +++ b/image.go @@ -5,6 +5,24 @@ import ( "time" ) +type SaveResult struct { + // Digest is the digest of the image + Digest string + // Outcomes is a map of image name to `error` or `nil` if saved properly. + Outcomes map[string]error +} + +func NewFailedResult(imageNames []string, err error) SaveResult { + errs := map[string]error{} + for _, n := range imageNames { + errs[n] = err + } + + return SaveResult{ + Outcomes: errs, + } +} + type Image interface { Name() string Rename(name string) @@ -20,9 +38,11 @@ type Image interface { AddLayer(path string) error ReuseLayer(sha string) error TopLayer() (string, error) - Save() (string, error) + // Save saves the image as `Name()` and any additional names provided to this method. + Save(additionalNames ...string) SaveResult + // Found tells whether the image exists in the repository by `Name()`. Found() bool - GetLayer(string) (io.ReadCloser, error) + GetLayer(sha string) (io.ReadCloser, error) Delete() error CreatedAt() (time.Time, error) } diff --git a/local.go b/local.go index 543ba109..08eed621 100644 --- a/local.go +++ b/local.go @@ -12,7 +12,6 @@ import ( "os" "path/filepath" "strings" - "sync" "time" "github.com/docker/docker/api/types" @@ -20,49 +19,103 @@ import ( "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" + "golang.org/x/sync/singleflight" ) -type localImage struct { +type LocalImage struct { repoName string docker *client.Client inspect types.ImageInspect layerPaths []string currentTempImage string - prevDir string - prevMap map[string]string - prevOnce *sync.Once + requestGroup singleflight.Group + prevName string easyAddLayers []string } -func EmptyLocalImage(repoName string, dockerClient *client.Client) Image { - inspect := types.ImageInspect{} - inspect.Config = &container.Config{ - Labels: map[string]string{}, +type FileSystemLocalImage struct { + dir string + layersMap map[string]string +} + +type LocalImageOption func(image *LocalImage) (*LocalImage, error) + +func verifyImage(docker *client.Client, imageName string, optional bool) (types.ImageInspect, error) { + var ( + err error + inspect types.ImageInspect + ) + + if inspect, _, err = docker.ImageInspectWithRaw(context.Background(), imageName); err != nil { + if client.IsErrNotFound(err) { + if optional { + return inspect, nil + } else { + return inspect, fmt.Errorf("there is no image with name '%s'", imageName) + } + } + + return inspect, errors.Wrapf(err, "verifying image '%s'", imageName) + } + + return inspect, nil +} + +func WithPreviousLocalImage(imageName string) LocalImageOption { + return func(l *LocalImage) (*LocalImage, error) { + if _, err := verifyImage(l.docker, imageName, true); err != nil { + return l, err + } + + l.prevName = imageName + + return l, nil } - return &localImage{ - repoName: repoName, - docker: dockerClient, - inspect: inspect, - prevOnce: &sync.Once{}, +} + +func FromLocalImageBase(imageName string) LocalImageOption { + return func(l *LocalImage) (*LocalImage, error) { + var ( + err error + inspect types.ImageInspect + ) + + if inspect, err = verifyImage(l.docker, imageName, true); err != nil { + return l, err + } + + l.inspect = inspect + l.layerPaths = make([]string, len(l.inspect.RootFS.Layers)) + + return l, nil } } -func NewLocalImage(repoName string, dockerClient *client.Client) (Image, error) { - inspect, _, err := dockerClient.ImageInspectWithRaw(context.Background(), repoName) - if err != nil && !client.IsErrNotFound(err) { - return nil, err +func NewLocalImage(repoName string, dockerClient *client.Client, ops ...LocalImageOption) (Image, error) { + inspect := types.ImageInspect{} + inspect.Config = &container.Config{ + Labels: map[string]string{}, } - return &localImage{ + image := &LocalImage{ docker: dockerClient, repoName: repoName, inspect: inspect, layerPaths: make([]string, len(inspect.RootFS.Layers)), - prevOnce: &sync.Once{}, - }, nil + } + + var err error + for _, v := range ops { + image, err = v(image) + if err != nil { + return nil, err + } + } + + return image, nil } -func (l *localImage) Label(key string) (string, error) { +func (l *LocalImage) Label(key string) (string, error) { if l.inspect.Config == nil { return "", nil } @@ -70,7 +123,7 @@ func (l *localImage) Label(key string) (string, error) { return labels[key], nil } -func (l *localImage) Env(key string) (string, error) { +func (l *LocalImage) Env(key string) (string, error) { if l.inspect.Config == nil { return "", nil } @@ -83,7 +136,7 @@ func (l *localImage) Env(key string) (string, error) { return "", nil } -func (l *localImage) Rename(name string) { +func (l *LocalImage) Rename(name string) { l.easyAddLayers = nil if prevInspect, _, err := l.docker.ImageInspectWithRaw(context.TODO(), name); err == nil { if l.sameBase(prevInspect) { @@ -94,7 +147,7 @@ func (l *localImage) Rename(name string) { l.repoName = name } -func (l *localImage) sameBase(prevInspect types.ImageInspect) bool { +func (l *LocalImage) sameBase(prevInspect types.ImageInspect) bool { if len(prevInspect.RootFS.Layers) < len(l.inspect.RootFS.Layers) { return false } @@ -106,15 +159,15 @@ func (l *localImage) sameBase(prevInspect types.ImageInspect) bool { return true } -func (l *localImage) Name() string { +func (l *LocalImage) Name() string { return l.repoName } -func (l *localImage) Found() bool { - return l.inspect.Config != nil +func (l *LocalImage) Found() bool { + return l.inspect.ID != "" } -func (l *localImage) Digest() (string, error) { +func (l *LocalImage) Digest() (string, error) { if !l.Found() { return "", fmt.Errorf("failed to get digest, image '%s' does not exist", l.repoName) } @@ -128,7 +181,7 @@ func (l *localImage) Digest() (string, error) { return parts[1], nil } -func (l *localImage) CreatedAt() (time.Time, error) { +func (l *LocalImage) CreatedAt() (time.Time, error) { createdAtTime := l.inspect.Created createdTime, err := time.Parse(time.RFC3339Nano, createdAtTime) @@ -138,7 +191,7 @@ func (l *localImage) CreatedAt() (time.Time, error) { return createdTime, nil } -func (l *localImage) Rebase(baseTopLayer string, newBase Image) error { +func (l *LocalImage) Rebase(baseTopLayer string, newBase Image) error { ctx := context.Background() // FIND TOP LAYER @@ -161,13 +214,14 @@ func (l *localImage) Rebase(baseTopLayer string, newBase Image) error { l.inspect.RootFS.Layers = newBaseInspect.RootFS.Layers l.layerPaths = make([]string, len(l.inspect.RootFS.Layers)) - // SAVE CURRENT IMAGE TO DISK - if err := l.prevDownload(); err != nil { + // DOWNLOAD IMAGE + fsImage, err := l.downloadImageOnce(l.repoName) + if err != nil { return err } // READ MANIFEST.JSON - b, err := ioutil.ReadFile(filepath.Join(l.prevDir, "manifest.json")) + b, err := ioutil.ReadFile(filepath.Join(fsImage.dir, "manifest.json")) if err != nil { return err } @@ -181,7 +235,7 @@ func (l *localImage) Rebase(baseTopLayer string, newBase Image) error { // ADD EXISTING LAYERS for _, filename := range manifest[0].Layers[(len(manifest[0].Layers) - keepLayers):] { - if err := l.AddLayer(filepath.Join(l.prevDir, filename)); err != nil { + if err := l.AddLayer(filepath.Join(fsImage.dir, filename)); err != nil { return err } } @@ -189,15 +243,16 @@ func (l *localImage) Rebase(baseTopLayer string, newBase Image) error { return nil } -func (l *localImage) SetLabel(key, val string) error { +func (l *LocalImage) SetLabel(key, val string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set label, image '%s' does not exist", l.repoName) } + l.inspect.Config.Labels[key] = val return nil } -func (l *localImage) SetEnv(key, val string) error { +func (l *LocalImage) SetEnv(key, val string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set env var, image '%s' does not exist", l.repoName) } @@ -205,7 +260,7 @@ func (l *localImage) SetEnv(key, val string) error { return nil } -func (l *localImage) SetWorkingDir(dir string) error { +func (l *LocalImage) SetWorkingDir(dir string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set working dir, image '%s' does not exist", l.repoName) } @@ -213,7 +268,7 @@ func (l *localImage) SetWorkingDir(dir string) error { return nil } -func (l *localImage) SetEntrypoint(ep ...string) error { +func (l *LocalImage) SetEntrypoint(ep ...string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set entrypoint, image '%s' does not exist", l.repoName) } @@ -221,7 +276,7 @@ func (l *localImage) SetEntrypoint(ep ...string) error { return nil } -func (l *localImage) SetCmd(cmd ...string) error { +func (l *LocalImage) SetCmd(cmd ...string) error { if l.inspect.Config == nil { return fmt.Errorf("failed to set cmd, image '%s' does not exist", l.repoName) } @@ -229,25 +284,31 @@ func (l *localImage) SetCmd(cmd ...string) error { return nil } -func (l *localImage) TopLayer() (string, error) { +func (l *LocalImage) TopLayer() (string, error) { all := l.inspect.RootFS.Layers + + if len(all) == 0 { + return "", fmt.Errorf("image '%s' has no layers", l.repoName) + } + topLayer := all[len(all)-1] return topLayer, nil } -func (l *localImage) GetLayer(sha string) (io.ReadCloser, error) { - if err := l.prevDownload(); err != nil { +func (l *LocalImage) GetLayer(sha string) (io.ReadCloser, error) { + fsImage, err := l.downloadImageOnce(l.repoName) + if err != nil { return nil, err } - layerID, ok := l.prevMap[sha] + layerID, ok := fsImage.layersMap[sha] if !ok { return nil, fmt.Errorf("image '%s' does not contain layer with diff ID '%s'", l.repoName, sha) } - return os.Open(filepath.Join(l.prevDir, layerID)) + return os.Open(filepath.Join(fsImage.dir, layerID)) } -func (l *localImage) AddLayer(path string) error { +func (l *LocalImage) AddLayer(path string) error { f, err := os.Open(path) if err != nil { return errors.Wrapf(err, "AddLayer: open layer: %s", path) @@ -266,7 +327,7 @@ func (l *localImage) AddLayer(path string) error { return nil } -func (l *localImage) ReuseLayer(sha string) error { +func (l *LocalImage) ReuseLayer(sha string) error { if len(l.easyAddLayers) > 0 && l.easyAddLayers[0] == sha { l.inspect.RootFS.Layers = append(l.inspect.RootFS.Layers, sha) l.layerPaths = append(l.layerPaths, "") @@ -274,19 +335,49 @@ func (l *localImage) ReuseLayer(sha string) error { return nil } - if err := l.prevDownload(); err != nil { + if l.prevName == "" { + return errors.New("no previous image provided to reuse layers from") + } + + fsImage, err := l.downloadImageOnce(l.prevName) + if err != nil { return err } - reuseLayer, ok := l.prevMap[sha] + reuseLayer, ok := fsImage.layersMap[sha] if !ok { return fmt.Errorf("SHA %s was not found in %s", sha, l.repoName) } - return l.AddLayer(filepath.Join(l.prevDir, reuseLayer)) + return l.AddLayer(filepath.Join(fsImage.dir, reuseLayer)) } -func (l *localImage) Save() (string, error) { +func (l *LocalImage) Save(additionalNames ...string) SaveResult { + var ( + err error + errs = map[string]error{} + ) + + allNames := append([]string{l.repoName}, additionalNames...) + + ctx := context.Background() + digest, err := l.doSave() + if err != nil { + return NewFailedResult(allNames, err) + } + + errs[l.repoName] = nil + for _, n := range additionalNames { + errs[n] = l.docker.ImageTag(ctx, l.repoName, n) + } + + return SaveResult{ + Digest: digest, + Outcomes: errs, + } +} + +func (l *LocalImage) doSave() (string, error) { ctx := context.Background() done := make(chan error) @@ -362,16 +453,11 @@ func (l *localImage) Save() (string, error) { pw.Close() err = <-done - if l.prevDir != "" { - os.RemoveAll(l.prevDir) - l.prevDir = "" - l.prevMap = nil - l.prevOnce = &sync.Once{} - } + l.requestGroup.Forget(l.repoName) if _, _, err = l.docker.ImageInspectWithRaw(context.Background(), imgID); err != nil { if client.IsErrNotFound(err) { - return "", fmt.Errorf("save image '%s'", l.repoName) + return "", errors.Wrapf(err, "save image '%s'", l.repoName) } return "", err } @@ -379,7 +465,7 @@ func (l *localImage) Save() (string, error) { return imgID, err } -func (l *localImage) configFile() ([]byte, error) { +func (l *LocalImage) configFile() ([]byte, error) { imgConfig := map[string]interface{}{ "os": "linux", "created": time.Now().Format(time.RFC3339), @@ -392,7 +478,7 @@ func (l *localImage) configFile() ([]byte, error) { return json.Marshal(imgConfig) } -func (l *localImage) Delete() error { +func (l *LocalImage) Delete() error { if !l.Found() { return nil } @@ -404,81 +490,85 @@ func (l *localImage) Delete() error { return err } -func (l *localImage) prevDownload() error { - var outerErr error - l.prevOnce.Do(func() { - ctx := context.Background() +func (l *LocalImage) downloadImageOnce(imageName string) (*FileSystemLocalImage, error) { + v, err, _ := l.requestGroup.Do(imageName, func() (details interface{}, err error) { + return downloadImage(l.docker, imageName) + }) - tarFile, err := l.docker.ImageSave(ctx, []string{l.repoName}) - if err != nil { - outerErr = err - return - } - defer tarFile.Close() + if err != nil { + return nil, err + } - l.prevDir, err = ioutil.TempDir("", "imgutil.local.reuse-layer.") - if err != nil { - outerErr = errors.Wrap(err, "local reuse-layer create temp dir") - return - } + return v.(*FileSystemLocalImage), nil +} - err = untar(tarFile, l.prevDir) - if err != nil { - outerErr = err - return - } +func downloadImage(docker *client.Client, imageName string) (*FileSystemLocalImage, error) { + ctx := context.Background() - mf, err := os.Open(filepath.Join(l.prevDir, "manifest.json")) - if err != nil { - outerErr = err - return - } - defer mf.Close() + tarFile, err := docker.ImageSave(ctx, []string{imageName}) + if err != nil { + return nil, err + } + defer tarFile.Close() - var manifest []struct { - Config string - Layers []string - } - if err := json.NewDecoder(mf).Decode(&manifest); err != nil { - outerErr = err - return - } + tmpDir, err := ioutil.TempDir("", "imgutil.local.image.") + if err != nil { + return nil, errors.Wrap(err, "local reuse-layer create temp dir") + } - if len(manifest) != 1 { - outerErr = fmt.Errorf("manifest.json had unexpected number of entries: %d", len(manifest)) - return - } + err = untar(tarFile, tmpDir) + if err != nil { + return nil, err + } - df, err := os.Open(filepath.Join(l.prevDir, manifest[0].Config)) - if err != nil { - outerErr = err - return - } - defer df.Close() + mf, err := os.Open(filepath.Join(tmpDir, "manifest.json")) + if err != nil { + return nil, err + } + defer mf.Close() - var details struct { - RootFS struct { - DiffIDs []string `json:"diff_ids"` - } `json:"rootfs"` - } + var manifest []struct { + Config string + Layers []string + } + if err := json.NewDecoder(mf).Decode(&manifest); err != nil { + return nil, err + } - if err = json.NewDecoder(df).Decode(&details); err != nil { - outerErr = err - return - } + if len(manifest) != 1 { + return nil, fmt.Errorf("manifest.json had unexpected number of entries: %d", len(manifest)) + } - if len(manifest[0].Layers) != len(details.RootFS.DiffIDs) { - outerErr = fmt.Errorf("layers and diff IDs do not match, there are %d layers and %d diffIDs", len(manifest[0].Layers), len(details.RootFS.DiffIDs)) - return - } + df, err := os.Open(filepath.Join(tmpDir, manifest[0].Config)) + if err != nil { + return nil, err + } + defer df.Close() - l.prevMap = make(map[string]string, len(manifest[0].Layers)) - for i, diffID := range details.RootFS.DiffIDs { - layerID := manifest[0].Layers[i] - l.prevMap[diffID] = layerID - } - }) - return outerErr + var details struct { + RootFS struct { + DiffIDs []string `json:"diff_ids"` + } `json:"rootfs"` + } + + if err = json.NewDecoder(df).Decode(&details); err != nil { + return nil, err + } + + if len(manifest[0].Layers) != len(details.RootFS.DiffIDs) { + return nil, fmt.Errorf("layers and diff IDs do not match, there are %d layers and %d diffIDs", len(manifest[0].Layers), len(details.RootFS.DiffIDs)) + } + + layersMap := make(map[string]string, len(manifest[0].Layers)) + for i, diffID := range details.RootFS.DiffIDs { + layerID := manifest[0].Layers[i] + layersMap[diffID] = layerID + } + + return &FileSystemLocalImage{ + dir: tmpDir, + layersMap: layersMap, + }, nil } func addTextToTar(tw *tar.Writer, name string, contents []byte) error { diff --git a/local_test.go b/local_test.go index bc0ee42d..87731921 100644 --- a/local_test.go +++ b/local_test.go @@ -36,25 +36,28 @@ func TestLocal(t *testing.T) { spec.Run(t, "LocalImage", testLocalImage, spec.Parallel(), spec.Report(report.Terminal{})) } +func newLocalTestImageName() string { + return "localhost:" + localTestRegistry.Port + "/pack-image-test-" + h.RandString(10) +} + func testLocalImage(t *testing.T, when spec.G, it spec.S) { - var repoName string var dockerClient *client.Client it.Before(func() { var err error dockerClient = h.DockerCli(t) h.AssertNil(t, err) - repoName = "pack-image-test-" + h.RandString(10) }) when("#NewLocalImage", func() { when("image is available locally", func() { + var repoName = newLocalTestImageName() + it.After(func() { h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) }) it("returns the local image", func() { - repoName = "test/repo" labels := make(map[string]string) labels["some.label"] = "some.value" @@ -64,7 +67,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { ENV MY_VAR=my_val `, repoName), labels) - localImage, err := imgutil.NewLocalImage(repoName, dockerClient) + localImage, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) labelValue, err := localImage.Label("some.label") @@ -75,30 +78,44 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("image is not available locally", func() { it("returns an empty image", func() { - repoName = "localhost:" + localTestRegistry.Port + "/pack-image-test-" + h.RandString(10) + _, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient) + h.AssertNil(t, err) + }) + }) - localImage, e := imgutil.NewLocalImage(repoName, dockerClient) - h.AssertNil(t, e) + when("#FromLocalImageBase", func() { + when("base image does not exist", func() { + it("doesn't error", func() { + _, err := imgutil.NewLocalImage( + newLocalTestImageName(), + dockerClient, + imgutil.FromLocalImageBase("some-bad-repo-name"), + ) - _, err := localImage.Digest() - h.AssertError(t, err, "does not exist") + h.AssertNil(t, err) + }) }) }) - }) - when("#EmptyLocalImage", func() { - it("returns a scratch image", func() { - img := imgutil.EmptyLocalImage(repoName, dockerClient) + when("#WithPreviousLocalImage", func() { + when("previous image does not exist", func() { + it("doesn't error", func() { + _, err := imgutil.NewLocalImage( + "busybox", + dockerClient, + imgutil.WithPreviousLocalImage("some-bad-repo-name"), + ) - t.Log("check that the empty image is useable image") - h.AssertNil(t, img.SetLabel("some-key", "some-val")) - _, err := img.Save() - h.AssertNil(t, err) + h.AssertNil(t, err) + }) + }) }) }) when("#Label", func() { when("image exists", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM scratch @@ -112,7 +129,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns the label value", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) label, err := img.Label("mykey") @@ -132,7 +149,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("image NOT exists", func() { it("returns an empty string", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient) h.AssertNil(t, err) label, err := img.Label("some-label") @@ -144,6 +161,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#Env", func() { when("image exists", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM scratch @@ -157,7 +176,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns the label value", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) val, err := img.Env("MY_VAR") @@ -177,7 +196,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("image NOT exists", func() { it("returns an empty string", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient) h.AssertNil(t, err) val, err := img.Env("SOME_VAR") @@ -189,6 +208,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#Name", func() { it("always returns the original name", func() { + var repoName = newLocalTestImageName() + img, err := imgutil.NewLocalImage(repoName, dockerClient) h.AssertNil(t, err) @@ -205,7 +226,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns the containers created at time", func() { - img, err := imgutil.NewLocalImage(reference, dockerClient) + img, err := imgutil.NewLocalImage(reference, dockerClient, imgutil.FromLocalImageBase(reference)) h.AssertNil(t, err) expectedTime := time.Date(2018, 10, 2, 17, 19, 34, 239926273, time.UTC) @@ -229,7 +250,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { it("returns the image digest", func() { expectedDigest = "sha256:2a03a6059f21e150ae84b0973863609494aad70f0a80eaeb64bddd8d92465812" - img, err := imgutil.NewLocalImage(reference, dockerClient) + img, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient, imgutil.FromLocalImageBase(reference)) h.AssertNil(t, err) digest, err := img.Digest() h.AssertNil(t, err) @@ -238,6 +259,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) when("image exists but has no digest", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM scratch @@ -251,20 +274,35 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns an empty string", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) digest, err := img.Digest() h.AssertNil(t, err) h.AssertEq(t, digest, "") }) }) + + when("base image doesn't exist", func() { + when("digest is requested", func() { + it("returns an error", func() { + var repoName = newLocalTestImageName() + + localImage, e := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) + h.AssertNil(t, e) + + _, err := localImage.Digest() + h.AssertError(t, err, "does not exist") + }) + }) + }) }) when("#SetLabel", func() { when("image exists", func() { var ( - img imgutil.Image - origID string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { var err error @@ -289,8 +327,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) h.AssertEq(t, label, "new-val") t.Log("save label") - _, err = img.Save() - h.AssertNil(t, err) + + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) h.AssertNil(t, err) @@ -302,8 +341,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#SetEnv", func() { var ( - img imgutil.Image - origID string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { var err error @@ -325,8 +365,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.SetEnv("ENV_KEY", "ENV_VAL") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) h.AssertNil(t, err) @@ -337,8 +377,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#SetWorkingDir", func() { var ( - img imgutil.Image - origID string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { var err error @@ -359,8 +400,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.SetWorkingDir("/some/work/dir") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) h.AssertNil(t, err) @@ -371,8 +412,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#SetEntrypoint", func() { var ( - img imgutil.Image - origID string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { var err error @@ -393,8 +435,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.SetEntrypoint("some", "entrypoint") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) h.AssertNil(t, err) @@ -405,8 +447,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#SetCmd", func() { var ( - img imgutil.Image - origID string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { @@ -428,8 +471,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.SetCmd("some", "cmd") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) h.AssertNil(t, err) @@ -440,8 +483,12 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#Rebase", func() { when("image exists", func() { - var oldBase, oldTopLayer, newBase, origID string - var origNumLayers int + var ( + oldBase, oldTopLayer, newBase, origID string + origNumLayers int + repoName = newLocalTestImageName() + ) + it.Before(func() { var wg sync.WaitGroup wg.Add(1) @@ -492,13 +539,15 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, txt, "old-base\n") // Run rebase - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) - newBaseImg, err := imgutil.NewLocalImage(newBase, dockerClient) + newBaseImg, err := imgutil.NewLocalImage(newBase, dockerClient, imgutil.FromLocalImageBase(newBase)) h.AssertNil(t, err) err = img.Rebase(oldTopLayer, newBaseImg) h.AssertNil(t, err) - _, err = img.Save() + + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, err) // After @@ -527,7 +576,10 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#TopLayer", func() { when("image exists", func() { - var expectedTopLayer string + var ( + expectedTopLayer string + repoName = newLocalTestImageName() + ) it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM busybox @@ -546,7 +598,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns the digest for the top layer (useful for rebasing)", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) actualTopLayer, err := img.TopLayer() @@ -555,13 +607,24 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, actualTopLayer, expectedTopLayer) }) }) + + when("image has no layers", func() { + it("returns error", func() { + img, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient) + h.AssertNil(t, err) + + _, err = img.TopLayer() + h.AssertError(t, err, "has no layers") + }) + }) }) when("#AddLayer", func() { var ( - tarPath string - img imgutil.Image - origID string + tarPath string + img imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` @@ -578,7 +641,11 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) tarPath = tarFile.Name() - img, err = imgutil.NewLocalImage(repoName, dockerClient) + img, err = imgutil.NewLocalImage( + repoName, dockerClient, + imgutil.FromLocalImageBase(repoName), + imgutil.WithPreviousLocalImage(repoName), + ) h.AssertNil(t, err) origID = h.ImageID(t, repoName) }) @@ -593,8 +660,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.AddLayer(tarPath) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) output, err := h.CopySingleFileFromImage(dockerClient, repoName, "old-layer.txt") h.AssertNil(t, err) @@ -608,6 +675,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#GetLayer", func() { when("the layer exists", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM busybox @@ -616,8 +685,12 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { `, repoName), nil) }) + it.After(func() { + h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) + }) + it("returns a layer tar", func() { - img, err := imgutil.NewLocalImage(repoName, dockerClient) + img, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) topLayer, err := img.TopLayer() h.AssertNil(t, err) @@ -640,6 +713,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) when("the layer doesn't exist", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM busybox @@ -647,6 +722,10 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { `, repoName), nil) }) + it.After(func() { + h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) + }) + it("returns an error", func() { img, err := imgutil.NewLocalImage(repoName, dockerClient) h.AssertNil(t, err) @@ -665,42 +744,44 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { layer1SHA string layer2SHA string img imgutil.Image - origID string + prevName = newLocalTestImageName() + repoName = newLocalTestImageName() ) it.Before(func() { var err error - h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` + h.CreateImageOnLocal(t, dockerClient, prevName, fmt.Sprintf(` FROM busybox LABEL repo_name_for_randomisation=%s RUN echo -n old-layer-1 > layer-1.txt RUN echo -n old-layer-2 > layer-2.txt - `, repoName), nil) + `, prevName), nil) - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) + inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), prevName) h.AssertNil(t, err) - origID = inspect.ID layer1SHA = inspect.RootFS.Layers[1] layer2SHA = inspect.RootFS.Layers[2] - img, err = imgutil.NewLocalImage("busybox", dockerClient) - h.AssertNil(t, err) - - img.Rename(repoName) + img, err = imgutil.NewLocalImage( + repoName, + dockerClient, + imgutil.FromLocalImageBase("busybox"), + imgutil.WithPreviousLocalImage(prevName), + ) h.AssertNil(t, err) }) it.After(func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName, origID)) + h.AssertNil(t, h.DockerRmi(dockerClient, repoName, prevName)) }) it("reuses a layer", func() { err := img.ReuseLayer(layer2SHA) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) output, err := h.CopySingleFileFromImage(dockerClient, repoName, "layer-2.txt") h.AssertNil(t, err) @@ -715,8 +796,8 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err := img.ReuseLayer(layer1SHA) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) output, err := h.CopySingleFileFromImage(dockerClient, repoName, "layer-1.txt") h.AssertNil(t, err) @@ -730,9 +811,10 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("#Save", func() { var ( - img imgutil.Image - origID string - tarPath string + img imgutil.Image + origID string + tarPath string + repoName = newLocalTestImageName() ) when("image exists", func() { @@ -772,24 +854,81 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { err = img.AddLayer(tarPath) h.AssertNil(t, err) - imgDigest, err := img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), imgDigest) + inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), result.Digest) h.AssertNil(t, err) label := inspect.Config.Labels["mykey"] h.AssertEq(t, strings.TrimSpace(label), "newValue") t.Log("image has history") - history, err := dockerClient.ImageHistory(context.TODO(), imgDigest) + history, err := dockerClient.ImageHistory(context.TODO(), result.Digest) h.AssertNil(t, err) h.AssertEq(t, len(history), len(inspect.RootFS.Layers)) }) }) + + when("additional names are provided", func() { + var ( + repoName = newLocalTestImageName() + additionalRepoNames = []string{ + repoName + ":" + h.RandString(5), + newLocalTestImageName(), + newLocalTestImageName(), + } + allRepoNames = append([]string{repoName}, additionalRepoNames...) + ) + + it.After(func() { + h.AssertNil(t, h.DockerRmi(dockerClient, allRepoNames...)) + }) + + it("saves to multiple names", func() { + image, err := imgutil.NewLocalImage(repoName, dockerClient) + h.AssertNil(t, err) + + result := image.Save(additionalRepoNames...) + + for _, n := range allRepoNames { + err, ok := result.Outcomes[n] + h.AssertEq(t, ok, true) + h.AssertNil(t, err) + + _, _, err = dockerClient.ImageInspectWithRaw(context.TODO(), n) + h.AssertNil(t, err) + } + }) + + when("a single image name fails", func() { + it("returns results with errors for those that failed", func() { + failingName := newLocalTestImageName() + ":🧨" + + image, err := imgutil.NewLocalImage(repoName, dockerClient) + h.AssertNil(t, err) + + result := image.Save(append([]string{failingName}, additionalRepoNames...)...) + h.AssertError(t, result.Outcomes[failingName], "invalid reference format") + + // check all but failing name + for _, n := range allRepoNames { + err, ok := result.Outcomes[n] + h.AssertEq(t, ok, true) + h.AssertNil(t, err) + + _, _, err = dockerClient.ImageInspectWithRaw(context.TODO(), n) + h.AssertNil(t, err) + } + }) + }) + }) + }) when("#Found", func() { when("it exists", func() { + var repoName = newLocalTestImageName() + it.Before(func() { h.CreateImageOnLocal(t, dockerClient, repoName, fmt.Sprintf(` FROM scratch @@ -802,7 +941,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { }) it("returns true, nil", func() { - image, err := imgutil.NewLocalImage(repoName, dockerClient) + image, err := imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) h.AssertEq(t, image.Found(), true) @@ -811,7 +950,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("it does not exist", func() { it("returns false, nil", func() { - image, err := imgutil.NewLocalImage(repoName, dockerClient) + image, err := imgutil.NewLocalImage(newLocalTestImageName(), dockerClient) h.AssertNil(t, err) h.AssertEq(t, image.Found(), false) @@ -831,8 +970,9 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { when("the image does exist", func() { var ( - origImg imgutil.Image - origID string + origImg imgutil.Image + origID string + repoName = newLocalTestImageName() ) it.Before(func() { @@ -842,7 +982,7 @@ func testLocalImage(t *testing.T, when spec.G, it spec.S) { LABEL repo_name_for_randomisation=%s LABEL mykey=oldValue `, repoName), nil) - origImg, err = imgutil.NewLocalImage(repoName, dockerClient) + origImg, err = imgutil.NewLocalImage(repoName, dockerClient, imgutil.FromLocalImageBase(repoName)) h.AssertNil(t, err) origID = h.ImageID(t, repoName) diff --git a/remote.go b/remote.go index d51dd463..748c59c0 100644 --- a/remote.go +++ b/remote.go @@ -5,7 +5,6 @@ import ( "io" "net/http" "strings" - "sync" "time" "github.com/google/go-containerregistry/pkg/authn" @@ -20,26 +19,65 @@ import ( "github.com/pkg/errors" ) -type remoteImage struct { +type RemoteImage struct { keychain authn.Keychain repoName string image v1.Image prevLayers []v1.Layer - prevOnce *sync.Once } -func NewRemoteImage(repoName string, keychain authn.Keychain) (Image, error) { - image, err := newV1Image(keychain, repoName) +type RemoteImageOption func(*RemoteImage) (*RemoteImage, error) + +func WithPreviousRemoteImage(imageName string) RemoteImageOption { + return func(r *RemoteImage) (*RemoteImage, error) { + var err error + + prevImage, err := newV1Image(r.keychain, imageName) + if err != nil { + return nil, err + } + + prevLayers, err := prevImage.Layers() + if err != nil { + return nil, errors.Wrapf(err, "failed to get layers for previous image with repo name '%s'", imageName) + } + + r.prevLayers = prevLayers + return r, nil + } +} + +func FromRemoteImageBase(imageName string) RemoteImageOption { + return func(r *RemoteImage) (*RemoteImage, error) { + var err error + r.image, err = newV1Image(r.keychain, imageName) + if err != nil { + return nil, err + } + return r, nil + } +} + +func NewRemoteImage(repoName string, keychain authn.Keychain, ops ...RemoteImageOption) (Image, error) { + image, err := emptyRemoteImage() if err != nil { return nil, err } - return &remoteImage{ + ri := &RemoteImage{ keychain: keychain, repoName: repoName, image: image, - prevOnce: &sync.Once{}, - }, nil + } + + for _, op := range ops { + ri, err = op(ri) + if err != nil { + return nil, err + } + } + + return ri, nil } func newV1Image(keychain authn.Keychain, repoName string) (v1.Image, error) { @@ -78,7 +116,7 @@ func referenceForRepoName(keychain authn.Keychain, ref string) (name.Reference, return r, auth, nil } -func (r *remoteImage) Label(key string) (string, error) { +func (r *RemoteImage) Label(key string) (string, error) { cfg, err := r.image.ConfigFile() if err != nil || cfg == nil { return "", fmt.Errorf("failed to get config file for image '%s'", r.repoName) @@ -88,7 +126,7 @@ func (r *remoteImage) Label(key string) (string, error) { } -func (r *remoteImage) Env(key string) (string, error) { +func (r *RemoteImage) Env(key string) (string, error) { cfg, err := r.image.ConfigFile() if err != nil || cfg == nil { return "", fmt.Errorf("failed to get config file for image '%s'", r.repoName) @@ -102,15 +140,15 @@ func (r *remoteImage) Env(key string) (string, error) { return "", nil } -func (r *remoteImage) Rename(name string) { +func (r *RemoteImage) Rename(name string) { r.repoName = name } -func (r *remoteImage) Name() string { +func (r *RemoteImage) Name() string { return r.repoName } -func (r *remoteImage) Found() bool { +func (r *RemoteImage) Found() bool { ref, auth, err := referenceForRepoName(r.keychain, r.repoName) if err != nil { return false @@ -122,7 +160,7 @@ func (r *remoteImage) Found() bool { return true } -func (r *remoteImage) Digest() (string, error) { +func (r *RemoteImage) Digest() (string, error) { hash, err := r.image.Digest() if err != nil { return "", fmt.Errorf("failed to get digest for image '%s': %s", r.repoName, err) @@ -130,7 +168,7 @@ func (r *remoteImage) Digest() (string, error) { return hash.String(), nil } -func (r *remoteImage) CreatedAt() (time.Time, error) { +func (r *RemoteImage) CreatedAt() (time.Time, error) { configFile, err := r.image.ConfigFile() if err != nil { return time.Time{}, fmt.Errorf("failed to get createdAt time for image '%s': %s", r.repoName, err) @@ -138,8 +176,8 @@ func (r *remoteImage) CreatedAt() (time.Time, error) { return configFile.Created.UTC(), nil } -func (r *remoteImage) Rebase(baseTopLayer string, newBase Image) error { - newBaseRemote, ok := newBase.(*remoteImage) +func (r *RemoteImage) Rebase(baseTopLayer string, newBase Image) error { + newBaseRemote, ok := newBase.(*RemoteImage) if !ok { return errors.New("expected new base to be a remote image") } @@ -152,7 +190,7 @@ func (r *remoteImage) Rebase(baseTopLayer string, newBase Image) error { return nil } -func (r *remoteImage) SetLabel(key, val string) error { +func (r *RemoteImage) SetLabel(key, val string) error { configFile, err := r.image.ConfigFile() if err != nil { return err @@ -166,7 +204,7 @@ func (r *remoteImage) SetLabel(key, val string) error { return err } -func (r *remoteImage) SetEnv(key, val string) error { +func (r *RemoteImage) SetEnv(key, val string) error { configFile, err := r.image.ConfigFile() if err != nil { return err @@ -188,7 +226,7 @@ func (r *remoteImage) SetEnv(key, val string) error { return err } -func (r *remoteImage) SetWorkingDir(dir string) error { +func (r *RemoteImage) SetWorkingDir(dir string) error { configFile, err := r.image.ConfigFile() if err != nil { return err @@ -199,7 +237,7 @@ func (r *remoteImage) SetWorkingDir(dir string) error { return err } -func (r *remoteImage) SetEntrypoint(ep ...string) error { +func (r *RemoteImage) SetEntrypoint(ep ...string) error { configFile, err := r.image.ConfigFile() if err != nil { return err @@ -210,7 +248,7 @@ func (r *remoteImage) SetEntrypoint(ep ...string) error { return err } -func (r *remoteImage) SetCmd(cmd ...string) error { +func (r *RemoteImage) SetCmd(cmd ...string) error { configFile, err := r.image.ConfigFile() if err != nil { return err @@ -221,11 +259,14 @@ func (r *remoteImage) SetCmd(cmd ...string) error { return err } -func (r *remoteImage) TopLayer() (string, error) { +func (r *RemoteImage) TopLayer() (string, error) { all, err := r.image.Layers() if err != nil { return "", err } + if len(all) == 0 { + return "", fmt.Errorf("image %s has no layers", r.Name()) + } topLayer := all[len(all)-1] hex, err := topLayer.DiffID() if err != nil { @@ -234,11 +275,21 @@ func (r *remoteImage) TopLayer() (string, error) { return hex.String(), nil } -func (r *remoteImage) GetLayer(string) (io.ReadCloser, error) { - panic("not implemented") +func (r *RemoteImage) GetLayer(sha string) (io.ReadCloser, error) { + layers, err := r.image.Layers() + if err != nil { + return nil, err + } + + layer, err := findLayerWithSha(layers, sha) + if err != nil { + return nil, err + } + + return layer.Compressed() } -func (r *remoteImage) AddLayer(path string) error { +func (r *RemoteImage) AddLayer(path string) error { layer, err := tarball.LayerFromFile(path) if err != nil { return err @@ -250,27 +301,7 @@ func (r *remoteImage) AddLayer(path string) error { return nil } -func (r *remoteImage) ReuseLayer(sha string) error { - var outerErr error - - r.prevOnce.Do(func() { - prevImage, err := newV1Image(r.keychain, r.repoName) - if err != nil { - outerErr = err - return - } - r.prevLayers, err = prevImage.Layers() - if err != nil { - outerErr = fmt.Errorf("failed to get layers for previous image with repo name '%s': %s", r.repoName, err) - } - }) - if outerErr != nil { - return outerErr - } - if len(r.prevLayers) == 0 { - return fmt.Errorf("there is no previous image with name '%s'", r.repoName) - } - +func (r *RemoteImage) ReuseLayer(sha string) error { layer, err := findLayerWithSha(r.prevLayers, sha) if err != nil { return err @@ -292,30 +323,46 @@ func findLayerWithSha(layers []v1.Layer, sha string) (v1.Layer, error) { return nil, fmt.Errorf(`previous image did not have layer with sha '%s'`, sha) } -func (r *remoteImage) Save() (string, error) { - ref, auth, err := referenceForRepoName(r.keychain, r.repoName) +func (r *RemoteImage) Save(additionalNames ...string) SaveResult { + var err error + + allNames := append([]string{r.repoName}, additionalNames...) + + r.image, err = mutate.CreatedAt(r.image, v1.Time{Time: time.Now()}) if err != nil { - return "", err + return NewFailedResult(allNames, err) } - r.image, err = mutate.CreatedAt(r.image, v1.Time{Time: time.Now()}) + hex, err := r.image.Digest() if err != nil { - return "", err + return NewFailedResult(allNames, err) } - if err := remote.Write(ref, r.image, remote.WithAuth(auth)); err != nil { - return "", err + var errs = map[string]error{} + for _, n := range append([]string{r.repoName}, additionalNames...) { + errs[n] = r.doSave(n) } - hex, err := r.image.Digest() + return SaveResult{ + Digest: hex.String(), + Outcomes: errs, + } +} + +func (r *RemoteImage) doSave(imageName string) error { + ref, auth, err := referenceForRepoName(r.keychain, imageName) if err != nil { - return "", err + return err } - return hex.String(), nil + if err := remote.Write(ref, r.image, remote.WithAuth(auth)); err != nil { + return err + } + + return nil } -func (r *remoteImage) Delete() error { +func (r *RemoteImage) Delete() error { return errors.New("remote image does not implement Delete") } diff --git a/remote_test.go b/remote_test.go index 37e92811..917843fe 100644 --- a/remote_test.go +++ b/remote_test.go @@ -26,6 +26,10 @@ import ( var registryPort string +func newRemoteTestImageName() string { + return "localhost:" + registryPort + "/pack-image-test-" + h.RandString(10) +} + func TestRemoteImage(t *testing.T) { rand.Seed(time.Now().UTC().UnixNano()) @@ -46,10 +50,71 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { var err error dockerClient = h.DockerCli(t) h.AssertNil(t, err) - repoName = "localhost:" + registryPort + "/pack-image-test-" + h.RandString(10) + repoName = newRemoteTestImageName() + }) + + when("#NewRemote", func() { + when("#FromRemoteBaseImage", func() { + when("base image exists", func() { + var ( + baseName = "busybox" + err error + existingLayerSha string + ) + + it.Before(func() { + err = h.PullImage(dockerClient, baseName) + h.AssertNil(t, err) + + inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), baseName) + h.AssertNil(t, err) + + existingLayerSha = inspect.RootFS.Layers[0] + }) + + it("sets the initial state to match the base image", func() { + img, err := imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.FromRemoteImageBase(baseName), + ) + h.AssertNil(t, err) + + readCloser, err := img.GetLayer(existingLayerSha) + h.AssertNil(t, err) + defer readCloser.Close() + }) + }) + + when("base image does not exist", func() { + it("don't error", func() { + _, err := imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.FromRemoteImageBase("some-bad-repo-name"), + ) + + h.AssertNil(t, err) + }) + }) + }) + + when("#WithPreviousImage", func() { + when("previous image does not exist", func() { + it("don't error", func() { + _, err := imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.WithPreviousRemoteImage("some-bad-repo-name"), + ) + + h.AssertNil(t, err) + }) + }) + }) }) - when("#label", func() { + when("#Label", func() { when("image exists", func() { var img imgutil.Image it.Before(func() { @@ -60,7 +125,10 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { `, repoName), nil) var err error - img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + img, err = imgutil.NewRemoteImage( + repoName, authn.DefaultKeychain, + imgutil.FromRemoteImageBase(repoName), + ) h.AssertNil(t, err) }) @@ -77,7 +145,7 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { }) }) - when("image NOT exists", func() { + when("image is empty", func() { it("returns an empty label", func() { img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) @@ -91,34 +159,39 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { when("#Env", func() { when("image exists", func() { + var ( + img imgutil.Image + ) it.Before(func() { h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` FROM scratch LABEL repo_name_for_randomisation=%s ENV MY_VAR=my_val `, repoName), nil) - }) - it("returns the label value", func() { - img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + var err error + img, err = imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.FromRemoteImageBase(repoName), + ) h.AssertNil(t, err) + }) + it("returns the label value", func() { val, err := img.Env("MY_VAR") h.AssertNil(t, err) h.AssertEq(t, val, "my_val") }) it("returns an empty string for a missing label", func() { - img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) - h.AssertNil(t, err) - val, err := img.Env("MISSING_VAR") h.AssertNil(t, err) h.AssertEq(t, val, "") }) }) - when("image NOT exists", func() { + when("image is empty", func() { it("returns an empty string", func() { img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) @@ -141,7 +214,7 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { when("#CreatedAt", func() { const reference = "busybox@sha256:f79f7a10302c402c052973e3fa42be0344ae6453245669783a9e16da3d56d5b4" it("returns the containers created at time", func() { - img, err := imgutil.NewRemoteImage(reference, authn.DefaultKeychain) + img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain, imgutil.FromRemoteImageBase(reference)) h.AssertNil(t, err) expectedTime := time.Date(2019, 4, 2, 23, 32, 10, 727183061, time.UTC) @@ -157,7 +230,11 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { it("returns the image digest", func() { // The SHA of a particular iteration of busybox:1.29 expectedDigest := "sha256:915f390a8912e16d4beb8689720a17348f3f6d1a7b659697df850ab625ea29d5" - img, err := imgutil.NewRemoteImage("busybox@sha256:915f390a8912e16d4beb8689720a17348f3f6d1a7b659697df850ab625ea29d5", authn.DefaultKeychain) + img, err := imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.FromRemoteImageBase("busybox@sha256:915f390a8912e16d4beb8689720a17348f3f6d1a7b659697df850ab625ea29d5"), + ) h.AssertNil(t, err) digest, err := img.Digest() h.AssertNil(t, err) @@ -169,12 +246,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { var img imgutil.Image when("image exists", func() { it.Before(func() { - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` - FROM scratch - LABEL repo_name_for_randomisation=%s - LABEL mykey=myvalue other=data - `, repoName), nil) - var err error img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) @@ -189,8 +260,9 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { it("saves label", func() { h.AssertNil(t, img.SetLabel("mykey", "new-val")) - _, err := img.Save() - h.AssertNil(t, err) + + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) // After Pull label := remoteLabel(t, dockerClient, repoName, "mykey") @@ -205,11 +277,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { var err error - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` - FROM scratch - LABEL repo_name_for_randomisation=%s - LABEL some-key=some-value - `, repoName), nil) img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) }) @@ -218,8 +285,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err := img.SetEnv("ENV_KEY", "ENV_VAL") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) defer h.DockerRmi(dockerClient, repoName) @@ -237,10 +304,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { var err error - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` - FROM scratch - LABEL repo_name_for_randomisation=%s - `, repoName), nil) img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) }) @@ -249,8 +312,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err := img.SetWorkingDir("/some/work/dir") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) defer h.DockerRmi(dockerClient, repoName) @@ -268,10 +331,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { var err error - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` - FROM scratch - LABEL repo_name_for_randomisation=%s - `, repoName), nil) img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) }) @@ -280,8 +339,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err := img.SetEntrypoint("some", "entrypoint") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) defer h.DockerRmi(dockerClient, repoName) @@ -299,10 +358,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { var err error - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` - FROM scratch - LABEL repo_name_for_randomisation=%s - `, repoName), nil) img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) }) @@ -311,8 +366,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err := img.SetCmd("some", "cmd") h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) defer h.DockerRmi(dockerClient, repoName) @@ -376,14 +431,14 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { ) // Run rebase - img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain, imgutil.FromRemoteImageBase(repoName)) h.AssertNil(t, err) - newBaseImg, err := imgutil.NewRemoteImage(newBase, authn.DefaultKeychain) + newBaseImg, err := imgutil.NewRemoteImage(newBase, authn.DefaultKeychain, imgutil.FromRemoteImageBase(newBase)) h.AssertNil(t, err) err = img.Rebase(oldTopLayer, newBaseImg) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) // After h.AssertEq(t, @@ -404,7 +459,7 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { RUN echo text-old-base > otherfile.txt `, repoName), nil) - img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain, imgutil.FromRemoteImageBase(repoName)) h.AssertNil(t, err) actualTopLayer, err := img.TopLayer() @@ -413,6 +468,16 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, actualTopLayer, expectedTopLayer) }) }) + + when("the image has no layers", func() { + it("returns an error", func() { + img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + _, err = img.TopLayer() + h.AssertError(t, err, "has no layers") + }) + }) }) when("#AddLayer", func() { @@ -435,7 +500,7 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) tarPath = tarFile.Name() - img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + img, err = imgutil.NewRemoteImage(repoName, authn.DefaultKeychain, imgutil.FromRemoteImageBase(repoName)) h.AssertNil(t, err) }) @@ -448,8 +513,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err := img.AddLayer(tarPath) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) // After Pull h.AssertNil(t, h.PullImage(dockerClient, repoName)) @@ -467,40 +532,47 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { when("#ReuseLayer", func() { when("previous image", func() { var ( - layer2SHA string - img imgutil.Image + layer2SHA string + img imgutil.Image + prevImageName string ) it.Before(func() { var err error - h.CreateImageOnRemote(t, dockerClient, repoName, fmt.Sprintf(` + prevImageName = "localhost:" + registryPort + "/pack-image-test-" + h.RandString(10) + h.CreateImageOnRemote(t, dockerClient, prevImageName, fmt.Sprintf(` FROM busybox LABEL repo_name_for_randomisation=%s RUN echo -n old-layer-1 > layer-1.txt RUN echo -n old-layer-2 > layer-2.txt `, repoName), nil) - h.AssertNil(t, h.PullImage(dockerClient, repoName)) - defer func() { - h.AssertNil(t, h.DockerRmi(dockerClient, repoName)) - }() - inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), repoName) + h.AssertNil(t, h.PullImage(dockerClient, prevImageName)) + inspect, _, err := dockerClient.ImageInspectWithRaw(context.TODO(), prevImageName) h.AssertNil(t, err) layer2SHA = inspect.RootFS.Layers[2] - img, err = imgutil.NewRemoteImage("busybox", authn.DefaultKeychain) + img, err = imgutil.NewRemoteImage( + repoName, + authn.DefaultKeychain, + imgutil.WithPreviousRemoteImage(prevImageName), + imgutil.FromRemoteImageBase("busybox"), + ) h.AssertNil(t, err) }) + it.After(func() { + h.AssertNil(t, h.DockerRmi(dockerClient, prevImageName)) + }) + it("reuses a layer", func() { - img.Rename(repoName) err := img.ReuseLayer(layer2SHA) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) defer h.DockerRmi(dockerClient, repoName) @@ -520,16 +592,6 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, err, "previous image did not have layer with sha 'some-bad-sha'") }) }) - - it("returns errors on nonexistent prev image", func() { - img, err := imgutil.NewRemoteImage("busybox", authn.DefaultKeychain) - h.AssertNil(t, err) - img.Rename("some-bad-repo-name") - - err = img.ReuseLayer("some-bad-sha") - - h.AssertError(t, err, "there is no previous image with name 'some-bad-repo-name'") - }) }) when("#Save", func() { @@ -549,11 +611,11 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { err = img.SetLabel("mykey", "newValue") h.AssertNil(t, err) - imgDigest, err := img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) // After Pull - label := remoteLabel(t, dockerClient, repoName+"@"+imgDigest, "mykey") + label := remoteLabel(t, dockerClient, repoName+"@"+result.Digest, "mykey") h.AssertEq(t, "newValue", label) }) @@ -568,8 +630,8 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { img, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) h.AssertNil(t, err) - _, err = img.Save() - h.AssertNil(t, err) + result := img.Save() + h.AssertNil(t, result.Outcomes[repoName]) h.AssertNil(t, h.PullImage(dockerClient, repoName)) inspect, _, err = dockerClient.ImageInspectWithRaw(context.TODO(), repoName) @@ -586,6 +648,58 @@ func testRemoteImage(t *testing.T, when spec.G, it spec.S) { } }) }) + + when("additional names are provided", func() { + var ( + repoName = newRemoteTestImageName() + additionalRepoNames = []string{ + repoName + ":" + h.RandString(5), + newRemoteTestImageName(), + newRemoteTestImageName(), + } + allRepoNames = append([]string{repoName}, additionalRepoNames...) + ) + + it.After(func() { + h.AssertNil(t, h.DockerRmi(dockerClient, allRepoNames...)) + }) + + it("saves to multiple names", func() { + image, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + result := image.Save(additionalRepoNames...) + + for _, n := range allRepoNames { + err, ok := result.Outcomes[n] + h.AssertEq(t, ok, true) + h.AssertNil(t, err) + + h.AssertNil(t, h.PullImage(dockerClient, n)) + } + }) + + when("a single image name fails", func() { + it("returns results with errors for those that failed", func() { + failingName := newRemoteTestImageName() + ":🧨" + + image, err := imgutil.NewRemoteImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + result := image.Save(append([]string{failingName}, additionalRepoNames...)...) + h.AssertError(t, result.Outcomes[failingName], "could not parse reference") + + // check all but failing name + for _, n := range allRepoNames { + err, ok := result.Outcomes[n] + h.AssertEq(t, ok, true) + h.AssertNil(t, err) + + h.AssertNil(t, h.PullImage(dockerClient, n)) + } + }) + }) + }) }) when("#Found", func() { diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 49c9710e..a3fae2ef 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -38,15 +38,31 @@ func AssertEq(t *testing.T, actual, expected interface{}) { } } -func AssertContains(t *testing.T, slice []string, expected string) { +func AssertContains(t *testing.T, slice []string, elements ...string) { t.Helper() - for _, actual := range slice { - if diff := cmp.Diff(actual, expected); diff == "" { - return + +outer: + for _, el := range elements { + for _, actual := range slice { + if diff := cmp.Diff(actual, el); diff == "" { + continue outer + } } + + t.Fatalf("Expected %+v to contain: %s", slice, el) } - t.Fatalf("Expected %+v to contain: %s", slice, expected) +} +func AssertDoesNotContain(t *testing.T, slice []string, elements ...string) { + t.Helper() + + for _, el := range elements { + for _, actual := range slice { + if diff := cmp.Diff(actual, el); diff == "" { + t.Fatalf("Expected %+v to NOT contain: %s", slice, el) + } + } + } } func AssertMatch(t *testing.T, actual string, expected *regexp.Regexp) {