Skip to content

Commit 60dcb11

Browse files
committed
download hotfix and then run it
1 parent fc54ded commit 60dcb11

5 files changed

Lines changed: 78 additions & 63 deletions

File tree

aks-node-controller/app.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,18 @@ func (a *App) Run(ctx context.Context, args []string) int {
108108
{
109109
Name: "version",
110110
Usage: "Print the version",
111-
Action: func(context.Context, *cli.Command) error {
112-
_, _ = fmt.Println(Version)
111+
Action: func(_ context.Context, cmd *cli.Command) error {
112+
_, _ = fmt.Fprintln(cmd.Root().Writer, Version)
113113
return nil
114114
},
115115
},
116+
{
117+
Name: "download-hotfix",
118+
Usage: "Download the requested hotfix binary",
119+
Action: func(ctx context.Context, cmd *cli.Command) error {
120+
return a.runDownloadHotfixCommand(ctx)
121+
},
122+
},
116123
},
117124
}
118125

@@ -121,10 +128,6 @@ func (a *App) Run(ctx context.Context, args []string) int {
121128
}
122129

123130
func (a *App) runProvisionCommand(ctx context.Context, flags ProvisionFlags, dryRun bool) error {
124-
// Self-update before provisioning: check for hotfix version and install if needed.
125-
// On success, syscall.Exec replaces this process and never returns.
126-
// On any failure, selfUpdate logs a warning and returns nil (best-effort).
127-
a.selfUpdate(ctx)
128131
slog.Info("aks-node-controller started", "task", "Provision")
129132

130133
startTime := time.Now()
@@ -144,7 +147,6 @@ func (a *App) runProvisionCommand(ctx context.Context, flags ProvisionFlags, dry
144147
}
145148

146149
func (a *App) runProvisionWaitCommand(ctx context.Context, provisionStatusFiles ProvisionStatusFiles) (string, error) {
147-
a.selfUpdate(ctx)
148150
slog.Info("aks-node-controller started", "task", "ProvisionWait")
149151

150152
startTime := time.Now()
@@ -163,6 +165,17 @@ func (a *App) runProvisionWaitCommand(ctx context.Context, provisionStatusFiles
163165
return provisionOutput, err
164166
}
165167

168+
func (a *App) runDownloadHotfixCommand(ctx context.Context) error {
169+
slog.Info("aks-node-controller hotfix download started")
170+
err := a.downloadHotfix(ctx)
171+
if err != nil {
172+
slog.Error("aks-node-controller hotfix download failed", "error", err)
173+
return err
174+
}
175+
slog.Info("aks-node-controller hotfix download finished")
176+
return nil
177+
}
178+
166179
func buildCmdFromProvisionConfig(ctx context.Context, path string) (*exec.Cmd, error) {
167180
inputJSON, err := os.ReadFile(path)
168181
if err != nil {

aks-node-controller/app_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,26 @@ func TestApp_Run(t *testing.T) {
9090
assert.Equal(t, 0, exitCode)
9191
})
9292

93+
t.Run("download-hotfix rejects unexpected arguments", func(t *testing.T) {
94+
tt := NewTestApp(t, TestAppConfig{})
95+
exitCode := tt.App.Run(context.Background(), []string{"aks-node-controller", "download-hotfix", "extra"})
96+
assert.Equal(t, 1, exitCode)
97+
})
98+
99+
t.Run("download-hotfix returns success when VHD version already matches target", func(t *testing.T) {
100+
tt := NewTestApp(t, TestAppConfig{})
101+
origVersion := Version
102+
Version = "202603.30.0-hotfix1"
103+
defer func() { Version = origVersion }()
104+
105+
configPath := filepath.Join(t.TempDir(), "hotfix-config.json")
106+
require.NoError(t, os.WriteFile(configPath, []byte(`{"version": "202603.30.0-hotfix1"}`), 0o644))
107+
tt.App.hotfixVersionPath = configPath
108+
109+
exitCode := tt.App.Run(context.Background(), []string{"aks-node-controller", "download-hotfix"})
110+
assert.Equal(t, 0, exitCode)
111+
})
112+
93113
t.Run("provision command with missing flag", func(t *testing.T) {
94114
tt := NewTestApp(t, TestAppConfig{})
95115
exitCode := tt.App.Run(context.Background(), []string{"aks-node-controller", "provision"})
Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os/exec"
1010
"path/filepath"
1111
"strings"
12-
"syscall"
1312
"time"
1413
)
1514

@@ -23,58 +22,45 @@ const (
2322
vhdBinaryPath = "/opt/azure/containers/aks-node-controller"
2423
// hotfixBinaryPath is where the hotfix binary is placed alongside the VHD-baked binary.
2524
// The wrapper script checks for this path and prefers it over the VHD-baked binary.
26-
// This avoids overwriting a running binary, which is not possible on Windows.
2725
hotfixBinaryPath = "/opt/azure/containers/aks-node-controller-hotfix"
2826
// pkgBinaryPath is where apt/dnf package installs the binary.
2927
pkgBinaryPath = "/usr/bin/aks-node-controller"
3028
)
3129

32-
// selfUpdate checks for a hotfix version and installs it from PMC if needed.
33-
// It is called before command dispatch for provision and provision-wait commands.
34-
// On successful install, it re-execs the process with the new binary and never returns.
35-
// On any failure, it logs a warning so the VHD-baked binary proceeds.
36-
func (a *App) selfUpdate(ctx context.Context) {
30+
// downloadHotfix installs the requested hotfix and stages it alongside the VHD-baked binary.
31+
// The wrapper script decides which binary to execute after this command returns.
32+
func (a *App) downloadHotfix(ctx context.Context) error {
3733
hotfixPath := a.hotfixVersionPath
3834
if hotfixPath == "" {
3935
hotfixPath = defaultHotfixVersionPath
4036
}
4137
hotfixVersion, err := readHotfixVersion(hotfixPath)
4238
if err != nil {
43-
slog.Error("failed to read hotfix version, proceeding with VHD-baked version",
44-
"path", hotfixPath, "error", err)
45-
return
39+
return fmt.Errorf("read hotfix version from %s: %w", hotfixPath, err)
4640
}
4741

4842
if hotfixVersion == "" {
49-
return
43+
slog.Info("hotfix config does not request a version, skipping download", "path", hotfixPath)
44+
return nil
5045
}
46+
5147
if Version == hotfixVersion {
52-
slog.Info("ANC already at hotfix version, skipping self-update", "version", Version)
53-
return
48+
slog.Info("ANC already at hotfix version, skipping download", "version", Version)
49+
return nil
5450
}
5551

56-
slog.Info("ANC self-update triggered", "current", Version, "target", hotfixVersion)
52+
slog.Info("downloading ANC hotfix", "current", Version, "target", hotfixVersion)
5753

58-
installErr := a.installFromPMC(ctx, hotfixVersion)
59-
if installErr != nil {
60-
slog.Error("failed to install hotfix, proceeding with VHD-baked version",
61-
"target", hotfixVersion, "error", installErr)
62-
return
54+
if err := a.installFromPMC(ctx, hotfixVersion); err != nil {
55+
return fmt.Errorf("install hotfix version %s: %w", hotfixVersion, err)
6356
}
6457

65-
// Copy the hotfix binary alongside the VHD-baked binary rather than overwriting it.
66-
// This avoids replacing a running binary (which is not possible on Windows) and lets
67-
// the wrapper script choose the hotfix on subsequent restarts.
6858
if err := copyBinaryAlongside(pkgBinaryPath, hotfixBinaryPath, vhdBinaryPath); err != nil {
69-
slog.Error("failed to copy hotfix binary alongside VHD binary, proceeding with current binary",
70-
"error", err)
71-
return
59+
return fmt.Errorf("stage hotfix binary: %w", err)
7260
}
7361

74-
if err := a.reExec(); err != nil {
75-
slog.Error("failed to re-exec after hotfix install, proceeding with current binary",
76-
"error", err)
77-
}
62+
slog.Info("downloaded ANC hotfix", "target", hotfixVersion, "path", hotfixBinaryPath)
63+
return nil
7864
}
7965

8066
// hotfixConfig is the JSON structure of the hotfix configuration file.
@@ -292,14 +278,3 @@ func copyBinaryAlongside(src, dst, refPath string) error {
292278
slog.Info("installed hotfix binary alongside VHD binary", "src", src, "hotfixPath", dst)
293279
return nil
294280
}
295-
296-
// reExec replaces the current process with the updated hotfix binary.
297-
// On Linux this uses syscall.Exec which atomically replaces the process in-place.
298-
// TODO(windows): syscall.Exec is not available on Windows. When Windows hotfix support
299-
// is added, this will need a platform-specific implementation (e.g., spawn the hotfix
300-
// binary as a child process and exit, or defer to the wrapper script for restart).
301-
func (a *App) reExec() error {
302-
args := append([]string{hotfixBinaryPath}, os.Args[1:]...)
303-
slog.Info("re-executing with hotfix binary", "path", hotfixBinaryPath, "args", args)
304-
return syscall.Exec(hotfixBinaryPath, args, os.Environ())
305-
}
Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -99,39 +99,36 @@ func TestResolveMicrosoftProdSourceListPath(t *testing.T) {
9999
})
100100
}
101101

102-
func TestSelfUpdate_NoHotfixFile(t *testing.T) {
103-
// When no hotfix-version file exists, selfUpdate should be a no-op.
102+
func TestDownloadHotfix_NoHotfixFile(t *testing.T) {
104103
tt := NewTestApp(t, TestAppConfig{})
105104
tt.App.hotfixVersionPath = filepath.Join(t.TempDir(), "nonexistent")
106-
tt.App.selfUpdate(context.Background()) // should not panic
105+
require.NoError(t, tt.App.downloadHotfix(context.Background()))
107106
}
108107

109-
func TestSelfUpdate_VersionMatch(t *testing.T) {
110-
// When the compiled version matches the hotfix version, selfUpdate should skip.
108+
func TestDownloadHotfix_VersionMatch(t *testing.T) {
111109
origVersion := Version
112110
Version = "202603.30.0-hotfix1"
113111
defer func() { Version = origVersion }()
114112

115113
dir := t.TempDir()
116114
path := filepath.Join(dir, "hotfix-config.json")
117-
require.NoError(t, os.WriteFile(path, []byte(`{"version": "202603.30.0-hotfix1"}`), 0644))
115+
require.NoError(t, os.WriteFile(path, []byte(`{"version": "202603.30.0-hotfix1"}`), 0o644))
118116

119117
tt := NewTestApp(t, TestAppConfig{})
120118
tt.App.hotfixVersionPath = path
121-
tt.App.selfUpdate(context.Background()) // should not panic
119+
require.NoError(t, tt.App.downloadHotfix(context.Background()))
122120
}
123121

124-
func TestSelfUpdate_UnreadableFile(t *testing.T) {
125-
// When the hotfix file exists but is unreadable, selfUpdate should log warning and continue.
122+
func TestDownloadHotfix_UnreadableFile(t *testing.T) {
126123
dir := t.TempDir()
127124
path := filepath.Join(dir, "hotfix-config.json")
128-
require.NoError(t, os.WriteFile(path, []byte(`{"version": "1.0.0"}`), 0644))
129-
require.NoError(t, os.Chmod(path, 0000))
130-
t.Cleanup(func() { _ = os.Chmod(path, 0644) })
125+
require.NoError(t, os.WriteFile(path, []byte(`{"version": "1.0.0"}`), 0o644))
126+
require.NoError(t, os.Chmod(path, 0o000))
127+
t.Cleanup(func() { _ = os.Chmod(path, 0o644) })
131128

132129
tt := NewTestApp(t, TestAppConfig{})
133130
tt.App.hotfixVersionPath = path
134-
tt.App.selfUpdate(context.Background()) // should not panic, logs warning
131+
require.Error(t, tt.App.downloadHotfix(context.Background()))
135132
}
136133

137134
func TestRetryCommand_SuccessOnFirstAttempt(t *testing.T) {

parts/linux/cloud-init/artifacts/aks-node-controller-wrapper.sh

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ done
77

88
BIN_PATH="${BIN_PATH:-/opt/azure/containers/aks-node-controller}"
99
HOTFIX_BIN="${BIN_PATH}-hotfix"
10+
HOTFIX_JSON="/opt/azure/containers/aks-node-controller-hotfix.json"
1011
CONFIG_PATH="${CONFIG_PATH:-/opt/azure/containers/aks-node-controller-config.json}"
1112
NBC_CMD_PATH="${NBC_CMD_PATH:-/opt/azure/containers/aks-node-controller-nbc-cmd.sh}"
1213
LOGGER_TAG="aks-node-controller-wrapper"
@@ -18,16 +19,25 @@ log() {
1819
echo "$message"
1920
}
2021

22+
# this is to ensure that shellspec won't interpret any further lines below
23+
${__SOURCED__:+return}
24+
25+
if [ -f "$HOTFIX_JSON" ]; then
26+
log "Downloading ANC hotfix from ${HOTFIX_JSON}"
27+
if "$BIN_PATH" download-hotfix; then
28+
log "Finished downloading ANC hotfix from ${HOTFIX_JSON}"
29+
else
30+
log "Failed to download ANC hotfix from ${HOTFIX_JSON}; falling back to staged binaries"
31+
fi
32+
fi
33+
2134
if [ -x "$HOTFIX_BIN" ]; then
2235
BIN_PATH="$HOTFIX_BIN"
2336
log "Using hotfix binary: $HOTFIX_BIN"
2437
else
2538
log "Using VHD-baked binary: $BIN_PATH"
2639
fi
2740

28-
# this is to ensure that shellspec won't interpret any further lines below
29-
${__SOURCED__:+return}
30-
3141
command=("$BIN_PATH" provision)
3242
if [ -f "$CONFIG_PATH" ]; then
3343
log "Launching aks-node-controller with config ${CONFIG_PATH}"

0 commit comments

Comments
 (0)