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

Single image analysis #20

Merged
merged 8 commits into from
Aug 18, 2017
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 10 additions & 2 deletions .container-diff-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@ while IFS=$' \n\r' read -r flag differ image1 image2 file; do
fi
done < tests/differ_runs.txt

while IFS=$' \n\r' read -r flag analyzer image file; do
go run main.go $image $flag -j > $file
if [[ $? -ne 0 ]]; then
echo "container-diff" "$analyzer" "analyzer failed"
exit 1
fi
done < tests/analyzer_runs.txt

success=0
while IFS=$' \n\r' read -r differ actual expected; do
while IFS=$' \n\r' read -r type analyzer actual expected; do
diff=$(jq --argfile a "$actual" --argfile b "$expected" -n 'def walk(f): . as $in | if type == "object" then reduce keys[] as $key ( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f elif type == "array" then map( walk(f) ) | f else f end; ($a | walk(if type == "array" then sort else . end)) as $a | ($b | walk(if type == "array" then sort else . end)) as $b | $a == $b')
if ! "$diff" ; then
echo "container diff" "$differ" "diff output is not as expected"
echo "container-diff" "$analyzer" "$type" "output is not as expected"
success=1
fi
done < tests/diff_comparisons.txt
Expand Down
265 changes: 174 additions & 91 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
goflag "flag"
"fmt"
"os"
"reflect"
"sort"
"sync"

