treewide: replace stampsrcs with embed

A rules_go maintainer suggested using embed instead of stampsrcs:
https://github.com/bazel-contrib/rules_go/issues/3507

For Kubernetes, this means we need to patch the version libraries.
Instead of creating a separate file for each variable, I put them all in
one file, which is parsed in an init function. This init function needs
to run before all other init functions, which access the variables.

Another benefit of this change is that versions are stamped in all
binaries which include Kubernetes client libraries, not just hyperkube.

Change-Id: Ib1157d3686fc35e0c4191d2fc8e165862a1973c7
Reviewed-on: https://review.monogon.dev/c/monogon/+/4208
Tested-by: Jenkins CI
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
diff --git a/build/BUILD.bazel b/build/BUILD.bazel
index 3708a0b..9cd94ec 100644
--- a/build/BUILD.bazel
+++ b/build/BUILD.bazel
@@ -1,18 +1,42 @@
-load("//build/filter_stamp:def.bzl", "filtered_stamp")
+load("@aspect_bazel_lib//lib:expand_template.bzl", "expand_template")
 
-# This is a filtered stable status file which contains only the variables which
-# do not change on every commit. Using this instead of the status file avoids
-# unnecessary rebuilds.
-filtered_stamp(
-    name = "stabler_status",
-    vars = [
-        "KUBERNETES_gitMajor",
-        "KUBERNETES_gitMinor",
-        "KUBERNETES_gitVersion",
-        "KUBERNETES_gitCommit",
-        "KUBERNETES_gitTreeState",
-        "KUBERNETES_buildDate",
-        "MONOGON_copyright",
-    ],
+# The copyright and Kubernetes stamp variables change less often than the status
+# file. Instead of stamping these into Go binaries through x_defs, we create a
+# file for each variable and stamp through go:embed. With this indirection, only
+# the expand_template actions are executed each time the status file changes,
+# instead of relinking the Go binaries, which would be more expensive.
+
+expand_template(
+    name = "copyright_line",
+    out = "copyright_line.txt",
+    stamp = 1,
+    stamp_substitutions = {"copyright": "{{STABLE_MONOGON_copyright}}"},
+    template = ["copyright"],
+    visibility = ["//visibility:public"],
+)
+
+kubernetes_vars = [
+    "gitMajor",
+    "gitMinor",
+    "gitVersion",
+    "gitCommit",
+    "gitTreeState",
+    "buildDate",
+]
+
+[
+    expand_template(
+        name = "kubernetes_%s" % var,
+        out = "kubernetes_%s.txt" % var,
+        stamp = 1,
+        stamp_substitutions = {"value": "{{STABLE_KUBERNETES_%s}}" % var},
+        template = ["value"],
+    )
+    for var in kubernetes_vars
+]
+
+filegroup(
+    name = "kubernetes_stamp",
+    srcs = ["kubernetes_%s.txt" % var for var in kubernetes_vars],
     visibility = ["//visibility:public"],
 )
diff --git a/build/bazel/go.MODULE.bazel b/build/bazel/go.MODULE.bazel
index eb4aea5..d83abc6 100644
--- a/build/bazel/go.MODULE.bazel
+++ b/build/bazel/go.MODULE.bazel
@@ -235,6 +235,9 @@
         ],
     },
     "k8s.io/client-go": {
+        "patches": [
+            "//third_party/com_k8s_io_client_go:version-stamp.patch",
+        ],
         "pre_patches": [
             "//third_party/com_k8s_io_client_go:k8s-fix-websocket-custom-dialer.patch",
         ],
@@ -369,6 +372,7 @@
     "k8s.io/component-base": {
         "patches": [
             "//third_party/com_k8s_io_component_base:k8s-fix-metrics-data-race.patch",
+            "//third_party/com_k8s_io_component_base:version-stamp.patch",
         ],
     },
     "k8s.io/mount-utils": {
diff --git a/build/filter_stamp/BUILD.bazel b/build/filter_stamp/BUILD.bazel
deleted file mode 100644
index 693b12e..0000000
--- a/build/filter_stamp/BUILD.bazel
+++ /dev/null
@@ -1,14 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
-
-go_library(
-    name = "filter_stamp_lib",
-    srcs = ["main.go"],
-    importpath = "source.monogon.dev/build/filter_stamp",
-    visibility = ["//visibility:private"],
-)
-
-go_binary(
-    name = "filter_stamp",
-    embed = [":filter_stamp_lib"],
-    visibility = ["//visibility:public"],
-)
diff --git a/build/filter_stamp/def.bzl b/build/filter_stamp/def.bzl
deleted file mode 100644
index 650a025..0000000
--- a/build/filter_stamp/def.bzl
+++ /dev/null
@@ -1,32 +0,0 @@
-def _filtered_stamp_impl(ctx):
-    output = ctx.actions.declare_file(ctx.label.name + ".txt")
-    ctx.actions.run(
-        mnemonic = "FilterStamp",
-        executable = ctx.executable._filter_stamp,
-        arguments = ["-status", ctx.info_file.path, "-out", output.path, "--"] + ctx.attr.vars,
-        inputs = [ctx.info_file],
-        outputs = [output],
-    )
-    return [DefaultInfo(files = depset([output]))]
-
-filtered_stamp = rule(
-    implementation = _filtered_stamp_impl,
-    doc = """
-        Build a stamp file with a subset of
-        variables from the stable status file.
-    """,
-    attrs = {
-        "vars": attr.string_list(
-            doc = """
-                List of variables to include in the output.
-            """,
-            mandatory = True,
-        ),
-        # Tool
-        "_filter_stamp": attr.label(
-            default = Label("//build/filter_stamp"),
-            executable = True,
-            cfg = "exec",
-        ),
-    },
-)
diff --git a/build/filter_stamp/main.go b/build/filter_stamp/main.go
deleted file mode 100644
index 6b8d561..0000000
--- a/build/filter_stamp/main.go
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright The Monogon Project Authors.
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"os"
-	"strings"
-)
-
-var (
-	statusFile = flag.String("status", "", "Path to bazel workspace status file")
-	outFile    = flag.String("out", "", "Output stamp file path")
-)
-
-func main() {
-	flag.Parse()
-
-	vars := make(map[string]bool)
-	for _, variable := range flag.Args() {
-		vars[variable] = true
-	}
-
-	statusFileContent, err := os.ReadFile(*statusFile)
-	if err != nil {
-		log.Fatalf("Failed to open bazel workspace status file: %v\n", err)
-	}
-
-	var filtered []string
-	for line := range strings.SplitSeq(string(statusFileContent), "\n") {
-		if line == "" {
-			continue
-		}
-		variable, value, ok := strings.Cut(line, " ")
-		if !ok {
-			log.Fatalf("Invalid line in status file: %q\n", line)
-		}
-		variable, ok = strings.CutPrefix(variable, "STABLE_")
-		if ok && vars[variable] {
-			filtered = append(filtered, fmt.Sprintf("STABLER_%s %s\n", variable, value))
-		}
-	}
-
-	filteredContent := []byte(strings.Join(filtered, ""))
-	err = os.WriteFile(*outFile, filteredContent, 0644)
-	if err != nil {
-		log.Fatalf("Failed to write output file: %v", err)
-	}
-}
diff --git a/build/print-workspace-status.py b/build/print-workspace-status.py
index 992a8fc..3b86ee4 100755
--- a/build/print-workspace-status.py
+++ b/build/print-workspace-status.py
@@ -227,6 +227,10 @@
     variables["STABLE_KUBERNETES_gitCommit"] = git_commit
     variables["STABLE_KUBERNETES_gitTreeState"] = git_tree_state
     variables["STABLE_KUBERNETES_buildDate"] = git_commit_date
+else:
+    variables["STABLE_KUBERNETES_gitCommit"] = ""
+    variables["STABLE_KUBERNETES_gitTreeState"] = ""
+    variables["STABLE_KUBERNETES_buildDate"] = "1970-01-01T00:00:00Z"
 
 # Emit variables to stdout for consumption by Bazel and targets.
 for key in sorted(variables.keys()):