Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bb191a9
agent: make initial bootstrap resumable with strict reset
plombardi89 Sep 16, 2026
e1dee36
Merge branch 'main' into acl-extract/bootstrap-recovery
plombardi89 Sep 16, 2026
ba6e67e
agent: simplify bootstrap recovery and retry admission
plombardi89 Sep 16, 2026
9981bd5
Merge remote-tracking branch 'origin/acl-extract/bootstrap-recovery' …
plombardi89 Sep 16, 2026
87d452c
agent: remove unreachable recovery defenses and duplicated helpers
plombardi89 Sep 16, 2026
b7f97b8
agent: document bootstrap compatibility fixtures in their test
plombardi89 Sep 16, 2026
5bd989c
docs: drop stale cleanup-tool tolerance from agent retry guide
plombardi89 Sep 16, 2026
801bd48
agent: move installation ownership into the agent binary
plombardi89 Sep 16, 2026
63dffa1
agent: tighten the installation ownership package surface
plombardi89 Sep 16, 2026
f01a507
agent: stop exporting symbols with no cross-package callers
plombardi89 Sep 16, 2026
2e186af
agent: fix installation identity and applied config on retry
plombardi89 Sep 17, 2026
f9830e4
agent: make reset and repair fail loudly instead of silently
plombardi89 Sep 17, 2026
4927c61
agent: drop the unused host prefix from the ownership record
plombardi89 Sep 17, 2026
d5b1811
Merge remote-tracking branch 'origin/main' into acl-extract/bootstrap…
plombardi89 Sep 17, 2026
fe28c24
Merge branch 'main' into acl-extract/bootstrap-recovery
plombardi89 Sep 17, 2026
1c82c96
agent: keep the installer placing the agent binary
plombardi89 Sep 17, 2026
c0245d8
Merge branch 'main' into acl-extract/bootstrap-recovery
plombardi89 Sep 17, 2026
b32be68
agent: document why the bootstrap reporter is built late
plombardi89 Sep 18, 2026
6627c6e
agent: take the installation lock from gofrs/flock
plombardi89 Sep 18, 2026
34971d3
agent: do not flush nftables while a node is running
plombardi89 Sep 18, 2026
d508d8e
agent: restart node services whose configuration actually changed
plombardi89 Sep 18, 2026
0c69053
agent: leave a registered machine's rootfs in place instead of refusing
plombardi89 Sep 18, 2026
542c002
Merge remote-tracking branch 'origin/main' into acl-extract/bootstrap…
plombardi89 Sep 18, 2026
554e1ac
agent: record which phase an installation is in, not how far it got
plombardi89 Sep 18, 2026
5e92e59
agent e2e: assert the retry converges rather than resuming
plombardi89 Sep 18, 2026
8dd8fa6
docs: describe bootstrap retry as reapplying rather than resuming
plombardi89 Sep 18, 2026
4165868
agent: do not restate the applied config on a retry that found the no…
plombardi89 Sep 18, 2026
9286031
agent: remove duplication the convergence work left behind
plombardi89 Sep 18, 2026
207a0f2
agent: keep CheckBindAddress exported
plombardi89 Sep 18, 2026
61e7034
Merge remote-tracking branch 'origin/main' into acl-extract/bootstrap…
plombardi89 Sep 18, 2026
3c8f03c
agent: let the daemon stand down instead of looking like a crash
plombardi89 Sep 18, 2026
9cfb298
agent: let reset finish on a host without the packages it inspects with
plombardi89 Sep 19, 2026
1b6e56f
agent: do not let an unreadable record block the reset that deletes it
plombardi89 Sep 19, 2026
cd9096c
agent: repair a current daemon link that resolves to nothing
plombardi89 Sep 19, 2026
5585fed
agent: stage the downloaded binary somewhere it can be executed
plombardi89 Sep 19, 2026
7f9696c
agent: ask about the slot being built, and correct two claims that we…
plombardi89 Sep 19, 2026
d20def1
agent: stop cobra narrating a deferred daemon as an error
plombardi89 Sep 19, 2026
4825742
docs: describe what an inactive agent daemon means
plombardi89 Sep 19, 2026
364cb10
agent: add the host installation prefix and resolve paths from it
plombardi89 Sep 17, 2026
cdb5de9
agent: resolve the daemon binaries under the installation prefix
plombardi89 Sep 19, 2026
ee491a0
agent: record the installation prefix before the first host mutation
plombardi89 Sep 19, 2026
9d0ff46
agent: make the installation prefix part of bootstrap identity
plombardi89 Sep 19, 2026
24605fb
kubectl-unbounded: add the Ignition config encoder
plombardi89 Sep 21, 2026
0b3ab62
kubectl-unbounded: emit an Ignition bootstrap config
plombardi89 Sep 21, 2026
cc4c154
agent: remove the first-boot bootstrap unit on reset
plombardi89 Sep 21, 2026
55505d1
agent: write the ownership record only when a repair changed something
plombardi89 Sep 21, 2026
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
34 changes: 34 additions & 0 deletions .github/workflows/agent-e2e-kind.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,40 @@ jobs:
if: always()
run: python3 ./hack/agent/e2e-kind/e2e.py --verbose cleanup

