Skip to content
Open
20 changes: 11 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,6 @@ jobs:
- name: Set up KVM
run: sudo chmod 666 /dev/kvm

- name: Configure kernel
run: |
# Unload AppArmor profiles — the passt profile blocks remount
# operations needed for passt's self-sandboxing inside containers.
sudo aa-teardown 2>/dev/null || true
# Allow unprivileged user namespace creation (needed by passt
# inside containers).
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Enable KSM
run: |
sudo sh -c 'echo 1 > /sys/kernel/mm/ksm/run'
Expand All @@ -172,6 +163,17 @@ jobs:
sudo apt-get update
sudo apt-get install -y podman

- name: Configure kernel
run: |
# Unload AppArmor profiles — the host-loaded passt profile
# blocks passt's self-sandboxing inside bink node containers.
# Must run AFTER installing podman, whose passt dependency
# loads the profile during package installation.
sudo aa-teardown 2>/dev/null || true
# Allow unprivileged user namespace creation (needed by passt
# inside containers).
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Start podman socket
run: systemctl --user start podman.socket

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ e2e: ## Run e2e tests (requires: make deploy-bink). V=1 for verbose. RUN=<regex>
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 20m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) .
go test -timeout 30m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) .

##@ Build

Expand Down
2 changes: 1 addition & 1 deletion api/v1alpha1/bootcnode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ type BootcNodeStatus struct {

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:resource:scope=Cluster,shortName=bn

// BootcNode represents a single node managed by the bootc operator.
// BootcNode objects are auto-created by the controller (one per managed
Expand Down
6 changes: 5 additions & 1 deletion api/v1alpha1/bootcnodepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,11 @@ type BootcNodePoolStatus struct {

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:resource:scope=Cluster,shortName=bnp
// +kubebuilder:printcolumn:name="Nodes",type=integer,JSONPath=`.status.nodeCount`
// +kubebuilder:printcolumn:name="Updated",type=integer,JSONPath=`.status.updatedCount`
// +kubebuilder:printcolumn:name="Updating",type=integer,JSONPath=`.status.updatingCount`
// +kubebuilder:printcolumn:name="Degraded",type=integer,JSONPath=`.status.degradedCount`

// BootcNodePool defines a group of nodes and their desired OS image state.
// Users create BootcNodePool resources to register nodes with the bootc
Expand Down
17 changes: 16 additions & 1 deletion config/crd/bases/node.bootc.dev_bootcnodepools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ spec:
kind: BootcNodePool
listKind: BootcNodePoolList
plural: bootcnodepools
shortNames:
- bnp
singular: bootcnodepool
scope: Cluster
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .status.nodeCount
name: Nodes
type: integer
- jsonPath: .status.updatedCount
name: Updated
type: integer
- jsonPath: .status.updatingCount
name: Updating
type: integer
- jsonPath: .status.degradedCount
name: Degraded
type: integer
name: v1alpha1
schema:
openAPIV3Schema:
description: |-
Expand Down
2 changes: 2 additions & 0 deletions config/crd/bases/node.bootc.dev_bootcnodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ spec:
kind: BootcNode
listKind: BootcNodeList
plural: bootcnodes
shortNames:
- bn
singular: bootcnode
scope: Cluster
versions:
Expand Down
2 changes: 1 addition & 1 deletion docs/IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ the full controller+daemon loop can be tested end-to-end.
NotReady), verify the controller stops assigning new slots even
when `maxUnavailable` has capacity.

### 3c. Pool status aggregation
### 3c. Pool status aggregation

- Compute `nodeCount`, `updatedCount`, `updatingCount`, `degradedCount`
- `UpToDate` condition with reasons: `AllUpdated`, `RolloutInProgress`,
Expand Down
8 changes: 7 additions & 1 deletion internal/controller/bootcnodepool_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,19 @@ func (r *BootcNodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Reques
// reconciliation run.

// Drive the rollout state machine.
if err := r.driveRollout(ctx, &pool, ownedBootcNodes); err != nil {
rs, err := r.driveRollout(ctx, &pool, ownedBootcNodes)
if err != nil {
if isInvalidSpecError(err) {
return r.setInvalidSpecCondition(ctx, &pool, err)
}
return ctrl.Result{}, fmt.Errorf("driving rollout: %w", err)
}

// Early-return paths above (TargetDigest empty, InvalidSpec) skip
// aggregation. In-flight updates may complete during error conditions
// but counts catch up on the next successful reconcile.
syncPoolStatus(&pool, rs)

return complete(resolveResult)
}

Expand Down
16 changes: 8 additions & 8 deletions internal/controller/rollout.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ func (rs *rolloutState) nodeCount() int {
}

// driveRollout is the main function that advances the rollout state machine.
func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv1alpha1.BootcNodePool, ownedBootcNodes map[string]*bootcv1alpha1.BootcNode) error {
func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv1alpha1.BootcNodePool, ownedBootcNodes map[string]*bootcv1alpha1.BootcNode) (*rolloutState, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably run go fmt throughout the project as a separate PR

log := logf.FromContext(ctx)

// Process drain results first. This isn't really ordering dependent,
// but it feels natural to do this upfront before classifying.
if err := r.collectDrainResults(ctx, ownedBootcNodes); err != nil {
return fmt.Errorf("collecting drain results: %w", err)
return nil, fmt.Errorf("collecting drain results: %w", err)
}

rs := buildRolloutState(log, ownedBootcNodes)
Expand All @@ -80,7 +80,7 @@ func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv
// the desired image. This runs before computing available slots so
// that freed capacity is immediately usable for new candidates.
if err := r.freeCompletedSlots(ctx, rs); err != nil {
return fmt.Errorf("freeing completed slots: %w", err)
return nil, fmt.Errorf("freeing completed slots: %w", err)
}

// Check for unhealthy nodes on the target digest in reboot slots. If
Expand All @@ -107,12 +107,12 @@ func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv
// reconciles will still have their results collected and
// desiredImageState set to Booted. Trying to "un-drain" and
// uncordon fully drained nodes is out of scope for now.
return nil
return rs, nil
}

maxUnavail, err := resolveMaxUnavailable(pool, rs.nodeCount())
if err != nil {
return err
return nil, err
}

availableSlots := max(0, maxUnavail-rs.occupiedSlots)
Expand All @@ -136,10 +136,10 @@ func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv
for _, bn := range candidates {
var node corev1.Node
if err := r.Get(ctx, types.NamespacedName{Name: bn.Name}, &node); err != nil {
return fmt.Errorf("fetching node %s: %w", bn.Name, err)
return nil, fmt.Errorf("fetching node %s: %w", bn.Name, err)
}
if err := r.assignRebootSlot(ctx, bn, &node); err != nil {
return fmt.Errorf("assigning reboot slot to %s: %w", bn.Name, err)
return nil, fmt.Errorf("assigning reboot slot to %s: %w", bn.Name, err)
}
}

Expand All @@ -155,7 +155,7 @@ func (r *BootcNodePoolReconciler) driveRollout(ctx context.Context, pool *bootcv
r.ensureDrain(ctx, pool, bn)
}

return nil
return rs, nil
}

