Skip to content

Commit 4347a4a

Browse files
DevinwongCopilot
andcommitted
agent: plumb ANC hotfix version from toggle to cloud-init
Add server-side support for delivering aks-node-controller hotfix version to nodes via cloud-init write_files: - Add GetANCHotfixVersion() to Toggles interface + default impl - Add ANCHotfixVersion field to NodeBootstrappingConfiguration - Resolve toggle BEFORE template rendering in GetNodeBootstrapping with semver validation and precedence guard - Add GetANCHotfixVersion template function in baker.go - Add conditional write_files entry in nodecustomdata.yml that writes /opt/azure/containers/aks-node-controller-hotfix.json when set - Add functional tests for hotfix version in customData - Update test mock to satisfy new interface method The hotfix JSON file is consumed by ANC selfUpdate() (PR #8257) which reads it at startup to determine if a PMC package upgrade is needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 45408f0 commit 4347a4a

6 files changed

Lines changed: 121 additions & 0 deletions

File tree

parts/linux/cloud-init/nodecustomdata.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,3 +397,11 @@ write_files:
397397
content: !!binary |
398398
{{GetVariableProperty "cloudInitData" "customSearchDomainsScript"}}
399399
{{- end}}
400+
401+
{{- if GetANCHotfixVersion}}
402+
- path: /opt/azure/containers/aks-node-controller-hotfix.json
403+
permissions: "0644"
404+
owner: root
405+
content: |
406+
{"version":"{{GetANCHotfixVersion}}"}
407+
{{- end}}

pkg/agent/baker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,7 @@ func getContainerServiceFuncMap(config *datamodel.NodeBootstrappingConfiguration
13081308
return cs.Properties.OrchestratorProfile.KubernetesConfig.BlockIptables
13091309
},
13101310
"EnableScriptlessCSECmd": func() bool { return config.EnableScriptlessCSECmd },
1311+
"GetANCHotfixVersion": func() string { return config.ANCHotfixVersion },
13111312
}
13121313
}
13131314

pkg/agent/bakerapi.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ package agent
66
import (
77
"context"
88
"fmt"
9+
"regexp"
910

1011
"github.com/Azure/agentbaker/pkg/agent/datamodel"
1112
"github.com/Azure/agentbaker/pkg/agent/toggles"
1213
)
1314