agent-bootstrap-recovery:
name: agent bootstrap recovery (Ubuntu)
runs-on: ubuntu-24.04
timeout-minutes: 45
env:
KIND_CLUSTER_NAME: agent-bootstrap-recovery
VM_NAME: agent-bootstrap-recovery
VM_SUBNET: "192.168.100"
VM_IP: "192.168.100.10"
AGENT_MACHINE_NAME: agent-bootstrap-recovery
HOST_BASE_OS: ubuntu2404
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up test control plane
uses: ./.github/actions/agent-e2e-kind-control-plane
with:
cluster-name: ${{ env.KIND_CLUSTER_NAME }}
vm-subnet: ${{ env.VM_SUBNET }}
- name: Set up machina resources
uses: ./.github/actions/agent-e2e-machina-setup
- name: Retry late bootstrap and repair after ordinary repave
run: python3 ./hack/agent/e2e-kind/e2e.py --verbose run-suite --suite bootstrap-recovery
- name: Verify strict reset
run: python3 ./hack/agent/e2e-kind/e2e.py --verbose reset-agent
- name: Collect logs
if: always()
uses: ./.github/actions/agent-e2e-kind-logs
with:
artifact-name: agent-bootstrap-recovery-logs
- name: Cleanup
if: always()
run: python3 ./hack/agent/e2e-kind/e2e.py --verbose cleanup

agent-config-e2e:
name: agent config e2e
runs-on: ubuntu-24.04
Expand Down
8 changes: 8 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,14 @@ notices:
license:
- name: Apache License, Version 2.0
link: https://github.com/go-logr/logr/blob/v1.4.4/LICENSE
- dependency: github.com/gofrs/flock
ecosystem: go
copyright:
- Copyright (c) 2018-2024, The Gofrs
- Copyright (c) 2015-2020, Tim Heckman
license:
- name: BSD 3-Clause License
link: https://github.com/gofrs/flock/blob/v0.10.0/LICENSE
- dependency: github.com/golang-jwt/jwt/v5
ecosystem: go
copyright:
Expand Down
167 changes: 167 additions & 0 deletions cmd/agent/internal/bootstrap/coordinator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation.
// SPDX-License-Identifier: Apache-2.0

// Package bootstrap reapplies the stages of an owned initial installation.
//
// Every stage runs on every attempt. Each decides what to do by looking at the
// host rather than at a record of what a previous attempt claimed to have done,
// so a host that changed in between converges instead of being skipped.
package bootstrap

import (
"context"
"fmt"
"log/slog"

"github.com/Azure/unbounded/cmd/agent/internal/installstate"
)

// Identity is what makes one installation distinguishable from another.
//
// HostPrefix is the resolved installation prefix. It is carried here so the
// record written before the first host mutation knows where this installation
// puts its files, which is the only thing teardown can consult after a
// bootstrap that failed before the node started.
type Identity struct {
MachineName string
ConfigFingerprint string
HostPrefix string
}

