Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for building OCI multi-arch images #93

Merged
merged 1 commit into from
Aug 15, 2022
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ _(As a convention in the list below, all task parameters are specified with a
format (`rootfs/`, `metadata.json`) for use with the [`image` task step
option](https://concourse-ci.org/jobs.html#schema.step.task-step.image).

* `$OUTPUT_OCI` (default `false`): outputs an OCI compliant image, allowing
for multi-arch image builds when setting IMAGE_PLATFORM to [multiple platforms]
(https://docs.docker.com/desktop/extensions-sdk/extensions/multi-arch/). The
image output format will be a directory when this flag is set to true.

* `$BUILDKIT_ADD_HOSTS` (default empty): extra host definitions for `buildkit`
to properly resolve custom hostnames. The value is as comma-separated
(`,`) list of key-value pairs (using syntax `hostname=ip-address`), each
Expand Down
33 changes: 16 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@ module github.com/concourse/oci-build-task
go 1.12

require (
github.com/BurntSushi/toml v0.3.1
github.com/Azure/azure-sdk-for-go v42.3.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.10.2 // indirect
github.com/Azure/go-autorest/autorest/validation v0.2.0 // indirect
github.com/BurntSushi/toml v0.4.1
github.com/VividCortex/ewma v1.1.1 // indirect
github.com/concourse/go-archive v1.0.1
github.com/containerd/stargz-snapshotter/estargz v0.0.0-20210105085455-7f45f7438617 // indirect
github.com/docker/cli v20.10.2+incompatible // indirect
github.com/docker/docker v20.10.2+incompatible // indirect
github.com/fatih/color v1.10.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-containerregistry v0.3.0
github.com/containerd/containerd v1.3.0 // indirect
github.com/coreos/bbolt v1.3.2 // indirect
github.com/fatih/color v1.13.0
github.com/google/go-containerregistry v0.9.0
github.com/googleapis/gnostic v0.2.2 // indirect
github.com/julienschmidt/httprouter v1.3.0
github.com/onsi/gomega v1.10.3 // indirect
github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 // indirect
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2 // indirect
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.7.0
github.com/stretchr/testify v1.6.1
github.com/prometheus/tsdb v0.7.1 // indirect
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/u-root/u-root v7.0.0+incompatible
github.com/ugorji/go v1.1.4 // indirect
github.com/vbauerster/mpb v3.4.0+incompatible
github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db // indirect
github.com/vrischmann/envconfig v1.3.0
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/code-generator v0.17.2 // indirect
)
822 changes: 822 additions & 0 deletions go.sum

Large diffs are not rendered by default.

92 changes: 77 additions & 15 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"

v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -116,6 +117,11 @@ func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, erro
var targets []string
var imagePaths []string

outputType := "docker"
if cfg.OutputOCI {
outputType = "oci"
}

for _, t := range cfg.AdditionalTargets {
// prevent re-use of the buildctlArgs slice as it is appended to later on,
// and that would clobber args for all targets if the slice was re-used
Expand All @@ -131,7 +137,7 @@ func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, erro
imagePaths = append(imagePaths, imagePath)

targetArgs = append(targetArgs,
"--output", "type=docker,dest="+imagePath,
"--output", "type="+outputType+",dest="+imagePath,
)
}

Expand All @@ -145,7 +151,7 @@ func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, erro
imagePaths = append(imagePaths, imagePath)

buildctlArgs = append(buildctlArgs,
"--output", "type=docker,dest="+imagePath,
"--output", "type="+outputType+",dest="+imagePath,
)
}

Expand Down Expand Up @@ -202,39 +208,95 @@ func Build(buildkitd *Buildkitd, outputsDir string, req Request) (Response, erro
}
}

if req.Config.OutputOCI {
err = loadOciImages(imagePaths, req)
if err != nil {
return Response{}, err
}
} else {
err = loadImages(imagePaths, req)
if err != nil {
return Response{}, err
}
}

return res, nil
}

func loadImages(imagePaths []string, req Request) error {
for _, imagePath := range imagePaths {
image, err := tarball.ImageFromPath(imagePath, nil)
if err != nil {
return Response{}, errors.Wrap(err, "open oci image")
return errors.Wrap(err, "open oci image")
}

outputDir := filepath.Dir(imagePath)

err = writeDigest(outputDir, image)
m, err := image.Manifest()
if err != nil {
return Response{}, err
return errors.Wrap(err, "get image manifest")
}

err = writeDigest(outputDir, m.Config.Digest)
if err != nil {
return err
}

if req.Config.UnpackRootfs {
err = unpackRootfs(outputDir, image, cfg)
err = unpackRootfs(outputDir, image, req.Config)
if err != nil {
return Response{}, errors.Wrap(err, "unpack rootfs")
return errors.Wrap(err, "unpack rootfs")
}
}
}

return res, nil
return nil
}

