diff --git a/cmd/build.go b/cmd/build.go index 70b5f9b..4e738af 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -190,7 +190,7 @@ func build(cmd *cobra.Command, _ []string) error { } // pull the base image - baseImg, err := containerutil.Pull(cmd.Context(), baseImage) + baseImg, err := containerutil.Get(cmd.Context(), baseImage) if err != nil { return err } diff --git a/pkg/containerutil/append.go b/pkg/containerutil/append.go index 5e644af..ed66797 100644 --- a/pkg/containerutil/append.go +++ b/pkg/containerutil/append.go @@ -3,14 +3,15 @@ package containerutil import ( "context" "fmt" + "path/filepath" + "strings" + "github.com/chainguard-dev/go-apk/pkg/fs" "github.com/go-logr/logr" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/types" - "path/filepath" - "strings" ) const MagicImageScratch = "scratch" @@ -88,7 +89,7 @@ func (ib *Image) Append(ctx context.Context, fs fs.FullFS, platform *v1.Platform if mt, err := ib.baseImage.MediaType(); err == nil { log.V(1).Info("detected base image media type", "mediaType", mt) } - baseImage := mutate.MediaType(ib.baseImage, types.OCIManifestSchema1) + baseImage := ib.baseImage // append our layer layers := []mutate.Addendum{ diff --git a/pkg/containerutil/get.go b/pkg/containerutil/get.go new file mode 100644 index 0000000..49c18f8 --- /dev/null +++ b/pkg/containerutil/get.go @@ -0,0 +1,46 @@ +package containerutil + +import ( + "context" + "fmt" + + "github.com/djcass44/ci-tools/pkg/ociutil" + "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func Get(ctx context.Context, ref string) (v1.Image, error) { + log := logr.FromContextOrDiscard(ctx).WithValues("ref", ref) + log.Info("getting image") + + if ref == MagicImageScratch { + return empty.Image, nil + } + + remoteRef, err := name.ParseReference(ref) + if err != nil { + return nil, fmt.Errorf("parsing name %s: %w", ref, err) + } + + // fetch the image without actually + // pulling it + rmt, err := remote.Get(remoteRef, remote.WithContext(ctx), remote.WithAuthFromKeychain(ociutil.KeyChain(ociutil.Auth{}))) + if err != nil { + return nil, fmt.Errorf("getting %s: %w", ref, err) + } + + img, err := rmt.Image() + if err != nil { + return nil, err + } + + // normalise the image + img, err = NormaliseImage(ctx, img) + if err != nil { + return nil, fmt.Errorf("normalising %s: %w", ref, err) + } + return img, nil +} diff --git a/pkg/containerutil/get_test.go b/pkg/containerutil/get_test.go new file mode 100644 index 0000000..7c3807b --- /dev/null +++ b/pkg/containerutil/get_test.go @@ -0,0 +1,32 @@ +package containerutil + +import ( + "context" + "testing" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/testr" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/stretchr/testify/assert" +) + +func TestGet(t *testing.T) { + ctx := logr.NewContext(context.TODO(), testr.NewWithOptions(t, testr.Options{Verbosity: 10})) + + t.Run("real image", func(t *testing.T) { + img, err := Get(ctx, "busybox") + assert.NoError(t, err) + size, err := img.Size() + assert.NoError(t, err) + assert.NotZero(t, size) + }) + t.Run("scratch image", func(t *testing.T) { + img, err := Get(ctx, "scratch") + assert.NoError(t, err) + size, err := img.Size() + assert.NoError(t, err) + assert.NotZero(t, size) + + assert.Equal(t, empty.Image, img) + }) +} diff --git a/pkg/containerutil/mutate.go b/pkg/containerutil/mutate.go new file mode 100644 index 0000000..0a5cd69 --- /dev/null +++ b/pkg/containerutil/mutate.go @@ -0,0 +1,106 @@ +package containerutil + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Drops docker specific properties +// See: https://github.com/opencontainers/image-spec/blob/main/config.md +func toOCIV1Config(config v1.Config) v1.Config { + return v1.Config{ + User: config.User, + ExposedPorts: config.ExposedPorts, + Env: config.Env, + Entrypoint: config.Entrypoint, + Cmd: config.Cmd, + Volumes: config.Volumes, + WorkingDir: config.WorkingDir, + Labels: config.Labels, + StopSignal: config.StopSignal, + } +} + +func toOCIV1ConfigFile(cf *v1.ConfigFile) *v1.ConfigFile { + return &v1.ConfigFile{ + Created: cf.Created, + Author: cf.Author, + Architecture: cf.Architecture, + OS: cf.OS, + OSVersion: cf.OSVersion, + History: cf.History, + RootFS: cf.RootFS, + Config: toOCIV1Config(cf.Config), + } +} + +// NormaliseImage mutates the provided v1.Image to be OCI compliant v1.Image. +// +// Check image-spec to see which properties are ported and which are dropped. +// https://github.com/opencontainers/image-spec/blob/main/config.md +func NormaliseImage(ctx context.Context, base v1.Image) (v1.Image, error) { + log := logr.FromContextOrDiscard(ctx) + log.V(1).Info("normalising base image") + // get the original manifest + m, err := base.Manifest() + if err != nil { + return nil, err + } + // convert config + cfg, err := base.ConfigFile() + if err != nil { + return nil, err + } + cfg = toOCIV1ConfigFile(cfg) + + layers, err := base.Layers() + if err != nil { + return nil, err + } + + newLayers := []v1.Layer{} + + // go through each layer and convert it to + // OCI format + for _, layer := range layers { + mediaType, err := layer.MediaType() + if err != nil { + return nil, err + } + log.V(2).Info("checking layer", "mediaType", mediaType) + switch mediaType { + case types.DockerLayer: + layer, err = tarball.LayerFromOpener(layer.Compressed, tarball.WithMediaType(types.OCILayer)) + if err != nil { + return nil, fmt.Errorf("building layer: %w", err) + } + case types.DockerUncompressedLayer: + layer, err = tarball.LayerFromOpener(layer.Uncompressed, tarball.WithMediaType(types.OCIUncompressedLayer)) + if err != nil { + return nil, fmt.Errorf("building layer: %w", err) + } + } + newLayers = append(newLayers, layer) + } + + base, err = mutate.AppendLayers(empty.Image, newLayers...) + if err != nil { + return nil, err + } + + base = mutate.MediaType(base, types.OCIManifestSchema1) + base = mutate.ConfigMediaType(base, types.OCIConfigJSON) + base = mutate.Annotations(base, m.Annotations).(v1.Image) + base, err = mutate.ConfigFile(base, cfg) + if err != nil { + return nil, err + } + return base, nil +}