15+
// semverPattern validates ANC hotfix version strings to prevent injection into cloud-init JSON.
16+
var semverPattern = regexp.MustCompile(`^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$`)
17+
1418
type AgentBaker interface {
1519
GetNodeBootstrapping(ctx context.Context, config *datamodel.NodeBootstrappingConfiguration) (*datamodel.NodeBootstrapping, error)
1620
GetLatestSigImageConfig(sigConfig datamodel.SIGConfig, distro datamodel.Distro, envInfo *datamodel.EnvironmentInfo) (*datamodel.SigImageConfig, error)
@@ -43,6 +47,16 @@ func (agentBaker *agentBakerImpl) GetNodeBootstrapping(ctx context.Context, conf
4347
ValidateAndSetLinuxNodeBootstrappingConfiguration(config)
4448
}
4549

50+
// resolve ANC(AKS-Node-Controller) hotfix version toggle before template rendering so cloud-init can include it.
51+
// Only apply when the config doesn't already have a version set (toggle is the default source).
52+
if !config.AgentPoolProfile.IsWindows() && config.ANCHotfixVersion == "" {
53+
e := toggles.NewEntityFromNodeBootstrappingConfiguration(config)
54+
ancHotfixVersion := agentBaker.toggles.GetANCHotfixVersion(e)
55+
if ancHotfixVersion != "" && semverPattern.MatchString(ancHotfixVersion) {
56+
config.ANCHotfixVersion = ancHotfixVersion
57+
}
58+
}
59+
4660
templateGenerator := InitializeTemplateGenerator()
4761
nodeBootstrapping := &datamodel.NodeBootstrapping{
4862
CustomData: templateGenerator.getNodeBootstrappingPayload(config),

pkg/agent/bakerapi_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package agent
22

33
import (
4+
"bytes"
5+
"compress/gzip"
46
"context"
7+
"encoding/base64"
8+
"io"
59

610
"github.com/Azure/agentbaker/pkg/agent/datamodel"
711
agenttoggles "github.com/Azure/agentbaker/pkg/agent/toggles"
@@ -13,6 +17,7 @@ import (
1317
type testToggles struct {
1418
defaultNodeImageVersionOverride string
1519
nodeImageVersionOverrides map[datamodel.Distro]string
20+
ancHotfixVersion string
1621
}
1722

1823
func (t *testToggles) GetLinuxNodeImageVersion(entity *agenttoggles.Entity, distro datamodel.Distro) string {
@@ -22,6 +27,10 @@ func (t *testToggles) GetLinuxNodeImageVersion(entity *agenttoggles.Entity, dist
2227
return t.defaultNodeImageVersionOverride
2328
}
2429

30+
func (t *testToggles) GetANCHotfixVersion(entity *agenttoggles.Entity) string {
31+
return t.ancHotfixVersion
32+
}
33+
2534
var _ = Describe("AgentBaker API implementation tests", func() {
2635
var (
2736
cs *datamodel.ContainerService
@@ -271,6 +280,70 @@ var _ = Describe("AgentBaker API implementation tests", func() {
271280
_, err = agentBaker.GetNodeBootstrapping(context.Background(), config)
272281
Expect(err).NotTo(HaveOccurred())
273282
})
283+
284+
It("should include ANC hotfix config file in customData when toggle is set", func() {
285+
toggles := &testToggles{
286+
ancHotfixVersion: "0.1.2",
287+
}
288+
agentBaker, err := NewAgentBaker()
289+
Expect(err).NotTo(HaveOccurred())
290+
agentBaker = agentBaker.WithToggles(toggles)
291+
292+
nodeBootStrapping, err := agentBaker.GetNodeBootstrapping(context.Background(), config)
293+
Expect(err).NotTo(HaveOccurred())
294+
295+
customData := decodeCustomData(nodeBootStrapping.CustomData)
296+
Expect(customData).To(ContainSubstring("/opt/azure/containers/aks-node-controller-hotfix.json"))
297+
Expect(customData).To(ContainSubstring(`{"version":"0.1.2"}`))
298+
})
299+
300+
It("should NOT include ANC hotfix config file in customData when toggle is empty", func() {
301+
agentBaker, err := NewAgentBaker()
302+
Expect(err).NotTo(HaveOccurred())
303+
304+
nodeBootStrapping, err := agentBaker.GetNodeBootstrapping(context.Background(), config)
305+
Expect(err).NotTo(HaveOccurred())
306+
307+
customData := decodeCustomData(nodeBootStrapping.CustomData)
308+
Expect(customData).NotTo(ContainSubstring("aks-node-controller-hotfix.json"))
309+
})
310+
311+
It("should NOT set ANC hotfix version for Windows nodes even when toggle is set", func() {
312+
config.AgentPoolProfile.OSType = datamodel.Windows
313+
toggles := &testToggles{
314+
ancHotfixVersion: "0.1.2",
315+
}
316+
agentBaker, err := NewAgentBaker()
317+
Expect(err).NotTo(HaveOccurred())
318+
agentBaker = agentBaker.WithToggles(toggles)
319+
320+
// The IsWindows guard in bakerapi.go should prevent setting ANCHotfixVersion.
321+
// We verify config.ANCHotfixVersion stays empty after the toggle resolution code runs.
322+
// We can't do full GetNodeBootstrapping here because the test config lacks a complete
323+
// WindowsProfile, but the guard is the important thing to test.
324+
Expect(config.ANCHotfixVersion).To(Equal(""))
325+
326+
// Simulate what bakerapi.go does: the guard should skip toggle resolution
327+
if !config.AgentPoolProfile.IsWindows() {
328+
config.ANCHotfixVersion = toggles.GetANCHotfixVersion(nil)
329+
}
330+
Expect(config.ANCHotfixVersion).To(Equal(""))
331+
})
332+
333+
It("should NOT include ANC hotfix config when toggle value is not valid semver", func() {
334+
toggles := &testToggles{
335+
ancHotfixVersion: `0.1.2"; malicious`,
336+
}
337+
agentBaker, err := NewAgentBaker()
338+
Expect(err).NotTo(HaveOccurred())
339+
agentBaker = agentBaker.WithToggles(toggles)
340+
341+
nodeBootStrapping, err := agentBaker.GetNodeBootstrapping(context.Background(), config)
342+
Expect(err).NotTo(HaveOccurred())
343+
344+
customData := decodeCustomData(nodeBootStrapping.CustomData)
345+
Expect(customData).NotTo(ContainSubstring("aks-node-controller-hotfix.json"))
346+
})
274347
})
275348

276349
Context("GetLatestSigImageConfig", func() {
@@ -501,3 +574,18 @@ var _ = Describe("AgentBaker API implementation tests", func() {
501574

502575
})
503576
})
577+
578+
// decodeCustomData decodes the base64+gzip encoded customData string back to the raw cloud-init YAML.
579+
func decodeCustomData(encoded string) string {
580+
decoded, err := base64.StdEncoding.DecodeString(encoded)
581+
Expect(err).NotTo(HaveOccurred())
582+
reader, err := gzip.NewReader(bytes.NewReader(decoded))
583+
if err != nil {
584+
// not gzipped, return as-is (e.g. Windows)
585+
return string(decoded)
586+
}
587+
defer reader.Close()
588+
data, err := io.ReadAll(reader)
589+
Expect(err).NotTo(HaveOccurred())
590+
return string(data)
591+
}

pkg/agent/datamodel/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1801,6 +1801,11 @@ type NodeBootstrappingConfiguration struct {
18011801
// EnableScriptlessNBCCSECmd enables scriptless phase 2 in which the cse cmd generated from NBC is passed to
18021802
// AKS Node Controller and uses the NBC cmd to start provisioning.
18031803
EnableScriptlessNBCCSECmd bool
1804+
1805+
// ANCHotfixVersion is the version of aks-node-controller hotfix to install from PMC.
1806+
// When set, ANC will self-update to this version before running provisioning scripts.
1807+
// Delivered to the node via cloud-init write_files as a JSON config file.
1808+
ANCHotfixVersion string `json:"ancHotfixVersion,omitempty"`
18041809
}
18051810

18061811
func (config *NodeBootstrappingConfiguration) IsAzureLinux() bool {

pkg/agent/toggles/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Entity struct {
1717

1818
type Toggles interface {
1919
GetLinuxNodeImageVersion(entity *Entity, distro datamodel.Distro) string
20+
GetANCHotfixVersion(entity *Entity) string
2021
}
2122

2223
type defaultToggles struct{}
@@ -25,6 +26,10 @@ func (t *defaultToggles) GetLinuxNodeImageVersion(entity *Entity, distro datamod
2526
return ""
2627
}
2728

29+
func (t *defaultToggles) GetANCHotfixVersion(entity *Entity) string {
30+
return ""
31+
}
32+
2833
func NewDefaultToggles() Toggles {
2934
return &defaultToggles{}
3035
}

0 commit comments

Comments
 (0)