From 29021ced67103d5b695a00802c548889b742ff81 Mon Sep 17 00:00:00 2001 From: Alex Rodriguez <131964409+ezekiel-alexrod@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:30:29 +0200 Subject: [PATCH 1/3] chore(agent): build with Go 1.27 Renovate bumps the go directive to 1.27.2 in #14, but the image still builds with golang:1.26 and fails with "go.mod requires go >= 1.27.2". The devcontainer and the docs move along. Refs: MK8S-470 --- CONTRIBUTING.md | 9 +++++---- README.md | 4 ++-- agent/.devcontainer/devcontainer.json | 2 +- agent/Dockerfile | 2 +- agent/README.md | 6 +++--- agent/go.mod | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0ed4b9..65922cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,10 +29,11 @@ in [agent/README.md](agent/README.md#deploying). ## Development environment -The agent needs Go 1.26+. controller-gen, kustomize, envtest, golangci-lint and -kind are downloaded into `agent/bin/` at pinned versions by the `Makefile`, so -those need no manual install. `kubectl` does: the deploy and e2e targets expect -it on your `PATH`. CI pins it in `.github/workflows/agent-ci.yaml`. +The agent needs the Go version set in `agent/go.mod`. controller-gen, +kustomize, envtest, golangci-lint and kind are downloaded into `agent/bin/` at +pinned versions by the `Makefile`, so those need no manual install. `kubectl` +does: the deploy and e2e targets expect it on your `PATH`. CI pins it in +`.github/workflows/agent-ci.yaml`. One version escapes the pinning. The `logcheck` plugin of the custom golangci-lint build resolves to `latest` in `agent/.custom-gcl.yml`, so a new diff --git a/README.md b/README.md index 09b2623..1ba332d 100644 --- a/README.md +++ b/README.md @@ -255,8 +255,8 @@ under the same tag. ## Development -The agent needs Go 1.26+. The RPM tooling needs Docker, and Go for the -`imagecachectl` package. +The agent needs the Go version set in `agent/go.mod`. The RPM tooling needs +Docker, and Go for the `imagecachectl` package. ```console make -C agent test # unit tests and the envtest suite diff --git a/agent/.devcontainer/devcontainer.json b/agent/.devcontainer/devcontainer.json index a96838b..075cfad 100644 --- a/agent/.devcontainer/devcontainer.json +++ b/agent/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "golang:1.26", + "image": "golang:1.27", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": false, diff --git a/agent/Dockerfile b/agent/Dockerfile index 6312071..7bc8994 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.26 AS builder +FROM golang:1.27 AS builder ARG TARGETOS=linux # The agent only ships for amd64 (see DESIGN.md). ARG TARGETARCH=amd64 diff --git a/agent/README.md b/agent/README.md index 315e51c..3af65ae 100644 --- a/agent/README.md +++ b/agent/README.md @@ -17,9 +17,9 @@ All the commands below run from this directory. ## Prerequisites -- Go 1.26+ for building and testing. controller-gen, kustomize, envtest, - golangci-lint and kind are downloaded into `bin/` at pinned versions by the - `Makefile`. +- The Go version set in `go.mod` for building and testing. controller-gen, + kustomize, envtest, golangci-lint and kind are downloaded into `bin/` at + pinned versions by the `Makefile`. - `kubectl`. It is not downloaded: the targets expect it on your `PATH`. - Docker, or another `CONTAINER_TOOL`, for the image. - A cluster on Kubernetes 1.25+, which is where CEL validation of custom diff --git a/agent/go.mod b/agent/go.mod index d0951f7..8feab54 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -1,6 +1,6 @@ module github.com/scality/image-cache/agent -go 1.26.0 +go 1.27.2 require ( github.com/fsnotify/fsnotify v1.10.1 From 4a03f8fb15b8c5c7ac84ffcc8c3242f175a4f87c Mon Sep 17 00:00:00 2001 From: Alex Rodriguez <131964409+ezekiel-alexrod@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:30:29 +0200 Subject: [PATCH 2/3] refactor(agent): drop embedded type names from struct literals Go 1.27 lets a composite literal set a promoted field directly, and the modernize embedlit check flags every literal that still names the embedded type (ObjectMeta, NamespacedName, Descriptor). Applied with golangci-lint --fix, no behaviour change. Refs: MK8S-470 --- agent/internal/controller/fswatch.go | 3 +- .../controller/imagecache_validation_test.go | 6 +- agent/internal/controller/node_reconciler.go | 5 +- .../controller/node_reconciler_test.go | 61 ++++++++----------- agent/internal/controller/suite_test.go | 7 +-- agent/internal/puller/puller_test.go | 18 ++---- 6 files changed, 40 insertions(+), 60 deletions(-) diff --git a/agent/internal/controller/fswatch.go b/agent/internal/controller/fswatch.go index 6fde894..636cc1f 100644 --- a/agent/internal/controller/fswatch.go +++ b/agent/internal/controller/fswatch.go @@ -8,7 +8,6 @@ import ( "github.com/fsnotify/fsnotify" "github.com/scality/go-errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/event" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -51,7 +50,7 @@ func NewFSWatcher(nodeName string) (*FSWatcher, error) { paths: map[string]bool{}, trigger: event.GenericEvent{ Object: &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: nodeName}, + Name: nodeName, }, }, Events: make(chan event.GenericEvent, 1), diff --git a/agent/internal/controller/imagecache_validation_test.go b/agent/internal/controller/imagecache_validation_test.go index a1941c8..8a987c6 100644 --- a/agent/internal/controller/imagecache_validation_test.go +++ b/agent/internal/controller/imagecache_validation_test.go @@ -4,8 +4,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - imagecachev1alpha1 "github.com/scality/image-cache/agent/api/v1alpha1" ) @@ -14,8 +12,8 @@ import ( var _ = Describe("ImageCache validation", func() { create := func(name, source string) error { ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: name}, - Spec: imagecachev1alpha1.ImageCacheSpec{Source: source}, + Name: name, + Spec: imagecachev1alpha1.ImageCacheSpec{Source: source}, } err := k8sClient.Create(ctx, ic) if err == nil { diff --git a/agent/internal/controller/node_reconciler.go b/agent/internal/controller/node_reconciler.go index 39e9881..fa18dec 100644 --- a/agent/internal/controller/node_reconciler.go +++ b/agent/internal/controller/node_reconciler.go @@ -10,7 +10,6 @@ import ( "github.com/scality/go-errors" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/tools/events" "k8s.io/client-go/util/workqueue" @@ -225,7 +224,7 @@ func (r *NodeReconciler) patchLabels(ctx context.Context, node *corev1.Node, wan func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { toNode := handler.EnqueueRequestsFromMapFunc( func(context.Context, client.Object) []reconcile.Request { - return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: r.NodeName}}} + return []reconcile.Request{{Name: r.NodeName}} }) b := ctrl.NewControllerManagedBy(mgr). Named("node"). @@ -236,7 +235,7 @@ func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { // are cleaned up without waiting for a resource event. b = b.WatchesRawSource(source.Func( func(_ context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { - q.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: r.NodeName}}) + q.Add(reconcile.Request{Name: r.NodeName}) return nil })) diff --git a/agent/internal/controller/node_reconciler_test.go b/agent/internal/controller/node_reconciler_test.go index dec889b..a18d392 100644 --- a/agent/internal/controller/node_reconciler_test.go +++ b/agent/internal/controller/node_reconciler_test.go @@ -16,7 +16,6 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/tools/events" @@ -130,7 +129,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { It("becomes synced once the reconciler pulls and extracts it", func() { By("creating a matching ImageCache") ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: workerResourceName}, + Name: workerResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{osLabelKey: osLabelLinux}, Source: "registry.example.com/boot-cache-worker:134.0.0", @@ -158,7 +157,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { It("does not label the node for a resource that does not select it", func() { By("creating a non-matching ImageCache") ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: "other-134-0-0"}, + Name: "other-134-0-0", Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{zoneLabelKey: "mars"}, Source: "registry.example.com/boot-cache-other:134.0.0", @@ -176,7 +175,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { It("removes the label and the directory once the resource is deleted", func() { By("deleting the resource created in the first case") - ic := &imagecachev1alpha1.ImageCache{ObjectMeta: metav1.ObjectMeta{Name: workerResourceName}} + ic := &imagecachev1alpha1.ImageCache{Name: workerResourceName} Expect(k8sClient.Delete(ctx, ic)).To(Succeed()) By("waiting for the label to disappear") @@ -196,7 +195,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { By("creating a matching ImageCache while the puller fails") ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: "broken-134-0-0"}, + Name: "broken-134-0-0", Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{osLabelKey: osLabelLinux}, Source: "registry.example.com/boot-cache-broken:134.0.0", @@ -228,7 +227,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { It("keeps coexisting versions independent (upgrade scenario)", func() { By("creating two coexisting versions of the same boot cache") older := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: worker133ResourceName}, + Name: worker133ResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{osLabelKey: osLabelLinux}, Source: "registry.example.com/boot-cache-worker:133.0.0", @@ -236,7 +235,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { } Expect(k8sClient.Create(ctx, older)).To(Succeed()) newer := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: workerResourceName}, + Name: workerResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{osLabelKey: osLabelLinux}, Source: "registry.example.com/boot-cache-worker:134.0.0", @@ -302,7 +301,7 @@ var _ = Describe("NodeReconciler", Ordered, func() { By("triggering a pass over the cache path with a resource that selects nothing here") trigger := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: "trigger-134-0-0"}, + Name: "trigger-134-0-0", Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{zoneLabelKey: "mars"}, Source: "registry.example.com/boot-cache-other:134.0.0", @@ -453,13 +452,11 @@ var _ = Describe("startup pass", func() { By("creating a node carrying a stale synced label from a resource that no longer exists") node := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: staleNodeName, - // Deliberately does not match any suite CR selector (those - // use kubernetes.io/os): this node must stay untouched by - // every other test's ImageCache resources. - Labels: map[string]string{zoneLabelKey: "stale", LabelPrefix + "gone-1-0-0": StatusSynced}, - }, + Name: staleNodeName, + // Deliberately does not match any suite CR selector (those + // use kubernetes.io/os): this node must stay untouched by + // every other test's ImageCache resources. + Labels: map[string]string{zoneLabelKey: "stale", LabelPrefix + "gone-1-0-0": StatusSynced}, } Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { @@ -497,16 +494,16 @@ var _ = Describe("lost sentinel", func() { } { It("fills "+what+" again", func() { ctx := context.Background() - node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{ + node := &corev1.Node{ Name: lostNodeName, Labels: map[string]string{lostLabelKey: "yes"}, - }} + } Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { Expect(k8sClient.Delete(context.Background(), node)).To(Succeed()) }) ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: lostResourceName}, + Name: lostResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{lostLabelKey: "yes"}, Source: "registry.example.com/boot-cache-lost:1.0.0", @@ -528,7 +525,7 @@ var _ = Describe("lost sentinel", func() { Puller: &fakePuller{}, Resync: time.Hour, } - _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: lostNodeName}}) + _, err := r.Reconcile(ctx, ctrl.Request{Name: lostNodeName}) Expect(err).NotTo(HaveOccurred()) Expect(os.ReadFile(filepath.Join(res, etcdTarName))).To(Equal([]byte("etcd"))) Expect(filepath.Join(res, ".image-cache-agent.json")).To(BeARegularFile()) @@ -542,7 +539,7 @@ var _ = Describe("lost sentinel", func() { var _ = Describe("stray cache entries", func() { It("removes them", func() { ctx := context.Background() - node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: strayNodeName}} + node := &corev1.Node{Name: strayNodeName} Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { Expect(k8sClient.Delete(context.Background(), node)).To(Succeed()) @@ -559,7 +556,7 @@ var _ = Describe("stray cache entries", func() { Puller: &fakePuller{}, Resync: time.Hour, } - res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: strayNodeName}}) + res, err := r.Reconcile(ctx, ctrl.Request{Name: strayNodeName}) Expect(err).NotTo(HaveOccurred()) Expect(res.RequeueAfter).To(Equal(time.Hour)) Expect(stray).NotTo(BeAnExistingFile()) @@ -567,7 +564,7 @@ var _ = Describe("stray cache entries", func() { It("fails the pass when it cannot tell whether a directory is seeded", func() { ctx := context.Background() - node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: strayNodeName}} + node := &corev1.Node{Name: strayNodeName} Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { Expect(k8sClient.Delete(context.Background(), node)).To(Succeed()) @@ -585,7 +582,7 @@ var _ = Describe("stray cache entries", func() { Puller: &fakePuller{}, Resync: time.Hour, } - _, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: types.NamespacedName{Name: strayNodeName}}) + _, err := r.Reconcile(ctx, ctrl.Request{Name: strayNodeName}) Expect(err).To(MatchError(cache.ErrGC)) Expect(unreadable).To(BeADirectory()) }) @@ -601,10 +598,8 @@ var _ = Describe("missing cache path", func() { By("creating a node dedicated to this suite") node := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: missingNodeName, - Labels: map[string]string{missingPathLabelKey: missingPathLabelYes}, - }, + Name: missingNodeName, + Labels: map[string]string{missingPathLabelKey: missingPathLabelYes}, } Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { @@ -622,7 +617,7 @@ var _ = Describe("missing cache path", func() { By("creating a matching ImageCache") ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: missingResourceName}, + Name: missingResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{missingPathLabelKey: missingPathLabelYes}, Source: "registry.example.com/boot-cache-missing:134.0.0", @@ -673,10 +668,8 @@ var _ = Describe("filesystem repair", func() { By("creating a node dedicated to this suite") node := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: fsNodeName, - Labels: map[string]string{fsRepairLabelKey: fsRepairLabelYes}, - }, + Name: fsNodeName, + Labels: map[string]string{fsRepairLabelKey: fsRepairLabelYes}, } Expect(k8sClient.Create(ctx, node)).To(Succeed()) DeferCleanup(func() { @@ -707,7 +700,7 @@ var _ = Describe("filesystem repair", func() { By("creating a matching ImageCache") ic := &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: fsResourceName}, + Name: fsResourceName, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{fsRepairLabelKey: fsRepairLabelYes}, Source: "registry.example.com/boot-cache-fsrepair:1.0.0", @@ -774,7 +767,7 @@ func placeDirectory(dir, content, sentinel string) { // matchingResource is a resource that selects the suite's node. func matchingResource(name, source string) *imagecachev1alpha1.ImageCache { return &imagecachev1alpha1.ImageCache{ - ObjectMeta: metav1.ObjectMeta{Name: name}, + Name: name, Spec: imagecachev1alpha1.ImageCacheSpec{ NodeSelector: map[string]string{osLabelKey: osLabelLinux}, Source: source, diff --git a/agent/internal/controller/suite_test.go b/agent/internal/controller/suite_test.go index 64d30a1..e675b57 100644 --- a/agent/internal/controller/suite_test.go +++ b/agent/internal/controller/suite_test.go @@ -27,7 +27,6 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -107,10 +106,8 @@ var _ = BeforeSuite(func() { By("creating the test node") node := &corev1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: testNodeName, - Labels: map[string]string{osLabelKey: osLabelLinux}, - }, + Name: testNodeName, + Labels: map[string]string{osLabelKey: osLabelLinux}, } Expect(k8sClient.Create(ctx, node)).To(Succeed()) diff --git a/agent/internal/puller/puller_test.go b/agent/internal/puller/puller_test.go index 70bf30d..a9f49cf 100644 --- a/agent/internal/puller/puller_test.go +++ b/agent/internal/puller/puller_test.go @@ -118,16 +118,12 @@ func TestRemotePullResolvesMultiArchIndex(t *testing.T) { idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{ - Add: imgAmd, - Descriptor: v1.Descriptor{ - Platform: &v1.Platform{OS: linuxOS, Architecture: amd64Arch}, - }, + Add: imgAmd, + Platform: &v1.Platform{OS: linuxOS, Architecture: amd64Arch}, }, mutate.IndexAddendum{ - Add: imgArm, - Descriptor: v1.Descriptor{ - Platform: &v1.Platform{OS: linuxOS, Architecture: arm64Arch}, - }, + Add: imgArm, + Platform: &v1.Platform{OS: linuxOS, Architecture: arm64Arch}, }, ) @@ -216,10 +212,8 @@ func TestRemotePullNoMatchingPlatform(t *testing.T) { idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{ - Add: imgArm, - Descriptor: v1.Descriptor{ - Platform: &v1.Platform{OS: linuxOS, Architecture: arm64Arch}, - }, + Add: imgArm, + Platform: &v1.Platform{OS: linuxOS, Architecture: arm64Arch}, }, ) From 3da8c7104ddabcf374a2b44a61a3f8545d98894a Mon Sep 17 00:00:00 2001 From: Alex Rodriguez <131964409+ezekiel-alexrod@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:19:24 +0200 Subject: [PATCH 3/3] chore(agent): pin the golang images by digest The builder and devcontainer images use golang:1.27@sha256, like the other platform repos. The tag follows the Go minor, the digest brings each Go patch, and Renovate bumps both in the golang group, together with the go directive. Renovate dates a Docker tag by its last push, and the official golang images are rebuilt about once a week, so with a 7-day delay the image missed that group and #14 came without the Dockerfile. platform-renovate drops the delay for the golang image: the image goes first, and the directive, which keeps its delay, joins the same PR. The Dockerfile uses the BASE_IMAGE form of the kubebuilder v4.16 scaffold, so the next kubebuilder update has less to merge. Refs: MK8S-470 --- agent/.devcontainer/devcontainer.json | 2 +- agent/Dockerfile | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/agent/.devcontainer/devcontainer.json b/agent/.devcontainer/devcontainer.json index 075cfad..4db8c36 100644 --- a/agent/.devcontainer/devcontainer.json +++ b/agent/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Kubebuilder DevContainer", - "image": "golang:1.27", + "image": "golang:1.27@sha256:f24164268fc1e741bbdc0066dbadcf1c8426c5a8489f0729c02c3905a53032fd", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": false, diff --git a/agent/Dockerfile b/agent/Dockerfile index 7bc8994..e986232 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -1,5 +1,9 @@ # Build the manager binary -FROM golang:1.27 AS builder +# Override BASE_IMAGE to build from another registry. Keep a digest and a Go +# version no older than the go directive of go.mod, e.g. +# /library/golang:1.27@sha256:. +ARG BASE_IMAGE=golang:1.27@sha256:f24164268fc1e741bbdc0066dbadcf1c8426c5a8489f0729c02c3905a53032fd +FROM ${BASE_IMAGE} AS builder ARG TARGETOS=linux # The agent only ships for amd64 (see DESIGN.md). ARG TARGETARCH=amd64