treewide: add product info to OCI OS images

Add the product info to the OCI OS image config.

Change-Id: I70c572f2698c8d8bb0edc0ba969d8c6b37ae4c00
Reviewed-on: https://review.monogon.dev/c/monogon/+/4193
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/osbase/build/genproductinfo/test.bzl b/osbase/build/genproductinfo/test.bzl
new file mode 100644
index 0000000..e639867
--- /dev/null
+++ b/osbase/build/genproductinfo/test.bzl
@@ -0,0 +1,39 @@
+# Copyright The Monogon Project Authors.
+# SPDX-License-Identifier: Apache-2.0
+
+def _test_product_info_impl(ctx):
+    raw_product_info = json.encode({
+        "id": ctx.attr.os_id,
+        "name": ctx.attr.os_name,
+        "version": "0.0.0",
+        "variant": ctx.attr.architecture,
+        "architecture": ctx.attr.architecture,
+    })
+    product_info_file = ctx.actions.declare_file(ctx.label.name + ".json")
+    ctx.actions.write(product_info_file, raw_product_info)
+    return [DefaultInfo(files = depset([product_info_file]))]
+
+_test_product_info = rule(
+    implementation = _test_product_info_impl,
+    attrs = {
+        "os_name": attr.string(mandatory = True),
+        "os_id": attr.string(mandatory = True),
+        "architecture": attr.string(mandatory = True),
+    },
+)
+
+def _test_product_info_macro_impl(**kwargs):
+    _test_product_info(
+        architecture = select({
+            "@platforms//cpu:x86_64": "x86_64",
+            "@platforms//cpu:aarch64": "aarch64",
+        }),
+        **kwargs
+    )
+
+test_product_info = macro(
+    inherit_attrs = _test_product_info,
+    attrs = {"architecture": None},
+    implementation = _test_product_info_macro_impl,
+    doc = "This is a simplified variant of product_info for use in tests.",
+)
diff --git a/osbase/build/mkoci/def.bzl b/osbase/build/mkoci/def.bzl
index f7f491d..8b688a7 100644
--- a/osbase/build/mkoci/def.bzl
+++ b/osbase/build/mkoci/def.bzl
@@ -1,6 +1,10 @@
 def _oci_os_image_impl(ctx):
     inputs = []
     arguments = []
+
+    inputs.append(ctx.file.product_info)
+    arguments += ["-product_info", ctx.file.product_info.path]
+
     for name, label in ctx.attr.srcs.items():
         files = label[DefaultInfo].files.to_list()
         if len(files) != 1:
@@ -42,6 +46,13 @@
         Build an OS image OCI artifact.
     """,
     attrs = {
+        "product_info": attr.label(
+            doc = """
+                Product info of the OS in JSON format.
+            """,
+            mandatory = True,
+            allow_single_file = True,
+        ),
         "srcs": attr.string_keyed_label_dict(
             doc = """
                 Payloads to include in the OCI artifact.
diff --git a/osbase/build/mkoci/main.go b/osbase/build/mkoci/main.go
index 16dd268..1fb2254 100644
--- a/osbase/build/mkoci/main.go
+++ b/osbase/build/mkoci/main.go
@@ -28,6 +28,7 @@
 var payloadNameRegexp = regexp.MustCompile(`^[0-9A-Za-z-](?:[0-9A-Za-z._-]{0,78}[0-9A-Za-z_-])?$`)
 
 var (
+	productInfoPath  = flag.String("product_info", "", "Path to the product info JSON file")
 	payloadName      = flag.String("payload_name", "", "Payload name for the next payload_file flag")
 	compressionLevel = flag.Int("compression_level", int(zstd.SpeedDefault), "Compression level")
 	outPath          = flag.String("out", "", "Output OCI Image Layout directory path")
@@ -201,9 +202,19 @@
 	})
 	flag.Parse()
 
+	rawProductInfo, err := os.ReadFile(*productInfoPath)
+	if err != nil {
+		log.Fatalf("Failed to read product info file: %v", err)
+	}
+	var productInfo osimage.ProductInfo
+	err = json.Unmarshal(rawProductInfo, &productInfo)
+	if err != nil {
+		log.Fatal(err)
+	}
+
 	// Create blobs directory.
 	blobsPath := filepath.Join(*outPath, "blobs", "sha256")
-	err := os.MkdirAll(blobsPath, 0755)
+	err = os.MkdirAll(blobsPath, 0755)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -223,6 +234,7 @@
 	// Write the OS image config.
 	imageConfig := osimage.Config{
 		FormatVersion: osimage.ConfigVersion,
+		ProductInfo:   productInfo,
 		Payloads:      payloadInfos,
 	}
 	imageConfigBytes, err := json.MarshalIndent(imageConfig, "", "\t")
diff --git a/osbase/oci/osimage/BUILD.bazel b/osbase/oci/osimage/BUILD.bazel
index f54cdfb..62a1401 100644
--- a/osbase/oci/osimage/BUILD.bazel
+++ b/osbase/oci/osimage/BUILD.bazel
@@ -23,6 +23,7 @@
         # the chunking, but also not too large.
         "test": "//third_party/linux",
     },
+    product_info = ":test_product_info.json",
     visibility = ["//osbase/oci:__subpackages__"],
 )
 
@@ -32,6 +33,7 @@
         "test": "//third_party/linux",
     },
     compression_level = 0,
+    product_info = ":test_product_info.json",
     visibility = ["//osbase/oci:__subpackages__"],
 )
 
diff --git a/osbase/oci/osimage/test_product_info.json b/osbase/oci/osimage/test_product_info.json
new file mode 100644
index 0000000..ba6096f
--- /dev/null
+++ b/osbase/oci/osimage/test_product_info.json
@@ -0,0 +1,10 @@
+{
+	"id": "testos",
+	"name": "Test OS",
+	"version": "0.0.0",
+	"variant": "x86_64",
+	"architecture": "x86_64",
+	"commit_hash": "",
+	"commit_date": "",
+	"build_tree_dirty": false
+}
diff --git a/osbase/oci/osimage/types.go b/osbase/oci/osimage/types.go
index d251a74..c94ae35 100644
--- a/osbase/oci/osimage/types.go
+++ b/osbase/oci/osimage/types.go
@@ -21,6 +21,8 @@
 	// FormatVersion should be incremented when making breaking changes to the
 	// image format. Readers must stop when they see an unknown version.
 	FormatVersion string `json:"format_version"`
+	// ProductInfo contains information about the content of the image.
+	ProductInfo ProductInfo `json:"product_info"`
 	// Payloads describes the payloads contained in the image. It has the same
 	// length and order as the layers list in the image manifest.
 	Payloads []PayloadInfo `json:"payloads"`