Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v1alpha1/httpbootconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ type HTTPBootConfigSpec struct {
type HTTPBootConfigStatus struct {
State HTTPBootConfigState `json:"state,omitempty"`

// ObservedGeneration is the generation of the HTTPBootConfig that was last reconciled by the controller.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Conditions represent the latest available observations of the IPXEBootConfig's state
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/ipxebootconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type IPXEBootConfigStatus struct {
// Important: Run "make" to regenerate code after modifying this file
State IPXEBootConfigState `json:"state,omitempty"`

// ObservedGeneration is the generation of the IPXEBootConfig that was last reconciled by the controller.
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// Conditions represent the latest available observations of the IPXEBootConfig's state
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,16 @@ func main() {
}
}

if err = (&controller.ServerBootConfigurationReadinessReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
RequireHTTPBoot: controllers.Enabled(serverBootConfigControllerHttp),
RequireIPXEBoot: controllers.Enabled(serverBootConfigControllerPxe),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ServerBootConfigReadiness")
os.Exit(1)
}

//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/boot.ironcore.dev_httpbootconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the generation of the HTTPBootConfig
that was last reconciled by the controller.
format: int64
type: integer
state:
type: string
type: object
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/boot.ironcore.dev_ipxebootconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the generation of the IPXEBootConfig
that was last reconciled by the controller.
format: int64
type: integer
state:
description: 'Important: Run "make" to regenerate code after modifying
this file'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the generation of the HTTPBootConfig
that was last reconciled by the controller.
format: int64
type: integer
state:
type: string
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ spec:
- type
type: object
type: array
observedGeneration:
description: ObservedGeneration is the generation of the IPXEBootConfig
that was last reconciled by the controller.
format: int64
type: integer
state:
description: 'Important: Run "make" to regenerate code after modifying
this file'
Expand Down
2 changes: 2 additions & 0 deletions docs/api-reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `state` _[HTTPBootConfigState](#httpbootconfigstate)_ | | | |
| `observedGeneration` _integer_ | ObservedGeneration is the generation of the HTTPBootConfig that was last reconciled by the controller. | | |
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | Conditions represent the latest available observations of the IPXEBootConfig's state | | |


Expand Down Expand Up @@ -164,6 +165,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `state` _[IPXEBootConfigState](#ipxebootconfigstate)_ | Important: Run "make" to regenerate code after modifying this file | | |
| `observedGeneration` _integer_ | ObservedGeneration is the generation of the IPXEBootConfig that was last reconciled by the controller. | | |
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#condition-v1-meta) array_ | Conditions represent the latest available observations of the IPXEBootConfig's state | | |


3 changes: 2 additions & 1 deletion internal/controller/httpbootconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ func (r *HTTPBootConfigReconciler) delete(_ context.Context, log logr.Logger, _
}

func (r *HTTPBootConfigReconciler) patchStatus(ctx context.Context, config *bootv1alpha1.HTTPBootConfig, state bootv1alpha1.HTTPBootConfigState) error {
if config.Status.State == state {
if config.Status.State == state && config.Status.ObservedGeneration == config.Generation {
return nil
}

base := config.DeepCopy()
config.Status.State = state
config.Status.ObservedGeneration = config.Generation

if err := r.Status().Patch(ctx, config, client.MergeFrom(base)); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions internal/controller/ipxebootconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func (r *IPXEBootConfigReconciler) patchStatus(
) error {
base := ipxeBootConfig.DeepCopy()
ipxeBootConfig.Status.State = state
ipxeBootConfig.Status.ObservedGeneration = ipxeBootConfig.Generation

if err := r.Status().Patch(ctx, ipxeBootConfig, client.MergeFrom(base)); err != nil {
return fmt.Errorf("error patching ipxeBootConfig: %w", err)
Expand Down
27 changes: 27 additions & 0 deletions internal/controller/serverbootconfig_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
Expand Down Expand Up @@ -143,3 +144,29 @@ func PatchServerBootConfigWithError(

return c.Status().Patch(ctx, &cur, client.MergeFrom(base))
}

// PatchServerBootConfigCondition patches a single condition on the ServerBootConfiguration status.
// Callers should only set condition types they own. Retries on conflict so concurrent condition
// writes from HTTP and PXE controllers do not lose each other's updates.
func PatchServerBootConfigCondition(
ctx context.Context,
c client.Client,
namespacedName types.NamespacedName,
condition metav1.Condition,
) error {
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var cur metalv1alpha1.ServerBootConfiguration
if fetchErr := c.Get(ctx, namespacedName, &cur); fetchErr != nil {
return fmt.Errorf("failed to fetch ServerBootConfiguration: %w", fetchErr)
}
base := cur.DeepCopy()

// Default to current generation if caller didn't set it.
if condition.ObservedGeneration == 0 {
condition.ObservedGeneration = cur.Generation
}
apimeta.SetStatusCondition(&cur.Status.Conditions, condition)

return c.Status().Patch(ctx, &cur, client.MergeFrom(base))
})
}
77 changes: 53 additions & 24 deletions internal/controller/serverbootconfiguration_http_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
bootv1alpha1 "github.com/ironcore-dev/boot-operator/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
Expand Down Expand Up @@ -90,9 +91,16 @@ func (r *ServerBootConfigurationHTTPReconciler) reconcile(ctx context.Context, l
ukiURL, err := r.constructUKIURL(ctx, config.Spec.Image)
if err != nil {
log.Error(err, "Failed to construct UKI URL")
if patchErr := PatchServerBootConfigWithError(ctx, r.Client,
types.NamespacedName{Name: config.Name, Namespace: config.Namespace}, err); patchErr != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch state to error: %w (original error: %w)", patchErr, err)
if patchErr := PatchServerBootConfigCondition(ctx, r.Client,
types.NamespacedName{Name: config.Name, Namespace: config.Namespace},
metav1.Condition{
Type: HTTPBootReadyConditionType,
Status: metav1.ConditionFalse,
Reason: "UKIURLConstructionFailed",
Message: err.Error(),
ObservedGeneration: config.Generation,
}); patchErr != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch %s condition: %w (original error: %w)", HTTPBootReadyConditionType, patchErr, err)
}
return ctrl.Result{}, err
}
Expand Down Expand Up @@ -135,37 +143,58 @@ func (r *ServerBootConfigurationHTTPReconciler) reconcile(ctx context.Context, l
return ctrl.Result{}, fmt.Errorf("failed to get HTTPBoot config: %w", err)
}

if err := r.patchConfigStateFromHTTPState(ctx, httpBootConfig, config); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch server boot config state to %s: %w", httpBootConfig.Status.State, err)
if err := r.patchHTTPBootReadyConditionFromHTTPState(ctx, httpBootConfig, config); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch %s condition from HTTPBootConfig state %s: %w", HTTPBootReadyConditionType, httpBootConfig.Status.State, err)
}
log.V(1).Info("Patched server boot config state")
log.V(1).Info("Patched server boot config condition", "condition", HTTPBootReadyConditionType)

log.V(1).Info("Reconciled ServerBootConfiguration")
return ctrl.Result{}, nil
}

func (r *ServerBootConfigurationHTTPReconciler) patchConfigStateFromHTTPState(ctx context.Context, httpBootConfig *bootv1alpha1.HTTPBootConfig, cfg *metalv1alpha1.ServerBootConfiguration) error {
func (r *ServerBootConfigurationHTTPReconciler) patchHTTPBootReadyConditionFromHTTPState(ctx context.Context, httpBootConfig *bootv1alpha1.HTTPBootConfig, cfg *metalv1alpha1.ServerBootConfiguration) error {
key := types.NamespacedName{Name: cfg.Name, Namespace: cfg.Namespace}
var cur metalv1alpha1.ServerBootConfiguration
if err := r.Get(ctx, key, &cur); err != nil {
return err
}
base := cur.DeepCopy()
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var cur metalv1alpha1.ServerBootConfiguration
if err := r.Get(ctx, key, &cur); err != nil {
return err
}
base := cur.DeepCopy()

switch httpBootConfig.Status.State {
case bootv1alpha1.HTTPBootConfigStateReady:
cur.Status.State = metalv1alpha1.ServerBootConfigurationStateReady
// Remove ImageValidation condition when transitioning to Ready
apimeta.RemoveStatusCondition(&cur.Status.Conditions, "ImageValidation")
case bootv1alpha1.HTTPBootConfigStateError:
cur.Status.State = metalv1alpha1.ServerBootConfigurationStateError
}
if cur.Generation != cfg.Generation {
// The SBC has been updated since this reconcile started; a newer reconcile
// will handle the fresh generation. Avoid stamping stale data on it.
return nil
}

for _, c := range httpBootConfig.Status.Conditions {
apimeta.SetStatusCondition(&cur.Status.Conditions, c)
}
cond := metav1.Condition{
Type: HTTPBootReadyConditionType,
// Use cfg.Generation, not cur.Generation: the condition content was
// derived from cfg's HTTPBootConfig, so it reflects that generation.
ObservedGeneration: cfg.Generation,
}
switch {
case httpBootConfig.Status.ObservedGeneration < httpBootConfig.Generation:
// Child controller hasn't reconciled the new spec yet; don't write anything.
// The Owns() watch will re-trigger this reconcile once the child updates its status.
return nil
case httpBootConfig.Status.State == bootv1alpha1.HTTPBootConfigStateReady:
cond.Status = metav1.ConditionTrue
cond.Reason = "BootConfigReady"
cond.Message = "HTTP boot configuration is ready."
case httpBootConfig.Status.State == bootv1alpha1.HTTPBootConfigStateError:
cond.Status = metav1.ConditionFalse
cond.Reason = "BootConfigError"
cond.Message = "HTTPBootConfig reported an error."
default:
cond.Status = metav1.ConditionUnknown
cond.Reason = BootConfigPendingReason
cond.Message = "Waiting for HTTPBootConfig to become Ready."
}

return r.Status().Patch(ctx, &cur, client.MergeFrom(base))
apimeta.SetStatusCondition(&cur.Status.Conditions, cond)
return r.Status().Patch(ctx, &cur, client.MergeFrom(base))
})
}

// getSystemUUIDFromServer fetches the UUID from the referenced Server object.
Expand Down
74 changes: 54 additions & 20 deletions internal/controller/serverbootconfiguration_pxe_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/ironcore-dev/boot-operator/internal/registry"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -107,9 +108,16 @@ func (r *ServerBootConfigurationPXEReconciler) reconcile(ctx context.Context, lo

kernelURL, initrdURL, squashFSURL, err := r.getImageDetailsFromConfig(ctx, bootConfig)
if err != nil {
if patchErr := PatchServerBootConfigWithError(ctx, r.Client,
types.NamespacedName{Name: bootConfig.Name, Namespace: bootConfig.Namespace}, err); patchErr != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch server boot config state: %w (original error: %w)", patchErr, err)
if patchErr := PatchServerBootConfigCondition(ctx, r.Client,
types.NamespacedName{Name: bootConfig.Name, Namespace: bootConfig.Namespace},
metav1.Condition{
Type: IPXEBootReadyConditionType,
Status: metav1.ConditionFalse,
Reason: "ImageDetailsFailed",
Message: err.Error(),
ObservedGeneration: bootConfig.Generation,
}); patchErr != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch %s condition: %w (original error: %w)", IPXEBootReadyConditionType, patchErr, err)
}
return ctrl.Result{}, err
}
Expand Down Expand Up @@ -154,32 +162,58 @@ func (r *ServerBootConfigurationPXEReconciler) reconcile(ctx context.Context, lo
return ctrl.Result{}, fmt.Errorf("failed to get IPXE config: %w", err)
}

if err := r.patchConfigStateFromIPXEState(ctx, config, bootConfig); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch server boot config state to %s: %w", config.Status.State, err)
if err := r.patchIPXEBootReadyConditionFromIPXEState(ctx, config, bootConfig); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to patch %s condition from IPXEBootConfig state %s: %w", IPXEBootReadyConditionType, config.Status.State, err)
}
log.V(1).Info("Patched server boot config state")
log.V(1).Info("Patched server boot config condition", "condition", IPXEBootReadyConditionType)

log.V(1).Info("Reconciled ServerBootConfiguration")
return ctrl.Result{}, nil
}

