From d40e57e9c07b2fd5706b3e33d84c27974b11652c Mon Sep 17 00:00:00 2001 From: Abdul Asfari <52782978+aboodasfari@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:28:18 +1300 Subject: [PATCH 1/3] feat: build aks-node-controller for windows --- .github/workflows/go-test.yml | 2 ++ aks-node-controller/app.go | 3 +++ aks-node-controller/{const.go => const_unix.go} | 3 +++ aks-node-controller/const_windows.go | 14 ++++++++++++++ aks-node-controller/filelock_unix.go | 13 +++++++++++++ aks-node-controller/filelock_windows.go | 12 ++++++++++++ aks-node-controller/goal_state.go | 3 +-- aks-node-controller/main.go | 2 +- 8 files changed, 49 insertions(+), 3 deletions(-) rename aks-node-controller/{const.go => const_unix.go} (80%) create mode 100644 aks-node-controller/const_windows.go create mode 100644 aks-node-controller/filelock_unix.go create mode 100644 aks-node-controller/filelock_windows.go diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index ee175c7a279..d30114c62ce 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -37,6 +37,8 @@ jobs: # shellcheck disable=SC2046 # word splitting is intentional: expand to multiple package args go test $(go list ./... | grep -v e2e) -coverprofile=coverage/agentbaker.out -covermode=atomic (cd aks-node-controller && go build -mod=readonly ./... && go test ./... -coverprofile=../coverage/aks-node-controller.out -covermode=atomic) + # aks-node-controller will also run on Windows nodes; keep it (and its tests) compiling there. + (cd aks-node-controller && GOOS=windows go build -mod=readonly ./... && GOOS=windows go vet ./...) (cd aks-live-patching && go build -mod=readonly ./... && go test ./... -coverprofile=../coverage/aks-live-patching.out -covermode=atomic) (cd vhdbuilder/cosi-uploader && go build -mod=readonly ./... && go test ./... -coverprofile=../../coverage/cosi-uploader.out -covermode=atomic) - name: Upload CSE minification diffs diff --git a/aks-node-controller/app.go b/aks-node-controller/app.go index 29e3fbdaa73..72e583f42f6 100644 --- a/aks-node-controller/app.go +++ b/aks-node-controller/app.go @@ -723,6 +723,9 @@ func (a *App) reportProvisionStatus(ctx context.Context, result *ProvisionResult if markerPath == "" { markerPath = readyReportMarkerPath } + if markerPath == "" { + return + } if _, err := os.Stat(markerPath); err != nil { if !errors.Is(err, os.ErrNotExist) { slog.Error("failed to stat Ready reporting handoff marker", "path", markerPath, "error", err) diff --git a/aks-node-controller/const.go b/aks-node-controller/const_unix.go similarity index 80% rename from aks-node-controller/const.go rename to aks-node-controller/const_unix.go index 3508cb2f150..db48c5cc139 100644 --- a/aks-node-controller/const.go +++ b/aks-node-controller/const_unix.go @@ -1,3 +1,5 @@ +//go:build !windows + package main // Some options are intentionally non-configurable to avoid customization by users @@ -7,4 +9,5 @@ const ( provisionJSONFilePath = "/var/log/azure/aks/provision.json" provisionCompleteFilePath = "/opt/azure/containers/provision.complete" readyReportMarkerPath = "/var/lib/waagent/experimental_skip_ready_report" + eventsDirPath = "/var/log/azure/Microsoft.Azure.Extensions.CustomScript/events" ) diff --git a/aks-node-controller/const_windows.go b/aks-node-controller/const_windows.go new file mode 100644 index 00000000000..b00b49626de --- /dev/null +++ b/aks-node-controller/const_windows.go @@ -0,0 +1,14 @@ +package main + +// Windows equivalents of const_unix.go. The controller uses its own directory under +// C:\AzureData because the legacy Windows CSE already writes a provision.complete +// there with different contents (the CSE result rather than an empty sentinel). +const ( + logPath = `C:\AzureData\aks-node-controller\aks-node-controller.log` + provisionJSONFilePath = `C:\AzureData\aks-node-controller\provision.json` + provisionCompleteFilePath = `C:\AzureData\aks-node-controller\provision.complete` + // The Ready-report handoff is a Linux waagent feature; an empty path disables it. + readyReportMarkerPath = "" + // Matches $global:EventsLoggingDir in parts/windows/windowscsehelper.ps1. + eventsDirPath = `C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\Events\` +) diff --git a/aks-node-controller/filelock_unix.go b/aks-node-controller/filelock_unix.go new file mode 100644 index 00000000000..28cb1af277a --- /dev/null +++ b/aks-node-controller/filelock_unix.go @@ -0,0 +1,13 @@ +//go:build !windows + +package main + +import ( + "os" + "syscall" +) + +// lockFileExclusive blocks until it holds an exclusive lock on f. Closing f releases it. +func lockFileExclusive(f *os.File) error { + return syscall.Flock(int(f.Fd()), syscall.LOCK_EX) +} diff --git a/aks-node-controller/filelock_windows.go b/aks-node-controller/filelock_windows.go new file mode 100644 index 00000000000..626108f22e8 --- /dev/null +++ b/aks-node-controller/filelock_windows.go @@ -0,0 +1,12 @@ +package main + +import ( + "errors" + "os" +) + +// lockFileExclusive is unsupported on Windows: its only caller writes the Linux Hyper-V +// KVP pool file, and Windows exposes KVP through the registry rather than a file. +func lockFileExclusive(*os.File) error { + return errors.ErrUnsupported +} diff --git a/aks-node-controller/goal_state.go b/aks-node-controller/goal_state.go index f0b7530bb90..a9a28e415d4 100644 --- a/aks-node-controller/goal_state.go +++ b/aks-node-controller/goal_state.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "strings" - "syscall" "time" "unicode/utf8" @@ -262,7 +261,7 @@ func writeKVPRecord(path, key, value string) error { } defer f.Close() // Closing the file releases the lock; os.File.Write is unbuffered. - if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { + if err = lockFileExclusive(f); err != nil { return err } _, err = f.Write(record) diff --git a/aks-node-controller/main.go b/aks-node-controller/main.go index be34e75f27c..79454896c5b 100644 --- a/aks-node-controller/main.go +++ b/aks-node-controller/main.go @@ -16,7 +16,7 @@ func main() { logCleanup := configureLogging() app := App{ cmdRun: cmdRunner, - eventLogger: helpers.NewEventLogger("/var/log/azure/Microsoft.Azure.Extensions.CustomScript/events"), + eventLogger: helpers.NewEventLogger(eventsDirPath), } exitCode := app.Run(context.Background(), os.Args) From f13c6a03d7c9e8ec30782caebb771b5893490b59 Mon Sep 17 00:00:00 2001 From: Abdul Asfari <52782978+aboodasfari@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:17:41 +1300 Subject: [PATCH 2/3] refactor: use runtime os config instead of build tags --- .github/workflows/go-test.yml | 2 -- aks-node-controller/app.go | 22 +++++++------- aks-node-controller/const.go | 45 ++++++++++++++++++++++++++++ aks-node-controller/const_test.go | 15 ++++++++++ aks-node-controller/const_unix.go | 13 -------- aks-node-controller/const_windows.go | 14 --------- aks-node-controller/main.go | 6 ++-- 7 files changed, 74 insertions(+), 43 deletions(-) create mode 100644 aks-node-controller/const.go create mode 100644 aks-node-controller/const_test.go delete mode 100644 aks-node-controller/const_unix.go delete mode 100644 aks-node-controller/const_windows.go diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index d30114c62ce..ee175c7a279 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -37,8 +37,6 @@ jobs: # shellcheck disable=SC2046 # word splitting is intentional: expand to multiple package args go test $(go list ./... | grep -v e2e) -coverprofile=coverage/agentbaker.out -covermode=atomic (cd aks-node-controller && go build -mod=readonly ./... && go test ./... -coverprofile=../coverage/aks-node-controller.out -covermode=atomic) - # aks-node-controller will also run on Windows nodes; keep it (and its tests) compiling there. - (cd aks-node-controller && GOOS=windows go build -mod=readonly ./... && GOOS=windows go vet ./...) (cd aks-live-patching && go build -mod=readonly ./... && go test ./... -coverprofile=../coverage/aks-live-patching.out -covermode=atomic) (cd vhdbuilder/cosi-uploader && go build -mod=readonly ./... && go test ./... -coverprofile=../../coverage/cosi-uploader.out -covermode=atomic) - name: Upload CSE minification diffs diff --git a/aks-node-controller/app.go b/aks-node-controller/app.go index 72e583f42f6..04e725e1e68 100644 --- a/aks-node-controller/app.go +++ b/aks-node-controller/app.go @@ -168,8 +168,8 @@ func (a *App) Run(ctx context.Context, args []string) int { Usage: "Wait for provisioning to complete", Action: func(ctx context.Context, cmd *cli.Command) error { provisionStatusFiles := ProvisionStatusFiles{ - ProvisionJSONFile: provisionJSONFilePath, - ProvisionCompleteFile: provisionCompleteFilePath, + ProvisionJSONFile: currentOSConfig().provisionJSONFilePath, + ProvisionCompleteFile: currentOSConfig().provisionCompleteFilePath, } provisionOutput, err := a.runProvisionWaitCommand(ctx, provisionStatusFiles) _, _ = fmt.Fprintln(cmd.Root().Writer, provisionOutput) @@ -721,7 +721,7 @@ func (a *App) Provision(ctx context.Context, flags ProvisionFlags) (*ProvisionRe func (a *App) reportProvisionStatus(ctx context.Context, result *ProvisionResult, provisionErr error) { markerPath := a.readyReportMarkerPath if markerPath == "" { - markerPath = readyReportMarkerPath + markerPath = currentOSConfig().readyReportMarkerPath } if markerPath == "" { return @@ -793,27 +793,27 @@ func (a *App) writeCompleteFileOnError(provisionResult *ProvisionResult, err err if err == nil { return } - if _, statErr := os.Stat(provisionJSONFilePath); statErr != nil && errors.Is(statErr, os.ErrNotExist) { + if _, statErr := os.Stat(currentOSConfig().provisionJSONFilePath); statErr != nil && errors.Is(statErr, os.ErrNotExist) { data, err := json.Marshal(provisionResult) if err != nil { slog.Error("failed to marshal provision result", "error", err) } - baseDir := filepath.Dir(provisionJSONFilePath) + baseDir := filepath.Dir(currentOSConfig().provisionJSONFilePath) if writeErr := os.MkdirAll(baseDir, 0755); writeErr != nil { slog.Error("failed to create directory for provision.json file", "path", baseDir, "error", writeErr) } - if writeErr := os.WriteFile(provisionJSONFilePath, data, 0600); writeErr != nil { - slog.Error("failed to write provision.json file", "path", provisionJSONFilePath, "error", writeErr) + if writeErr := os.WriteFile(currentOSConfig().provisionJSONFilePath, data, 0600); writeErr != nil { + slog.Error("failed to write provision.json file", "path", currentOSConfig().provisionJSONFilePath, "error", writeErr) } } - if _, statErr := os.Stat(provisionCompleteFilePath); statErr == nil { + if _, statErr := os.Stat(currentOSConfig().provisionCompleteFilePath); statErr == nil { return // already exists } else if !errors.Is(statErr, os.ErrNotExist) { // unexpected error - slog.Error("failed to stat provision.complete file", "path", provisionCompleteFilePath, "error", statErr) + slog.Error("failed to stat provision.complete file", "path", currentOSConfig().provisionCompleteFilePath, "error", statErr) return } - if writeErr := os.WriteFile(provisionCompleteFilePath, []byte{}, 0600); writeErr != nil { - slog.Error("failed to write provision.complete file", "path", provisionCompleteFilePath, "error", writeErr) + if writeErr := os.WriteFile(currentOSConfig().provisionCompleteFilePath, []byte{}, 0600); writeErr != nil { + slog.Error("failed to write provision.complete file", "path", currentOSConfig().provisionCompleteFilePath, "error", writeErr) } } diff --git a/aks-node-controller/const.go b/aks-node-controller/const.go new file mode 100644 index 00000000000..bf651a9062f --- /dev/null +++ b/aks-node-controller/const.go @@ -0,0 +1,45 @@ +package main + +import "runtime" + +// Holds the OS-specific locations. Some options are intentionally non-configurable +// to avoid customization by users; it will help us avoid introducing breaking changes in the future. +type osConfig struct { + logPath string + provisionJSONFilePath string + provisionCompleteFilePath string + readyReportMarkerPath string + eventsDirPath string +} + +func linuxConfig() osConfig { + return osConfig{ + logPath: "/var/log/azure/aks-node-controller.log", + provisionJSONFilePath: "/var/log/azure/aks/provision.json", + provisionCompleteFilePath: "/opt/azure/containers/provision.complete", + readyReportMarkerPath: "/var/lib/waagent/experimental_skip_ready_report", + eventsDirPath: "/var/log/azure/Microsoft.Azure.Extensions.CustomScript/events", + } +} + +func windowsConfig() osConfig { + return osConfig{ + logPath: `C:\AzureData\aks-node-controller\aks-node-controller.log`, + provisionJSONFilePath: `C:\AzureData\aks-node-controller\provision.json`, + provisionCompleteFilePath: `C:\AzureData\aks-node-controller\provision.complete`, + // The Ready-report handoff is a Linux waagent feature. + readyReportMarkerPath: "", + eventsDirPath: `C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\Events\`, + } +} + +func configForOS(goos string) osConfig { + if goos == "windows" { + return windowsConfig() + } + return linuxConfig() +} + +func currentOSConfig() osConfig { + return configForOS(runtime.GOOS) +} diff --git a/aks-node-controller/const_test.go b/aks-node-controller/const_test.go new file mode 100644 index 00000000000..8a9c46a8950 --- /dev/null +++ b/aks-node-controller/const_test.go @@ -0,0 +1,15 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigForOS(t *testing.T) { + require.Equal(t, windowsConfig(), configForOS("windows")) + require.Equal(t, linuxConfig(), configForOS("linux")) + require.Equal(t, linuxConfig(), configForOS("darwin")) + require.Empty(t, windowsConfig().readyReportMarkerPath) + require.NotEmpty(t, linuxConfig().readyReportMarkerPath) +} diff --git a/aks-node-controller/const_unix.go b/aks-node-controller/const_unix.go deleted file mode 100644 index db48c5cc139..00000000000 --- a/aks-node-controller/const_unix.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !windows - -package main - -// Some options are intentionally non-configurable to avoid customization by users -// it will help us to avoid introducing any breaking changes in the future. -const ( - logPath = "/var/log/azure/aks-node-controller.log" - provisionJSONFilePath = "/var/log/azure/aks/provision.json" - provisionCompleteFilePath = "/opt/azure/containers/provision.complete" - readyReportMarkerPath = "/var/lib/waagent/experimental_skip_ready_report" - eventsDirPath = "/var/log/azure/Microsoft.Azure.Extensions.CustomScript/events" -) diff --git a/aks-node-controller/const_windows.go b/aks-node-controller/const_windows.go deleted file mode 100644 index b00b49626de..00000000000 --- a/aks-node-controller/const_windows.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -// Windows equivalents of const_unix.go. The controller uses its own directory under -// C:\AzureData because the legacy Windows CSE already writes a provision.complete -// there with different contents (the CSE result rather than an empty sentinel). -const ( - logPath = `C:\AzureData\aks-node-controller\aks-node-controller.log` - provisionJSONFilePath = `C:\AzureData\aks-node-controller\provision.json` - provisionCompleteFilePath = `C:\AzureData\aks-node-controller\provision.complete` - // The Ready-report handoff is a Linux waagent feature; an empty path disables it. - readyReportMarkerPath = "" - // Matches $global:EventsLoggingDir in parts/windows/windowscsehelper.ps1. - eventsDirPath = `C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\Events\` -) diff --git a/aks-node-controller/main.go b/aks-node-controller/main.go index 79454896c5b..36c4f1e7724 100644 --- a/aks-node-controller/main.go +++ b/aks-node-controller/main.go @@ -16,7 +16,7 @@ func main() { logCleanup := configureLogging() app := App{ cmdRun: cmdRunner, - eventLogger: helpers.NewEventLogger(eventsDirPath), + eventLogger: helpers.NewEventLogger(currentOSConfig().eventsDirPath), } exitCode := app.Run(context.Background(), os.Args) @@ -54,8 +54,8 @@ func configureLogging() func() { func setupLogPath() string { // Try to create production directory first - if err := os.MkdirAll(filepath.Dir(logPath), 0755); err == nil { - return logPath + if err := os.MkdirAll(filepath.Dir(currentOSConfig().logPath), 0755); err == nil { + return currentOSConfig().logPath } // If directory creation fails, fallback to current directory return "aks-node-controller.log" From a727ffd81172b80500db9f5343a3923474c316c4 Mon Sep 17 00:00:00 2001 From: Abdul Asfari <52782978+aboodasfari@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:45:09 +1300 Subject: [PATCH 3/3] refactor: per-os filelock files and shared os config --- aks-node-controller/app.go | 22 +++++++++---------- aks-node-controller/const.go | 4 +--- .../{filelock_unix.go => filelock_darwin.go} | 2 -- aks-node-controller/filelock_linux.go | 11 ++++++++++ aks-node-controller/main.go | 6 ++--- 5 files changed, 26 insertions(+), 19 deletions(-) rename aks-node-controller/{filelock_unix.go => filelock_darwin.go} (91%) create mode 100644 aks-node-controller/filelock_linux.go diff --git a/aks-node-controller/app.go b/aks-node-controller/app.go index 04e725e1e68..6f26e72a9a2 100644 --- a/aks-node-controller/app.go +++ b/aks-node-controller/app.go @@ -168,8 +168,8 @@ func (a *App) Run(ctx context.Context, args []string) int { Usage: "Wait for provisioning to complete", Action: func(ctx context.Context, cmd *cli.Command) error { provisionStatusFiles := ProvisionStatusFiles{ - ProvisionJSONFile: currentOSConfig().provisionJSONFilePath, - ProvisionCompleteFile: currentOSConfig().provisionCompleteFilePath, + ProvisionJSONFile: osCfg.provisionJSONFilePath, + ProvisionCompleteFile: osCfg.provisionCompleteFilePath, } provisionOutput, err := a.runProvisionWaitCommand(ctx, provisionStatusFiles) _, _ = fmt.Fprintln(cmd.Root().Writer, provisionOutput) @@ -721,7 +721,7 @@ func (a *App) Provision(ctx context.Context, flags ProvisionFlags) (*ProvisionRe func (a *App) reportProvisionStatus(ctx context.Context, result *ProvisionResult, provisionErr error) { markerPath := a.readyReportMarkerPath if markerPath == "" { - markerPath = currentOSConfig().readyReportMarkerPath + markerPath = osCfg.readyReportMarkerPath } if markerPath == "" { return @@ -793,27 +793,27 @@ func (a *App) writeCompleteFileOnError(provisionResult *ProvisionResult, err err if err == nil { return } - if _, statErr := os.Stat(currentOSConfig().provisionJSONFilePath); statErr != nil && errors.Is(statErr, os.ErrNotExist) { + if _, statErr := os.Stat(osCfg.provisionJSONFilePath); statErr != nil && errors.Is(statErr, os.ErrNotExist) { data, err := json.Marshal(provisionResult) if err != nil { slog.Error("failed to marshal provision result", "error", err) } - baseDir := filepath.Dir(currentOSConfig().provisionJSONFilePath) + baseDir := filepath.Dir(osCfg.provisionJSONFilePath) if writeErr := os.MkdirAll(baseDir, 0755); writeErr != nil { slog.Error("failed to create directory for provision.json file", "path", baseDir, "error", writeErr) } - if writeErr := os.WriteFile(currentOSConfig().provisionJSONFilePath, data, 0600); writeErr != nil { - slog.Error("failed to write provision.json file", "path", currentOSConfig().provisionJSONFilePath, "error", writeErr) + if writeErr := os.WriteFile(osCfg.provisionJSONFilePath, data, 0600); writeErr != nil { + slog.Error("failed to write provision.json file", "path", osCfg.provisionJSONFilePath, "error", writeErr) } } - if _, statErr := os.Stat(currentOSConfig().provisionCompleteFilePath); statErr == nil { + if _, statErr := os.Stat(osCfg.provisionCompleteFilePath); statErr == nil { return // already exists } else if !errors.Is(statErr, os.ErrNotExist) { // unexpected error - slog.Error("failed to stat provision.complete file", "path", currentOSConfig().provisionCompleteFilePath, "error", statErr) + slog.Error("failed to stat provision.complete file", "path", osCfg.provisionCompleteFilePath, "error", statErr) return } - if writeErr := os.WriteFile(currentOSConfig().provisionCompleteFilePath, []byte{}, 0600); writeErr != nil { - slog.Error("failed to write provision.complete file", "path", currentOSConfig().provisionCompleteFilePath, "error", writeErr) + if writeErr := os.WriteFile(osCfg.provisionCompleteFilePath, []byte{}, 0600); writeErr != nil { + slog.Error("failed to write provision.complete file", "path", osCfg.provisionCompleteFilePath, "error", writeErr) } } diff --git a/aks-node-controller/const.go b/aks-node-controller/const.go index bf651a9062f..6a668d32882 100644 --- a/aks-node-controller/const.go +++ b/aks-node-controller/const.go @@ -40,6 +40,4 @@ func configForOS(goos string) osConfig { return linuxConfig() } -func currentOSConfig() osConfig { - return configForOS(runtime.GOOS) -} +var osCfg = configForOS(runtime.GOOS) //nolint:gochecknoglobals // read-only OS-specific paths resolved once at startup diff --git a/aks-node-controller/filelock_unix.go b/aks-node-controller/filelock_darwin.go similarity index 91% rename from aks-node-controller/filelock_unix.go rename to aks-node-controller/filelock_darwin.go index 28cb1af277a..759b474e9d3 100644 --- a/aks-node-controller/filelock_unix.go +++ b/aks-node-controller/filelock_darwin.go @@ -1,5 +1,3 @@ -//go:build !windows - package main import ( diff --git a/aks-node-controller/filelock_linux.go b/aks-node-controller/filelock_linux.go new file mode 100644 index 00000000000..759b474e9d3 --- /dev/null +++ b/aks-node-controller/filelock_linux.go @@ -0,0 +1,11 @@ +package main + +import ( + "os" + "syscall" +) + +// lockFileExclusive blocks until it holds an exclusive lock on f. Closing f releases it. +func lockFileExclusive(f *os.File) error { + return syscall.Flock(int(f.Fd()), syscall.LOCK_EX) +} diff --git a/aks-node-controller/main.go b/aks-node-controller/main.go index 36c4f1e7724..4b96b978b4e 100644 --- a/aks-node-controller/main.go +++ b/aks-node-controller/main.go @@ -16,7 +16,7 @@ func main() { logCleanup := configureLogging() app := App{ cmdRun: cmdRunner, - eventLogger: helpers.NewEventLogger(currentOSConfig().eventsDirPath), + eventLogger: helpers.NewEventLogger(osCfg.eventsDirPath), } exitCode := app.Run(context.Background(), os.Args) @@ -54,8 +54,8 @@ func configureLogging() func() { func setupLogPath() string { // Try to create production directory first - if err := os.MkdirAll(filepath.Dir(currentOSConfig().logPath), 0755); err == nil { - return currentOSConfig().logPath + if err := os.MkdirAll(filepath.Dir(osCfg.logPath), 0755); err == nil { + return osCfg.logPath } // If directory creation fails, fallback to current directory return "aks-node-controller.log"