Skip to content

Commit 767cb97

Browse files
committed
Use google/go-containerregistry to parse OCI images in packaging tests
1 parent fbc22fb commit 767cb97

File tree

3 files changed

+63
-113
lines changed

3 files changed

+63
-113
lines changed

dev-tools/packaging/testing/package_test.go

Lines changed: 54 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ import (
1717
"debug/buildinfo"
1818
"debug/elf"
1919
"encoding/hex"
20-
"encoding/json"
2120
"errors"
2221
"flag"
2322
"fmt"
2423
"hash"
2524
"io"
2625
"maps"
2726
"os"
27+
"path"
2828
"path/filepath"
2929
"regexp"
3030
"slices"
@@ -34,6 +34,7 @@ import (
3434

3535
"github.com/blakesmith/ar"
3636
"github.com/cavaliergopher/rpm"
37+
"github.com/google/go-containerregistry/pkg/v1/tarball"
3738
"github.com/stretchr/testify/assert"
3839
"github.com/stretchr/testify/require"
3940

@@ -1082,60 +1083,78 @@ func openZip(zipFile string) (*zip.ReadCloser, error) {
10821083
func readDocker(t *testing.T, dockerFile string, filterWorkingDir bool) (*packageFile, *dockerInfo, error) {
10831084
t.Helper()
10841085

1085-
// Read the manifest file first so that the config file and layer
1086-
// names are known in advance.
1087-
manifest, err := getDockerManifest(dockerFile)
1086+
// Decompress the .tar.gz file, go-containerregistry wants uncompressed here
1087+
f, err := os.Open(dockerFile)
10881088
require.NoError(t, err)
10891089

1090-
file, err := os.Open(dockerFile)
1090+
gz, err := gzip.NewReader(f)
10911091
require.NoError(t, err)
1092-
defer file.Close()
10931092

1094-
var info *dockerInfo
1093+
_, dockerFileName := path.Split(dockerFile)
10951094

1096-
stat, err := file.Stat()
1095+
tempDir := t.TempDir()
1096+
uncompressed, err := os.CreateTemp(tempDir, dockerFileName)
10971097
require.NoError(t, err)
10981098

1099-
layers := make(map[string]*packageFile)
1100-
1101-
gzipReader, err := gzip.NewReader(file)
1099+
_, err = io.Copy(uncompressed, gz)
11021100
require.NoError(t, err)
1103-
defer gzipReader.Close()
1101+
require.NoError(t, uncompressed.Close())
11041102

1105-
tarReader := tar.NewReader(gzipReader)
1106-
for {
1107-
header, err := tarReader.Next()
1108-
if err != nil {
1109-
if errors.Is(err, io.EOF) {
1110-
break
1111-
}
1112-
require.NoError(t, err)
1113-
}
1103+
// Load the Docker image tarball using go-containerregistry
1104+
img, err := tarball.ImageFromPath(uncompressed.Name(), nil)
1105+
require.NoError(t, err, "failed to load Docker image from %s", dockerFile)
11141106

1115-
switch {
1116-
case header.Name == manifest.Config:
1117-
info, err = readDockerInfo(tarReader)
1118-
require.NoError(t, err)
1119-
case slices.Contains(manifest.Layers, header.Name):
1120-
layer, err := readTarContents(header.Name, tarReader)
1121-
require.NoError(t, err)
1122-
layers[header.Name] = layer
1123-
}
1107+
// Get the config file which contains entrypoint, labels, user, workingDir
1108+
configFile, err := img.ConfigFile()
1109+
require.NoError(t, err, "failed to get config file from image")
1110+
1111+
imgSize, err := img.Size()
1112+
require.NoError(t, err, "failed to get image size")
1113+
info := &dockerInfo{
1114+
Size: imgSize,
11241115
}
1116+
info.Config.Entrypoint = configFile.Config.Entrypoint
1117+
info.Config.Labels = configFile.Config.Labels
1118+
info.Config.User = configFile.Config.User
1119+
info.Config.WorkingDir = configFile.Config.WorkingDir
11251120

11261121
require.NotZero(t, len(info.Config.Entrypoint), "no entrypoint")
11271122
workingDir := info.Config.WorkingDir
11281123
entrypoint := info.Config.Entrypoint[0]
11291124

1125+
// Get all layers and read their contents
1126+
layers, err := img.Layers()
1127+
require.NoError(t, err, "failed to get layers from image")
1128+
11301129
// Read layers in order and for each file keep only the entry seen in the later layer
11311130
p := &packageFile{Name: filepath.Base(dockerFile), Contents: map[string]packageEntry{}}
1132-
for _, layer := range manifest.Layers {
1133-
layerFile, found := layers[layer]
1134-
require.True(t, found, fmt.Sprintf("layer not found: %s", layer))
1135-
for name, entry := range layerFile.Contents {
1131+
for _, layer := range layers {
1132+
rc, err := layer.Uncompressed()
1133+
require.NoError(t, err, "failed to get uncompressed layer")
1134+
1135+
tarReader := tar.NewReader(rc)
1136+
for {
1137+
header, err := tarReader.Next()
1138+
if err != nil {
1139+
if errors.Is(err, io.EOF) {
1140+
break
1141+
}
1142+
assert.NoError(t, rc.Close())
1143+
require.NoError(t, err)
1144+
}
1145+
1146+
name := header.Name
11361147
if excludedPathsPattern.MatchString(name) {
11371148
continue
11381149
}
1150+
1151+
entry := packageEntry{
1152+
File: name,
1153+
UID: header.Uid,
1154+
GID: header.Gid,
1155+
Mode: header.FileInfo().Mode(),
1156+
}
1157+
11391158
// Check only files in working dir and entrypoint
11401159
if !filterWorkingDir || strings.HasPrefix("/"+name, workingDir) || "/"+name == entrypoint {
11411160
p.Contents[name] = entry
@@ -1147,19 +1166,13 @@ func readDocker(t *testing.T, dockerFile string, filterWorkingDir bool) (*packag
11471166
}
11481167
}
11491168
}
1169+
assert.NoError(t, rc.Close())
11501170
}
11511171

11521172
require.NotZero(t, len(p.Contents), fmt.Sprintf("no files found in docker working directory (%s)", info.Config.WorkingDir))
1153-
info.Size = stat.Size()
11541173
return p, info, nil
11551174
}
11561175

1157-
type dockerManifest struct {
1158-
Config string
1159-
RepoTags []string
1160-
Layers []string
1161-
}
1162-
11631176
type dockerInfo struct {
11641177
Config struct {
11651178
Entrypoint []string
@@ -1170,78 +1183,6 @@ type dockerInfo struct {
11701183
Size int64
11711184
}
11721185

1173-
func readDockerInfo(r io.Reader) (*dockerInfo, error) {
1174-
data, err := io.ReadAll(r)
1175-
if err != nil {
1176-
return nil, err
1177-
}
1178-
1179-
var info dockerInfo
1180-
err = json.Unmarshal(data, &info)
1181-
if err != nil {
1182-
return nil, err
1183-
}
1184-
1185-
return &info, nil
1186-
}
1187-
1188-
// getDockerManifest opens a gzipped tar file to read the Docker manifest.json
1189-
// that it is expected to contain.
1190-
func getDockerManifest(file string) (*dockerManifest, error) {
1191-
f, err := os.Open(file)
1192-
if err != nil {
1193-
return nil, err
1194-
}
1195-
defer f.Close()
1196-
1197-
gzipReader, err := gzip.NewReader(f)
1198-
if err != nil {
1199-
return nil, err
1200-
}
1201-
defer gzipReader.Close()
1202-
1203-
var manifest *dockerManifest
1204-
tarReader := tar.NewReader(gzipReader)
1205-
for {
1206-
header, err := tarReader.Next()
1207-
if err != nil {
1208-
if errors.Is(err, io.EOF) {
1209-
break
1210-
}
1211-
return nil, err
1212-
}
1213-
1214-
if header.Name == "manifest.json" {
1215-
manifest, err = readDockerManifest(tarReader)
1216-
if err != nil {
1217-
return nil, err
1218-
}
1219-
break
1220-
}
1221-
}
1222-
1223-
return manifest, nil
1224-
}
1225-
1226-
func readDockerManifest(r io.Reader) (*dockerManifest, error) {
1227-
data, err := io.ReadAll(r)
1228-
if err != nil {
1229-
return nil, err
1230-
}
1231-
1232-
var manifests []*dockerManifest
1233-
err = json.Unmarshal(data, &manifests)
1234-
if err != nil {
1235-
return nil, err
1236-
}
1237-
1238-
if len(manifests) != 1 {
1239-
return nil, fmt.Errorf("one and only one manifest expected, %d found", len(manifests))
1240-
}
1241-
1242-
return manifests[0], nil
1243-
}
1244-
12451186
func checkSha512PackageHash(t *testing.T, packageFile string) {
12461187
t.Run("check hash file", func(t *testing.T) {
12471188
expectedHashFile := packageFile + ".sha512"

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/gofrs/flock v0.12.1
3434
github.com/gofrs/uuid/v5 v5.4.0
3535
github.com/google/go-cmp v0.7.0
36+
github.com/google/go-containerregistry v0.20.3
3637
github.com/google/pprof v0.0.0-20250923004556-9e5a51aed1e8
3738
github.com/gorilla/mux v1.8.1
3839
github.com/jaypipes/ghw v0.12.0
@@ -261,6 +262,7 @@ require (
261262
github.com/containerd/errdefs/pkg v0.3.0 // indirect
262263
github.com/containerd/log v0.1.0 // indirect
263264
github.com/containerd/platforms v0.2.1 // indirect
265+
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
264266
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
265267
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
266268
github.com/cyphar/filepath-securejoin v0.6.0 // indirect
@@ -634,6 +636,7 @@ require (
634636
github.com/ugorji/go/codec v1.2.7 // indirect
635637
github.com/urfave/cli/v2 v2.27.4 // indirect
636638
github.com/valyala/fastjson v1.6.4 // indirect
639+
github.com/vbatts/tar-split v0.11.6 // indirect
637640
github.com/vmware/govmomi v0.52.0 // indirect
638641
github.com/vultr/govultr/v2 v2.17.2 // indirect
639642
github.com/x448/float16 v0.8.4 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
396396
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
397397
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
398398
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
399+
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
400+
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
399401
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
400402
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
401403
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
@@ -772,6 +774,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
772774
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
773775
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
774776
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
777+
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
778+
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
775779
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
776780
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
777781
github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA=
@@ -1581,6 +1585,8 @@ github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
15811585
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
15821586
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
15831587
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
1588+
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
1589+
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
15841590
github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY=
15851591
github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w=
15861592
github.com/vmware/govmomi v0.52.0 h1:JyxQ1IQdllrY7PJbv2am9mRsv3p9xWlIQ66bv+XnyLw=

0 commit comments

Comments
 (0)