// assignRebootSlot marks a BootcNode as occupying a reboot slot and
Expand Down
87 changes: 87 additions & 0 deletions internal/controller/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-License-Identifier: Apache-2.0

package controller

import (
"fmt"
"strings"

apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

bootcv1alpha1 "github.com/bootc-dev/bootc-operator/api/v1alpha1"
)

// syncPoolStatus populates the pool's status counts, digest tracking,
// and UpToDate condition from the current rollout state.
func syncPoolStatus(pool *bootcv1alpha1.BootcNodePool, rs *rolloutState) {
if pool == nil || rs == nil {
return
}
pool.Status.ObservedGeneration = pool.Generation
pool.Status.NodeCount = int32(rs.nodeCount())
pool.Status.UpdatedCount = int32(len(rs.upToDate))
pool.Status.UpdatingCount = int32(len(rs.pending) + len(rs.staging) + len(rs.staged) + len(rs.rebooting))
pool.Status.DegradedCount = int32(len(rs.degraded))

if pool.Status.NodeCount == pool.Status.UpdatedCount {
pool.Status.DeployedDigest = pool.Status.TargetDigest
}
pool.Status.UpdateAvailable = pool.Status.TargetDigest != pool.Status.DeployedDigest

syncUpToDateCondition(pool, rs)
}

func syncUpToDateCondition(pool *bootcv1alpha1.BootcNodePool, rs *rolloutState) {
if pool == nil || rs == nil {
return
}
switch {
case pool.Spec.Rollout != nil && pool.Spec.Rollout.Paused && pool.Status.NodeCount != pool.Status.UpdatedCount:
apimeta.SetStatusCondition(&pool.Status.Conditions, metav1.Condition{
Type: bootcv1alpha1.PoolUpToDate,
Status: metav1.ConditionFalse,
Reason: bootcv1alpha1.PoolPaused,
Message: rolloutBreakdown(pool, rs),
})
case pool.Status.NodeCount != pool.Status.UpdatedCount:
apimeta.SetStatusCondition(&pool.Status.Conditions, metav1.Condition{
Type: bootcv1alpha1.PoolUpToDate,
Status: metav1.ConditionFalse,
Reason: bootcv1alpha1.PoolRolloutInProgress,
Message: rolloutBreakdown(pool, rs),
})
default:
apimeta.SetStatusCondition(&pool.Status.Conditions, metav1.Condition{
Type: bootcv1alpha1.PoolUpToDate,
Status: metav1.ConditionTrue,
Reason: bootcv1alpha1.PoolAllUpdated,
})
}
}

func rolloutBreakdown(pool *bootcv1alpha1.BootcNodePool, rs *rolloutState) string {
var b strings.Builder
fmt.Fprintf(&b, "%d/%d updated", len(rs.upToDate), rs.nodeCount())

type bucket struct {
name string
count int
}
buckets := []bucket{
{"pending", len(rs.pending)},
{"staging", len(rs.staging)},
{"staged", len(rs.staged)},
{"rebooting", len(rs.rebooting)},
{"degraded", len(rs.degraded)},
}

b.WriteString("; ")
for i, bk := range buckets {
if i > 0 {
b.WriteString(", ")
}
fmt.Fprintf(&b, "%d %s", bk.count, bk.name)
}
return b.String()
}
Loading