Repository: chainguard-dev/apko
Pull request #82 (2022-03-09 16:43:37)

Include arm v6 and v7 in multiarch support
This turned out to be a little more complicated than I wanted, since the image's config doesn't include the CPU variant. So instead, I just pass through a `map[types.Architecture]v1.Image` to `PublishIndex` (which I also don't super love).

Ideas welcome. I'm also fine just punting on this if we think we'll come up with a better idea later. I don't really think it needs to block anything.

edit: An example image built with this change:
```
index.docker.io/imjasonh/alpine-multi@sha256:735bfedcd7f5e4c7a4655a7d8d23c218e837aa0290c045019270a61fcc0e184a
```


Patch:
diff --git a/pkg/build/oci/oci.go b/pkg/build/oci/oci.go
index 4ff48a2..56bf9d5 100644
--- a/pkg/build/oci/oci.go
+++ b/pkg/build/oci/oci.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"log"
+	"sort"
 	"time"
 
 	"chainguard.dev/apko/pkg/build/types"
@@ -161,9 +162,17 @@ func PublishImageFromLayer(layerTarGZ string, ic types.ImageConfiguration, creat
 	return digest, v1Image, nil
 }
 
-func PublishIndex(imgs []v1.Image, tags ...string) (name.Digest, error) {
+func PublishIndex(imgs map[types.Architecture]v1.Image, tags ...string) (name.Digest, error) {
 	var idx v1.ImageIndex = empty.Index
-	for _, img := range imgs {
+	archs := make([]types.Architecture, 0, len(imgs))
+	for arch := range imgs {
+		archs = append(archs, arch)
+	}
+	sort.Slice(archs, func(i, j int) bool {
+		return archs[i] < archs[j]
+	})
+	for _, arch := range archs {
+		img := imgs[arch]
 		mt, err := img.MediaType()
 		if err != nil {
 			return name.Digest{}, fmt.Errorf("failed to get mediatype: %w", err)
@@ -179,22 +188,13 @@ func PublishIndex(imgs []v1.Image, tags ...string) (name.Digest, error) {
 			return name.Digest{}, fmt.Errorf("failed to compute size: %w", err)
 		}
 
-		cfg, err := img.ConfigFile()
-		if err != nil {
-			return name.Digest{}, fmt.Errorf("failed to get config file: %w", err)
-		}
-		plat := v1.Platform{
-			OS:           cfg.OS,
-			Architecture: cfg.Architecture,
-		}
-
 		idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
 			Add: img,
 			Descriptor: v1.Descriptor{
 				MediaType: mt,
 				Digest:    h,
 				Size:      size,
-				Platform:  &plat,
+				Platform:  arch.ToOCIPlatform(),
 			},
 		})
 	}
diff --git a/pkg/build/types/types.go b/pkg/build/types/types.go
index 8640a3f..7b6359b 100644
--- a/pkg/build/types/types.go
+++ b/pkg/build/types/types.go
@@ -14,7 +14,11 @@
 
 package types
 
-import "sort"
+import (
+	"sort"
+
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+)
 
 type User struct {
 	UserName string
@@ -53,18 +57,28 @@ type ImageConfiguration struct {
 type Architecture string
 
 const (
+	_386    Architecture = "386"
 	amd64   Architecture = "amd64"
 	arm64   Architecture = "arm64"
+	armv6   Architecture = "arm/v6"
+	armv7   Architecture = "arm/v7"
 	ppc64le Architecture = "ppc64le"
-	s390x   Architecture = "s390x"
-	_386    Architecture = "386"
 	riscv64 Architecture = "riscv64"
-	// TODO: armv7 and armhf (av6)
+	s390x   Architecture = "s390x"
 )
 
 // AllArchs contains the standard set of supported architectures, which are
 // used by `apko publish` when no architectures are specified.
-var AllArchs = []Architecture{amd64, arm64, ppc64le, s390x, _386, riscv64}
+var AllArchs = []Architecture{
+	_386,
+	amd64,
+	arm64,
+	armv6,
+	armv7,
+	ppc64le,
+	riscv64,
+	s390x,
+}
 
 // ToAPK returns the apk-style equivalent string for the Architecture.
 func (a Architecture) ToAPK() string {
@@ -75,11 +89,30 @@ func (a Architecture) ToAPK() string {
 		return "x86_64"
 	case arm64:
 		return "aarch64"
+	case armv6:
+		return "armhf"
+	case armv7:
+		return "armv7"
 	default:
 		return string(a)
 	}
 }
 
+func (a Architecture) ToOCIPlatform() *v1.Platform {
+	plat := v1.Platform{OS: "linux"}
+	switch a {
+	case armv6:
+		plat.Architecture = "arm"
+		plat.Variant = "v6"
+	case armv7:
+		plat.Architecture = "arm"
+		plat.Variant = "v7"
+	default:
+		plat.Architecture = string(a)
+	}
+	return &plat
+}
+
 // ParseArchitectures parses architecture values in string form, and returns
 // the equivalent slice of Architectures.
 //
@@ -97,6 +130,10 @@ func ParseArchitectures(in []string) []Architecture {
 			a = amd64
 		case "aarch64":
 			a = arm64
+		case "armhf":
+			a = armv6
+		case "armv7":
+			a = armv7
 		}
 		uniq[a] = struct{}{}
 	}
diff --git a/pkg/cli/publish.go b/pkg/cli/publish.go
index 12f271f..b1761ee 100644
--- a/pkg/cli/publish.go
+++ b/pkg/cli/publish.go
@@ -108,7 +108,7 @@ func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architectu
 			return fmt.Errorf("failed to build OCI image: %w", err)
 		}
 	default:
-		var imgs []v1.Image
+		imgs := map[types.Architecture]v1.Image{}
 		workDir := bc.WorkDir
 		for _, arch := range archs {
 			bc.Arch = arch
@@ -123,7 +123,7 @@ func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architectu
 			if err != nil {
 				return fmt.Errorf("failed to build OCI image for %q: %w", arch, err)
 			}
-			imgs = append(imgs, img)
+			imgs[arch] = img
 		}
 		digest, err = oci.PublishIndex(imgs, bc.Tags...)
 		if err != nil {


Test patch:
diff --git a/pkg/build/types/types_test.go b/pkg/build/types/types_test.go
index b6220d6..b030410 100644
--- a/pkg/build/types/types_test.go
+++ b/pkg/build/types/types_test.go
@@ -31,16 +31,16 @@ func TestParseArchitectures(t *testing.T) {
 		want: []Architecture{},
 	}, {
 		desc: "sort",
-		in:   []string{"riscv64", "amd64"},
-		want: []Architecture{amd64, riscv64},
+		in:   []string{"riscv64", "amd64", "arm/v6"},
+		want: []Architecture{amd64, armv6, riscv64},
 	}, {
 		desc: "dedupe",
 		in:   []string{"amd64", "amd64", "arm64"},
 		want: []Architecture{amd64, arm64},
 	}, {
 		desc: "dedupe w/ apk style",
-		in:   []string{"x86_64", "amd64", "arm64"},
-		want: []Architecture{amd64, arm64},
+		in:   []string{"x86_64", "amd64", "arm64", "arm/v6", "armhf"},
+		want: []Architecture{amd64, armv6, arm64},
 	}} {
 		t.Run(c.desc, func(t *testing.T) {
 			got := ParseArchitectures(c.in)