diff --git a/README.md b/README.md index 85cf35f7..7f82e6c5 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Status](https://travis-ci.org/GoogleContainerTools/container-diff.svg?branch=mas container-diff is a tool for analyzing and comparing container images. container-diff can examine images along several different criteria, including: - Docker Image History - Image file system +- Image size - Apt packages - RPM packages - pip packages @@ -45,6 +46,7 @@ To use `container-diff analyze` to perform analysis on a single image, you need container-diff analyze [Run default analyzers] container-diff analyze --type=history [History] container-diff analyze --type=file [File System] +container-diff analyze --type=size [Size] container-diff analyze --type=rpm [RPM] container-diff analyze --type=pip [Pip] container-diff analyze --type=apt [Apt] @@ -60,6 +62,7 @@ To use container-diff to perform a diff analysis on two images, you need two Doc container-diff diff [Run default differs] container-diff diff --type=history [History] container-diff diff --type=file [File System] +container-diff diff --type=size [Size] container-diff diff --type=rpm [RPM] container-diff diff --type=pip [Pip] container-diff diff --type=apt [Apt] diff --git a/cmd/root.go b/cmd/root.go index 25180e5f..1f75cbf7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -208,6 +208,7 @@ func getImageForName(imageName string) (pkgutil.Image, error) { } layers = append(layers, pkgutil.Layer{ FSPath: path, + Digest: digest, }) elapsed := time.Now().Sub(layerStart) logrus.Infof("time elapsed retrieving layer: %fs", elapsed.Seconds()) @@ -216,7 +217,11 @@ func getImageForName(imageName string) (pkgutil.Image, error) { logrus.Infof("time elapsed retrieving image layers: %fs", elapsed.Seconds()) } - path, err := getExtractPathForImage(imageName, img) + imageDigest, err := getImageDigest(img) + if err != nil { + return pkgutil.Image{}, err + } + path, err := getExtractPathForName(pkgutil.RemoveTag(imageName) + "@" + imageDigest.String()) if err != nil { return pkgutil.Image{}, err } @@ -231,19 +236,20 @@ func getImageForName(imageName string) (pkgutil.Image, error) { Image: img, Source: imageName, FSPath: path, + Digest: imageDigest, Layers: layers, }, nil } -func getExtractPathForImage(imageName string, image v1.Image) (string, error) { +func getImageDigest(image v1.Image) (digest v1.Hash, err error) { start := time.Now() - digest, err := image.Digest() + digest, err = image.Digest() if err != nil { - return "", err + return digest, err } elapsed := time.Now().Sub(start) logrus.Infof("time elapsed retrieving image digest: %fs", elapsed.Seconds()) - return getExtractPathForName(pkgutil.RemoveTag(imageName) + "@" + digest.String()) + return digest, nil } func getExtractPathForName(name string) (string, error) { diff --git a/differs/differs.go b/differs/differs.go index 2a187ee1..c810fa05 100644 --- a/differs/differs.go +++ b/differs/differs.go @@ -28,6 +28,8 @@ const historyAnalyzer = "history" const metadataAnalyzer = "metadata" const fileAnalyzer = "file" const layerAnalyzer = "layer" +const sizeAnalyzer = "size" +const sizeLayerAnalyzer = "sizelayer" const aptAnalyzer = "apt" const aptLayerAnalyzer = "aptlayer" const rpmAnalyzer = "rpm" @@ -53,19 +55,21 @@ type Analyzer interface { } var Analyzers = map[string]Analyzer{ - historyAnalyzer: HistoryAnalyzer{}, - metadataAnalyzer: MetadataAnalyzer{}, - fileAnalyzer: FileAnalyzer{}, - layerAnalyzer: FileLayerAnalyzer{}, - aptAnalyzer: AptAnalyzer{}, - aptLayerAnalyzer: AptLayerAnalyzer{}, - rpmAnalyzer: RPMAnalyzer{}, - rpmLayerAnalyzer: RPMLayerAnalyzer{}, - pipAnalyzer: PipAnalyzer{}, - nodeAnalyzer: NodeAnalyzer{}, + historyAnalyzer: HistoryAnalyzer{}, + metadataAnalyzer: MetadataAnalyzer{}, + fileAnalyzer: FileAnalyzer{}, + layerAnalyzer: FileLayerAnalyzer{}, + sizeAnalyzer: SizeAnalyzer{}, + sizeLayerAnalyzer: SizeLayerAnalyzer{}, + aptAnalyzer: AptAnalyzer{}, + aptLayerAnalyzer: AptLayerAnalyzer{}, + rpmAnalyzer: RPMAnalyzer{}, + rpmLayerAnalyzer: RPMLayerAnalyzer{}, + pipAnalyzer: PipAnalyzer{}, + nodeAnalyzer: NodeAnalyzer{}, } -var LayerAnalyzers = [...]string{layerAnalyzer, aptLayerAnalyzer, rpmLayerAnalyzer} +var LayerAnalyzers = [...]string{layerAnalyzer, sizeLayerAnalyzer, aptLayerAnalyzer, rpmLayerAnalyzer} func (req DiffRequest) GetDiff() (map[string]util.Result, error) { img1 := req.Image1 diff --git a/differs/size_diff.go b/differs/size_diff.go new file mode 100644 index 00000000..5717a64e --- /dev/null +++ b/differs/size_diff.go @@ -0,0 +1,129 @@ +/* +Copyright 2018 Google, Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package differs + +import ( + "strconv" + + pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util" + "github.com/GoogleContainerTools/container-diff/util" +) + +type SizeAnalyzer struct { +} + +func (a SizeAnalyzer) Name() string { + return "SizeAnalyzer" +} + +// SizeDiff diffs two images and compares their size +func (a SizeAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { + diff := []util.SizeDiff{} + size1 := pkgutil.GetSize(image1.FSPath) + size2 := pkgutil.GetSize(image2.FSPath) + + if size1 != size2 { + diff = append(diff, util.SizeDiff{ + Size1: size1, + Size2: size2, + }) + } + + return &util.SizeDiffResult{ + Image1: image1.Source, + Image2: image2.Source, + DiffType: "Size", + Diff: diff, + }, nil +} + +func (a SizeAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { + entries := []util.SizeEntry{ + { + Name: image.Source, + Digest: image.Digest, + Size: pkgutil.GetSize(image.FSPath), + }, + } + + return &util.SizeAnalyzeResult{ + Image: image.Source, + AnalyzeType: "Size", + Analysis: entries, + }, nil +} + +type SizeLayerAnalyzer struct { +} + +func (a SizeLayerAnalyzer) Name() string { + return "SizeLayerAnalyzer" +} + +// SizeLayerDiff diffs the layers of two images and compares their size +func (a SizeLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) { + var layerDiffs []util.SizeDiff + + maxLayer := len(image1.Layers) + if len(image2.Layers) > maxLayer { + maxLayer = len(image2.Layers) + } + + for index := 0; index < maxLayer; index++ { + var size1, size2 int64 = -1, -1 + if index < len(image1.Layers) { + size1 = pkgutil.GetSize(image1.Layers[index].FSPath) + } + if index < len(image2.Layers) { + size2 = pkgutil.GetSize(image2.Layers[index].FSPath) + } + + if size1 != size2 { + diff := util.SizeDiff{ + Name: strconv.Itoa(index), + Size1: size1, + Size2: size2, + } + layerDiffs = append(layerDiffs, diff) + } + } + + return &util.SizeLayerDiffResult{ + Image1: image1.Source, + Image2: image2.Source, + DiffType: "SizeLayer", + Diff: layerDiffs, + }, nil +} + +func (a SizeLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) { + var entries []util.SizeEntry + for index, layer := range image.Layers { + entry := util.SizeEntry{ + Name: strconv.Itoa(index), + Digest: layer.Digest, + Size: pkgutil.GetSize(layer.FSPath), + } + entries = append(entries, entry) + } + + return &util.SizeLayerAnalyzeResult{ + Image: image.Source, + AnalyzeType: "SizeLayer", + Analysis: entries, + }, nil +} diff --git a/pkg/util/image_utils.go b/pkg/util/image_utils.go index e2c628b9..875d280b 100644 --- a/pkg/util/image_utils.go +++ b/pkg/util/image_utils.go @@ -37,12 +37,14 @@ const tagRegexStr = ".*:([^/]+$)" type Layer struct { FSPath string + Digest v1.Hash } type Image struct { Image v1.Image Source string FSPath string + Digest v1.Hash Layers []Layer } diff --git a/tests/integration_test.go b/tests/integration_test.go index 8d6b7ebb..f06899f6 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -117,6 +117,22 @@ func TestDiffAndAnalysis(t *testing.T) { differFlags: []string{"--type=layer", "--no-cache"}, expectedFile: "file_layer_diff_expected.json", }, + { + description: "size differ", + subcommand: "diff", + imageA: diffLayerBase, + imageB: diffLayerModifed, + differFlags: []string{"--type=size", "--no-cache"}, + expectedFile: "size_diff_expected.json", + }, + { + description: "size layer differ", + subcommand: "diff", + imageA: diffLayerBase, + imageB: diffLayerModifed, + differFlags: []string{"--type=sizelayer", "--no-cache"}, + expectedFile: "size_layer_diff_expected.json", + }, { description: "apt differ", subcommand: "diff", @@ -209,6 +225,20 @@ func TestDiffAndAnalysis(t *testing.T) { differFlags: []string{"--type=layer", "--no-cache"}, expectedFile: "file_layer_analysis_expected.json", }, + { + description: "size analysis", + subcommand: "analyze", + imageA: diffBase, + differFlags: []string{"--type=size", "--no-cache"}, + expectedFile: "size_analysis_expected.json", + }, + { + description: "size layer analysis", + subcommand: "analyze", + imageA: diffLayerBase, + differFlags: []string{"--type=sizelayer", "--no-cache"}, + expectedFile: "size_layer_analysis_expected.json", + }, { description: "pip analysis", subcommand: "analyze", diff --git a/tests/size_analysis_expected.json b/tests/size_analysis_expected.json new file mode 100644 index 00000000..a535bb0a --- /dev/null +++ b/tests/size_analysis_expected.json @@ -0,0 +1,13 @@ +[ + { + "Image": "gcr.io/gcp-runtimes/diff-base", + "AnalyzeType": "Size", + "Analysis": [ + { + "Name": "gcr.io/gcp-runtimes/diff-base", + "Digest": "sha256:d4e51212be9fffca8d3573a35ac12a69d050c3f65f706e053d24f41317762cca", + "Size": 383043168 + } + ] + } +] \ No newline at end of file diff --git a/tests/size_diff_expected.json b/tests/size_diff_expected.json new file mode 100644 index 00000000..cf8fbc5a --- /dev/null +++ b/tests/size_diff_expected.json @@ -0,0 +1,14 @@ +[ + { + "Image1": "gcr.io/gcp-runtimes/diff-layer-base", + "Image2": "gcr.io/gcp-runtimes/diff-layer-modified", + "DiffType": "Size", + "Diff": [ + { + "Name": "", + "Size1": 19, + "Size2": 15 + } + ] + } +] \ No newline at end of file diff --git a/tests/size_layer_analysis_expected.json b/tests/size_layer_analysis_expected.json new file mode 100644 index 00000000..74729409 --- /dev/null +++ b/tests/size_layer_analysis_expected.json @@ -0,0 +1,23 @@ +[ + { + "Image": "gcr.io/gcp-runtimes/diff-layer-base", + "AnalyzeType": "SizeLayer", + "Analysis": [ + { + "Name": "0", + "Digest": "sha256:74a9fa414ff7212ca555a1024826b94e27ec6a436fd971f8e6c2a909b23630a2", + "Size": 6 + }, + { + "Name": "1", + "Digest": "sha256:e39d9b94d5c118b34cce2f4af7efd4c7ed2553e697d7f676e24c4091e42a4998", + "Size": 7 + }, + { + "Name": "2", + "Digest": "sha256:bda36409f35ed9e71049d22abdce890d6335056ca15bfed9f323be69b0607e24", + "Size": 6 + } + ] + } +] \ No newline at end of file diff --git a/tests/size_layer_diff_expected.json b/tests/size_layer_diff_expected.json new file mode 100644 index 00000000..0d6e97a6 --- /dev/null +++ b/tests/size_layer_diff_expected.json @@ -0,0 +1,19 @@ +[ + { + "Image1": "gcr.io/gcp-runtimes/diff-layer-base", + "Image2": "gcr.io/gcp-runtimes/diff-layer-modified", + "DiffType": "SizeLayer", + "Diff": [ + { + "Name": "1", + "Size1": 7, + "Size2": 9 + }, + { + "Name": "2", + "Size1": 6, + "Size2": -1 + } + ] + } +] \ No newline at end of file diff --git a/util/analyze_output_utils.go b/util/analyze_output_utils.go index 3c7a006e..ddb4bdf1 100644 --- a/util/analyze_output_utils.go +++ b/util/analyze_output_utils.go @@ -340,3 +340,69 @@ func (r FileLayerAnalyzeResult) OutputText(analyzeType string, format string) er } return TemplateOutputFromFormat(strResult, "FileLayerAnalyze", format) } + +type SizeAnalyzeResult AnalyzeResult + +func (r SizeAnalyzeResult) OutputStruct() interface{} { + analysis, valid := r.Analysis.([]SizeEntry) + if !valid { + logrus.Error("Unexpected structure of Analysis. Should be of type []SizeEntry") + return errors.New("Could not output SizeAnalyzer analysis result") + } + r.Analysis = analysis + return r +} + +func (r SizeAnalyzeResult) OutputText(analyzeType string, format string) error { + analysis, valid := r.Analysis.([]SizeEntry) + if !valid { + logrus.Error("Unexpected structure of Analysis. Should be of type []SizeEntry") + return errors.New("Could not output SizeAnalyzer analysis result") + } + + strAnalysis := stringifySizeEntries(analysis) + + strResult := struct { + Image string + AnalyzeType string + Analysis []StrSizeEntry + }{ + Image: r.Image, + AnalyzeType: r.AnalyzeType, + Analysis: strAnalysis, + } + return TemplateOutputFromFormat(strResult, "SizeAnalyze", format) +} + +type SizeLayerAnalyzeResult AnalyzeResult + +func (r SizeLayerAnalyzeResult) OutputStruct() interface{} { + analysis, valid := r.Analysis.([]SizeEntry) + if !valid { + logrus.Error("Unexpected structure of Analysis. Should be of type []SizeEntry") + return errors.New("Could not output SizeLayerAnalyzer analysis result") + } + r.Analysis = analysis + return r +} + +func (r SizeLayerAnalyzeResult) OutputText(analyzeType string, format string) error { + analysis, valid := r.Analysis.([]SizeEntry) + if !valid { + logrus.Error("Unexpected structure of Analysis. Should be of type []SizeEntry") + return errors.New("Could not output SizeLayerAnalyzer analysis result") + } + + strAnalysis := stringifySizeEntries(analysis) + + strResult := struct { + Image string + AnalyzeType string + Analysis []StrSizeEntry + }{ + Image: r.Image, + AnalyzeType: r.AnalyzeType, + Analysis: strAnalysis, + } + return TemplateOutputFromFormat(strResult, "SizeLayerAnalyze", format) +} diff --git a/util/diff_output_utils.go b/util/diff_output_utils.go index c8e58b55..ac65f832 100644 --- a/util/diff_output_utils.go +++ b/util/diff_output_utils.go @@ -297,6 +297,78 @@ func (r DirDiffResult) OutputText(diffType string, format string) error { return TemplateOutputFromFormat(strResult, "DirDiff", format) } +type SizeDiffResult DiffResult + +func (r SizeDiffResult) OutputStruct() interface{} { + diff, valid := r.Diff.([]SizeDiff) + if !valid { + logrus.Error("Unexpected structure of Diff. Should be of type []SizeDiff") + return errors.New("Could not output SizeAnalyzer diff result") + } + + r.Diff = diff + return r +} + +func (r SizeDiffResult) OutputText(diffType string, format string) error { + diff, valid := r.Diff.([]SizeDiff) + if !valid { + logrus.Error("Unexpected structure of Diff. Should be of type []SizeDiff") + return errors.New("Could not output SizeAnalyzer diff result") + } + + strDiff := stringifySizeDiffs(diff) + + strResult := struct { + Image1 string + Image2 string + DiffType string + Diff []StrSizeDiff + }{ + Image1: r.Image1, + Image2: r.Image2, + DiffType: r.DiffType, + Diff: strDiff, + } + return TemplateOutputFromFormat(strResult, "SizeDiff", format) +} + +type SizeLayerDiffResult DiffResult + +func (r SizeLayerDiffResult) OutputStruct() interface{} { + diff, valid := r.Diff.([]SizeDiff) + if !valid { + logrus.Error("Unexpected structure of Diff. Should be of type []SizeDiff") + return errors.New("Could not output SizeLayerAnalyzer diff result") + } + + r.Diff = diff + return r +} + +func (r SizeLayerDiffResult) OutputText(diffType string, format string) error { + diff, valid := r.Diff.([]SizeDiff) + if !valid { + logrus.Error("Unexpected structure of Diff. Should be of type []SizeDiff") + return errors.New("Could not output SizeLayerAnalyzer diff result") + } + + strDiff := stringifySizeDiffs(diff) + + strResult := struct { + Image1 string + Image2 string + DiffType string + Diff []StrSizeDiff + }{ + Image1: r.Image1, + Image2: r.Image2, + DiffType: r.DiffType, + Diff: strDiff, + } + return TemplateOutputFromFormat(strResult, "SizeLayerDiff", format) +} + type MultipleDirDiffResult DiffResult func (r MultipleDirDiffResult) OutputStruct() interface{} { diff --git a/util/format_utils.go b/util/format_utils.go index 2e93eb6c..83717feb 100644 --- a/util/format_utils.go +++ b/util/format_utils.go @@ -39,6 +39,10 @@ var templates = map[string]string{ "ListAnalyze": ListAnalysisOutput, "FileAnalyze": FileAnalysisOutput, "FileLayerAnalyze": FileLayerAnalysisOutput, + "SizeAnalyze": SizeAnalysisOutput, + "SizeLayerAnalyze": SizeLayerAnalysisOutput, + "SizeDiff": SizeDiffOutput, + "SizeLayerDiff": SizeLayerDiffOutput, "MultiVersionPackageAnalyze": MultiVersionPackageOutput, "SingleVersionPackageAnalyze": SingleVersionPackageOutput, "SingleVersionPackageLayerAnalyze": SingleVersionPackageLayerOutput, diff --git a/util/output_text_utils.go b/util/output_text_utils.go index 133bffe9..8aeea425 100644 --- a/util/output_text_utils.go +++ b/util/output_text_utils.go @@ -121,3 +121,31 @@ func stringifyEntryDiffs(entries []EntryDiff) (strEntries []StrEntryDiff) { } return } + +type StrSizeEntry struct { + Name string + Digest string + Size string +} + +func stringifySizeEntries(entries []SizeEntry) (strEntries []StrSizeEntry) { + for _, entry := range entries { + strEntry := StrSizeEntry{Name: entry.Name, Digest: entry.Digest.String(), Size: stringifySize(entry.Size)} + strEntries = append(strEntries, strEntry) + } + return +} + +type StrSizeDiff struct { + Name string + Size1 string + Size2 string +} + +func stringifySizeDiffs(entries []SizeDiff) (strEntries []StrSizeDiff) { + for _, entry := range entries { + strEntry := StrSizeDiff{Name: entry.Name, Size1: stringifySize(entry.Size1), Size2: stringifySize(entry.Size2)} + strEntries = append(strEntries, strEntry) + } + return +} diff --git a/util/size_utils.go b/util/size_utils.go new file mode 100644 index 00000000..ad780cb6 --- /dev/null +++ b/util/size_utils.go @@ -0,0 +1,31 @@ +/* +Copyright 2018 Google, Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "github.com/google/go-containerregistry/pkg/v1" + +type SizeEntry struct { + Name string + Digest v1.Hash + Size int64 +} + +type SizeDiff struct { + Name string + Size1 int64 + Size2 int64 +} diff --git a/util/template_utils.go b/util/template_utils.go index 14da15fb..afc4c303 100644 --- a/util/template_utils.go +++ b/util/template_utils.go @@ -100,6 +100,22 @@ const FilenameDiffOutput = ` {{.Diff}} ` +const SizeDiffOutput = ` +-----{{.DiffType}}----- + +Image size difference between {{.Image1}} and {{.Image2}}:{{if not .Diff}} None{{else}} +SIZE1 SIZE2{{range .Diff}}{{"\n"}}{{.Size1}} {{.Size2}}{{end}} +{{end}} +` + +const SizeLayerDiffOutput = ` +-----{{.DiffType}}----- + +Layer size differences between {{.Image1}} and {{.Image2}}:{{if not .Diff}} None{{else}} +LAYER SIZE1 SIZE2{{range .Diff}}{{"\n"}}{{.Name}} {{.Size1}} {{.Size2}}{{end}} +{{end}} +` + const ListAnalysisOutput = ` -----{{.AnalyzeType}}----- @@ -124,6 +140,22 @@ FILE SIZE{{range $analysis}}{{"\n"}}{{.Name}} {{.Size}}{{end}} {{end}} ` +const SizeAnalysisOutput = ` +-----{{.AnalyzeType}}----- + +Analysis for {{.Image}}:{{if not .Analysis}} None{{else}} +IMAGE DIGEST SIZE{{range .Analysis}}{{"\n"}}{{.Name}} {{.Digest}} {{.Size}}{{end}} +{{end}} +` + +const SizeLayerAnalysisOutput = ` +-----{{.AnalyzeType}}----- + +Analysis for {{.Image}}:{{if not .Analysis}} None{{else}} +LAYER DIGEST SIZE{{range .Analysis}}{{"\n"}}{{.Name}} {{.Digest}} {{.Size}}{{end}} +{{end}} +` + const MultiVersionPackageOutput = ` -----{{.AnalyzeType}}-----