Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ef0ba27
Raise the agent library to a build with configurable host paths
plombardi89 Sep 21, 2026
7eb9e51
Add a host installation prefix to the node config
plombardi89 Sep 21, 2026
4f8a22e
Resolve the agent upgrade layout from the install prefix
plombardi89 Sep 21, 2026
c78e838
Install the recovery script under the install prefix
plombardi89 Sep 21, 2026
b56f8a2
Update the pinned agent library build
plombardi89 Sep 21, 2026
21e9811
Point the recovery script at the prefixed last-good binary
plombardi89 Sep 23, 2026
2b1ce69
Uninstall the service with the prefix it was installed under
plombardi89 Sep 23, 2026
7157f08
Remove LocalDNS state and the nspawn helper on reset
plombardi89 Sep 23, 2026
a4f790d
Pass the host prefix to the repave network cleanup
plombardi89 Sep 23, 2026
e18d2c8
Install under the host prefix and refuse Azure Container Linux withou…
plombardi89 Sep 23, 2026
1cf3859
Install the bootstrap binary under the host prefix
plombardi89 Sep 23, 2026
5d4f6d6
Run the bootstrap script tests with the installer tests
plombardi89 Sep 23, 2026
3b59d75
Add an ignition command for hosts provisioned by Ignition
plombardi89 Sep 23, 2026
3582700
Add a local harness for Azure Container Linux
plombardi89 Sep 23, 2026
79b2808
Document the host prefix
plombardi89 Sep 23, 2026
9c074cf
Update the pinned agent library and the copied UKI patch
plombardi89 Sep 24, 2026
b6b5b6b
Run the ignition registration test serially
plombardi89 Sep 24, 2026
6bec9c5
Install under /opt/unbounded and link older hosts to /usr/local
plombardi89 Sep 25, 2026
12bdae6
Merge main into phlombar/acl-support
plombardi89 Sep 25, 2026
a8d8b98
e2e: rejoin through install.sh when reset left only the layout
plombardi89 Sep 25, 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
2 changes: 1 addition & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- name: Run tests
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...

- name: Run installer tests
- name: Run installer and bootstrap script tests
run: make test-install

- name: Generate coverage report
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,8 @@ jobs:
**Manual installation:**
1. Download the appropriate binary for your platform
2. Extract the archive: \`tar -xzf aks-flex-node-*.tar.gz\`
3. Move the binary to your PATH: \`sudo mv aks-flex-node-* /usr/local/bin/aks-flex-node\`
4. Make it executable: \`sudo chmod +x /usr/local/bin/aks-flex-node\`
3. Install the binary where the agent looks for it: \`sudo install -D -m 0755 aks-flex-node-linux-amd64 /opt/unbounded/bin/aks-flex-node\`, or \`aks-flex-node-linux-arm64\` on ARM64
4. Run it as \`/opt/unbounded/bin/aks-flex-node\`, or add \`/opt/unbounded/bin\` to your PATH

### Supported Platforms

Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ test: test-install
test-install:
@echo "Running installer tests..."
@scripts/install_test.sh
@scripts/uninstall_test.sh
@scripts/bootstrap_test.sh

.PHONY: test-coverage
test-coverage:
Expand Down Expand Up @@ -195,7 +197,7 @@ help:
@echo ""
@echo "Test & Quality Targets:"
@echo " test Run tests"
@echo " test-install Run installer tests"
@echo " test-install Run installer, uninstaller, and bootstrap script tests"
@echo " test-coverage Run tests with coverage report"
@echo " test-race Run tests with race detector"
@echo " lint Run golangci-lint"
Expand Down
4 changes: 4 additions & 0 deletions cmd/aks-flex-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (

"github.com/Azure/AKSFlexNode/pkg/cmd/bootstrapdata"
"github.com/Azure/AKSFlexNode/pkg/cmd/daemon"
"github.com/Azure/AKSFlexNode/pkg/cmd/hostroot"
"github.com/Azure/AKSFlexNode/pkg/cmd/ignition"
"github.com/Azure/AKSFlexNode/pkg/cmd/nspawnlifecycle"
"github.com/Azure/AKSFlexNode/pkg/cmd/preflight"
"github.com/Azure/AKSFlexNode/pkg/cmd/reset"
Expand Down Expand Up @@ -47,10 +49,12 @@ func newRootCommand() *cobra.Command {
rootCmd.AddCommand(start.NewCommand())
rootCmd.AddCommand(bootstrapdata.NewCommand())
rootCmd.AddCommand(preflight.NewCommand())
rootCmd.AddCommand(ignition.NewCommand())
rootCmd.AddCommand(daemon.NewCommands()...)
rootCmd.AddCommand(nspawnlifecycle.NewCommand())
rootCmd.AddCommand(reset.NewCommand())
rootCmd.AddCommand(version.NewCommand())
rootCmd.AddCommand(hostroot.NewCommand())
rootCmd.AddCommand(token.Command)

return rootCmd
Expand Down
22 changes: 22 additions & 0 deletions cmd/aks-flex-node/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,25 @@ func TestRootCommandRegistersGeneratedNSpawnLifecycleShape(t *testing.T) {
t.Fatalf("Find() remaining args = %v, want [kube1]", remaining)
}
}

// TestRootCommandRegistersTopLevelCommands is not parallel. newRootCommand
// attaches the package-level token.Command, so building two roots at once races
// on it. The subtests share one root for the same reason.
//
// host-root is what the install scripts and AgentUpgrade ask a release for, so
// a build that dropped it would be taken for a release before the host root.
func TestRootCommandRegistersTopLevelCommands(t *testing.T) {
root := newRootCommand()

for _, name := range []string{"ignition", "host-root"} {
t.Run(name, func(t *testing.T) {
cmd, _, err := root.Find([]string{name})
if err != nil {
t.Fatalf("Find() error = %v", err)
}
if cmd.Name() != name {
t.Fatalf("Find() command = %q, want %s", cmd.Name(), name)
}
})
}
}
49 changes: 37 additions & 12 deletions docs/design/storage-backed-bootstrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,16 @@ how to retry failed provisioning. A systemd oneshot wrapper can use
`ConditionPathExists=!/var/lib/aks-flex-node/first-boot-complete` to prevent a
successful node from being bootstrapped again after reboot.

`aks-flex-node ignition` renders such a wrapper for hosts provisioned by
Ignition, `aks-flex-node-bootstrap.service`, but conditions it on the agent
unit, `/etc/systemd/system/aks-flex-node-agent.service`, instead of a marker.
The agent unit exists once bootstrap has installed the agent, and
`aks-flex-node reset` removes it together with the wrapper unit. A marker under
`/var/lib/aks-flex-node` would survive reset and block the host from being
provisioned again. The wrapper removes the script, which carries the base
config, once bootstrap succeeds, and keeps the credential file that the config
references.

### 7. Verify convergence

The provisioning system should not treat script exit alone as complete cluster
Expand Down Expand Up @@ -408,6 +418,10 @@ AKS_FLEX_NODE_INSTALL_DIR
AKS_FLEX_NODE_CONFIG_PATH
```

