Skip to content

Commit 4a10108

Browse files
authored
feat(image): support Podman (fanal#149)
* refactor(daemon): replace Image with DockerImage * feat(image): support Podman * chore(mod): update testdocker
1 parent 3f35881 commit 4a10108

File tree

10 files changed

+359
-135
lines changed

10 files changed

+359
-135
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect
88
github.com/alicebob/miniredis/v2 v2.14.1
99
github.com/aquasecurity/go-dep-parser v0.0.0-20201028043324-889d4a92b8e0
10-
github.com/aquasecurity/testdocker v0.0.0-20201220111429-5278b43e3eba
10+
github.com/aquasecurity/testdocker v0.0.0-20210106133225-0b17fe083674
1111
github.com/aws/aws-sdk-go v1.27.1
1212
github.com/deckarep/golang-set v1.7.1
1313
github.com/dgrijalva/jwt-go v3.2.0+incompatible

go.sum

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo
5050
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
5151
github.com/aquasecurity/go-dep-parser v0.0.0-20201028043324-889d4a92b8e0 h1:cLH3SebzhbJ+jU1GIad8A1N8p7m7OjHhtY6JePISiVc=
5252
github.com/aquasecurity/go-dep-parser v0.0.0-20201028043324-889d4a92b8e0/go.mod h1:X42mTIRhgPalSm81Om2kD+3ydeunbC8TZtZj1bvgRo8=
53-
github.com/aquasecurity/testdocker v0.0.0-20200426142840-5f05bce6f12a h1:hsw7PpiymXP64evn/K7gsj3hWzMqLrdoeE6JkqDocVg=
54-
github.com/aquasecurity/testdocker v0.0.0-20200426142840-5f05bce6f12a/go.mod h1:psfu0MVaiTDLpNxCoNsTeILSKY2EICBwv345f3M+Ffs=
55-
github.com/aquasecurity/testdocker v0.0.0-20201220111429-5278b43e3eba h1:0Tp/eLlMRmBHJFjV3HZImdb//tEzxBGXbI0OXUn6exg=
56-
github.com/aquasecurity/testdocker v0.0.0-20201220111429-5278b43e3eba/go.mod h1:psfu0MVaiTDLpNxCoNsTeILSKY2EICBwv345f3M+Ffs=
53+
github.com/aquasecurity/testdocker v0.0.0-20210106133225-0b17fe083674 h1:Xq/HxWFGaB4G/prC6czH/F5woB91GMCCilJxs/5DnDk=
54+
github.com/aquasecurity/testdocker v0.0.0-20210106133225-0b17fe083674/go.mod h1:psfu0MVaiTDLpNxCoNsTeILSKY2EICBwv345f3M+Ffs=
5755
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
5856
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
5957
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -501,39 +499,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEha
501499
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
502500
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
503501
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
504-
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
505-
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
506-
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
507-
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
508-
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
509-
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
510-
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
511-
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
512-
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
513-
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
514-
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
515-
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
516-
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
517-
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
518-
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
519-
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
520-
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
521-
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
522-
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
523-
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
524-
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
525-
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
526-
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
527-
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
528-
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
529-
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
530-
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775 h1:TC0v2RSO1u2kn1ZugjrFXkRZAEaqMN/RW+OTZkBzmLE=
531-
golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
532-
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
533502
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
534503
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
535-
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
536-
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
537504
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
538505
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
539506
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

image/daemon.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@ import (
77
v1 "github.com/google/go-containerregistry/pkg/v1"
88
)
99

10-
func tryDaemon(ref name.Reference) (v1.Image, extender, func(), error) {
11-
img, inspect, cleanup, err := daemon.Image(ref)
10+
func tryDockerDaemon(ref name.Reference) (v1.Image, extender, func(), error) {
11+
img, inspect, cleanup, err := daemon.DockerImage(ref)
12+
if err != nil {
13+
return nil, nil, nil, err
14+
}
15+
return img, daemonExtender{inspect: inspect}, cleanup, nil
16+
17+
}
18+
19+
func tryPodmanDaemon(ref string) (v1.Image, extender, func(), error) {
20+
img, inspect, cleanup, err := daemon.PodmanImage(ref)
1221
if err != nil {
1322
return nil, nil, nil, err
1423
}

image/daemon/docker.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package daemon
2+
3+
import (
4+
"context"
5+
"io/ioutil"
6+
"os"
7+
8+
"github.com/docker/docker/api/types"
9+
"github.com/docker/docker/client"
10+
"github.com/google/go-containerregistry/pkg/name"
11+
"github.com/google/go-containerregistry/pkg/v1"
12+
"golang.org/x/xerrors"
13+
)
14+
15+
// DockerImage implements v1.Image by extending daemon.Image.
16+
// The caller must call cleanup() to remove a temporary file.
17+
func DockerImage(ref name.Reference) (v1.Image, *types.ImageInspect, func(), error) {
18+
cleanup := func() {}
19+
20+
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
21+
if err != nil {
22+
return nil, nil, cleanup, xerrors.Errorf("failed to initialize a docker client: %w", err)
23+
}
24+
defer func() {
25+
if err != nil {
26+
c.Close()
27+
}
28+
}()
29+
30+
inspect, _, err := c.ImageInspectWithRaw(context.Background(), ref.Name())
31+
if err != nil {
32+
return nil, nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", ref.Name(), err)
33+
}
34+
35+
f, err := ioutil.TempFile("", "fanal-*")
36+
if err != nil {
37+
return nil, nil, cleanup, xerrors.Errorf("failed to create a temporary file")
38+
}
39+
40+
cleanup = func() {
41+
c.Close()
42+
f.Close()
43+
_ = os.Remove(f.Name())
44+
}
45+
46+
return &image{
47+
opener: imageOpener(ref.Name(), f, c.ImageSave),
48+
inspect: inspect,
49+
}, &inspect, cleanup, nil
50+
}

image/daemon/docker_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package daemon
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/docker/docker/api/types"
10+
"github.com/google/go-containerregistry/pkg/name"
11+
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
)
13+
14+
func TestDockerImage(t *testing.T) {
15+
type fields struct {
16+
Image v1.Image
17+
opener opener
18+
inspect types.ImageInspect
19+
}
20+
tests := []struct {
21+
name string
22+
imageName string
23+
fields fields
24+
want *v1.ConfigFile
25+
wantErr bool
26+
}{
27+
{
28+
name: "happy path",
29+
imageName: "alpine:3.11",
30+
wantErr: false,
31+
},
32+
{
33+
name: "unknown image",
34+
imageName: "alpine:unknown",
35+
wantErr: true,
36+
},
37+
}
38+
for _, tt := range tests {
39+
t.Run(tt.name, func(t *testing.T) {
40+
ref, err := name.ParseReference(tt.imageName)
41+
require.NoError(t, err)
42+
43+
_, _, cleanup, err := DockerImage(ref)
44+
assert.Equal(t, tt.wantErr, err != nil, err)
45+
defer func() {
46+
if cleanup != nil {
47+
cleanup()
48+
}
49+
}()
50+
})
51+
}
52+
}

image/daemon/image.go

Lines changed: 20 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,25 @@ package daemon
33
import (
44
"context"
55
"io"
6-
"io/ioutil"
76
"os"
87
"sync"
98

109
"github.com/docker/docker/api/types"
11-
"github.com/docker/docker/client"
12-
"github.com/google/go-containerregistry/pkg/name"
1310
"github.com/google/go-containerregistry/pkg/v1"
1411
"github.com/google/go-containerregistry/pkg/v1/tarball"
1512
"golang.org/x/xerrors"
1613
)
1714

1815
var mu sync.Mutex
1916

20-
// image is a wrapper for github.com/google/go-containerregistry/pkg/v1/daemon.Image
21-
// daemon.Image loads the entire image into the memory at first,
22-
// but it doesn't need to load it if the information is already in the cache,
23-
// To avoid entire loading, this wrapper uses ImageInspectWithRaw and checks image ID and layer IDs.
24-
type image struct {
25-
v1.Image
26-
opener opener
27-
inspect types.ImageInspect
28-
}
29-
3017
type opener func() (v1.Image, error)
3118

32-
// Image implements v1.Image by extending daemon.Image.
33-
// The caller must call cleanup() to remove a temporary file.
34-
func Image(ref name.Reference) (v1.Image, *types.ImageInspect, func(), error) {
35-
cleanup := func() {}
19+
type imageSave func(context.Context, []string) (io.ReadCloser, error)
3620

37-
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
38-
if err != nil {
39-
return nil, nil, cleanup, xerrors.Errorf("failed to initialize a docker client: %w", err)
40-
}
41-
defer func() {
42-
if err != nil {
43-
c.Close()
44-
}
45-
}()
46-
47-
inspect, _, err := c.ImageInspectWithRaw(context.Background(), ref.Name())
48-
if err != nil {
49-
return nil, nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", ref.Name(), err)
50-
}
51-
52-
f, err := ioutil.TempFile("", "fanal-*")
53-
if err != nil {
54-
return nil, nil, cleanup, xerrors.Errorf("failed to create a temporary file")
55-
}
56-
57-
cleanup = func() {
58-
c.Close()
59-
f.Close()
60-
_ = os.Remove(f.Name())
61-
}
62-
63-
return &image{
64-
opener: imageOpener(c, ref, f),
65-
inspect: inspect,
66-
}, &inspect, cleanup, nil
67-
}
68-
69-
func imageOpener(c *client.Client, ref name.Reference, f *os.File) opener {
21+
func imageOpener(ref string, f *os.File, imageSave imageSave) opener {
7022
return func() (v1.Image, error) {
7123
// Store the tarball in local filesystem and return a new reader into the bytes each time we need to access something.
72-
rc, err := c.ImageSave(context.Background(), []string{ref.Name()})
24+
rc, err := imageSave(context.Background(), []string{ref})
7325
if err != nil {
7426
return nil, xerrors.Errorf("unable to export the image: %w", err)
7527
}
@@ -89,6 +41,16 @@ func imageOpener(c *client.Client, ref name.Reference, f *os.File) opener {
8941
}
9042
}
9143

44+
// image is a wrapper for github.com/google/go-containerregistry/pkg/v1/daemon.Image
45+
// daemon.Image loads the entire image into the memory at first,
46+
// but it doesn't need to load it if the information is already in the cache,
47+
// To avoid entire loading, this wrapper uses ImageInspectWithRaw and checks image ID and layer IDs.
48+
type image struct {
49+
v1.Image
50+
opener opener
51+
inspect types.ImageInspect
52+
}
53+
9254
// populateImage initializes an "image" struct.
9355
// This method is called by some goroutines at the same time.
9456
// To prevent multiple heavy initializations, the lock is necessary.
@@ -114,6 +76,13 @@ func (img *image) ConfigName() (v1.Hash, error) {
11476
}
11577

11678
func (img *image) ConfigFile() (*v1.ConfigFile, error) {
79+
if len(img.inspect.RootFS.Layers) == 0 {
80+
// Podman doesn't return RootFS...
81+
if err := img.populateImage(); err != nil {
82+
return nil, xerrors.Errorf("unable to populate: %w", err)
83+
}
84+
return img.Image.ConfigFile()
85+
}
11786
var diffIDs []v1.Hash
11887
for _, l := range img.inspect.RootFS.Layers {
11988
h, err := v1.NewHash(l)

image/daemon/image_test.go

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"github.com/stretchr/testify/require"
1111

12-
"github.com/docker/docker/api/types"
1312
"github.com/google/go-containerregistry/pkg/name"
1413
v1 "github.com/google/go-containerregistry/pkg/v1"
1514

@@ -22,6 +21,8 @@ func TestMain(m *testing.M) {
2221
"index.docker.io/library/alpine:3.11": "../../test/testdata/alpine-311.tar.gz",
2322
"gcr.io/distroless/base:latest": "../../test/testdata/distroless.tar.gz",
2423
}
24+
25+
// for Docker
2526
opt := engine.Option{
2627
APIVersion: "1.38",
2728
ImagePaths: imagePaths,
@@ -34,46 +35,6 @@ func TestMain(m *testing.M) {
3435
os.Exit(m.Run())
3536
}
3637

37-
func TestImage(t *testing.T) {
38-
type fields struct {
39-
Image v1.Image
40-
opener opener
41-
inspect types.ImageInspect
42-
}
43-
tests := []struct {
44-
name string
45-
imageName string
46-
fields fields
47-
want *v1.ConfigFile
48-
wantErr bool
49-
}{
50-
{
51-
name: "happy path",
52-
imageName: "alpine:3.11",
53-
wantErr: false,
54-
},
55-
{
56-
name: "unknown image",
57-
imageName: "alpine:unknown",
58-
wantErr: true,
59-
},
60-
}
61-
for _, tt := range tests {
62-
t.Run(tt.name, func(t *testing.T) {
63-
ref, err := name.ParseReference(tt.imageName)
64-
require.NoError(t, err)
65-
66-
_, _, cleanup, err := Image(ref)
67-
assert.Equal(t, tt.wantErr, err != nil, err)
68-
defer func() {
69-
if cleanup != nil {
70-
cleanup()
71-
}
72-
}()
73-
})
74-
}
75-
}
76-
7738
func Test_image_ConfigName(t *testing.T) {
7839
tests := []struct {
7940
name string
@@ -96,7 +57,7 @@ func Test_image_ConfigName(t *testing.T) {
9657
ref, err := name.ParseReference(tt.imageName)
9758
require.NoError(t, err)
9859

99-
img, _, cleanup, err := Image(ref)
60+
img, _, cleanup, err := DockerImage(ref)
10061
require.NoError(t, err)
10162
defer cleanup()
10263

@@ -156,7 +117,7 @@ func Test_image_ConfigFile(t *testing.T) {
156117
ref, err := name.ParseReference(tt.imageName)
157118
require.NoError(t, err)
158119

159-
img, _, cleanup, err := Image(ref)
120+
img, _, cleanup, err := DockerImage(ref)
160121
require.NoError(t, err)
161122
defer cleanup()
162123

@@ -201,7 +162,7 @@ func Test_image_LayerByDiffID(t *testing.T) {
201162
ref, err := name.ParseReference(tt.imageName)
202163
require.NoError(t, err)
203164

204-
img, _, cleanup, err := Image(ref)
165+
img, _, cleanup, err := DockerImage(ref)
205166
require.NoError(t, err)
206167
defer cleanup()
207168

@@ -230,7 +191,7 @@ func Test_image_RawConfigFile(t *testing.T) {
230191
ref, err := name.ParseReference(tt.imageName)
231192
require.NoError(t, err)
232193

233-
img, _, cleanup, err := Image(ref)
194+
img, _, cleanup, err := DockerImage(ref)
234195
require.NoError(t, err)
235196
defer cleanup()
236197

0 commit comments

Comments
 (0)