-
Notifications
You must be signed in to change notification settings - Fork 7
Add tag-based image resolution to the controller (milestone 5a) #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d265d5b
6cb0f55
2760452
c415770
6a9ced8
7adf30d
865c49c
deb3fef
4376138
e3d8661
fc96c9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,7 @@ import ( | |
| "sigs.k8s.io/controller-runtime/pkg/source" | ||
|
|
||
| bootcv1alpha1 "github.com/bootc-dev/bootc-operator/api/v1alpha1" | ||
| "github.com/bootc-dev/bootc-operator/internal/registry" | ||
| ) | ||
|
|
||
| // drainStatus tracks an in-progress drain goroutine for a single node. | ||
|
|
@@ -52,6 +53,9 @@ type BootcNodePoolReconciler struct { | |
| KubeClient kubernetes.Interface | ||
| Recorder events.EventRecorder | ||
|
|
||
| TagResolver registry.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,12 +243,22 @@ 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) | ||
| } | ||
| if pool.Status.TargetDigest == "" { | ||
| // First tag resolution failed — nothing to roll out yet. | ||
| if !reflect.DeepEqual(pool.Status, *statusOrig) { | ||
| if err := r.Status().Update(ctx, &pool); err != nil { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative is to update here is to move it at the end. I had this version but I had to encapsulate the entire logic in a long if statement and I preferred to have an early return here with the update |
||
| return ctrl.Result{}, fmt.Errorf("updating pool status: %w", err) | ||
| } | ||
| } | ||
| return resolveResult, nil | ||
| } | ||
|
|
||
| // Sync pool membership and retrieve BootcNodes we own. | ||
| ownedBootcNodes, err := r.syncMembership(ctx, &pool) | ||
|
|
@@ -274,24 +288,56 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques | |
| } | ||
| } | ||
|
|
||
| return ctrl.Result{}, nil | ||
| return resolveResult, nil | ||
| } | ||
|
|
||
| // 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() | ||
| 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.PoolRegistryError, | ||
| Message: err.Error(), | ||
| }) | ||
| next := metav1.NewTime(now.Add(r.TagResolutionInterval)) | ||
| pool.Status.NextTagResolutionTime = &next | ||
| return ctrl.Result{RequeueAfter: r.TagResolutionInterval}, nil | ||
| } | ||
|
|
||
| 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 | ||
| // Requeue the tag resolution for the next interval | ||
| return ctrl.Result{RequeueAfter: r.TagResolutionInterval}, nil | ||
| } | ||
|
|
||
| // parseImageRef parses an image reference string into a named | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we generalize this to e.g.
PoolTagResolutionError? I.e. it's anything to do with tag resolution, which yes, the main fallible part is the registry contacting.