Skip to content
This repository was archived by the owner on Mar 27, 2024. It is now read-only.

Handle error gracefully when we can't retrieve an image #251

Merged
merged 6 commits into from
Jul 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ limitations under the License.
package cmd

import (
"errors"
"fmt"
"os"

"github.com/GoogleContainerTools/container-diff/cmd/util/output"
"github.com/GoogleContainerTools/container-diff/differs"
pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -56,27 +56,27 @@ func checkAnalyzeArgNum(args []string) error {
func analyzeImage(imageName string, analyzerArgs []string) error {
analyzeTypes, err := differs.GetAnalyzers(analyzerArgs)
if err != nil {
return err
return errors.Wrap(err, "getting analyzers")
}

image, err := getImageForName(imageName)
if err != nil {
return err
return errors.Wrapf(err, "error retrieving image %s", imageName)
}

if noCache && !save {
defer pkgutil.CleanupImage(image)
}
if err != nil {
return fmt.Errorf("Error processing image: %s", err)
return fmt.Errorf("error processing image: %s", err)
}

req := differs.SingleRequest{
Image: image,
AnalyzeTypes: analyzeTypes}
analyses, err := req.GetAnalysis()
if err != nil {
return fmt.Errorf("Error performing image analysis: %s", err)
return fmt.Errorf("error performing image analysis: %s", err)
}

logrus.Info("retrieving analyses")
Expand Down
8 changes: 4 additions & 4 deletions cmd/analyze_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ var analyzeArgNumTests = []testpair{
func TestAnalyzeArgNum(t *testing.T) {
for _, test := range analyzeArgNumTests {
err := checkAnalyzeArgNum(test.input)
if (err == nil) != test.expected_output {
if test.expected_output {
t.Errorf("Got unexpected error: %s", err)
} else {
if (err == nil) != test.shouldError {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, shouldError is a way better name!

if test.shouldError {
t.Errorf("Expected error but got none")
} else {
t.Errorf("Got unexpected error: %s", err)
}
}
}
Expand Down
65 changes: 45 additions & 20 deletions cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ limitations under the License.
package cmd

import (
"errors"
"fmt"
"os"
"strings"
"sync"

"github.com/GoogleContainerTools/container-diff/cmd/util/output"
"github.com/GoogleContainerTools/container-diff/differs"
pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util"
"github.com/GoogleContainerTools/container-diff/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -66,50 +67,74 @@ func checkFilenameFlag(_ []string) error {
return nil
}
}
return errors.New("Please include --types=file with the --filename flag")
return errors.New("please include --types=file with the --filename flag")
}

// processImage is a concurrency-friendly wrapper around getImageForName
func processImage(imageName string, imageMap map[string]*pkgutil.Image, wg *sync.WaitGroup, errChan chan<- error) {
defer wg.Done()
image, err := getImageForName(imageName)
if err != nil {
errChan <- fmt.Errorf("error retrieving image %s: %s", imageName, err)
}
imageMap[imageName] = &image
}

// collects errors from a channel and combines them
// assumes channel has already been closed
func readErrorsFromChannel(c chan error) error {
errs := []string{}
for {
err, ok := <-c
if !ok {
break
}
errs = append(errs, err.Error())
}

if len(errs) > 0 {
return errors.New(strings.Join(errs, "\n"))
}
return nil
}

func diffImages(image1Arg, image2Arg string, diffArgs []string) error {
diffTypes, err := differs.GetAnalyzers(diffArgs)
if err != nil {
return err
return errors.Wrap(err, "getting analyzers")
}

var wg sync.WaitGroup
wg.Add(2)

logrus.Infof("starting diff on images %s and %s, using differs: %s\n", image1Arg, image2Arg, diffArgs)

imageMap := map[string]*pkgutil.Image{
image1Arg: {},
image2Arg: {},
}
// TODO: fix error handling here
for imageArg := range imageMap {
go func(imageName string, imageMap map[string]*pkgutil.Image) {
defer wg.Done()
image, err := getImageForName(imageName)
imageMap[imageName] = &image
if err != nil {
logrus.Warningf("Diff may be inaccurate: %s", err)
}
}(imageArg, imageMap)
}
imageMap := map[string]*pkgutil.Image{}
errChan := make(chan error, 2)

go processImage(image1Arg, imageMap, &wg, errChan)
go processImage(image2Arg, imageMap, &wg, errChan)

wg.Wait()
close(errChan)

if noCache && !save {
defer pkgutil.CleanupImage(*imageMap[image1Arg])
defer pkgutil.CleanupImage(*imageMap[image2Arg])
}

if err := readErrorsFromChannel(errChan); err != nil {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

niiiiiiiice

return err
}

logrus.Info("computing diffs")
req := differs.DiffRequest{
Image1: *imageMap[image1Arg],
Image2: *imageMap[image2Arg],
DiffTypes: diffTypes}
diffs, err := req.GetDiff()
if err != nil {
return fmt.Errorf("Could not retrieve diff: %s", err)
return fmt.Errorf("could not retrieve diff: %s", err)
}
outputResults(diffs)

Expand All @@ -122,7 +147,7 @@ func diffImages(image1Arg, image2Arg string, diffArgs []string) error {
}

if noCache && save {
logrus.Infof("Images were saved at %s and %s", imageMap[image1Arg].FSPath,
logrus.Infof("images were saved at %s and %s", imageMap[image1Arg].FSPath,
imageMap[image2Arg].FSPath)
}
return nil
Expand Down
45 changes: 35 additions & 10 deletions cmd/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,46 @@ import (
)

var diffArgNumTests = []testpair{
{[]string{}, false},
{[]string{"one"}, false},
{[]string{"one", "two"}, true},
{[]string{"one", "two", "three"}, false},
{[]string{}, true},
{[]string{"one"}, true},
{[]string{"one", "two"}, false},
{[]string{"one", "two", "three"}, true},
}

func TestDiffArgNum(t *testing.T) {
for _, test := range diffArgNumTests {
err := checkDiffArgNum(test.input)
if (err == nil) != test.expected_output {
if test.expected_output {
t.Errorf("Got unexpected error: %s", err)
} else {
t.Errorf("Expected error but got none")
}
checkError(t, err, test.shouldError)
}
}

type imageDiff struct {
image1 string
image2 string
shouldError bool
}

var imageDiffs = []imageDiff{
{"", "", true},
{"gcr.io/google-appengine/python", "gcr.io/google-appengine/debian9", false},
{"gcr.io/google-appengine/python", "cats", true},
}

func TestDiffImages(t *testing.T) {
for _, test := range imageDiffs {
err := diffImages(test.image1, test.image2, []string{"apt"})
checkError(t, err, test.shouldError)
err = diffImages(test.image1, test.image2, []string{"metadata"})
checkError(t, err, test.shouldError)
}
}

func checkError(t *testing.T, err error, shouldError bool) {
if (err == nil) == shouldError {
if shouldError {
t.Errorf("expected error but got none")
} else {
t.Errorf("got unexpected error: %s", err)
}
}
}
33 changes: 20 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/GoogleContainerTools/container-diff/util"
"github.com/google/go-containerregistry/pkg/v1"
homedir "github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -131,6 +132,9 @@ func checkIfValidAnalyzer(_ []string) error {
return nil
}

// getImageForName infers the source of an image and retrieves a v1.Image reference to it.
// Once a reference is obtained, it attempts to unpack the v1.Image's reader's contents
// into a temp directory on the local filesystem.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ty ty, makes sense now!

func getImageForName(imageName string) (pkgutil.Image, error) {
logrus.Infof("retrieving image: %s", imageName)
var img v1.Image
Expand All @@ -139,17 +143,17 @@ func getImageForName(imageName string) (pkgutil.Image, error) {
start := time.Now()
img, err = tarball.ImageFromPath(imageName, nil)
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "retrieving tar from path")
}
elapsed := time.Now().Sub(start)
logrus.Infof("retrieving image from tar took %f seconds", elapsed.Seconds())
logrus.Infof("retrieving image ref from tar took %f seconds", elapsed.Seconds())
} else if strings.HasPrefix(imageName, DaemonPrefix) {
// remove the daemon prefix
imageName = strings.Replace(imageName, DaemonPrefix, "", -1)

ref, err := name.ParseReference(imageName, name.WeakValidation)
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "parsing image reference")
}

start := time.Now()
Expand All @@ -158,28 +162,28 @@ func getImageForName(imageName string) (pkgutil.Image, error) {
Buffer: true,
})
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "retrieving image from daemon")
}
elapsed := time.Now().Sub(start)
logrus.Infof("retrieving image from daemon took %f seconds", elapsed.Seconds())
logrus.Infof("retrieving local image ref took %f seconds", elapsed.Seconds())
} else {
// either has remote prefix or has no prefix, in which case we force remote
imageName = strings.Replace(imageName, RemotePrefix, "", -1)
ref, err := name.ParseReference(imageName, name.WeakValidation)
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "parsing image reference")
}
auth, err := authn.DefaultKeychain.Resolve(ref.Context().Registry)
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "resolving auth")
}
start := time.Now()
img, err = remote.Image(ref, auth, http.DefaultTransport)
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "retrieving remote image")
}
elapsed := time.Now().Sub(start)
logrus.Infof("retrieving remote image took %f seconds", elapsed.Seconds())
logrus.Infof("retrieving remote image ref took %f seconds", elapsed.Seconds())
}

