From a308e317517c103b54e376a8b23b0983b51b39c1 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Jun 2026 14:19:19 +0000 Subject: [PATCH 01/11] controller: add tag resolver using go-containerregistry Introduce GGCRResolver to resolve container image tags to digests via remote.Get. Assisted-by: AI Signed-off-by: Alice Frosi --- go.mod | 11 +++++++++-- go.sum | 31 ++++++++++++++++++++++--------- internal/registry/resolver.go | 27 +++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 internal/registry/resolver.go diff --git a/go.mod b/go.mod index 3ba4e9a..cd4f282 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-logr/logr v1.4.3 + github.com/google/go-containerregistry v0.21.7 github.com/onsi/gomega v1.42.1 k8s.io/api v0.36.2 k8s.io/apimachinery v0.36.2 @@ -22,6 +23,8 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v29.5.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect @@ -38,6 +41,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.6 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -47,6 +51,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect @@ -54,8 +59,9 @@ require ( github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -63,7 +69,7 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.56.0 // indirect - golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect @@ -74,6 +80,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.2 // indirect k8s.io/apiextensions-apiserver v0.36.0 // indirect k8s.io/cli-runtime v0.36.2 // indirect k8s.io/component-base v0.36.2 // indirect diff --git a/go.sum b/go.sum index f33a4f5..244d6cf 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs= +github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -56,6 +60,8 @@ github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnL github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU= +github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -69,8 +75,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -104,6 +110,8 @@ github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -125,10 +133,13 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -156,12 +167,12 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -173,8 +184,8 @@ golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= -golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= +golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= @@ -189,6 +200,8 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= diff --git a/internal/registry/resolver.go b/internal/registry/resolver.go new file mode 100644 index 0000000..24f454c --- /dev/null +++ b/internal/registry/resolver.go @@ -0,0 +1,27 @@ +package registry + +import ( + "context" + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// GGCRResolver resolves image tags to digests using go-containerregistry. +type GGCRResolver struct{} + +func (r *GGCRResolver) Resolve(ctx context.Context, ref string) (string, error) { + parsed, err := name.ParseReference(ref) + if err != nil { + return "", fmt.Errorf("parsing reference %q: %w", ref, err) + } + if d, ok := parsed.(name.Digest); ok { + return d.DigestStr(), nil + } + desc, err := remote.Get(parsed, remote.WithContext(ctx)) + if err != nil { + return "", fmt.Errorf("fetching manifest for %q: %w", ref, err) + } + return desc.Digest.String(), nil +} From f239f0733ce080de5fe223ea192de9b582ee1341 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Jun 2026 14:30:53 +0000 Subject: [PATCH 02/11] registry: add unit tests for GGCRResolver Test tag resolution against GGCR's in-memory registry, invalid reference parsing, and unreachable registry errors. Assisted-by: AI Signed-off-by: Alice Frosi --- internal/registry/resolver_test.go | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 internal/registry/resolver_test.go diff --git a/internal/registry/resolver_test.go b/internal/registry/resolver_test.go new file mode 100644 index 0000000..10a044e --- /dev/null +++ b/internal/registry/resolver_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +package registry + +import ( + "context" + "fmt" + "net/http/httptest" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + . "github.com/onsi/gomega" +) + +func TestResolveValidTag(t *testing.T) { + tags := []string{"latest", "v1.0", ""} + + g := NewWithT(t) + + srv := httptest.NewServer(registry.New()) + defer srv.Close() + + // Build a minimal OCI image (empty base + one random layer) to push. + layer, err := random.Layer(256, types.OCILayer) + g.Expect(err).NotTo(HaveOccurred()) + img, err := mutate.AppendLayers(empty.Image, layer) + g.Expect(err).NotTo(HaveOccurred()) + + for _, tag := range tags { + t.Run(tag, func(t *testing.T) { + var imageName string + if tag == "" { + imageName = fmt.Sprintf("%s/test/image", srv.Listener.Addr().String()) + } else { + imageName = fmt.Sprintf("%s/test/image:%s", srv.Listener.Addr().String(), tag) + } + ref, err := name.ParseReference(imageName) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(remote.Write(ref, img)).To(Succeed()) + + want, err := img.Digest() + g.Expect(err).NotTo(HaveOccurred()) + + resolver := &GGCRResolver{} + got, err := resolver.Resolve(context.Background(), ref.String()) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(got).To(Equal(want.String())) + }) + } +} + +func TestResolveDigestRef(t *testing.T) { + g := NewWithT(t) + + resolver := &GGCRResolver{} + digest := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + ref := "registry.example.com/test/image@" + digest + got, err := resolver.Resolve(context.Background(), ref) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(got).To(Equal(digest)) +} + +func TestResolveUnreachableRegistry(t *testing.T) { + g := NewWithT(t) + + resolver := &GGCRResolver{} + _, err := resolver.Resolve(context.Background(), "localhost:1/nonexistent/image:latest") + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("fetching manifest")) +} From 5fb099092aa6a650fc92cf551e8e4ca8ae92328a Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Jun 2026 14:32:27 +0000 Subject: [PATCH 03/11] api: add PoolTagResolutionError degraded reason New Degraded condition reason for when the controller fails to resolve a tag from the container registry. Assisted-by: AI Signed-off-by: Alice Frosi --- api/v1alpha1/bootcnodepool_types.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/v1alpha1/bootcnodepool_types.go b/api/v1alpha1/bootcnodepool_types.go index 552c9e9..2d3ecac 100644 --- a/api/v1alpha1/bootcnodepool_types.go +++ b/api/v1alpha1/bootcnodepool_types.go @@ -45,6 +45,10 @@ const ( // digest ref, or a malformed nodeSelector). PoolInvalidSpec string = "InvalidSpec" + // PoolTagResolutionError means the controller failed + // resolve the tag. + PoolTagResolutionError string = "TagResolutionError" + // PoolHealthy means no issues. PoolHealthy string = "Healthy" ) From 25569e6f4a7e3dc047a2d41bb021150f0129141e Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Jun 2026 14:34:00 +0000 Subject: [PATCH 04/11] api: add NextTagResolutionTime status field Tracks when the controller will next resolve a tag-based image ref to a digest. Assisted-by: AI Signed-off-by: Alice Frosi --- api/v1alpha1/bootcnodepool_types.go | 5 +++++ api/v1alpha1/zz_generated.deepcopy.go | 4 ++++ config/crd/bases/node.bootc.dev_bootcnodepools.yaml | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/api/v1alpha1/bootcnodepool_types.go b/api/v1alpha1/bootcnodepool_types.go index 2d3ecac..1818307 100644 --- a/api/v1alpha1/bootcnodepool_types.go +++ b/api/v1alpha1/bootcnodepool_types.go @@ -163,6 +163,11 @@ type BootcNodePoolStatus struct { // +optional TargetDigest string `json:"targetDigest,omitempty"` + // nextTagResolutionTime is when the controller will next resolve a + // tag ref to a digest. Only set for tag-based image refs. + // +optional + NextTagResolutionTime *metav1.Time `json:"nextTagResolutionTime,omitempty"` + // deployedDigest is the last digest fully rolled out to all nodes in // the pool. // +optional diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b91adab..8467775 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -169,6 +169,10 @@ func (in *BootcNodePoolSpec) DeepCopy() *BootcNodePoolSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BootcNodePoolStatus) DeepCopyInto(out *BootcNodePoolStatus) { *out = *in + if in.NextTagResolutionTime != nil { + in, out := &in.NextTagResolutionTime, &out.NextTagResolutionTime + *out = (*in).DeepCopy() + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/config/crd/bases/node.bootc.dev_bootcnodepools.yaml b/config/crd/bases/node.bootc.dev_bootcnodepools.yaml index 9735eaf..7e8da3e 100644 --- a/config/crd/bases/node.bootc.dev_bootcnodepools.yaml +++ b/config/crd/bases/node.bootc.dev_bootcnodepools.yaml @@ -243,6 +243,12 @@ spec: deployedDigest is the last digest fully rolled out to all nodes in the pool. type: string + nextTagResolutionTime: + description: |- + nextTagResolutionTime is when the controller will next resolve a + tag ref to a digest. Only set for tag-based image refs. + format: date-time + type: string nodeCount: description: nodeCount is the total number of nodes in this pool. format: int32 From be96afc58cee47309f3e95d9c498e97ce4000dcc Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 23 Jun 2026 15:19:33 +0000 Subject: [PATCH 05/11] controller: wire tag resolver and rewrite resolveTargetDigest Add TagResolver and TagResolutionInterval to the reconciler struct. The TagResolver defines an interface to solve the image digest. Rewrite resolveTargetDigest to resolve tag refs via the registry with periodic re-resolution, and set Degraded/RegistryError on failure. Wire GGCRResolver and --tag-resolution-interval flag in main.go. Assisted-by: AI Signed-off-by: Alice Frosi --- cmd/controller/main.go | 12 ++- .../controller/bootcnodepool_controller.go | 89 +++++++++++++++---- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index ecc710e..9885e0e 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -5,6 +5,7 @@ package main import ( "flag" "os" + "time" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -16,6 +17,7 @@ import ( bootcv1alpha1 "github.com/bootc-dev/bootc-operator/api/v1alpha1" "github.com/bootc-dev/bootc-operator/internal/controller" + "github.com/bootc-dev/bootc-operator/internal/registry" ) var ( @@ -31,7 +33,9 @@ func init() { func main() { var enableLeaderElection bool var probeAddr string + var tagResolutionInterval time.Duration flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.DurationVar(&tagResolutionInterval, "tag-resolution-interval", 5*time.Minute, "How often to re-resolve tag-based image refs.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -61,9 +65,11 @@ func main() { } if err := (&controller.BootcNodePoolReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - KubeClient: kubeClient, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + KubeClient: kubeClient, + TagResolver: ®istry.GGCRResolver{}, + TagResolutionInterval: tagResolutionInterval, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "bootcnodepool") os.Exit(1) diff --git a/internal/controller/bootcnodepool_controller.go b/internal/controller/bootcnodepool_controller.go index 7f0f3b3..999c062 100644 --- a/internal/controller/bootcnodepool_controller.go +++ b/internal/controller/bootcnodepool_controller.go @@ -45,6 +45,11 @@ type drainStatus struct { isStalled bool //nolint:unused // used by drain stall detection } +// TagResolver resolves a container image reference to a digest. +type TagResolver interface { + Resolve(ctx context.Context, ref string) (string, error) +} + // BootcNodePoolReconciler reconciles a BootcNodePool object type BootcNodePoolReconciler struct { client.Client @@ -52,6 +57,9 @@ type BootcNodePoolReconciler struct { KubeClient kubernetes.Interface Recorder events.EventRecorder + TagResolver TagResolver + TagResolutionInterval time.Duration + // drainCh receives events from drain goroutines to re-enqueue the // owning pool after a drain completes. drainCh chan event.GenericEvent @@ -239,13 +247,31 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques // the end. // Resolve the target digest from the image ref. - if err := r.resolveTargetDigest(&pool); err != nil { + resolveResult, err := r.resolveTargetDigest(ctx, &pool) + if err != nil { if isInvalidSpecError(err) { return r.setInvalidSpecCondition(ctx, &pool, err) } return ctrl.Result{}, fmt.Errorf("resolving target digest: %w", err) } + // complete handles the boilerplate exit logic for the happy path and writes + // the pool status if anything changed. + complete := func(result ctrl.Result) (ctrl.Result, error) { + if !reflect.DeepEqual(pool.Status, *statusOrig) { + if err := r.Status().Update(ctx, &pool); err != nil { + return ctrl.Result{}, fmt.Errorf("updating pool status: %w", err) + } + } + + return resolveResult, nil + } + + if pool.Status.TargetDigest == "" { + // First tag resolution failed — nothing to roll out yet. + return complete(resolveResult) + } + // Sync pool membership and retrieve BootcNodes we own. ownedBootcNodes, err := r.syncMembership(ctx, &pool) if err != nil { @@ -267,31 +293,56 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques return ctrl.Result{}, fmt.Errorf("driving rollout: %w", err) } - // Write pool status once if anything changed. - if !reflect.DeepEqual(pool.Status, *statusOrig) { - if err := r.Status().Update(ctx, &pool); err != nil { - return ctrl.Result{}, fmt.Errorf("updating pool status: %w", err) - } - } - - return ctrl.Result{}, nil + return complete(resolveResult) } -// resolveTargetDigest parses the digest from the pool's image ref and -// sets pool.Status.TargetDigest. For digest refs (the only kind -// supported now), the digest is extracted directly. Tag resolution is -// deferred to Milestone 5. -func (r *BootcNodePoolReconciler) resolveTargetDigest(pool *bootcv1alpha1.BootcNodePool) error { +// resolveTargetDigest resolves the target digest from the pool's image +// ref. Digest refs are extracted directly. Tag refs are resolved via +// the registry, respecting the re-resolution interval. +func (r *BootcNodePoolReconciler) resolveTargetDigest(ctx context.Context, pool *bootcv1alpha1.BootcNodePool) (ctrl.Result, error) { + log := logf.FromContext(ctx) + ref, err := parseImageRef(pool.Spec.Image.Ref) if err != nil { - return newInvalidSpecError(fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err)) + return ctrl.Result{}, newInvalidSpecError(fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err)) } + digested, ok := ref.(reference.Digested) - if !ok { - return newInvalidSpecError(fmt.Sprintf("image ref %q has no digest (tag resolution not yet supported)", pool.Spec.Image.Ref)) + if ok { + pool.Status.TargetDigest = digested.Digest().String() + // Reset the NextTagResolutionTime in case we pass from a tag referenced image to a digested one. + // Otherwise, it simply a nop + pool.Status.NextTagResolutionTime = nil + return ctrl.Result{}, nil } - pool.Status.TargetDigest = digested.Digest().String() - return nil + + // Tag ref — check if resolution is due. + now := time.Now() + if pool.Status.NextTagResolutionTime != nil && now.Before(pool.Status.NextTagResolutionTime.Time) { + remaining := pool.Status.NextTagResolutionTime.Sub(now) + log.V(1).Info("Tag resolution not yet due", "remaining", remaining) + return ctrl.Result{RequeueAfter: remaining}, nil + } + + digest, err := r.TagResolver.Resolve(ctx, pool.Spec.Image.Ref) + if err != nil { + log.Error(err, "Failed to resolve tag", "ref", pool.Spec.Image.Ref) + apimeta.SetStatusCondition(&pool.Status.Conditions, metav1.Condition{ + Type: bootcv1alpha1.PoolDegraded, + Status: metav1.ConditionTrue, + Reason: bootcv1alpha1.PoolTagResolutionError, + Message: err.Error(), + }) + } else { + if pool.Status.TargetDigest != digest { + log.Info("Resolved tag to new digest", "ref", pool.Spec.Image.Ref, "digest", digest) + } + pool.Status.TargetDigest = digest + } + + next := metav1.NewTime(now.Add(r.TagResolutionInterval)) + pool.Status.NextTagResolutionTime = &next + return ctrl.Result{RequeueAfter: r.TagResolutionInterval}, nil } // parseImageRef parses an image reference string into a named From d3201ff0d58f3ce8046fb98b125842c21a3b58f9 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 24 Jun 2026 14:55:49 +0000 Subject: [PATCH 06/11] controller: add --allow-insecure-registry flag Allow falling back to HTTP when resolving tag-based image refs against registries that do not serve TLS. Assisted-by: AI Signed-off-by: Alice Frosi --- cmd/controller/main.go | 5 ++++- internal/registry/resolver.go | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 9885e0e..64537d7 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -34,8 +34,11 @@ func main() { var enableLeaderElection bool var probeAddr string var tagResolutionInterval time.Duration + var allowInsecureRegistry bool flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.DurationVar(&tagResolutionInterval, "tag-resolution-interval", 5*time.Minute, "How often to re-resolve tag-based image refs.") + flag.BoolVar(&allowInsecureRegistry, "allow-insecure-registry", false, + "Allow falling back to HTTP when resolving tag-based image refs against registries that do not serve TLS.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -68,7 +71,7 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), KubeClient: kubeClient, - TagResolver: ®istry.GGCRResolver{}, + TagResolver: ®istry.GGCRResolver{AllowInsecure: allowInsecureRegistry}, TagResolutionInterval: tagResolutionInterval, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "bootcnodepool") diff --git a/internal/registry/resolver.go b/internal/registry/resolver.go index 24f454c..649507b 100644 --- a/internal/registry/resolver.go +++ b/internal/registry/resolver.go @@ -2,14 +2,21 @@ package registry import ( "context" + "crypto/tls" + "errors" "fmt" + "net/http" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" ) // GGCRResolver resolves image tags to digests using go-containerregistry. -type GGCRResolver struct{} +type GGCRResolver struct { + // AllowInsecure enables fallback to HTTP when the HTTPS connection + // to the registry fails. + AllowInsecure bool +} func (r *GGCRResolver) Resolve(ctx context.Context, ref string) (string, error) { parsed, err := name.ParseReference(ref) @@ -20,6 +27,14 @@ func (r *GGCRResolver) Resolve(ctx context.Context, ref string) (string, error) return d.DigestStr(), nil } desc, err := remote.Get(parsed, remote.WithContext(ctx)) + var tlsErr *tls.CertificateVerificationError + if (errors.As(err, &tlsErr) || errors.Is(err, http.ErrSchemeMismatch)) && r.AllowInsecure { + insecure, parseErr := name.ParseReference(ref, name.Insecure) + if parseErr != nil { + return "", fmt.Errorf("parsing reference %q: %w", ref, parseErr) + } + desc, err = remote.Get(insecure, remote.WithContext(ctx)) + } if err != nil { return "", fmt.Errorf("fetching manifest for %q: %w", ref, err) } From 8998b909ff6d2685421e02e66fc7193a39ead83c Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Wed, 24 Jun 2026 07:25:12 +0000 Subject: [PATCH 07/11] e2e: add NodeImageTagRef and RetagImage helpers Introduce helper functions for the image handling. The NodeImageTagRef returns the tag-based image reference for the seeded node image, for use in tag resolution e2e tests. The RetagImage takes an image and retags it. Assisted-by: AI Signed-off-by: Alice Frosi --- test/e2e/e2eutil/env.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/e2e/e2eutil/env.go b/test/e2e/e2eutil/env.go index 77efd88..aa55d83 100644 --- a/test/e2e/e2eutil/env.go +++ b/test/e2e/e2eutil/env.go @@ -18,6 +18,8 @@ import ( "testing" "time" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" . "github.com/onsi/gomega" //nolint:staticcheck corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes/scheme" @@ -224,6 +226,12 @@ func (e *Env) NodeImageDigestedPullSpec() string { return e.nodeImageRegistry + "@" + e.nodeImageDigest } +// NodeImageTagRef returns the tag-based reference for the seeded node +// image (e.g. "registry.cluster.local:5000/node:latest"). +func (e *Env) NodeImageTagRef() string { + return e.nodeImageRegistry + ":latest" +} + // NodeImageDigest returns the manifest digest of the seeded node image. func (e *Env) NodeImageDigest() string { return e.nodeImageDigest @@ -243,6 +251,33 @@ func (e *Env) NodeImageUpdateDigest() string { return e.nodeImageUpdateDigest } +// RetagImage reads the image at srcRef from the localhost registry and +// tags it as dstTag. +func RetagImage(t *testing.T, srcRef, dstTag string) { + t.Helper() + + src, err := name.ParseReference(srcRef, name.Insecure) + if err != nil { + t.Fatalf("parsing src ref %q: %v", srcRef, err) + } + desc, err := remote.Get(src) + if err != nil { + t.Fatalf("fetching %q: %v", srcRef, err) + } + img, err := desc.Image() + if err != nil { + t.Fatalf("getting image from descriptor: %v", err) + } + + dst, err := name.ParseReference(dstTag, name.Insecure) + if err != nil { + t.Fatalf("parsing dst ref %q: %v", dstTag, err) + } + if err := remote.Write(dst, img); err != nil { + t.Fatalf("writing %q: %v", dstTag, err) + } +} + // cleanup gathers diagnostic logs, then deletes test-scoped resources // and bink nodes. func (e *Env) cleanup(t *testing.T) { From 4f7f44fb85a27191d01fd4128e651c5a782dfed8 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Fri, 3 Jul 2026 11:17:42 +0000 Subject: [PATCH 08/11] e2e: add TestTagResolution for tag-based image refs The test TestTagResolution verifies that controller is triggering a bootc switch if the image reference by a tag changes in the registry. The test retags the updated image with the same referenced tag. Additionally, the polling time for the controller is reduce the polling time and it configures the insecure registries. Assisted-by: AI Signed-off-by: Alice Frosi --- test/e2e/bootcnode_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/test/e2e/bootcnode_test.go b/test/e2e/bootcnode_test.go index 2b57b22..7577755 100644 --- a/test/e2e/bootcnode_test.go +++ b/test/e2e/bootcnode_test.go @@ -199,3 +199,85 @@ func TestUpdateReboot(t *testing.T) { t.Logf("Verified update-marker exists on host via daemon pod") } + +// TestTagResolution creates a pool with a tag-based image ref, verifies +// the controller resolves the tag to a digest, then retags the image +// and verifies re-resolution triggers a rollout. +func TestTagResolution(t *testing.T) { + g := NewWithT(t) + g.SetDefaultEventuallyTimeout(pollTimeout) + g.SetDefaultEventuallyPollingInterval(pollInterval) + + env := e2eutil.New(t) + + ctx := context.Background() + + nodeName := env.AddNode(t) + + // The seed step already pushed node:latest with the original image. + // Create a pool using the tag ref. + pool := env.NewPool("tag", env.NodeImageTagRef()) + g.Expect(env.Client.Create(ctx, pool)).To(Succeed()) + + // Verify targetDigest is resolved to the original image digest. + g.Eventually(func(g Gomega) string { + var p bootcv1alpha1.BootcNodePool + g.Expect(env.Client.Get(ctx, client.ObjectKeyFromObject(pool), &p)).To(Succeed()) + return p.Status.TargetDigest + }).WithTimeout(1 * time.Minute).Should(Equal(env.NodeImageDigest())) + + t.Logf("Tag resolved to original digest %s", env.NodeImageDigest()) + + // Wait for node to reach Idle with the original image. + g.Eventually(func(g Gomega) bootcv1alpha1.BootcNodeStatus { + var bn bootcv1alpha1.BootcNode + g.Expect(env.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &bn)).To(Succeed()) + return bn.Status + }).WithTimeout(3 * time.Minute).Should(And( + HaveField("Booted", And( + Not(BeNil()), + HaveField("ImageDigest", Equal(env.NodeImageDigest())), + )), + HaveField("Conditions", ContainElement(And( + HaveField("Type", bootcv1alpha1.NodeIdle), + HaveField("Status", metav1.ConditionTrue), + ))), + )) + + t.Logf("Node %q is Idle with original image", nodeName) + + // Retag node:latest to point at the update image. + e2eutil.RetagImage(t, + "localhost:5000/node@"+env.NodeImageUpdateDigest(), + "localhost:5000/node:latest", + ) + + t.Logf("Retagged node:latest to update digest %s", env.NodeImageUpdateDigest()) + + // Wait for the controller to re-resolve and pick up the new digest. + g.Eventually(func(g Gomega) string { + var p bootcv1alpha1.BootcNodePool + g.Expect(env.Client.Get(ctx, client.ObjectKeyFromObject(pool), &p)).To(Succeed()) + return p.Status.TargetDigest + }).WithTimeout(1 * time.Minute).Should(Equal(env.NodeImageUpdateDigest())) + + t.Logf("Tag re-resolved to update digest %s", env.NodeImageUpdateDigest()) + + // Wait for node to reach Idle with the update image. + g.Eventually(func(g Gomega) bootcv1alpha1.BootcNodeStatus { + var bn bootcv1alpha1.BootcNode + g.Expect(env.Client.Get(ctx, client.ObjectKey{Name: nodeName}, &bn)).To(Succeed()) + return bn.Status + }).WithTimeout(5 * time.Minute).Should(And( + HaveField("Booted", And( + Not(BeNil()), + HaveField("ImageDigest", Equal(env.NodeImageUpdateDigest())), + )), + HaveField("Conditions", ContainElement(And( + HaveField("Type", bootcv1alpha1.NodeIdle), + HaveField("Status", metav1.ConditionTrue), + ))), + )) + + t.Logf("Node %q is Idle with update image", nodeName) +} From 2c29257570ce171406a0b5adcb12e71655d625f1 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Fri, 3 Jul 2026 16:31:40 +0200 Subject: [PATCH 09/11] e2e: increase test timeout to 20m TestTagResolution adds ~5m to the suite, pushing past the old 10m limit. Signed-off-by: Alice Frosi --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c9e5f7d..5bc1663 100644 --- a/Makefile +++ b/Makefile @@ -73,7 +73,7 @@ e2e: ## Run e2e tests (requires: make deploy-bink). V=1 for verbose. RUN= ARTIFACTS=$(ARTIFACTS) \ BINK_NODE_IMAGE_DIGEST=$$(skopeo inspect --tls-verify=false --format '{{.Digest}}' docker://localhost:5000/node:latest) \ BINK_NODE_IMAGE_UPDATE_DIGEST=$$(skopeo inspect --tls-verify=false docker://localhost:5000/node:update | jq -r '.Digest') \ - go test -timeout 10m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) . + go test -timeout 20m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) . ##@ Build From 81c25e9123f530f89f7bced528c58e1f7ea95f9b Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Mon, 6 Jul 2026 09:25:53 +0200 Subject: [PATCH 10/11] make: configure operator for bink deployment Allow insecure registries and reduce the polling time for the registry resolution tag for the controller. This allows the controller to talk to the bink registry and reduce the polling time to speed up the tests. Signed-off-by: Alice Frosi --- Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 5bc1663..5fec160 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,7 @@ deploy: manifests kustomize yq ## Deploy controller to the K8s cluster specified "$(KUSTOMIZE)" build config/default | \ "$(YQ)" '(select(.kind == "Deployment") | .spec.template.spec.containers[] | select(.name == "manager")).image = "$(IMG)"' | \ "$(YQ)" '(select(.kind == "DaemonSet") | .spec.template.spec.containers[] | select(.name == "daemon")).image = "$(IMG)"' | \ + $(if $(MANAGER_EXTRA_ARGS),"$(YQ)" '(select(.kind == "Deployment") | .spec.template.spec.containers[] | select(.name == "manager")).args += [$(MANAGER_EXTRA_ARGS)]' |) \ "$(KUBECTL)" apply --server-side -f - .PHONY: undeploy @@ -151,7 +152,8 @@ deploy-bink: start-bink build-update-image kustomize ## Deploy to a bink cluster # On re-deploy, restart the rollout to force a re-pull of the :latest tag. # On fresh deploy, skip the restart -- the pod is already pulling the correct image. @existed=$$(kubectl --kubeconfig $(KUBECONFIG_BINK) -n bootc-operator get deploy bootc-operator-controller-manager -o name 2>/dev/null || true) && \ - $(MAKE) deploy KUBECONFIG=$(abspath $(KUBECONFIG_BINK)) IMG=$(IMG_BINK) && \ + $(MAKE) deploy KUBECONFIG=$(abspath $(KUBECONFIG_BINK)) IMG=$(IMG_BINK) \ + MANAGER_EXTRA_ARGS='"--allow-insecure-registry","--tag-resolution-interval=10s"' && \ if [ -n "$$existed" ]; then \ kubectl --kubeconfig $(KUBECONFIG_BINK) -n bootc-operator rollout restart deployment/bootc-operator-controller-manager; \ fi From aa89b4c9eaba6006f3dba1df0b9152df95003188 Mon Sep 17 00:00:00 2001 From: Alice Frosi Date: Tue, 7 Jul 2026 17:05:31 +0200 Subject: [PATCH 11/11] Mark milestone-5a as complete Signed-off-by: Alice Frosi --- docs/IMPLEMENTATION_PLAN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index d6c6bd6..2dc165c 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -255,7 +255,7 @@ the polling interval, proving fsnotify is working). ## Milestone 5: Tag Resolution & Pull Secrets -### 5a. Tag resolution +### 5a. Tag resolution ✅ - Registry client (e.g. `go-containerregistry`) to resolve tags to digests