func writeDigest(dest string, image v1.Image) error {
digestPath := filepath.Join(dest, "digest")
func loadOciImages(imagePaths []string, req Request) error {
for _, imagePath := range imagePaths {
_, err := os.Stat(imagePath)
if err != nil {
return errors.Wrapf(err, "image path %s not valid", imagePath)
}

manifest, err := image.Manifest()
if err != nil {
return errors.Wrap(err, "get image digest")
// go-containerregistry does not currently have support for loading a OCI formated
// image from a tarball, so we decompress it before doing anything.
targetDir := filepath.Dir(imagePath)
imageDir := filepath.Join(targetDir, "image")
logrus.Infof("decompressing OCI image tar to: %s", imageDir)
err = os.MkdirAll(imageDir, 0700)
if err != nil {
return errors.Wrapf(err, "unable to create image dir %s", imageDir)
}
run(os.Stdout, "tar", "-xvf", imagePath, "-C", imageDir)

l, err := layout.ImageIndexFromPath(imageDir)
if err != nil {
return errors.Wrapf(err, "failed to load %s as OCI layout", imagePath)
}

m, err := l.IndexManifest()
if err != nil {
return errors.Wrap(err, "error getting index manifest")
}

manifest := m.Manifests[0]

outputDir := filepath.Dir(imagePath)

err = writeDigest(outputDir, manifest.Digest)
xtremerui marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
}

err = ioutil.WriteFile(digestPath, []byte(manifest.Config.Digest.String()), 0644)
return nil
}

func writeDigest(dest string, digest v1.Hash) error {
digestPath := filepath.Join(dest, "digest")

err := ioutil.WriteFile(digestPath, []byte(digest.String()), 0644)
if err != nil {
return errors.Wrap(err, "write digest file")
}
Expand Down
31 changes: 31 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/registry"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
"github.com/google/go-containerregistry/pkg/v1/random"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/tarball"
Expand Down Expand Up @@ -594,6 +595,36 @@ func (s *TaskSuite) TestImagePlatform() {
s.Equal("arm64", configFile.Architecture)
}

func (s *TaskSuite) TestOciImage() {
s.req.Config.ContextDir = "testdata/multi-arch"
s.req.Config.ImagePlatform = "linux/arm64,linux/amd64"
s.req.Config.OutputOCI = true

_, err := s.build()
s.NoError(err)

l, err := layout.ImageIndexFromPath(s.imagePath("image"))
s.NoError(err)

im, err := l.IndexManifest()
s.NoError(err)

desc := im.Manifests[0]
ii, err := l.ImageIndex(desc.Digest)
s.NoError(err)

images, err := ii.IndexManifest()
s.NoError(err)
Comment on lines +612 to +617
Copy link
Contributor

@xtremerui xtremerui Jul 13, 2022

Choose a reason for hiding this comment

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

	desc := im.Manifests[0]
	ii, err := l.ImageIndex(desc.Digest)
	s.NoError(err)

	images, err := ii.IndexManifest()
	s.NoError(err)

is this part necessary since im at line 609 is already IndexManifext? Could it use im instead of images at line 621?

Copy link
Contributor Author

@rdrgmnzs rdrgmnzs Jul 13, 2022

Choose a reason for hiding this comment

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

@xtremerui unfortunately it is, I was very confused by this at first as well. The first IndexManifest is for the bundle it self, you then provide the "Digest" of that bundle to get it's index and then you retrieve that manifest using IndexManifest and that's where the data for all the images (multiple images because of multiple CPU architectures) is contained.

When I was figuring this out I actually added print statements to the test, here's a sample output from that so you can see how they differ:

Contents of im: &{SchemaVersion:2 MediaType: Manifests:[{MediaType:application/vnd.oci.image.index.v1+json Size:703 Digest:sha256:1e0d02719eea72a55306e7d55d9d84b6ac25c3ac5705e0d78d72f26eee18e649 Data:[] URLs:[] Annotations:map[org.opencontainers.image.created:2022-07-13T16:48:31Z] Platform:<nil>}] Annotations:map[]} 

Contents of images: &{SchemaVersion:2 MediaType:application/vnd.oci.image.index.v1+json Manifests:[{MediaType:application/vnd.oci.image.manifest.v1+json Size:710 Digest:sha256:a6f9c6d46f97a85ace889664bc42437cff0e59c769b02629d98470378ff659d0 Data:[] URLs:[] Annotations:map[] Platform:linux/arm64} {MediaType:application/vnd.oci.image.manifest.v1+json Size:710 Digest:sha256:84b4c953792a04908939367e04c74baa03d8e4f2e701b1c3aa20751e9d286089 Data:[] URLs:[] Annotations:map[] Platform:linux/amd64}] Annotations:map[]} 


expectedArch := []string{"arm64", "amd64"}
var actualArch []string
for _, manifest := range images.Manifests {
actualArch = append(actualArch, string(manifest.Platform.Architecture))
}

s.True(reflect.DeepEqual(expectedArch, actualArch))
}

func (s *TaskSuite) build() (task.Response, error) {
return task.Build(s.buildkitd, s.outputsDir, s.req)
}
Expand Down
4 changes: 4 additions & 0 deletions testdata/multi-arch/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# syntax = docker/dockerfile:1.0-experimental
FROM alpine

RUN apk add --no-cache vim
2 changes: 2 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ type Config struct {
// Theoretically this would go away if/when we standardize on OCI.
UnpackRootfs bool `json:"unpack_rootfs" envconfig:"optional"`

OutputOCI bool `json:"output_oci" envconfig:"optional"`
Copy link
Contributor

Choose a reason for hiding this comment

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

@jmccann here you go!

@rdrgmnzs please update readme for this configuration. Thx!


// Images to pre-load in order to avoid fetching at build time. Mapping from
// build arg name to OCI image tarball path.
//
Expand Down