`AKS_FLEX_NODE_INSTALL_DIR` and `--install-dir` are deprecated. The binary
directory is the one the agent reports, and a value that names any other
directory is rejected.

The equivalent non-secret values have CLI flags. A service-principal client
secret has no CLI value because command arguments are process-visible. Use a
protected secret file or, when unavoidable, the dedicated environment variable.
Expand All @@ -426,20 +440,22 @@ The script processes JSON in this order:

1. Write the embedded base config into a mode `0700` temporary workspace.
2. Validate that it is a JSON object.
3. Apply dedicated cluster resource ID, pool name, and ARM endpoint overrides so
3. Download the agent and install it where it reports, as described in
[Agent download and installation](#agent-download-and-installation). This
happens before rendering because fetching bootstrap data runs the installed
binary.
4. Apply dedicated cluster resource ID, pool name, and ARM endpoint overrides so
they are available to the bootstrap-data request.
4. When enabled, acquire an ARM token with MSI or SP, call
5. When enabled, acquire an ARM token with MSI or SP, call
`listBootstrapData`, and deep-merge the response.
5. Deep-merge `AKS_FLEX_NODE_CONFIG_OVERRIDES`, when present.
6. Deep-merge each CLI `--config-overrides` object in invocation order.
7. Reapply dedicated cluster/pool/endpoint overrides so they remain
6. Deep-merge `AKS_FLEX_NODE_CONFIG_OVERRIDES`, when present.
7. Deep-merge each CLI `--config-overrides` object in invocation order.
8. Reapply dedicated cluster/pool/endpoint overrides so they remain
authoritative.
8. Apply dedicated rootfs and offline-artifact source overrides.
9. Set `agent.nodeName` from the lowercase host name only when absent.
10. Apply the dedicated auth selection.
11. Validate the final JSON with jq.
12. Keep the rendered result in the protected workspace while the agent archive
is downloaded and installed.
9. Apply dedicated rootfs and offline-artifact source overrides.
10. Set `agent.nodeName` from the lowercase host name only when absent.
11. Apply the dedicated auth selection.
12. Validate the final JSON with jq.
13. Atomically install the config at `/etc/aks-flex-node/config.json` with mode
`0600`.
14. Clear bootstrap environment variables, including signed artifact URLs and
Expand Down Expand Up @@ -586,7 +602,16 @@ The script:
3. Optionally validates the archive SHA-256.
4. Rejects absolute and parent-traversal tar paths.
5. Extracts `aks-flex-node-linux-<arch>` or `aks-flex-node`.
6. Atomically replaces `/usr/local/bin/aks-flex-node` with mode `0755`.
6. Asks the agent for its host root, and atomically replaces
`<host root>/bin/aks-flex-node` with mode `0755`.

The host root is what the agent's `host-root` command prints: `/opt/unbounded`,
or `/usr/local` on a host an earlier release installed. The command runs from a
copy under `/var/lib/aks-flex-node`, because the temp dir may be on a noexec
`/tmp` and a failure to run there would be taken for an earlier release. A
release without the command predates the host root and is installed in
`/usr/local/bin`; on a host with a read-only `/usr`, such as Azure Container
Linux, such a release cannot be installed.

The checksum covers the downloaded archive. Supplying a digest is strongly
recommended, especially for signed URLs or mirrors.
Expand Down
2 changes: 2 additions & 0 deletions docs/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ The following table is generated from the Cobra command tree. Run `make docs-cli
| `aks-flex-node daemon [flags]` | Run the AKS Flex Node daemon | `agent` | listed | `--config` (required) |
| `aks-flex-node fetch-bootstrap-data [flags]` | Fetch current FlexNodes join data from AKS RP | — | listed | `--agent-pool-name` (required); `--api-version` (default: `2026-05-02-preview`); `--auth` (required); `--authority-host` (default: `https://login.microsoftonline.com`); `--cluster-resource-id` (required); `--msi-client-id`; `--output`, `-o` (required); `--resource-manager-endpoint` (default: `https://management.azure.com`); `--sp-client-certificate-file`; `--sp-client-credential-file`; `--sp-client-id`; `--sp-client-secret-file`; `--sp-tenant-id` |
| `aks-flex-node help [command]` | Help about any command | — | listed | — |
| `aks-flex-node host-root` | Print the directory that holds the agent's host-side files | — | hidden | — |
| `aks-flex-node ignition [flags] -- BOOTSTRAP_ARGS...` | Render an Ignition config that bootstraps the host on first boot | — | listed | `--base-config`; `--output`, `-o` (default: `-`); `--sp-client-certificate-file`; `--sp-client-secret-file` |
| `aks-flex-node nspawn-lifecycle` | Run internal nspawn lifecycle hooks | — | hidden | — |
| `aks-flex-node nspawn-lifecycle post-start MACHINE` | Reconcile in-machine state after machine start | — | hidden | — |
| `aks-flex-node nspawn-lifecycle pre-start MACHINE` | Refresh host-side nspawn state before machine start | — | hidden | — |
Expand Down
38 changes: 32 additions & 6 deletions docs/usage/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The walkthrough uses a public AKS API endpoint and private Layer 3 connectivity

Run Azure CLI, `kubectl`, artifact download, and SSH commands in your **Bash environment**. Run host preparation and bootstrap commands on the separate **flex node host** only when a step explicitly directs you to.

The bootstrap script is downloaded and run interactively on the host. This guide doesn't use cloud-init. For the architecture and security rationale, see [Generated bootstrap script](../design/storage-backed-bootstrap.md).
The bootstrap script is downloaded and run interactively on the host. Hosts provisioned by Ignition, such as Azure Container Linux, get it in an Ignition config instead; see [Hosts provisioned by Ignition](#hosts-provisioned-by-ignition). This guide doesn't use cloud-init. For the architecture and security rationale, see [Generated bootstrap script](../design/storage-backed-bootstrap.md).

## Flow

Expand Down Expand Up @@ -447,12 +447,14 @@ For a host outside Azure, prefer an already-connected Azure Arc managed identity
The script performs these operations:

1. Loads the empty JSON base.
2. Applies the cluster and pool overrides.
3. Uses the Azure VM managed identity to request an ARM token.
4. Calls `listBootstrapData` for a fresh bootstrap token, API endpoint, CA, and
2. Downloads and verifies the AKS Flex Node agent archive from GitHub Releases,
and installs the binary at `/opt/unbounded/bin/aks-flex-node`. See
[Where the agent is installed](operations.md#where-the-agent-is-installed).
3. Applies the cluster and pool overrides.
4. Uses the Azure VM managed identity to request an ARM token.
5. Calls `listBootstrapData` for a fresh bootstrap token, API endpoint, CA, and
component version.
5. Applies runtime configuration overrides.
6. Downloads and verifies the AKS Flex Node agent archive from GitHub Releases.
6. Applies runtime configuration overrides.
7. Writes `/etc/aks-flex-node/config.json` as `0600 root:root`.
8. Runs non-mutating preflight.
9. Registers the ARM Machine and starts the nspawn worker.
Expand All @@ -467,6 +469,30 @@ install -d -m 0755 /var/lib/aks-flex-node
install -m 0600 /dev/null /var/lib/aks-flex-node/first-boot-complete
```

### Hosts provisioned by Ignition

Azure Container Linux and other hosts provisioned by Ignition have a read-only `/usr` and no interactive first boot. For those, render an Ignition config in your Bash environment instead of running the script on the host. `aks-flex-node ignition` embeds `bootstrap.sh` and passes everything after `--` to it:

```bash
aks-flex-node ignition --output node.ign -- \
--auth msi \
--msi-client-id "<user-assigned-managed-identity-client-id>" \
--fetch-bootstrap-data \
--cluster-resource-id "$AKS_RESOURCE_ID" \
--agent-pool-name "$FLEX_POOL_NAME" \
--agent-version "$AKS_FLEX_NODE_VERSION"
```

Use `node.ign` as the host's Ignition config; on Azure, that's the VM's custom data. On first boot, Ignition writes:

- `/etc/aks-flex-node/first-boot/bootstrap.sh`, with the `--base-config` file, if one was given, in place of the embedded config.
- For a service principal, the credential under `/etc/aks-flex-node/credentials/` with mode `0600`. Pass `--sp-client-secret-file` or `--sp-client-certificate-file` to `aks-flex-node ignition`, before `--`.
- `aks-flex-node-bootstrap.service`, which runs the script with the arguments after `--` once the network is online.

The agent is installed under `/opt/unbounded`, which is writable on these hosts. The unit retries failures with a delay that grows to five minutes, until `aks-flex-node-agent.service` is installed, and is skipped on later boots. Once bootstrap succeeds, it removes the script, which carries the base config. `aks-flex-node reset` disables and removes the unit. Follow progress on the host with `journalctl -u aks-flex-node-bootstrap.service`.

The command checks the arguments against the script's options and refuses the ones it sets itself, so mistakes are reported in your Bash environment rather than at first boot. The output contains the base config and any credential, so keep it as private as they are; `--output` creates the file with mode `0600` and doesn't overwrite an existing one.

<a id="7-verify-the-joined-node"></a>
<a id="8-verify-the-joined-node"></a>
## 6. Verify the joined node
Expand Down
6 changes: 6 additions & 0 deletions docs/usage/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ This guide covers host inspection, current lifecycle operations, reset, and trou

AKS or the operator owns workload disruption decisions. Cordon and drain the Kubernetes Node before a disruptive operation when the surrounding control-plane workflow hasn't already done so.

## Where the agent is installed

The agent keeps its binaries and helpers under `/opt/unbounded`: the `aks-flex-node` link in `/opt/unbounded/bin`, the blue/green binaries and recovery script in `/opt/unbounded/lib/aks-flex-node`, and the LocalDNS helper in `/opt/unbounded/libexec`. The config, state, logs, and systemd units stay under `/etc`, `/var`, and `/etc/systemd/system`. `/opt/unbounded/bin` is not on the default `PATH`, so run the host commands below as `/opt/unbounded/bin/aks-flex-node`, or add the directory to `PATH`.

Earlier releases installed these files under `/usr/local`. On a host installed by one of them, the first command of a newer release that changes the host, such as the agent after an AgentUpgrade, links `/opt/unbounded` to `/usr/local`. The files stay where they are and the units are unchanged, so the host can still be returned to the earlier release. `aks-flex-node reset` removes the helpers under both locations and keeps the binaries; the uninstall script removes the binaries from both, and the link. An AgentUpgrade to an earlier release is refused on a host installed under `/opt/unbounded`, because that release would look for its files under `/usr/local`.

## Preflight

Run preflight before mutating the host. The command validates the config, resolves the nspawn goal state, and checks host prerequisites, API server reachability, rootfs image reachability, and bootstrap artifact sources.
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8 v8.3.0-beta.2
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v9 v9.6.0-beta.1
github.com/Azure/kubelogin v0.2.19
github.com/Azure/unbounded v0.8.0
github.com/Azure/unbounded v0.8.1-0.20260925111623-d01744cd3f7c
github.com/go-logr/logr v1.4.4
github.com/google/renameio/v2 v2.0.2
github.com/spf13/cobra v1.10.2
Expand All @@ -17,7 +17,7 @@ require (
k8s.io/apimachinery v0.37.0
k8s.io/client-go v0.37.0
k8s.io/utils v0.0.0-20260626114624-be93311217bd
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/controller-runtime v0.25.1
)

require (
Expand Down Expand Up @@ -69,7 +69,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/keybase/go-keychain v0.0.1 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/klauspost/compress v1.19.2 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
Expand Down Expand Up @@ -99,8 +99,8 @@ require (
golang.org/x/crypto v0.56.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.15.0 // indirect
Expand Down
Loading
Loading