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
31 changes: 26 additions & 5 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"flag"
"fmt"
"os"
"time"

"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -14,6 +15,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log/zap"

bootcv1alpha1 "github.com/bootc-dev/bootc-operator/api/v1alpha1"
Expand All @@ -32,6 +34,9 @@ func init() {
}

func main() {
var pollInterval time.Duration
flag.DurationVar(&pollInterval, "bootc-poll-interval", 5*time.Minute, "Interval for polling bootc status as a fallback to fsnotify")

opts := zap.Options{
Development: true,
}
Expand Down Expand Up @@ -62,17 +67,33 @@ func main() {
os.Exit(1)
}

executor := bootc.NewHostExecutor()

watcher := &daemon.StatusWatcher{
PollInterval: pollInterval,
PrimaryPath: daemon.DefaultPrimaryPath,
FallbackPath: daemon.DefaultFallbackPath,
Events: make(chan event.GenericEvent, 1),
NodeName: nodeName,
Executor: executor,
}
if err := mgr.Add(watcher); err != nil {
setupLog.Error(err, "Failed to add status watcher")
os.Exit(1)
}

if err := (&daemon.BootcNodeReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
NodeName: nodeName,
Executor: bootc.NewHostExecutor(),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
NodeName: nodeName,
Executor: executor,
StatusWatcher: watcher,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "bootcnode")
os.Exit(1)
}

setupLog.Info("Starting daemon", "node", nodeName)
setupLog.Info("Starting daemon", "node", nodeName, "pollInterval", pollInterval)
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Failed to run daemon")
os.Exit(1)
Expand Down
11 changes: 0 additions & 11 deletions internal/daemon/fake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,6 @@ func (f *fakeExecutor) getRebooted() bool {
return f.rebooted
}

func (f *fakeExecutor) reset() {
f.mu.Lock()
defer f.mu.Unlock()
f.status = bootc.Status{}
f.statusErr = nil
f.stageErr = nil
f.stageImg = ""
f.stageHook = nil
f.rebooted = false
}

func newBootEntry(image, digest string) *bootc.BootEntry {
return &bootc.BootEntry{
Image: &bootc.ImageStatus{
Expand Down
19 changes: 8 additions & 11 deletions internal/daemon/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ type stageOp struct {
}

// BootcNodeReconciler reconciles the BootcNode for the node this daemon
// runs on. It reads bootc status from the host, detects image mismatches,
// and drives updates via bootc stage.
// runs on. It reads bootc status from the watcher's cache, detects image
// mismatches, and drives updates via bootc stage.
type BootcNodeReconciler struct {
client.Client
Scheme *runtime.Scheme
NodeName string
Executor bootc.Executor
Scheme *runtime.Scheme
NodeName string
Executor bootc.Executor
StatusWatcher *StatusWatcher

inflight stageOp
stageDone chan event.GenericEvent
Expand All @@ -67,6 +68,7 @@ func (r *BootcNodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&bootcv1alpha1.BootcNode{}).
WatchesRawSource(source.Channel(r.stageDone, &handler.EnqueueRequestForObject{})).
WatchesRawSource(source.Channel(r.StatusWatcher.Events, &handler.EnqueueRequestForObject{})).
Named("bootcnode").
Complete(r)
}
Expand Down Expand Up @@ -305,16 +307,11 @@ func (s *stageOp) run(ctx context.Context, nodeName, image string, executor boot
}

func (r *BootcNodeReconciler) populateBootcFields(ctx context.Context, bn *bootcv1alpha1.BootcNode) error {
data, err := r.Executor.Status(ctx)
status, err := r.StatusWatcher.GetStatus(ctx)
if err != nil {
return fmt.Errorf("getting bootc status: %w", err)
}

status, err := bootc.ParseStatus(data)
if err != nil {
return fmt.Errorf("failed to parse bootc status: %w", err)
}

bn.Status.Booted = convertBootEntry(status.Status.Booted)
bn.Status.Staged = convertBootEntry(status.Status.Staged)
bn.Status.Rollback = convertBootEntry(status.Status.Rollback)
Expand Down
16 changes: 9 additions & 7 deletions internal/daemon/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ func TestReconcilePopulatesStatus(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake := newTestEnv()

v1 := "v1"
v2 := "v2"
v3 := "v3"
Expand Down Expand Up @@ -99,7 +101,7 @@ func TestReconcileBootcStatusError(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.setStatusErr(errors.New(bootcStatusErrMsg))

bn := testutil.NewNode(testNodeName, testutil.ImageDigestRefA)
Expand All @@ -126,7 +128,7 @@ func TestStagingTriggered(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)

bn := testutil.NewNode(testNodeName, testutil.ImageDigestRefB)
Expand Down Expand Up @@ -163,7 +165,7 @@ func TestStagingError(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)
fake.setStageErr(errors.New(stageErrMsg))

Expand Down Expand Up @@ -196,7 +198,7 @@ func TestAlreadyStaged(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)
fake.status.Status.Staged = newBootEntry(testutil.ImageDigestRefB, testutil.DigestB)

Expand Down Expand Up @@ -225,7 +227,7 @@ func TestRebootingSet(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)
fake.status.Status.Staged = newBootEntry(testutil.ImageDigestRefB, testutil.DigestB)

Expand Down Expand Up @@ -254,7 +256,7 @@ func TestRollback(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)
fake.status.Status.Staged = newBootEntry(testutil.ImageDigestRefB, testutil.DigestB)

Expand Down Expand Up @@ -285,7 +287,7 @@ func TestCancelInflightStage(t *testing.T) {
g.SetDefaultEventuallyPollingInterval(pollInterval)
ctx := context.Background()

fake.reset()
fake := newTestEnv()
fake.status = newBootcStatus(testutil.DigestA)

firstBlock := make(chan struct{})
Expand Down
33 changes: 26 additions & 7 deletions internal/daemon/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"

Expand All @@ -22,9 +23,9 @@ import (
const testNodeName = "test-node"

var (
testEnv *envtest.Environment
k8sClient client.Client
fake *fakeExecutor
testEnv *envtest.Environment
k8sClient client.Client
testReconciler *BootcNodeReconciler
)

func TestMain(m *testing.M) {
Expand Down Expand Up @@ -52,8 +53,6 @@ func TestMain(m *testing.M) {
os.Exit(1)
}

fake = &fakeExecutor{}

mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
Metrics: metricsserver.Options{
Expand All @@ -65,12 +64,19 @@ func TestMain(m *testing.M) {
os.Exit(1)
}

if err := (&BootcNodeReconciler{
fake := &fakeExecutor{}
testReconciler = &BootcNodeReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
NodeName: testNodeName,
Executor: fake,
}).SetupWithManager(mgr); err != nil {
StatusWatcher: &StatusWatcher{
Events: make(chan event.GenericEvent, 1),
NodeName: testNodeName,
Executor: fake,
},
}
if err := testReconciler.SetupWithManager(mgr); err != nil {
fmt.Fprintf(os.Stderr, "Failed to setup reconciler: %v\n", err)
os.Exit(1)
}
Expand All @@ -97,3 +103,16 @@ func TestMain(m *testing.M) {

os.Exit(code)
}

// newTestEnv creates a fresh fakeExecutor and StatusWatcher for a test,
// wiring them into the shared reconciler. Each test gets its own cache.
func newTestEnv() *fakeExecutor {
fake := &fakeExecutor{}
testReconciler.Executor = fake
testReconciler.StatusWatcher = &StatusWatcher{
Events: testReconciler.StatusWatcher.Events,
NodeName: testNodeName,
Executor: fake,
}
return fake
}
Loading