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}}-----