Expand All @@ -18,16 +19,15 @@ import (

var json bool
var eng bool
var save bool

var apt bool
var node bool
var file bool
var history bool
var pip bool

var save bool

var diffFlagMap = map[string]*bool{
var analyzeFlagMap = map[string]*bool{
"apt": &apt,
"node": &node,
"file": &file,
Expand All @@ -36,9 +36,9 @@ var diffFlagMap = map[string]*bool{
}

var RootCmd = &cobra.Command{
Use: "[image1] [image2]",
Short: "Compare two images.",
Long: `Compares two images using the specifed differs as indicated via flags (see documentation for available differs).`,
Use: "To analyze a single image: [image]. To compare two images: [image1] [image2]",
Short: "Analyze a single image or compare two images.",
Long: `Analyzes a single image or compares two images using the specifed analyzers/differs as indicated via flags (see documentation for available ones).`,
Run: func(cmd *cobra.Command, args []string) {
if validArgs, err := validateArgs(args); !validArgs {
glog.Error(err.Error())
Expand All @@ -47,104 +47,190 @@ var RootCmd = &cobra.Command{

utils.SetDockerEngine(eng)

img1Arg := args[0]
img2Arg := args[1]
diffArgs := []string{}
allDiffers := getAllDiffers()
for _, name := range allDiffers {
if *diffFlagMap[name] == true {
diffArgs = append(diffArgs, name)
analyzeArgs := []string{}
allAnalyzers := getAllAnalyzers()
for _, name := range allAnalyzers {
if *analyzeFlagMap[name] == true {
analyzeArgs = append(analyzeArgs, name)
}
}
// If no differs are specified, perform all diffs as the default
if len(diffArgs) == 0 {
diffArgs = allDiffers

// If no differs/analyzers are specified, perform them all as the default
if len(analyzeArgs) == 0 {
analyzeArgs = allAnalyzers
}

var wg sync.WaitGroup
wg.Add(2)
var err error
// In the case of one image, "analyzes" it
// In the case of two images, takes their "diff"
if len(args) == 1 {
Copy link
Contributor

Choose a reason for hiding this comment

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

It makes more sense to me to have analyze as a separate command (such that the logic wouldn't be in root.go) both for clarity and for example in case of future additional commands. If someone adds a command and pattern matches, they seem forced into expanding this if else structure. The primary logic already seems separated out so is there a reason not to pull it into it's own command file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just thought only one command would be preferable for our tool... @nkubala @aaron-prindle do you have any thoughts as to whether we should split this up into a separate command?

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think this structure is fine. We should probably at least put a comment here saying semantically what each case means (analyze = one image, diff = two), and then put something about it in the usage/--help command. I do like the tool choosing which path to take based on how many images you provide it though, seems intuitive to me

err = analyzeImage(args[0], analyzeArgs)
} else {
err = diffImages(args[0], args[1], analyzeArgs)
}

glog.Infof("Starting diff on images %s and %s, using differs: %s", img1Arg, img2Arg, diffArgs)
if err != nil {
glog.Error(err)
os.Exit(1)
}
},
}

var image1, image2 utils.Image
var err error
go func() {
defer wg.Done()
image1, err = utils.ImagePrepper{img1Arg}.GetImage()
if err != nil {
glog.Error(err.Error())
os.Exit(1)
}
}()
func diffImages(image1Arg, image2Arg string, diffArgs []string) error {
var wg sync.WaitGroup
wg.Add(2)

go func() {
defer wg.Done()
image2, err = utils.ImagePrepper{img2Arg}.GetImage()
if err != nil {
glog.Error(err.Error())
os.Exit(1)
}
}()
glog.Infof("Starting diff on images %s and %s, using differs: %s", image1Arg, image2Arg, diffArgs)

diffTypes, err := differs.GetDiffers(diffArgs)
var image1, image2 utils.Image
var err error
go func() {
defer wg.Done()
image1, err = utils.ImagePrepper{image1Arg}.GetImage()
if err != nil {
glog.Error(err.Error())
os.Exit(1)
}
wg.Wait()
}()

req := differs.DiffRequest{image1, image2, diffTypes}
if diffs, err := req.GetDiff(); err == nil {
// Outputs diff results in alphabetical order by differ name
diffTypes := []string{}
for name := range diffs {
diffTypes = append(diffTypes, name)
}
sort.Strings(diffTypes)
glog.Info("Retrieving diffs")
diffResults := []utils.DiffResult{}
for _, diffType := range diffTypes {
diff := diffs[diffType]
if json {
diffResults = append(diffResults, diff.GetStruct())
} else {
err = diff.OutputText(diffType)
if err != nil {
glog.Error(err)
}
go func() {
defer wg.Done()
image2, err = utils.ImagePrepper{image2Arg}.GetImage()
if err != nil {
glog.Error(err.Error())
}
}()
wg.Wait()
if err != nil {
cleanupImage(image1)
cleanupImage(image2)
return errors.New("Could not perform image diff")
}

diffTypes, err := differs.GetAnalyzers(diffArgs)
if err != nil {
glog.Error(err.Error())
cleanupImage(image1)
cleanupImage(image2)
return errors.New("Could not perform image diff")
}

req := differs.DiffRequest{image1, image2, diffTypes}
if diffs, err := req.GetDiff(); err == nil {
// Outputs diff results in alphabetical order by differ name
sortedTypes := []string{}
for name := range diffs {
sortedTypes = append(sortedTypes, name)
}
sort.Strings(sortedTypes)
glog.Info("Retrieving diffs")
diffResults := []utils.DiffResult{}
for _, diffType := range sortedTypes {
diff := diffs[diffType]
if json {
diffResults = append(diffResults, diff.GetStruct())
} else {
err = diff.OutputText(diffType)
if err != nil {
glog.Error(err)
}
}
}
if json {
err = utils.JSONify(diffResults)
if err != nil {
glog.Error(err)
}
}
if !save {
cleanupImage(image1)
cleanupImage(image2)

} else {
dir, _ := os.Getwd()
glog.Infof("Images were saved at %s as %s and %s", dir, image1.FSPath, image2.FSPath)
}
} else {
glog.Error(err.Error())
cleanupImage(image1)
cleanupImage(image2)
return errors.New("Could not perform image diff")
}

return nil
}

func analyzeImage(imageArg string, analyzerArgs []string) error {
image, err := utils.ImagePrepper{imageArg}.GetImage()
if err != nil {
glog.Error(err.Error())
cleanupImage(image)
return errors.New("Could not perform image analysis")
}
analyzeTypes, err := differs.GetAnalyzers(analyzerArgs)
if err != nil {
glog.Error(err.Error())
cleanupImage(image)
return errors.New("Could not perform image analysis")
}

req := differs.SingleRequest{image, analyzeTypes}
if analyses, err := req.GetAnalysis(); err == nil {
// Outputs analysis results in alphabetical order by differ name
sortedTypes := []string{}
for name := range analyses {
sortedTypes = append(sortedTypes, name)
}
sort.Strings(sortedTypes)
glog.Info("Retrieving diffs")
analyzeResults := []utils.AnalyzeResult{}
for _, analyzeType := range sortedTypes {
analysis := analyses[analyzeType]
if json {
err = utils.JSONify(diffResults)
analyzeResults = append(analyzeResults, analysis.GetStruct())
} else {
err = analysis.OutputText(analyzeType)
if err != nil {
glog.Error(err)
}
}
fmt.Println()
glog.Info("Removing image file system directories from system")
if !save {
errMsg := remove(image1.FSPath, true)
errMsg += remove(image2.FSPath, true)
if errMsg != "" {
glog.Error(errMsg)
}
} else {
dir, _ := os.Getwd()
glog.Infof("Images were saved at %s as %s and %s", dir, image1.FSPath, image2.FSPath)
}
if json {
err = utils.JSONify(analyzeResults)
if err != nil {
glog.Error(err)
}
}
if !save {
cleanupImage(image)
} else {
glog.Error(err.Error())
os.Exit(1)
dir, _ := os.Getwd()
glog.Infof("Image was saved at %s as %s", dir, image.FSPath)
}
},
} else {
glog.Error(err.Error())
cleanupImage(image)
return errors.New("Could not perform image analysis")
}

return nil
}

func cleanupImage(image utils.Image) {
if !reflect.DeepEqual(image, (utils.Image{})) {
glog.Infof("Removing image filesystem directory %s from system", image.FSPath)
errMsg := remove(image.FSPath, true)
if errMsg != "" {
glog.Error(errMsg)
}
}
}

func getAllDiffers() []string {
allDiffers := []string{}
for name := range diffFlagMap {
allDiffers = append(allDiffers, name)
func getAllAnalyzers() []string {
allAnalyzers := []string{}
for name := range analyzeFlagMap {
allAnalyzers = append(allAnalyzers, name)
}
return allDiffers
return allAnalyzers
}

func validateArgs(args []string) (bool, error) {
Expand All @@ -165,11 +251,11 @@ func validateArgs(args []string) (bool, error) {

func checkArgNum(args []string) (bool, error) {
var errMessage string
if len(args) < 2 {
errMessage = "Too few arguments. Should have two images as arguments: [IMAGE1] [IMAGE2]."
if len(args) < 1 {
errMessage = "Too few arguments. Should have one or two images as arguments."
return false, errors.New(errMessage)
} else if len(args) > 2 {
errMessage = "Too many arguments. Should have two images as arguments: [IMAGE1] [IMAGE2]."
errMessage = "Too many arguments. Should have at most two images as arguments."
return false, errors.New(errMessage)
} else {
return true, nil
Expand All @@ -186,15 +272,12 @@ func checkImage(arg string) bool {
func checkArgType(args []string) (bool, error) {
var buffer bytes.Buffer
valid := true
if !checkImage(args[0]) {
valid = false
errMessage := fmt.Sprintf("Argument %s is not an image ID, URL, or tar\n", args[0])
buffer.WriteString(errMessage)
}
if !checkImage(args[1]) {
valid = false
errMessage := fmt.Sprintf("Argument %s is not an image ID, URL, or tar\n", args[1])
buffer.WriteString(errMessage)
for _, arg := range args {
if !checkImage(arg) {
valid = false
errMessage := fmt.Sprintf("Argument %s is not an image ID, URL, or tar\n", args[0])
buffer.WriteString(errMessage)
}
}
if !valid {
return false, errors.New(buffer.String())
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type testpair struct {

var argNumTests = []testpair{
{[]string{}, false},
{[]string{"one"}, false},
{[]string{"one"}, true},
{[]string{"one", "two"}, true},
{[]string{"one", "two", "three"}, false},
}
Expand Down
13 changes: 9 additions & 4 deletions differs/aptDiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ import (
"github.com/golang/glog"
)

type AptDiffer struct {
type AptAnalyzer struct {
}

// AptDiff compares the packages installed by apt-get.
func (d AptDiffer) Diff(image1, image2 utils.Image) (utils.DiffResult, error) {
diff, err := singleVersionDiff(image1, image2, d)
func (a AptAnalyzer) Diff(image1, image2 utils.Image) (utils.DiffResult, error) {
diff, err := singleVersionDiff(image1, image2, a)
return diff, err
}

func (d AptDiffer) getPackages(image utils.Image) (map[string]utils.PackageInfo, error) {
func (a AptAnalyzer) Analyze(image utils.Image) (utils.AnalyzeResult, error) {
analysis, err := singleVersionAnalysis(image, a)
return analysis, err
}

func (a AptAnalyzer) getPackages(image utils.Image) (map[string]utils.PackageInfo, error) {
path := image.FSPath
packages := make(map[string]utils.PackageInfo)
if _, err := os.Stat(path); err != nil {
Expand Down
Loading