Skip to content

Commit

Permalink
Merge pull request concourse#93 from rdrgmnzs/multi-arch
Browse files Browse the repository at this point in the history
Add support for building OCI multi-arch images
  • Loading branch information
xtremerui committed Aug 15, 2022
2 parents 0bea575 + 567071e commit ca21a55
Show file tree
Hide file tree
Showing 7 changed files with 957 additions and 32 deletions.
5 changes: 5 additions & 0 deletions README.md
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
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
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)
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
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)

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
@@ -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
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"`

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

0 comments on commit ca21a55

Please sign in to comment.