type Stages interface {
EnsureHostClean(context.Context) error
ResolveInputs(context.Context) error
PrepareHost(context.Context) error
PrepareRootFS(context.Context) error
EnsureNodeStarted(context.Context) error
EnsureDaemonInstalled(context.Context) error
RepairDaemon(context.Context) error
VerifyInstalled(context.Context) error
}

// Stage names the work being reported on. It is a label for status reporting
// and logs, deliberately not persisted: writing down which stage was reached is
// what lets a record disagree with the host.
type Stage string

const (
StagePrepareHost Stage = "preparing-host"
StagePrepareRootFS Stage = "preparing-rootfs"
StageStartNode Stage = "starting-node"
StageInstallDaemon Stage = "installing-daemon"
)

type Reporter interface {
StageStarted(context.Context, Stage)
StageFailed(context.Context, Stage, error)
}

type Coordinator struct {
log *slog.Logger
store *installstate.Store
stages Stages
reporter Reporter
}

func New(log *slog.Logger, store *installstate.Store, stages Stages, reporter Reporter) *Coordinator {
return &Coordinator{log: log, store: store, stages: stages, reporter: reporter}
}

type Outcome struct{ AlreadyComplete bool }

func (c *Coordinator) Run(ctx context.Context, id Identity) (Outcome, error) {
lock, err := c.store.AcquireLock()
if err != nil {
return Outcome{}, err
}
defer func() {
if err := lock.Release(); err != nil {
c.log.Error("release installation lock", "error", err)
}
}()

r, disposition, err := installstate.Admit(c.store, id.MachineName, id.ConfigFingerprint)
if err != nil {
return Outcome{}, err
}

if disposition == installstate.Fresh {
if err := c.stages.EnsureHostClean(ctx); err != nil {
return Outcome{}, err
}

r, err = installstate.NewRecord(id.MachineName, id.ConfigFingerprint, id.HostPrefix)
if err != nil {
return Outcome{}, err
}

if err := c.store.Save(r); err != nil {
return Outcome{}, err
}
}

if disposition == installstate.AlreadyComplete {
verifyErr := c.stages.VerifyInstalled(ctx)
if verifyErr != nil {
if err := c.stages.RepairDaemon(ctx); err != nil {
return Outcome{}, fmt.Errorf("repair daemon after %w: %w", verifyErr, err)
}

if err := c.stages.VerifyInstalled(ctx); err != nil {
return Outcome{}, err
}

// Only a repair can have changed anything, so only a repair needs
// to be committed. The record already says complete: rewriting it
// on a healthy host would be a durable write for no change, on
// every boot of every Ignition-provisioned node, since that unit
// has no completion condition and runs each time.
if err := c.store.MarkComplete(r); err != nil {
return Outcome{}, err
}
}

return Outcome{AlreadyComplete: true}, nil
}

if err := c.stages.ResolveInputs(ctx); err != nil {
return Outcome{}, fmt.Errorf("resolve bootstrap inputs: %w", err)
}

// Every stage runs, in order, on every attempt. Each one decides from the
// host what it still has to do: host preparation leaves a live nftables
// ruleset alone, the rootfs is left in place when a machine is registered
// from it, an already running machine is not restarted, and node services
// are restarted only when their configuration actually changed.
for _, stage := range []struct {
name Stage
run func(context.Context) error
}{
{StagePrepareHost, c.stages.PrepareHost},
{StagePrepareRootFS, c.stages.PrepareRootFS},
{StageStartNode, c.stages.EnsureNodeStarted},
{StageInstallDaemon, c.stages.EnsureDaemonInstalled},
} {
if err := ctx.Err(); err != nil {
return Outcome{}, err
}

if c.reporter != nil {
c.reporter.StageStarted(ctx, stage.name)
}

if err := stage.run(ctx); err != nil {
if c.reporter != nil {
c.reporter.StageFailed(ctx, stage.name, err)
}

return Outcome{}, fmt.Errorf("%s: %w", stage.name, err)
}
}

if err := c.store.MarkComplete(r); err != nil {
return Outcome{}, err
}

return Outcome{}, nil
}
Loading
Loading