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
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ e2e: ## Run e2e tests (requires: make deploy-bink). V=1 for verbose. RUN=<regex>
BINK_LOCAL_REGISTRY_NODE_IMAGE=$(BINK_LOCAL_REGISTRY_NODE_IMAGE) \
ARTIFACTS=$(ARTIFACTS) \
BINK_NODE_IMAGE_DIGEST=$$(skopeo inspect --tls-verify=false --format '{{.Digest}}' docker://localhost:5000/node:latest) \
UPDATE_IMAGE_DIGEST=$$(skopeo inspect --tls-verify=false docker://localhost:5000/node:update | jq -r '.Digest') \
go test -timeout 10m -count=1 $(if $(V),-v) $(if $(RUN),-run $(RUN)) .

##@ Build
Expand Down
2 changes: 1 addition & 1 deletion config/daemon/daemon.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ spec:
resources:
limits:
cpu: 500m
memory: 128Mi
memory: 512Mi
requests:
cpu: 10m
memory: 64Mi
Expand Down
51 changes: 40 additions & 11 deletions internal/bootc/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import (
"context"
"fmt"
"os/exec"
"strings"

logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// Executor abstracts the execution of bootc commands on the host.
// The real implementation uses nsenter to enter the host's mount and
// PID namespaces. Tests can provide a fake implementation.
type Executor interface {
Status(ctx context.Context) ([]byte, error)
Switch(ctx context.Context, image string) error
Reboot(ctx context.Context) error
}

// HostExecutor runs bootc commands on the host via nsenter.
Expand All @@ -23,21 +28,45 @@ func NewHostExecutor() *HostExecutor {
return &HostExecutor{}
}

func (e *HostExecutor) Status(ctx context.Context) ([]byte, error) {
cmd := exec.CommandContext(ctx,
"nsenter",
func (e *HostExecutor) nsenterCmd(ctx context.Context, args ...string) *exec.Cmd {
base := []string{
"--target", "1",
"--mount",
"--pid",
"--setuid", "0",
"--setgid", "0",
"--env",
"--",
"bootc", "status", "--json", "--format-version", "1",
)
"--mount", "--pid",
"--setuid", "0", "--setgid", "0",
"--env", "--",
}
return exec.CommandContext(ctx, "nsenter", append(base, args...)...)
}

func (e *HostExecutor) Status(ctx context.Context) ([]byte, error) {
cmd := e.nsenterCmd(ctx, "bootc", "status", "--json", "--format-version", "1")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("running bootc status: %w", err)
}
return out, nil
}

func (e *HostExecutor) Switch(ctx context.Context, image string) error {
log := logf.FromContext(ctx)

cmd := e.nsenterCmd(ctx, "bootc", "switch", image)
log.Info("Executing", "cmd", strings.Join(cmd.Args, " "))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running bootc switch: %s: %w", out, err)
}
return nil
}

func (e *HostExecutor) Reboot(ctx context.Context) error {
log := logf.FromContext(ctx)

cmd := e.nsenterCmd(ctx, "systemctl", "reboot")
log.Info("Executing", "cmd", strings.Join(cmd.Args, " "))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running systemctl reboot: %s: %w", out, err)
}
return nil
}
118 changes: 111 additions & 7 deletions internal/daemon/fake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,128 @@ package daemon

import (
"context"
"encoding/json"
"strings"
"sync"

testutil "github.com/jlebon/bootc-operator/test/util"

"github.com/jlebon/bootc-operator/internal/bootc"
)

type fakeExecutor struct {
mu sync.Mutex
data []byte
err error
mu sync.Mutex
status bootc.Status
statusErr error

switchErr error
switchImg string
switchHook func()
rebooted bool
}

func (f *fakeExecutor) Status(_ context.Context) ([]byte, error) {
f.mu.Lock()
defer f.mu.Unlock()
return f.data, f.err
if f.statusErr != nil {
return nil, f.statusErr
}
data, err := json.Marshal(f.status)
if err != nil {
return nil, err
}
return data, nil
}

func (f *fakeExecutor) Switch(_ context.Context, image string) error {
f.mu.Lock()
f.switchImg = image
hook := f.switchHook
err := f.switchErr
f.mu.Unlock()

if hook != nil {
hook()
}
if err != nil {
return err
}

f.mu.Lock()
defer f.mu.Unlock()
_, digest, _ := strings.Cut(image, "@")
f.status.Status.Staged = newBootEntry(image, digest)
return nil
}

func (f *fakeExecutor) Reboot(_ context.Context) error {
f.mu.Lock()
defer f.mu.Unlock()
f.rebooted = true
return nil
}

func (f *fakeExecutor) setStatusErr(err error) {
f.mu.Lock()
defer f.mu.Unlock()
f.statusErr = err
}

func (f *fakeExecutor) setSwitchErr(err error) {
f.mu.Lock()
defer f.mu.Unlock()
f.switchErr = err
}

func (f *fakeExecutor) setSwitchHook(hook func()) {
f.mu.Lock()
defer f.mu.Unlock()
f.switchHook = hook
}

func (f *fakeExecutor) set(data []byte, err error) {
func (f *fakeExecutor) getSwitchImg() string {
f.mu.Lock()
defer f.mu.Unlock()
f.data = data
f.err = err
return f.switchImg
}

func (f *fakeExecutor) getRebooted() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.rebooted
}

func (f *fakeExecutor) reset() {
f.mu.Lock()
defer f.mu.Unlock()
f.status = bootc.Status{}
f.statusErr = nil
f.switchErr = nil
f.switchImg = ""
f.switchHook = nil
f.rebooted = false
}

func newBootEntry(image, digest string) *bootc.BootEntry {
return &bootc.BootEntry{
Image: &bootc.ImageStatus{
Image: bootc.ImageReference{Image: image, Transport: "registry"},
ImageDigest: digest,
Architecture: "amd64",
},
}
}

func newBootcStatus(bootedDigest string) bootc.Status {
return bootc.Status{
APIVersion: "org.containers.bootc/v1alpha1",
Kind: "BootcHost",
Spec: bootc.StatusSpec{
Image: &bootc.ImageReference{Image: testutil.ImageTaggedRef, Transport: "registry"},
BootOrder: "default",
},
Status: bootc.StatusBody{
Booted: newBootEntry(testutil.ImageTaggedRef, bootedDigest),
},
}
}
Loading