func (r *ServerBootConfigurationPXEReconciler) patchConfigStateFromIPXEState(ctx context.Context, config *v1alpha1.IPXEBootConfig, bootConfig *metalv1alpha1.ServerBootConfiguration) error {
bootConfigBase := bootConfig.DeepCopy()
func (r *ServerBootConfigurationPXEReconciler) patchIPXEBootReadyConditionFromIPXEState(ctx context.Context, config *v1alpha1.IPXEBootConfig, bootConfig *metalv1alpha1.ServerBootConfiguration) error {
key := types.NamespacedName{Name: bootConfig.Name, Namespace: bootConfig.Namespace}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
var cur metalv1alpha1.ServerBootConfiguration
if err := r.Get(ctx, key, &cur); err != nil {
return err
}
base := cur.DeepCopy()

switch config.Status.State {
case v1alpha1.IPXEBootConfigStateReady:
bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateReady
// Remove ImageValidation condition when transitioning to Ready
apimeta.RemoveStatusCondition(&bootConfig.Status.Conditions, "ImageValidation")
case v1alpha1.IPXEBootConfigStateError:
bootConfig.Status.State = metalv1alpha1.ServerBootConfigurationStateError
}
if cur.Generation != bootConfig.Generation {
// The SBC has been updated since this reconcile started; a newer reconcile
// will handle the fresh generation. Avoid stamping stale data on it.
return nil
}

for _, c := range config.Status.Conditions {
apimeta.SetStatusCondition(&bootConfig.Status.Conditions, c)
}
cond := metav1.Condition{
Type: IPXEBootReadyConditionType,
// Use bootConfig.Generation, not cur.Generation: the condition content was
// derived from bootConfig's IPXEBootConfig, so it reflects that generation.
ObservedGeneration: bootConfig.Generation,
}
switch {
case config.Status.ObservedGeneration < config.Generation:
// Child controller hasn't reconciled the new spec yet; don't write anything.
// The Owns() watch will re-trigger this reconcile once the child updates its status.
return nil
case config.Status.State == v1alpha1.IPXEBootConfigStateReady:
cond.Status = metav1.ConditionTrue
cond.Reason = "BootConfigReady"
cond.Message = "IPXE boot configuration is ready."
case config.Status.State == v1alpha1.IPXEBootConfigStateError:
cond.Status = metav1.ConditionFalse
cond.Reason = "BootConfigError"
cond.Message = "IPXEBootConfig reported an error."
default:
cond.Status = metav1.ConditionUnknown
cond.Reason = BootConfigPendingReason
cond.Message = "Waiting for IPXEBootConfig to become Ready."
}

return r.Status().Patch(ctx, bootConfig, client.MergeFrom(bootConfigBase))
apimeta.SetStatusCondition(&cur.Status.Conditions, cond)
return r.Status().Patch(ctx, &cur, client.MergeFrom(base))
})
}

func (r *ServerBootConfigurationPXEReconciler) getSystemUUIDFromBootConfig(ctx context.Context, config *metalv1alpha1.ServerBootConfiguration) (string, error) {
Expand Down
Loading
Loading