// create tempdir and extract fs into it
Expand All @@ -188,7 +192,7 @@ func getImageForName(imageName string) (pkgutil.Image, error) {
start := time.Now()
imgLayers, err := img.Layers()
if err != nil {
return pkgutil.Image{}, err
return pkgutil.Image{}, errors.Wrap(err, "getting image layers")
}
for _, layer := range imgLayers {
layerStart := time.Now()
Expand All @@ -197,12 +201,12 @@ func getImageForName(imageName string) (pkgutil.Image, error) {
if err != nil {
return pkgutil.Image{
Layers: layers,
}, err
}, errors.Wrap(err, "getting extract path for layer")
}
if err := pkgutil.GetFileSystemForLayer(layer, path, nil); err != nil {
return pkgutil.Image{
Layers: layers,
}, err
}, errors.Wrap(err, "getting filesystem for layer")
}
layers = append(layers, pkgutil.Layer{
FSPath: path,
Expand All @@ -215,12 +219,15 @@ func getImageForName(imageName string) (pkgutil.Image, error) {
}

path, err := getExtractPathForImage(imageName, img)
if err != nil {
return pkgutil.Image{}, err
}
// extract fs into provided dir
if err := pkgutil.GetFileSystemForImage(img, path, nil); err != nil {
return pkgutil.Image{
FSPath: path,
Layers: layers,
}, err
}, errors.Wrap(err, "getting filesystem for image")
}
return pkgutil.Image{
Image: img,
Expand Down
4 changes: 2 additions & 2 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ limitations under the License.
package cmd

type testpair struct {
input []string
expected_output bool
input []string
shouldError bool
}
Loading