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

Layered analysis for single version packages #248

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
8 changes: 6 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const (
RemotePrefix = "remote://"
)

var layerAnalyzers = [...]string{"layer", "aptlayer"}

var RootCmd = &cobra.Command{
Use: "container-diff",
Short: "container-diff is a tool for analyzing and comparing container images",
Expand Down Expand Up @@ -268,8 +270,10 @@ func getExtractPathForName(name string) (string, error) {

func includeLayers() bool {
for _, t := range types {
if t == "layer" {
return true
for _, a := range layerAnalyzers {
if t == a {
return true
}
}
}
return false
Expand Down
52 changes: 49 additions & 3 deletions differs/apt_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import (
"github.com/sirupsen/logrus"
)

//APT package database location
const dpkgStatusFile string = "var/lib/dpkg/status"

type AptAnalyzer struct {
}

Expand All @@ -47,13 +50,16 @@ func (a AptAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) {
}

func (a AptAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) {
path := image.FSPath
return readStatusFile(image.FSPath)
}

func readStatusFile(root string) (map[string]util.PackageInfo, error) {
packages := make(map[string]util.PackageInfo)
if _, err := os.Stat(path); err != nil {
if _, err := os.Stat(root); err != nil {
// invalid image directory path
return packages, err
}
statusFile := filepath.Join(path, "var/lib/dpkg/status")
statusFile := filepath.Join(root, dpkgStatusFile)
if _, err := os.Stat(statusFile); err != nil {
// status file does not exist in this layer
return packages, nil
Expand Down Expand Up @@ -120,3 +126,43 @@ func parseLine(text string, currPackage string, packages map[string]util.Package
}
return currPackage
}

type AptLayerAnalyzer struct {
}

func (a AptLayerAnalyzer) Name() string {
return "AptLayerAnalyzer"
}

// AptDiff compares the packages installed by apt-get.
func (a AptLayerAnalyzer) Diff(image1, image2 pkgutil.Image) (util.Result, error) {
diff, err := singleVersionLayerDiff(image1, image2, a)
return diff, err
}

func (a AptLayerAnalyzer) Analyze(image pkgutil.Image) (util.Result, error) {
analysis, err := singleVersionLayerAnalysis(image, a)
return analysis, err
}

func (a AptLayerAnalyzer) getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this shares a lot of code with AptAnalyzer.getPackages() in apt_diff.go. Could you pull this out into a shared method between the two implementations?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure you are right.

var packages []map[string]util.PackageInfo
if _, err := os.Stat(image.FSPath); err != nil {
// invalid image directory path
return packages, err
}
statusFile := filepath.Join(image.FSPath, dpkgStatusFile)
if _, err := os.Stat(statusFile); err != nil {
// status file does not exist in this image
return packages, nil
}
for _, layer := range image.Layers {
layerPackages, err := readStatusFile(layer.FSPath)
if err != nil {
return packages, err
}
packages = append(packages, layerPackages)
}

return packages, nil
}
1 change: 1 addition & 0 deletions differs/differs.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ var Analyzers = map[string]Analyzer{
"file": FileAnalyzer{},
"layer": FileLayerAnalyzer{},
"apt": AptAnalyzer{},
"aptlayer": AptLayerAnalyzer{},
"rpm": RPMAnalyzer{},
"pip": PipAnalyzer{},
"node": NodeAnalyzer{},
Expand Down
50 changes: 50 additions & 0 deletions differs/package_differs.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ limitations under the License.
package differs

import (
"errors"
"strings"

pkgutil "github.com/GoogleContainerTools/container-diff/pkg/util"
"github.com/GoogleContainerTools/container-diff/util"
"github.com/sirupsen/logrus"
)

type MultiVersionPackageAnalyzer interface {
Expand All @@ -33,6 +35,11 @@ type SingleVersionPackageAnalyzer interface {
Name() string
}

type SingleVersionPackageLayerAnalyzer interface {
getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error)
Name() string
}

func multiVersionDiff(image1, image2 pkgutil.Image, differ MultiVersionPackageAnalyzer) (*util.MultiVersionPackageDiffResult, error) {
pack1, err := differ.getPackages(image1)
if err != nil {
Expand Down Expand Up @@ -71,6 +78,13 @@ func singleVersionDiff(image1, image2 pkgutil.Image, differ SingleVersionPackage
}, nil
}

// singleVersionLayerDiff returns an error as this diff is not supported as
// it is far from obvious to define it in meaningful way
func singleVersionLayerDiff(image1, image2 pkgutil.Image, differ SingleVersionPackageLayerAnalyzer) (*util.SingleVersionPackageLayerDiffResult, error) {
logrus.Warning("'diff' command for packages on layers is not supported, consider using 'analyze' on each image instead")
return &util.SingleVersionPackageLayerDiffResult{}, errors.New("Diff for packages on layers is not supported, only analysis is supported")
}

func multiVersionAnalysis(image pkgutil.Image, analyzer MultiVersionPackageAnalyzer) (*util.MultiVersionPackageAnalyzeResult, error) {
pack, err := analyzer.getPackages(image)
if err != nil {
Expand Down Expand Up @@ -98,3 +112,39 @@ func singleVersionAnalysis(image pkgutil.Image, analyzer SingleVersionPackageAna
}
return &analysis, nil
}

// singleVersionLayerAnalysis returns the packages included, deleted or
// updated in each layer
func singleVersionLayerAnalysis(image pkgutil.Image, analyzer SingleVersionPackageLayerAnalyzer) (*util.SingleVersionPackageLayerAnalyzeResult, error) {
pack, err := analyzer.getPackages(image)
if err != nil {
return &util.SingleVersionPackageLayerAnalyzeResult{}, err
}
var pkgDiffs []util.PackageDiff

// Each layer with modified packages includes a complete list of packages
// in its package database. Thus we diff the current layer with the
// previous one that contains a package database. Layers that do not
// include a package database are omitted.
preInd := -1
for i := range pack {
var pkgDiff util.PackageDiff
if preInd < 0 && len(pack[i]) > 0 {
pkgDiff = util.GetMapDiff(make(map[string]util.PackageInfo), pack[i])
preInd = i
} else if preInd >= 0 && len(pack[i]) > 0 {
pkgDiff = util.GetMapDiff(pack[preInd], pack[i])
preInd = i
}

pkgDiffs = append(pkgDiffs, pkgDiff)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Each layer with modified packages includes a complete list of packages in its package database. Thus we diff the current layer with the previous one.

Could you explain this a bit more? Are you saying that each layer with modified packages contains all packages installed in the previous layer, so diffing those two gives you only the packages modified between layer i and layer i-1?

I think this block would also be a little more clear if it looked something like

for i := range pack {
	if len(pack[i] == 0) {
		continue
	}
	if i == 0 {
		pkgDiffs = append(pkgDiffs, util.GetMapDiff(make(map[string]util.PackageInfo), pack[i]))
	} else {
		pkgDiffs = append(pkgDiffs, util.GetMapDiff(pack[i-1], pack[i]))
	}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, I haven't coded it as you suggest because we can't expect to have different packages in each layer. Image there are some layers that do not modify the package database, because they configure files or run some scripts or whatever; for those layers the reuslt of the getPackages is an empty map. I don't want to diff those empty maps with any other layer that actually modifies the package database, because the result will be the same as treating any package (regardless the layer in which they were appended) as a new addition.

So what I do here to compare only layers which actually include a modified package database. That means the diff is not with the immediate previous layer, but with the previous layer that actually includes some package database.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh ok, that makes sense now. Could you just update your comment slightly to explain that it's not the previous layer, but the previous layer that contains a package db?


return &util.SingleVersionPackageLayerAnalyzeResult{
Image: image.Source,
AnalyzeType: strings.TrimSuffix(analyzer.Name(), "Analyzer"),
Analysis: util.PackageLayerDiff{
PackageDiffs: pkgDiffs,
},
}, nil
}
72 changes: 72 additions & 0 deletions util/analyze_output_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,78 @@ func (r SingleVersionPackageAnalyzeResult) OutputText(diffType string, format st
return TemplateOutputFromFormat(strResult, "SingleVersionPackageAnalyze", format)
}

type SingleVersionPackageLayerAnalyzeResult AnalyzeResult

func (r SingleVersionPackageLayerAnalyzeResult) OutputStruct() interface{} {
analysis, valid := r.Analysis.(PackageLayerDiff)
if !valid {
logrus.Error("Unexpected structure of Analysis. Should be of type PackageLayerDiff")
return fmt.Errorf("Could not output %s analysis result", r.AnalyzeType)
}

type PkgDiff struct {
Packages1 []PackageOutput
Packages2 []PackageOutput
InfoDiff []Info
}

var analysisOutput []PkgDiff
for _, d := range analysis.PackageDiffs {
diffOutput := PkgDiff{
Packages1: getSingleVersionPackageOutput(d.Packages1),
Packages2: getSingleVersionPackageOutput(d.Packages2),
InfoDiff: getSingleVersionInfoDiffOutput(d.InfoDiff),
}
analysisOutput = append(analysisOutput, diffOutput)
}

output := struct {
Image string
AnalyzeType string
Analysis []PkgDiff
}{
Image: r.Image,
AnalyzeType: r.AnalyzeType,
Analysis: analysisOutput,
}
return output
}

func (r SingleVersionPackageLayerAnalyzeResult) OutputText(diffType string, format string) error {
analysis, valid := r.Analysis.(PackageLayerDiff)
if !valid {
logrus.Error("Unexpected structure of Analysis. Should be of type PackageLayerDiff")
return fmt.Errorf("Could not output %s analysis result", r.AnalyzeType)
}

type StrDiff struct {
Packages1 []StrPackageOutput
Packages2 []StrPackageOutput
InfoDiff []StrInfo
}

var analysisOutput []StrDiff
for _, d := range analysis.PackageDiffs {
diffOutput := StrDiff{
Packages1: stringifyPackages(getSingleVersionPackageOutput(d.Packages1)),
Packages2: stringifyPackages(getSingleVersionPackageOutput(d.Packages2)),
InfoDiff: stringifyPackageDiff(getSingleVersionInfoDiffOutput(d.InfoDiff)),
}
analysisOutput = append(analysisOutput, diffOutput)
}

strResult := struct {
Image string
AnalyzeType string
Analysis []StrDiff
}{
Image: r.Image,
AnalyzeType: r.AnalyzeType,
Analysis: analysisOutput,
}
return TemplateOutputFromFormat(strResult, "SingleVersionPackageLayerAnalyze", format)
}

type PackageOutput struct {
Name string
Path string `json:",omitempty"`
Expand Down
66 changes: 66 additions & 0 deletions util/diff_output_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,72 @@ func getSingleVersionInfoDiffOutput(infoDiff []Info) []Info {
return infoDiff
}

type SingleVersionPackageLayerDiffResult DiffResult

func (r SingleVersionPackageLayerDiffResult) OutputStruct() interface{} {
diff, valid := r.Diff.(PackageLayerDiff)
if !valid {
logrus.Error("Unexpected structure of Diff. Should follow the PackageLayerDiff struct")
return fmt.Errorf("Could not output %s diff result", r.DiffType)
}

type PkgDiff struct {
Packages1 []PackageOutput
Packages2 []PackageOutput
InfoDiff []Info
}

var diffOutputs []PkgDiff
for _, d := range diff.PackageDiffs {
diffOutput := PkgDiff{
Packages1: getSingleVersionPackageOutput(d.Packages1),
Packages2: getSingleVersionPackageOutput(d.Packages2),
InfoDiff: getSingleVersionInfoDiffOutput(d.InfoDiff),
}
diffOutputs = append(diffOutputs, diffOutput)
}

r.Diff = diffOutputs
return r
}

func (r SingleVersionPackageLayerDiffResult) OutputText(diffType string, format string) error {
diff, valid := r.Diff.(PackageLayerDiff)
if !valid {
logrus.Error("Unexpected structure of Diff. Should follow the PackageLayerDiff struct")
return fmt.Errorf("Could not output %s diff result", r.DiffType)
}

type StrDiff struct {
Packages1 []StrPackageOutput
Packages2 []StrPackageOutput
InfoDiff []StrInfo
}

var diffOutputs []StrDiff
for _, d := range diff.PackageDiffs {
diffOutput := StrDiff{
Packages1: stringifyPackages(getSingleVersionPackageOutput(d.Packages1)),
Packages2: stringifyPackages(getSingleVersionPackageOutput(d.Packages2)),
InfoDiff: stringifyPackageDiff(getSingleVersionInfoDiffOutput(d.InfoDiff)),
}
diffOutputs = append(diffOutputs, diffOutput)
}

strResult := struct {
Image1 string
Image2 string
DiffType string
Diff []StrDiff
}{
Image1: r.Image1,
Image2: r.Image2,
DiffType: r.DiffType,
Diff: diffOutputs,
}
return TemplateOutputFromFormat(strResult, "SingleVersionPackageLayerDiff", format)
}

type HistDiffResult DiffResult

func (r HistDiffResult) OutputStruct() interface{} {
Expand Down
25 changes: 13 additions & 12 deletions util/format_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ import (
)

var templates = map[string]string{
"SingleVersionPackageDiff": SingleVersionDiffOutput,
"MultiVersionPackageDiff": MultiVersionDiffOutput,
"HistDiff": HistoryDiffOutput,
"MetadataDiff": MetadataDiffOutput,
"DirDiff": FSDiffOutput,
"MultipleDirDiff": FSLayerDiffOutput,
"FilenameDiff": FilenameDiffOutput,
"ListAnalyze": ListAnalysisOutput,
"FileAnalyze": FileAnalysisOutput,
"FileLayerAnalyze": FileLayerAnalysisOutput,
"MultiVersionPackageAnalyze": MultiVersionPackageOutput,
"SingleVersionPackageAnalyze": SingleVersionPackageOutput,
"SingleVersionPackageDiff": SingleVersionDiffOutput,
"MultiVersionPackageDiff": MultiVersionDiffOutput,
"HistDiff": HistoryDiffOutput,
"MetadataDiff": MetadataDiffOutput,
"DirDiff": FSDiffOutput,
"MultipleDirDiff": FSLayerDiffOutput,
"FilenameDiff": FilenameDiffOutput,
"ListAnalyze": ListAnalysisOutput,
"FileAnalyze": FileAnalysisOutput,
"FileLayerAnalyze": FileLayerAnalysisOutput,
"MultiVersionPackageAnalyze": MultiVersionPackageOutput,
"SingleVersionPackageAnalyze": SingleVersionPackageOutput,
"SingleVersionPackageLayerAnalyze": SingleVersionPackageLayerOutput,
}

func JSONify(diff interface{}) error {
Expand Down
6 changes: 6 additions & 0 deletions util/package_diff_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ type PackageDiff struct {
InfoDiff []Info
}

// PackageLayerDiff stores the difference information between two images
// layer by layer in PackageDiff array
type PackageLayerDiff struct {
PackageDiffs []PackageDiff
}

// Info stores the information for one package in two different images.
type Info struct {
Package string
Expand Down
16 changes: 16 additions & 0 deletions util/template_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,19 @@ Packages found in {{.Image}}:{{if not .Analysis}} None{{else}}
NAME VERSION SIZE{{range .Analysis}}{{"\n"}}{{print "-"}}{{.Name}} {{.Version}} {{.Size}}{{end}}
{{end}}
`

const SingleVersionPackageLayerOutput = `
-----{{.AnalyzeType}}-----
{{range $index, $analysis := .Analysis}}
For Layer {{$index}}:{{if not (or (or $analysis.Packages1 $analysis.Packages2) $analysis.InfoDiff)}} No package changes {{else}}
{{if ne $index 0}}Deleted packages from previous layers:{{if not $analysis.Packages1}} None{{else}}
NAME VERSION SIZE{{range $analysis.Packages1}}{{"\n"}}{{print "-"}}{{.Name}} {{.Version}} {{.Size}}{{end}}{{end}}

{{end}}Packages added in this layer:{{if not $analysis.Packages2}} None{{else}}
NAME VERSION SIZE{{range $analysis.Packages2}}{{"\n"}}{{print "-"}}{{.Name}} {{.Version}} {{.Size}}{{end}}{{end}}
{{if ne $index 0}}
Version differences:{{if not $analysis.InfoDiff}} None{{else}}
PACKAGE PREV_LAYER CURRENT_LAYER {{range $analysis.InfoDiff}}{{"\n"}}{{print "-"}}{{.Package}} {{.Info1.Version}}, {{.Info1.Size}} {{.Info2.Version}}, {{.Info2.Size}}{{end}}
{{end}}{{end}}{{end}}
{{end}}
`