From 762b69120e344cf3bd9aab2d154923ce5fea7139 Mon Sep 17 00:00:00 2001
From: Ramkumar Chinchani <rchincha@cisco.com>
Date: Wed, 9 Nov 2022 21:18:28 +0000
Subject: [PATCH] feat: support writing os/arch info image config

Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
---
 build.go             |  5 ++---
 cache_test.go        |  2 +-
 doc/stacker_yaml.md  | 11 +++++++++++
 test/multi-arch.bats | 44 ++++++++++++++++++++++++++++++++++++++++++++
 types/layer.go       | 23 +++++++++++++++++++++++
 5 files changed, 81 insertions(+), 4 deletions(-)
 create mode 100644 test/multi-arch.bats

diff --git a/build.go b/build.go
index 1c3ba49f..2e980a78 100644
--- a/build.go
+++ b/build.go
@@ -8,7 +8,6 @@ import (
 	"os/exec"
 	"os/user"
 	"path"
-	"runtime"
 	"strings"
 	"time"
 
@@ -210,8 +209,8 @@ func (b *Builder) updateOCIConfigForOutput(sf *types.Stackerfile, s types.Storag
 	author := fmt.Sprintf("%s@%s", username, host)
 
 	meta.Created = time.Now()
-	meta.Architecture = runtime.GOARCH
-	meta.OS = runtime.GOOS
+	meta.Architecture = *l.Arch
+	meta.OS = *l.OS
 	meta.Author = author
 
 	annotations, err := mutator.Annotations(context.Background())
diff --git a/cache_test.go b/cache_test.go
index bb09249f..a6df77f0 100644
--- a/cache_test.go
+++ b/cache_test.go
@@ -126,5 +126,5 @@ func TestCacheEntryChanged(t *testing.T) {
 	// This test works because the type information is included in the
 	// hashstructure hash above, so using a zero valued CacheEntry is
 	// enough to capture changes in types.
-	assert.Equal(uint64(0x98ab47c70ff0b70), h)
+	assert.Equal(uint64(0x1ec739cbb7ee4e77), h)
 }
diff --git a/doc/stacker_yaml.md b/doc/stacker_yaml.md
index 11d69e71..7530fde0 100644
--- a/doc/stacker_yaml.md
+++ b/doc/stacker_yaml.md
@@ -233,3 +233,14 @@ While `config` section supports a similar `labels`, it is more pertitent to the
 image runtime. On the other hand, `annotations` is intended to be
 image-specific metadata aligned with the
 [annotations in the image spec](https://github.com/opencontainers/image-spec/blob/main/annotations.md).
+
+##### `os`
+
+`os` is a user-specified string value indicating which _operating system_ this image is being
+built for, for example, `linux`, `darwin`, etc. It is an optional field and it
+defaults to the host operating system if not specified.
+
+##### `arch`
+`arch` is a user-specified string value indicating which machine _architecture_ this image is being
+built for, for example, `amd64`, `arm64`, etc. It is an optional field and it
+defaults to the host machine architecture if not specified.
diff --git a/test/multi-arch.bats b/test/multi-arch.bats
new file mode 100644
index 00000000..3755b0d5
--- /dev/null
+++ b/test/multi-arch.bats
@@ -0,0 +1,44 @@
+load helpers
+
+function setup() {
+    stacker_setup
+}
+
+function teardown() {
+    cleanup
+}
+
+@test "multi-arch/os support" {
+    cat > stacker.yaml <<EOF
+centos:
+    os: darwin
+    arch: arm64
+    from:
+        type: oci
+        url: $CENTOS_OCI
+    import:
+        - https://www.cisco.com/favicon.ico
+EOF
+    stacker build
+
+    # check OCI image generation
+    manifest=$(cat oci/index.json | jq -r .manifests[0].digest | cut -f2 -d:)
+    layer=$(cat oci/blobs/sha256/$manifest | jq -r .layers[0].digest)
+    config=$(cat oci/blobs/sha256/$manifest | jq -r .config.digest | cut -f2 -d:)
+    [ "$(cat oci/blobs/sha256/$config | jq -r '.architecture')" = "arm64" ]
+    [ "$(cat oci/blobs/sha256/$config | jq -r '.os')" = "darwin" ]
+}
+
+@test "multi-arch/os bad config fails" {
+    cat > stacker.yaml <<EOF
+centos:
+    os:
+    from:
+        type: oci
+        url: $CENTOS_OCI
+    import:
+        - https://www.cisco.com/favicon.ico
+EOF
+    bad_stacker build
+    [ "$status" -eq 1 ]
+}
diff --git a/types/layer.go b/types/layer.go
index fe4886dc..8747a036 100644
--- a/types/layer.go
+++ b/types/layer.go
@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 	"reflect"
 	"regexp"
+	"runtime"
 	"strings"
 
 	"github.com/anmitsu/go-shlex"
@@ -192,6 +193,8 @@ type Layer struct {
 	Binds          Binds             `yaml:"binds"`
 	RuntimeUser    string            `yaml:"runtime_user"`
 	Annotations    map[string]string `yaml:"annotations"`
+	OS             *string           `yaml:"os"`
+	Arch           *string           `yaml:"arch"`
 }
 
 func parseLayers(referenceDirectory string, lms yaml.MapSlice, requireHash bool) (map[string]Layer, error) {
@@ -227,6 +230,12 @@ func parseLayers(referenceDirectory string, lms yaml.MapSlice, requireHash bool)
 					}
 				}
 			}
+
+			if directive.Key.(string) == "os" || directive.Key.(string) == "arch" {
+				if directive.Value == nil {
+					return nil, errors.Errorf("stackerfile: %q value cannot be empty", directive.Key.(string))
+				}
+			}
 		}
 	}
 
@@ -257,6 +266,18 @@ func parseLayers(referenceDirectory string, lms yaml.MapSlice, requireHash bool)
 			}
 		}
 
+		if layer.OS == nil {
+			// if not specified, default to runtime
+			os := runtime.GOOS
+			layer.OS = &os
+		}
+
+		if layer.Arch == nil {
+			// if not specified, default to runtime
+			arch := runtime.GOARCH
+			layer.Arch = &arch
+		}
+
 		ret[name], err = layer.absolutify(referenceDirectory)
 		if err != nil {
 			return nil, err
@@ -467,6 +488,8 @@ func init() {
 	layerType := reflect.TypeOf(Layer{})
 	for i := 0; i < layerType.NumField(); i++ {
 		tag := layerType.Field(i).Tag.Get("yaml")
+		// some fields are ",omitempty"
+		tag = strings.Split(tag, ",")[0]
 		layerFields = append(layerFields, tag)
 	}
 }