Skip to content

Commit b0e9519

Browse files
committed
controller: wire tag resolver and rewrite resolveTargetDigest
Add TagResolver and TagResolutionInterval to the reconciler struct. 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: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alice Frosi <afrosi@redhat.com>
1 parent fd9c1a2 commit b0e9519

2 files changed

Lines changed: 67 additions & 15 deletions

File tree

cmd/controller/main.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55
import (
66
"flag"
77
"os"
8+
"time"
89

910
"k8s.io/apimachinery/pkg/runtime"
1011
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -16,6 +17,7 @@ import (
1617

1718
bootcv1alpha1 "github.com/jlebon/bootc-operator/api/v1alpha1"
1819
"github.com/jlebon/bootc-operator/internal/controller"
20+
"github.com/jlebon/bootc-operator/internal/registry"
1921
)
2022

2123
var (
@@ -31,7 +33,9 @@ func init() {
3133
func main() {
3234
var enableLeaderElection bool
3335
var probeAddr string
36+
var tagResolutionInterval time.Duration
3437
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
38+
flag.DurationVar(&tagResolutionInterval, "tag-resolution-interval", 5*time.Minute, "How often to re-resolve tag-based image refs.")
3539
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
3640
"Enable leader election for controller manager. "+
3741
"Enabling this will ensure there is only one active controller manager.")
@@ -61,9 +65,11 @@ func main() {
6165
}
6266

6367
if err := (&controller.BootcNodePoolReconciler{
64-
Client: mgr.GetClient(),
65-
Scheme: mgr.GetScheme(),
66-
KubeClient: kubeClient,
68+
Client: mgr.GetClient(),
69+
Scheme: mgr.GetScheme(),
70+
KubeClient: kubeClient,
71+
TagResolver: &registry.GGCRResolver{},
72+
TagResolutionInterval: tagResolutionInterval,
6773
}).SetupWithManager(mgr); err != nil {
6874
setupLog.Error(err, "Failed to create controller", "controller", "bootcnodepool")
6975
os.Exit(1)

internal/controller/bootcnodepool_controller.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/source"
3636

3737
bootcv1alpha1 "github.com/jlebon/bootc-operator/api/v1alpha1"
38+
"github.com/jlebon/bootc-operator/internal/registry"
3839
)
3940

4041
// drainStatus tracks an in-progress drain goroutine for a single node.
@@ -52,6 +53,9 @@ type BootcNodePoolReconciler struct {
5253
KubeClient kubernetes.Interface
5354
Recorder events.EventRecorder
5455

56+
TagResolver registry.TagResolver
57+
TagResolutionInterval time.Duration
58+
5559
// drainCh receives events from drain goroutines to re-enqueue the
5660
// owning pool after a drain completes.
5761
drainCh chan event.GenericEvent
@@ -239,12 +243,22 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
239243
// the end.
240244

241245
// Resolve the target digest from the image ref.
242-
if err := r.resolveTargetDigest(&pool); err != nil {
246+
resolveResult, err := r.resolveTargetDigest(ctx, &pool)
247+
if err != nil {
243248
if isInvalidSpecError(err) {
244249
return r.setInvalidSpecCondition(ctx, &pool, err)
245250
}
246251
return ctrl.Result{}, fmt.Errorf("resolving target digest: %w", err)
247252
}
253+
if pool.Status.TargetDigest == "" {
254+
// First tag resolution failed — nothing to roll out yet.
255+
if !reflect.DeepEqual(pool.Status, *statusOrig) {
256+
if err := r.Status().Update(ctx, &pool); err != nil {
257+
return ctrl.Result{}, fmt.Errorf("updating pool status: %w", err)
258+
}
259+
}
260+
return resolveResult, nil
261+
}
248262

249263
// Sync pool membership and retrieve BootcNodes we own.
250264
ownedBootcNodes, err := r.syncMembership(ctx, &pool)
@@ -274,24 +288,56 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
274288
}
275289
}
276290

277-
return ctrl.Result{}, nil
291+
return resolveResult, nil
278292
}
279293

280-
// resolveTargetDigest parses the digest from the pool's image ref and
281-
// sets pool.Status.TargetDigest. For digest refs (the only kind
282-
// supported now), the digest is extracted directly. Tag resolution is
283-
// deferred to Milestone 5.
284-
func (r *BootcNodePoolReconciler) resolveTargetDigest(pool *bootcv1alpha1.BootcNodePool) error {
294+
// resolveTargetDigest resolves the target digest from the pool's image
295+
// ref. Digest refs are extracted directly. Tag refs are resolved via
296+
// the registry, respecting the re-resolution interval.
297+
func (r *BootcNodePoolReconciler) resolveTargetDigest(ctx context.Context, pool *bootcv1alpha1.BootcNodePool) (ctrl.Result, error) {
298+
log := logf.FromContext(ctx)
299+
285300
ref, err := parseImageRef(pool.Spec.Image.Ref)
286301
if err != nil {
287-
return newInvalidSpecError(fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err))
302+
return ctrl.Result{}, newInvalidSpecError(fmt.Sprintf("invalid image ref %q: %v", pool.Spec.Image.Ref, err))
288303
}
304+
289305
digested, ok := ref.(reference.Digested)
290-
if !ok {
291-
return newInvalidSpecError(fmt.Sprintf("image ref %q has no digest (tag resolution not yet supported)", pool.Spec.Image.Ref))
306+
if ok {
307+
pool.Status.TargetDigest = digested.Digest().String()
308+
return ctrl.Result{}, nil
292309
}
293-
pool.Status.TargetDigest = digested.Digest().String()
294-
return nil
310+
311+
// Tag ref — check if resolution is due.
312+
now := time.Now()
313+
if pool.Status.NextTagResolutionTime != nil && now.Before(pool.Status.NextTagResolutionTime.Time) {
314+
remaining := pool.Status.NextTagResolutionTime.Time.Sub(now)
315+
log.V(1).Info("Tag resolution not yet due", "remaining", remaining)
316+
return ctrl.Result{RequeueAfter: remaining}, nil
317+
}
318+
319+
digest, err := r.TagResolver.Resolve(ctx, pool.Spec.Image.Ref)
320+
if err != nil {
321+
log.Error(err, "Failed to resolve tag", "ref", pool.Spec.Image.Ref)
322+
apimeta.SetStatusCondition(&pool.Status.Conditions, metav1.Condition{
323+
Type: bootcv1alpha1.PoolDegraded,
324+
Status: metav1.ConditionTrue,
325+
Reason: bootcv1alpha1.PoolRegistryError,
326+
Message: err.Error(),
327+
})
328+
next := metav1.NewTime(now.Add(r.TagResolutionInterval))
329+
pool.Status.NextTagResolutionTime = &next
330+
return ctrl.Result{RequeueAfter: r.TagResolutionInterval}, nil
331+
}
332+
333+
if pool.Status.TargetDigest != digest {
334+
log.Info("Resolved tag to new digest", "ref", pool.Spec.Image.Ref, "digest", digest)
335+
}
336+
pool.Status.TargetDigest = digest
337+
next := metav1.NewTime(now.Add(r.TagResolutionInterval))
338+
pool.Status.NextTagResolutionTime = &next
339+
// Requeue the tag resolution for the next interval
340+
return ctrl.Result{RequeueAfter: r.TagResolutionInterval}, nil
295341
}
296342

297343
// parseImageRef parses an image reference string into a named

0 commit comments

Comments
 (0)