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

Add support for Alpine (apk) #372

Closed
wants to merge 1 commit into from
Closed
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
167 changes: 167 additions & 0 deletions differs/apk_diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
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 (
"bufio"
"os"
"path/filepath"
"strconv"
"strings"

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

// APK package database location
const apkInstalledDbFile string = "lib/apk/db/installed"

type ApkAnalyzer struct {
}

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

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

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

func (a ApkAnalyzer) getPackages(image pkgutil.Image) (map[string]util.PackageInfo, error) {
return apkReadInstalledDbFile(image.FSPath)
}

type ApkLayerAnalyzer struct {
}

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

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

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

func (a ApkLayerAnalyzer) getPackages(image pkgutil.Image) ([]map[string]util.PackageInfo, error) {
var packages []map[string]util.PackageInfo
if _, err := os.Stat(image.FSPath); err != nil {
// invalid image directory path
return packages, err
}
installedDbFile := filepath.Join(image.FSPath, apkInstalledDbFile)
if _, err := os.Stat(installedDbFile); err != nil {
// installed DB file does not exist in this image
return packages, nil
}
for _, layer := range image.Layers {
layerPackages, err := apkReadInstalledDbFile(layer.FSPath)
if err != nil {
return packages, err
}
packages = append(packages, layerPackages)
}

return packages, nil
}

func apkReadInstalledDbFile(root string) (map[string]util.PackageInfo, error) {
packages := make(map[string]util.PackageInfo)
if _, err := os.Stat(root); err != nil {
// invalid image directory path
return packages, err
}
installedDbFile := filepath.Join(root, apkInstalledDbFile)
if _, err := os.Stat(installedDbFile); err != nil {
// installed DB file does not exist in this layer
return packages, nil
}
if file, err := os.Open(installedDbFile); err == nil {
// make sure it gets closed
defer file.Close()

// create a new scanner and read the file line by line
scanner := bufio.NewScanner(file)
var currPackage string
for scanner.Scan() {
currPackage = apkParseLine(scanner.Text(), currPackage, packages)
}
} else {
return packages, err
}

return packages, nil
}

func apkParseLine(text string, currPackage string, packages map[string]util.PackageInfo) string {
line := strings.Split(text, ":")
if len(line) == 2 {
key := line[0]
value := line[1]

switch key {
case "P":
return value
case "V":
if packages[currPackage].Version != "" {
logrus.Warningln("Multiple versions of same package detected. Diffing such multi-versioning not yet supported.")
return currPackage
}
currPackageInfo, ok := packages[currPackage]
if !ok {
currPackageInfo = util.PackageInfo{}
}
currPackageInfo.Version = value
packages[currPackage] = currPackageInfo
return currPackage

case "I":
currPackageInfo, ok := packages[currPackage]
if !ok {
currPackageInfo = util.PackageInfo{}
}
var size int64
var err error
size, err = strconv.ParseInt(value, 10, 64)
if err != nil {
logrus.Errorf("Could not get size for %s: %s", currPackage, err)
size = -1
}
// I is in bytes, so *no* conversion needed to keep consistent with the tool's size units
currPackageInfo.Size = size
packages[currPackage] = currPackageInfo
return currPackage
default:
return currPackage
}
}
return currPackage
}
128 changes: 128 additions & 0 deletions differs/apk_diff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
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 (
"reflect"
"testing"

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

func TestApkParseLine(t *testing.T) {
testCases := []struct {
description string
line string
packages map[string]util.PackageInfo
currPackage string
expPackage string
expected map[string]util.PackageInfo
}{
{
description: "Not applicable line",
line: "G:garbage info",
packages: map[string]util.PackageInfo{},
expPackage: "",
expected: map[string]util.PackageInfo{},
},
{
description: "Package line",
line: "P:musl",
currPackage: "foo",
expPackage: "musl",
packages: map[string]util.PackageInfo{},
expected: map[string]util.PackageInfo{},
},
{
description: "Version line",
line: "V:1.2.2-r1",
packages: map[string]util.PackageInfo{},
currPackage: "musl",
expPackage: "musl",
expected: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1"}},
},
{
description: "Size line",
line: "I:622592",
packages: map[string]util.PackageInfo{},
currPackage: "musl",
expPackage: "musl",
expected: map[string]util.PackageInfo{"musl": {Size: 622592}},
},
{
description: "Pre-existing PackageInfo struct",
line: "I:622592",
packages: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1"}},
currPackage: "musl",
expPackage: "musl",
expected: map[string]util.PackageInfo{"musl": {Version: "1.2.2-r1", Size: 622592}},
},
}

for _, test := range testCases {
currPackage := apkParseLine(test.line, test.currPackage, test.packages)
if currPackage != test.expPackage {
t.Errorf("Expected current package to be: %s, but got: %s.", test.expPackage, currPackage)
}
if !reflect.DeepEqual(test.packages, test.expected) {
t.Errorf("Expected: %v but got: %v", test.expected, test.packages)
}
}
}

func TestGetApkPackages(t *testing.T) {
testCases := []struct {
description string
path string
expected map[string]util.PackageInfo
err bool
}{
{
description: "no directory",
path: "testDirs/notThere",
expected: map[string]util.PackageInfo{},
err: true,
},
{
description: "no packages",
path: "testDirs/noPackages",
expected: map[string]util.PackageInfo{},
},
{
description: "packages in expected location",
path: "testDirs/packageOne",
expected: map[string]util.PackageInfo{
"musl": {Version: "1.2.2-r1", Size: 622592},
"busybox": {Version: "1.32.1-r7", Size: 946176}},
},
}
for _, test := range testCases {
d := ApkAnalyzer{}
image := pkgutil.Image{FSPath: test.path}
packages, err := d.getPackages(image)
if err != nil && !test.err {
t.Errorf("Got unexpected error: %s", err)
}
if err == nil && test.err {
t.Errorf("Expected error but got none.")
}
if !reflect.DeepEqual(packages, test.expected) {
t.Errorf("Expected: %v but got: %v", test.expected, packages)
}
}
}
4 changes: 4 additions & 0 deletions differs/differs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const fileAnalyzer = "file"
const layerAnalyzer = "layer"
const sizeAnalyzer = "size"
const sizeLayerAnalyzer = "sizelayer"
const apkAnalyzer = "apk"
const apkLayerAnalyzer = "apklayer"
const aptAnalyzer = "apt"
const aptLayerAnalyzer = "aptlayer"
const rpmAnalyzer = "rpm"
Expand Down Expand Up @@ -62,6 +64,8 @@ var Analyzers = map[string]Analyzer{
layerAnalyzer: FileLayerAnalyzer{},
sizeAnalyzer: SizeAnalyzer{},
sizeLayerAnalyzer: SizeLayerAnalyzer{},
apkAnalyzer: ApkAnalyzer{},
apkLayerAnalyzer: ApkLayerAnalyzer{},
aptAnalyzer: AptAnalyzer{},
aptLayerAnalyzer: AptLayerAnalyzer{},
rpmAnalyzer: RPMAnalyzer{},
Expand Down
Empty file.
78 changes: 78 additions & 0 deletions differs/testDirs/packageOne/lib/apk/db/installed
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
C:Q1PLxH/xkzg+5Ny+Sid1STPEHi8e8=
P:musl
V:1.2.2-r1
A:x86_64
S:382799
I:622592
T:the musl c library (libc) implementation
U:https://musl.libc.org/
L:MIT
o:musl
m:Timo Teräs <timo.teras@iki.fi>
t:1623058830
c:bbbf656a5c008cd72af26290e7933a515af7f11f
p:so:libc.musl-x86_64.so.1=1
F:lib
R:ld-musl-x86_64.so.1
a:0:0:755
Z:Q1YVYdCRbKnapM6sWXMBeIM5V6azc=
R:libc.musl-x86_64.so.1
a:0:0:777
Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI=

C:Q1URbAAbfw05cknLVZMabtNfnjbtA=
P:busybox
V:1.32.1-r7
A:x86_64
S:497882
I:946176
T:Size optimized toolbox of many common UNIX utilities
U:https://busybox.net/
L:GPL-2.0-only
o:busybox
m:Natanael Copa <ncopa@alpinelinux.org>
t:1636715120
c:c434d5ff440c3e8bea3dbe139875e98a8c112202
D:so:libc.musl-x86_64.so.1
p:/bin/sh cmd:busybox cmd:sh
r:busybox-initscripts
F:bin
R:busybox
a:0:0:755
Z:Q1PtQZENNCmILe4Zi742XWtTRj+iw=
R:sh
a:0:0:777
Z:Q1pcfTfDNEbNKQc2s1tia7da05M8Q=
F:etc
R:securetty
Z:Q1mB95Hq2NUTZ599RDiSsj9w5FrOU=
R:udhcpd.conf
Z:Q1UAiPZcDIW1ClRzobfggcCQ77V28=
F:etc/logrotate.d
R:acpid
Z:Q1TylyCINVmnS+A/Tead4vZhE7Bks=
F:etc/network
F:etc/network/if-down.d
F:etc/network/if-post-down.d
F:etc/network/if-post-up.d
F:etc/network/if-pre-down.d
F:etc/network/if-pre-up.d
F:etc/network/if-up.d
R:dad
a:0:0:775
Z:Q1hlxd3qExrihH8bYxDQ3i7TsM/44=
F:sbin
F:tmp
M:0:0:1777
F:usr
F:usr/sbin
F:usr/share
F:usr/share/udhcpc
R:default.script
a:0:0:755
Z:Q1t9vir/ZrX3nbSIYT9BDLWZenkVQ=
F:var
F:var/cache
F:var/cache/misc
F:var/lib
F:var/lib/udhcpd