From ff90954fbabbf93bf5853d05c28347bdd038546b Mon Sep 17 00:00:00 2001 From: wenxuanW Date: Wed, 25 Mar 2026 11:24:25 -0700 Subject: [PATCH 1/7] AKS RP integration with Machine API - for bootstrap path --- components/aksmachine/action.pb.go | 1165 +++++++++++++++++ components/aksmachine/action.proto | 80 ++ components/aksmachine/redact.go | 9 + .../aksmachine/v20260301/ensure_machine.go | 284 ++++ .../v20260301/ensure_machine_test.go | 223 ++++ components/aksmachine/v20260301/exports.go | 13 + components/arc/action.pb.go | 61 +- components/arc/action.proto | 3 + components/arc/v20260301/arc_helpers.go | 2 +- components/arc/v20260301/arc_registration.go | 2 +- components/arc/v20260301/install_arc.go | 12 +- components/exports.go | 1 + go.mod | 2 +- go.sum | 12 +- pkg/bootstrapper/bootstrapper.go | 6 + pkg/bootstrapper/cluster_config_enricher.go | 9 +- pkg/bootstrapper/components.go | 58 + pkg/components/arc/arc_base.go | 2 +- pkg/kube/client.go | 2 +- pkg/spec/collector.go | 2 +- pkg/spec/collector_test.go | 2 +- 21 files changed, 1918 insertions(+), 32 deletions(-) create mode 100644 components/aksmachine/action.pb.go create mode 100644 components/aksmachine/action.proto create mode 100644 components/aksmachine/redact.go create mode 100644 components/aksmachine/v20260301/ensure_machine.go create mode 100644 components/aksmachine/v20260301/ensure_machine_test.go create mode 100644 components/aksmachine/v20260301/exports.go diff --git a/components/aksmachine/action.pb.go b/components/aksmachine/action.pb.go new file mode 100644 index 00000000..99147486 --- /dev/null +++ b/components/aksmachine/action.pb.go @@ -0,0 +1,1165 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.33.5 +// source: components/aksmachine/action.proto + +package aksmachine + +import ( + reflect "reflect" + unsafe "unsafe" + + api "github.com/Azure/AKSFlexNode/components/api" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AzureServicePrincipalCredential struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_TenantId *string `protobuf:"bytes,1,opt,name=tenant_id,json=tenantId"` + xxx_hidden_ClientId *string `protobuf:"bytes,2,opt,name=client_id,json=clientId"` + xxx_hidden_ClientSecret *string `protobuf:"bytes,3,opt,name=client_secret,json=clientSecret"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AzureServicePrincipalCredential) Reset() { + *x = AzureServicePrincipalCredential{} + mi := &file_components_aksmachine_action_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AzureServicePrincipalCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AzureServicePrincipalCredential) ProtoMessage() {} + +func (x *AzureServicePrincipalCredential) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *AzureServicePrincipalCredential) GetTenantId() string { + if x != nil { + if x.xxx_hidden_TenantId != nil { + return *x.xxx_hidden_TenantId + } + return "" + } + return "" +} + +func (x *AzureServicePrincipalCredential) GetClientId() string { + if x != nil { + if x.xxx_hidden_ClientId != nil { + return *x.xxx_hidden_ClientId + } + return "" + } + return "" +} + +func (x *AzureServicePrincipalCredential) GetClientSecret() string { + if x != nil { + if x.xxx_hidden_ClientSecret != nil { + return *x.xxx_hidden_ClientSecret + } + return "" + } + return "" +} + +func (x *AzureServicePrincipalCredential) SetTenantId(v string) { + x.xxx_hidden_TenantId = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 3) +} + +func (x *AzureServicePrincipalCredential) SetClientId(v string) { + x.xxx_hidden_ClientId = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 3) +} + +func (x *AzureServicePrincipalCredential) SetClientSecret(v string) { + x.xxx_hidden_ClientSecret = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 3) +} + +func (x *AzureServicePrincipalCredential) HasTenantId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *AzureServicePrincipalCredential) HasClientId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *AzureServicePrincipalCredential) HasClientSecret() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *AzureServicePrincipalCredential) ClearTenantId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_TenantId = nil +} + +func (x *AzureServicePrincipalCredential) ClearClientId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_ClientId = nil +} + +func (x *AzureServicePrincipalCredential) ClearClientSecret() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_ClientSecret = nil +} + +type AzureServicePrincipalCredential_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + TenantId *string + ClientId *string + ClientSecret *string +} + +func (b0 AzureServicePrincipalCredential_builder) Build() *AzureServicePrincipalCredential { + m0 := &AzureServicePrincipalCredential{} + b, x := &b0, m0 + _, _ = b, x + if b.TenantId != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 3) + x.xxx_hidden_TenantId = b.TenantId + } + if b.ClientId != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 3) + x.xxx_hidden_ClientId = b.ClientId + } + if b.ClientSecret != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 3) + x.xxx_hidden_ClientSecret = b.ClientSecret + } + return m0 +} + +type AzureMSICredential struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_ClientId *string `protobuf:"bytes,1,opt,name=client_id,json=clientId"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AzureMSICredential) Reset() { + *x = AzureMSICredential{} + mi := &file_components_aksmachine_action_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AzureMSICredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AzureMSICredential) ProtoMessage() {} + +func (x *AzureMSICredential) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *AzureMSICredential) GetClientId() string { + if x != nil { + if x.xxx_hidden_ClientId != nil { + return *x.xxx_hidden_ClientId + } + return "" + } + return "" +} + +func (x *AzureMSICredential) SetClientId(v string) { + x.xxx_hidden_ClientId = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 1) +} + +func (x *AzureMSICredential) HasClientId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *AzureMSICredential) ClearClientId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_ClientId = nil +} + +type AzureMSICredential_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Client ID of the managed identity; empty means system-assigned MI. + ClientId *string +} + +func (b0 AzureMSICredential_builder) Build() *AzureMSICredential { + m0 := &AzureMSICredential{} + b, x := &b0, m0 + _, _ = b, x + if b.ClientId != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 1) + x.xxx_hidden_ClientId = b.ClientId + } + return m0 +} + +// AzureCredential carries the ARM authentication credential for the action. +// If neither field is set the action falls back to Azure CLI credential. +type AzureCredential struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Credential isAzureCredential_Credential `protobuf_oneof:"credential"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AzureCredential) Reset() { + *x = AzureCredential{} + mi := &file_components_aksmachine_action_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AzureCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AzureCredential) ProtoMessage() {} + +func (x *AzureCredential) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *AzureCredential) GetServicePrincipal() *AzureServicePrincipalCredential { + if x != nil { + if x, ok := x.xxx_hidden_Credential.(*azureCredential_ServicePrincipal); ok { + return x.ServicePrincipal + } + } + return nil +} + +func (x *AzureCredential) GetManagedIdentity() *AzureMSICredential { + if x != nil { + if x, ok := x.xxx_hidden_Credential.(*azureCredential_ManagedIdentity); ok { + return x.ManagedIdentity + } + } + return nil +} + +func (x *AzureCredential) SetServicePrincipal(v *AzureServicePrincipalCredential) { + if v == nil { + x.xxx_hidden_Credential = nil + return + } + x.xxx_hidden_Credential = &azureCredential_ServicePrincipal{v} +} + +func (x *AzureCredential) SetManagedIdentity(v *AzureMSICredential) { + if v == nil { + x.xxx_hidden_Credential = nil + return + } + x.xxx_hidden_Credential = &azureCredential_ManagedIdentity{v} +} + +func (x *AzureCredential) HasCredential() bool { + if x == nil { + return false + } + return x.xxx_hidden_Credential != nil +} + +func (x *AzureCredential) HasServicePrincipal() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Credential.(*azureCredential_ServicePrincipal) + return ok +} + +func (x *AzureCredential) HasManagedIdentity() bool { + if x == nil { + return false + } + _, ok := x.xxx_hidden_Credential.(*azureCredential_ManagedIdentity) + return ok +} + +func (x *AzureCredential) ClearCredential() { + x.xxx_hidden_Credential = nil +} + +func (x *AzureCredential) ClearServicePrincipal() { + if _, ok := x.xxx_hidden_Credential.(*azureCredential_ServicePrincipal); ok { + x.xxx_hidden_Credential = nil + } +} + +func (x *AzureCredential) ClearManagedIdentity() { + if _, ok := x.xxx_hidden_Credential.(*azureCredential_ManagedIdentity); ok { + x.xxx_hidden_Credential = nil + } +} + +const AzureCredential_Credential_not_set_case case_AzureCredential_Credential = 0 +const AzureCredential_ServicePrincipal_case case_AzureCredential_Credential = 1 +const AzureCredential_ManagedIdentity_case case_AzureCredential_Credential = 2 + +func (x *AzureCredential) WhichCredential() case_AzureCredential_Credential { + if x == nil { + return AzureCredential_Credential_not_set_case + } + switch x.xxx_hidden_Credential.(type) { + case *azureCredential_ServicePrincipal: + return AzureCredential_ServicePrincipal_case + case *azureCredential_ManagedIdentity: + return AzureCredential_ManagedIdentity_case + default: + return AzureCredential_Credential_not_set_case + } +} + +type AzureCredential_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Fields of oneof xxx_hidden_Credential: + ServicePrincipal *AzureServicePrincipalCredential + ManagedIdentity *AzureMSICredential + // -- end of xxx_hidden_Credential +} + +func (b0 AzureCredential_builder) Build() *AzureCredential { + m0 := &AzureCredential{} + b, x := &b0, m0 + _, _ = b, x + if b.ServicePrincipal != nil { + x.xxx_hidden_Credential = &azureCredential_ServicePrincipal{b.ServicePrincipal} + } + if b.ManagedIdentity != nil { + x.xxx_hidden_Credential = &azureCredential_ManagedIdentity{b.ManagedIdentity} + } + return m0 +} + +type case_AzureCredential_Credential protoreflect.FieldNumber + +func (x case_AzureCredential_Credential) String() string { + md := file_components_aksmachine_action_proto_msgTypes[2].Descriptor() + if x == 0 { + return "not set" + } + return protoimpl.X.MessageFieldStringOf(md, protoreflect.FieldNumber(x)) +} + +type isAzureCredential_Credential interface { + isAzureCredential_Credential() +} + +type azureCredential_ServicePrincipal struct { + ServicePrincipal *AzureServicePrincipalCredential `protobuf:"bytes,1,opt,name=service_principal,json=servicePrincipal,oneof"` +} + +type azureCredential_ManagedIdentity struct { + ManagedIdentity *AzureMSICredential `protobuf:"bytes,2,opt,name=managed_identity,json=managedIdentity,oneof"` +} + +func (*azureCredential_ServicePrincipal) isAzureCredential_Credential() {} + +func (*azureCredential_ManagedIdentity) isAzureCredential_Credential() {} + +type EnsureMachine struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Metadata *api.Metadata `protobuf:"bytes,1,opt,name=metadata"` + xxx_hidden_Spec *EnsureMachineSpec `protobuf:"bytes,2,opt,name=spec"` + xxx_hidden_Status *EnsureMachineStatus `protobuf:"bytes,3,opt,name=status"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnsureMachine) Reset() { + *x = EnsureMachine{} + mi := &file_components_aksmachine_action_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnsureMachine) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnsureMachine) ProtoMessage() {} + +func (x *EnsureMachine) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *EnsureMachine) GetMetadata() *api.Metadata { + if x != nil { + return x.xxx_hidden_Metadata + } + return nil +} + +func (x *EnsureMachine) GetSpec() *EnsureMachineSpec { + if x != nil { + return x.xxx_hidden_Spec + } + return nil +} + +func (x *EnsureMachine) GetStatus() *EnsureMachineStatus { + if x != nil { + return x.xxx_hidden_Status + } + return nil +} + +func (x *EnsureMachine) SetMetadata(v *api.Metadata) { + x.xxx_hidden_Metadata = v +} + +func (x *EnsureMachine) SetSpec(v *EnsureMachineSpec) { + x.xxx_hidden_Spec = v +} + +func (x *EnsureMachine) SetStatus(v *EnsureMachineStatus) { + x.xxx_hidden_Status = v +} + +func (x *EnsureMachine) HasMetadata() bool { + if x == nil { + return false + } + return x.xxx_hidden_Metadata != nil +} + +func (x *EnsureMachine) HasSpec() bool { + if x == nil { + return false + } + return x.xxx_hidden_Spec != nil +} + +func (x *EnsureMachine) HasStatus() bool { + if x == nil { + return false + } + return x.xxx_hidden_Status != nil +} + +func (x *EnsureMachine) ClearMetadata() { + x.xxx_hidden_Metadata = nil +} + +func (x *EnsureMachine) ClearSpec() { + x.xxx_hidden_Spec = nil +} + +func (x *EnsureMachine) ClearStatus() { + x.xxx_hidden_Status = nil +} + +type EnsureMachine_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Metadata *api.Metadata + Spec *EnsureMachineSpec + Status *EnsureMachineStatus +} + +func (b0 EnsureMachine_builder) Build() *EnsureMachine { + m0 := &EnsureMachine{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Metadata = b.Metadata + x.xxx_hidden_Spec = b.Spec + x.xxx_hidden_Status = b.Status + return m0 +} + +type EnsureMachineSpec struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_SubscriptionId *string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId"` + xxx_hidden_ResourceGroup *string `protobuf:"bytes,2,opt,name=resource_group,json=resourceGroup"` + xxx_hidden_ClusterName *string `protobuf:"bytes,3,opt,name=cluster_name,json=clusterName"` + xxx_hidden_MachineName *string `protobuf:"bytes,4,opt,name=machine_name,json=machineName"` + xxx_hidden_KubernetesVersion *string `protobuf:"bytes,5,opt,name=kubernetes_version,json=kubernetesVersion"` + xxx_hidden_MaxPods int32 `protobuf:"varint,6,opt,name=max_pods,json=maxPods"` + xxx_hidden_NodeLabels map[string]string `protobuf:"bytes,7,rep,name=node_labels,json=nodeLabels" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + xxx_hidden_NodeTaints []string `protobuf:"bytes,8,rep,name=node_taints,json=nodeTaints"` + xxx_hidden_NodeInitializationTaints []string `protobuf:"bytes,9,rep,name=node_initialization_taints,json=nodeInitializationTaints"` + xxx_hidden_KubeletConfig *MachineKubeletConfig `protobuf:"bytes,10,opt,name=kubelet_config,json=kubeletConfig"` + xxx_hidden_Enabled bool `protobuf:"varint,11,opt,name=enabled"` + xxx_hidden_AzureCredential *AzureCredential `protobuf:"bytes,12,opt,name=azure_credential,json=azureCredential"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnsureMachineSpec) Reset() { + *x = EnsureMachineSpec{} + mi := &file_components_aksmachine_action_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnsureMachineSpec) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnsureMachineSpec) ProtoMessage() {} + +func (x *EnsureMachineSpec) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *EnsureMachineSpec) GetSubscriptionId() string { + if x != nil { + if x.xxx_hidden_SubscriptionId != nil { + return *x.xxx_hidden_SubscriptionId + } + return "" + } + return "" +} + +func (x *EnsureMachineSpec) GetResourceGroup() string { + if x != nil { + if x.xxx_hidden_ResourceGroup != nil { + return *x.xxx_hidden_ResourceGroup + } + return "" + } + return "" +} + +func (x *EnsureMachineSpec) GetClusterName() string { + if x != nil { + if x.xxx_hidden_ClusterName != nil { + return *x.xxx_hidden_ClusterName + } + return "" + } + return "" +} + +func (x *EnsureMachineSpec) GetMachineName() string { + if x != nil { + if x.xxx_hidden_MachineName != nil { + return *x.xxx_hidden_MachineName + } + return "" + } + return "" +} + +func (x *EnsureMachineSpec) GetKubernetesVersion() string { + if x != nil { + if x.xxx_hidden_KubernetesVersion != nil { + return *x.xxx_hidden_KubernetesVersion + } + return "" + } + return "" +} + +func (x *EnsureMachineSpec) GetMaxPods() int32 { + if x != nil { + return x.xxx_hidden_MaxPods + } + return 0 +} + +func (x *EnsureMachineSpec) GetNodeLabels() map[string]string { + if x != nil { + return x.xxx_hidden_NodeLabels + } + return nil +} + +func (x *EnsureMachineSpec) GetNodeTaints() []string { + if x != nil { + return x.xxx_hidden_NodeTaints + } + return nil +} + +func (x *EnsureMachineSpec) GetNodeInitializationTaints() []string { + if x != nil { + return x.xxx_hidden_NodeInitializationTaints + } + return nil +} + +func (x *EnsureMachineSpec) GetKubeletConfig() *MachineKubeletConfig { + if x != nil { + return x.xxx_hidden_KubeletConfig + } + return nil +} + +func (x *EnsureMachineSpec) GetEnabled() bool { + if x != nil { + return x.xxx_hidden_Enabled + } + return false +} + +func (x *EnsureMachineSpec) GetAzureCredential() *AzureCredential { + if x != nil { + return x.xxx_hidden_AzureCredential + } + return nil +} + +func (x *EnsureMachineSpec) SetSubscriptionId(v string) { + x.xxx_hidden_SubscriptionId = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 12) +} + +func (x *EnsureMachineSpec) SetResourceGroup(v string) { + x.xxx_hidden_ResourceGroup = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 12) +} + +func (x *EnsureMachineSpec) SetClusterName(v string) { + x.xxx_hidden_ClusterName = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 12) +} + +func (x *EnsureMachineSpec) SetMachineName(v string) { + x.xxx_hidden_MachineName = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 12) +} + +func (x *EnsureMachineSpec) SetKubernetesVersion(v string) { + x.xxx_hidden_KubernetesVersion = &v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 12) +} + +func (x *EnsureMachineSpec) SetMaxPods(v int32) { + x.xxx_hidden_MaxPods = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 5, 12) +} + +func (x *EnsureMachineSpec) SetNodeLabels(v map[string]string) { + x.xxx_hidden_NodeLabels = v +} + +func (x *EnsureMachineSpec) SetNodeTaints(v []string) { + x.xxx_hidden_NodeTaints = v +} + +func (x *EnsureMachineSpec) SetNodeInitializationTaints(v []string) { + x.xxx_hidden_NodeInitializationTaints = v +} + +func (x *EnsureMachineSpec) SetKubeletConfig(v *MachineKubeletConfig) { + x.xxx_hidden_KubeletConfig = v +} + +func (x *EnsureMachineSpec) SetEnabled(v bool) { + x.xxx_hidden_Enabled = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 10, 12) +} + +func (x *EnsureMachineSpec) SetAzureCredential(v *AzureCredential) { + x.xxx_hidden_AzureCredential = v +} + +func (x *EnsureMachineSpec) HasSubscriptionId() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *EnsureMachineSpec) HasResourceGroup() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *EnsureMachineSpec) HasClusterName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 2) +} + +func (x *EnsureMachineSpec) HasMachineName() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 3) +} + +func (x *EnsureMachineSpec) HasKubernetesVersion() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 4) +} + +func (x *EnsureMachineSpec) HasMaxPods() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 5) +} + +func (x *EnsureMachineSpec) HasKubeletConfig() bool { + if x == nil { + return false + } + return x.xxx_hidden_KubeletConfig != nil +} + +func (x *EnsureMachineSpec) HasEnabled() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 10) +} + +func (x *EnsureMachineSpec) HasAzureCredential() bool { + if x == nil { + return false + } + return x.xxx_hidden_AzureCredential != nil +} + +func (x *EnsureMachineSpec) ClearSubscriptionId() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_SubscriptionId = nil +} + +func (x *EnsureMachineSpec) ClearResourceGroup() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_ResourceGroup = nil +} + +func (x *EnsureMachineSpec) ClearClusterName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 2) + x.xxx_hidden_ClusterName = nil +} + +func (x *EnsureMachineSpec) ClearMachineName() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 3) + x.xxx_hidden_MachineName = nil +} + +func (x *EnsureMachineSpec) ClearKubernetesVersion() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 4) + x.xxx_hidden_KubernetesVersion = nil +} + +func (x *EnsureMachineSpec) ClearMaxPods() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 5) + x.xxx_hidden_MaxPods = 0 +} + +func (x *EnsureMachineSpec) ClearKubeletConfig() { + x.xxx_hidden_KubeletConfig = nil +} + +func (x *EnsureMachineSpec) ClearEnabled() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 10) + x.xxx_hidden_Enabled = false +} + +func (x *EnsureMachineSpec) ClearAzureCredential() { + x.xxx_hidden_AzureCredential = nil +} + +type EnsureMachineSpec_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Azure subscription ID of the target AKS cluster + SubscriptionId *string + // Azure resource group of the target AKS cluster + ResourceGroup *string + // Name of the target AKS cluster + ClusterName *string + // Name of the machine (hostname of the local machine) + MachineName *string + // Kubernetes version to join (e.g. "1.30" or "1.30.6") + KubernetesVersion *string + // Maximum number of pods that can run on the node + MaxPods *int32 + // Node labels + NodeLabels map[string]string + // Node taints (reconciled by AKS) + NodeTaints []string + // Node initialization taints (not reconciled by AKS, removable via kubectl) + NodeInitializationTaints []string + // Kubelet configuration + KubeletConfig *MachineKubeletConfig + // enabled controls whether the action performs Azure operations. + // Set to false when drift detection and remediation is disabled. + Enabled *bool + // azure_credential is used to authenticate against the Azure ARM API. + // If unset the action falls back to Azure CLI credential. + AzureCredential *AzureCredential +} + +func (b0 EnsureMachineSpec_builder) Build() *EnsureMachineSpec { + m0 := &EnsureMachineSpec{} + b, x := &b0, m0 + _, _ = b, x + if b.SubscriptionId != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 12) + x.xxx_hidden_SubscriptionId = b.SubscriptionId + } + if b.ResourceGroup != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 12) + x.xxx_hidden_ResourceGroup = b.ResourceGroup + } + if b.ClusterName != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 12) + x.xxx_hidden_ClusterName = b.ClusterName + } + if b.MachineName != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 12) + x.xxx_hidden_MachineName = b.MachineName + } + if b.KubernetesVersion != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 12) + x.xxx_hidden_KubernetesVersion = b.KubernetesVersion + } + if b.MaxPods != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 5, 12) + x.xxx_hidden_MaxPods = *b.MaxPods + } + x.xxx_hidden_NodeLabels = b.NodeLabels + x.xxx_hidden_NodeTaints = b.NodeTaints + x.xxx_hidden_NodeInitializationTaints = b.NodeInitializationTaints + x.xxx_hidden_KubeletConfig = b.KubeletConfig + if b.Enabled != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 10, 12) + x.xxx_hidden_Enabled = *b.Enabled + } + x.xxx_hidden_AzureCredential = b.AzureCredential + return m0 +} + +// MachineKubeletConfig holds the subset of kubelet settings sent to AKS on +// the PUT machine request. +type MachineKubeletConfig struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_ImageGcHighThreshold int32 `protobuf:"varint,1,opt,name=image_gc_high_threshold,json=imageGcHighThreshold"` + xxx_hidden_ImageGcLowThreshold int32 `protobuf:"varint,2,opt,name=image_gc_low_threshold,json=imageGcLowThreshold"` + XXX_raceDetectHookData protoimpl.RaceDetectHookData + XXX_presence [1]uint32 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MachineKubeletConfig) Reset() { + *x = MachineKubeletConfig{} + mi := &file_components_aksmachine_action_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MachineKubeletConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MachineKubeletConfig) ProtoMessage() {} + +func (x *MachineKubeletConfig) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *MachineKubeletConfig) GetImageGcHighThreshold() int32 { + if x != nil { + return x.xxx_hidden_ImageGcHighThreshold + } + return 0 +} + +func (x *MachineKubeletConfig) GetImageGcLowThreshold() int32 { + if x != nil { + return x.xxx_hidden_ImageGcLowThreshold + } + return 0 +} + +func (x *MachineKubeletConfig) SetImageGcHighThreshold(v int32) { + x.xxx_hidden_ImageGcHighThreshold = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 2) +} + +func (x *MachineKubeletConfig) SetImageGcLowThreshold(v int32) { + x.xxx_hidden_ImageGcLowThreshold = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 2) +} + +func (x *MachineKubeletConfig) HasImageGcHighThreshold() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 0) +} + +func (x *MachineKubeletConfig) HasImageGcLowThreshold() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 1) +} + +func (x *MachineKubeletConfig) ClearImageGcHighThreshold() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) + x.xxx_hidden_ImageGcHighThreshold = 0 +} + +func (x *MachineKubeletConfig) ClearImageGcLowThreshold() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 1) + x.xxx_hidden_ImageGcLowThreshold = 0 +} + +type MachineKubeletConfig_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + // Percent of disk usage above which image GC always runs (default 85) + ImageGcHighThreshold *int32 + // Percent of disk usage below which image GC never runs (default 80) + ImageGcLowThreshold *int32 +} + +func (b0 MachineKubeletConfig_builder) Build() *MachineKubeletConfig { + m0 := &MachineKubeletConfig{} + b, x := &b0, m0 + _, _ = b, x + if b.ImageGcHighThreshold != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 2) + x.xxx_hidden_ImageGcHighThreshold = *b.ImageGcHighThreshold + } + if b.ImageGcLowThreshold != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 2) + x.xxx_hidden_ImageGcLowThreshold = *b.ImageGcLowThreshold + } + return m0 +} + +type EnsureMachineStatus struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EnsureMachineStatus) Reset() { + *x = EnsureMachineStatus{} + mi := &file_components_aksmachine_action_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EnsureMachineStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EnsureMachineStatus) ProtoMessage() {} + +func (x *EnsureMachineStatus) ProtoReflect() protoreflect.Message { + mi := &file_components_aksmachine_action_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +type EnsureMachineStatus_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + +} + +func (b0 EnsureMachineStatus_builder) Build() *EnsureMachineStatus { + m0 := &EnsureMachineStatus{} + b, x := &b0, m0 + _, _ = b, x + return m0 +} + +var File_components_aksmachine_action_proto protoreflect.FileDescriptor + +const file_components_aksmachine_action_proto_rawDesc = "" + + "\n" + + "\"components/aksmachine/action.proto\x12\x1eaks.flex.components.aksmachine\x1a\x18components/api/api.proto\"\x80\x01\n" + + "\x1fAzureServicePrincipalCredential\x12\x1b\n" + + "\ttenant_id\x18\x01 \x01(\tR\btenantId\x12\x1b\n" + + "\tclient_id\x18\x02 \x01(\tR\bclientId\x12#\n" + + "\rclient_secret\x18\x03 \x01(\tR\fclientSecret\"1\n" + + "\x12AzureMSICredential\x12\x1b\n" + + "\tclient_id\x18\x01 \x01(\tR\bclientId\"\xf0\x01\n" + + "\x0fAzureCredential\x12n\n" + + "\x11service_principal\x18\x01 \x01(\v2?.aks.flex.components.aksmachine.AzureServicePrincipalCredentialH\x00R\x10servicePrincipal\x12_\n" + + "\x10managed_identity\x18\x02 \x01(\v22.aks.flex.components.aksmachine.AzureMSICredentialH\x00R\x0fmanagedIdentityB\f\n" + + "\n" + + "credential\"\xe2\x01\n" + + "\rEnsureMachine\x12=\n" + + "\bmetadata\x18\x01 \x01(\v2!.aks.flex.components.api.MetadataR\bmetadata\x12E\n" + + "\x04spec\x18\x02 \x01(\v21.aks.flex.components.aksmachine.EnsureMachineSpecR\x04spec\x12K\n" + + "\x06status\x18\x03 \x01(\v23.aks.flex.components.aksmachine.EnsureMachineStatusR\x06status\"\xc8\x05\n" + + "\x11EnsureMachineSpec\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12%\n" + + "\x0eresource_group\x18\x02 \x01(\tR\rresourceGroup\x12!\n" + + "\fcluster_name\x18\x03 \x01(\tR\vclusterName\x12!\n" + + "\fmachine_name\x18\x04 \x01(\tR\vmachineName\x12-\n" + + "\x12kubernetes_version\x18\x05 \x01(\tR\x11kubernetesVersion\x12\x19\n" + + "\bmax_pods\x18\x06 \x01(\x05R\amaxPods\x12b\n" + + "\vnode_labels\x18\a \x03(\v2A.aks.flex.components.aksmachine.EnsureMachineSpec.NodeLabelsEntryR\n" + + "nodeLabels\x12\x1f\n" + + "\vnode_taints\x18\b \x03(\tR\n" + + "nodeTaints\x12<\n" + + "\x1anode_initialization_taints\x18\t \x03(\tR\x18nodeInitializationTaints\x12[\n" + + "\x0ekubelet_config\x18\n" + + " \x01(\v24.aks.flex.components.aksmachine.MachineKubeletConfigR\rkubeletConfig\x12\x18\n" + + "\aenabled\x18\v \x01(\bR\aenabled\x12Z\n" + + "\x10azure_credential\x18\f \x01(\v2/.aks.flex.components.aksmachine.AzureCredentialR\x0fazureCredential\x1a=\n" + + "\x0fNodeLabelsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x82\x01\n" + + "\x14MachineKubeletConfig\x125\n" + + "\x17image_gc_high_threshold\x18\x01 \x01(\x05R\x14imageGcHighThreshold\x123\n" + + "\x16image_gc_low_threshold\x18\x02 \x01(\x05R\x13imageGcLowThreshold\"\x15\n" + + "\x13EnsureMachineStatusB4Z2github.com/Azure/AKSFlexNode/components/aksmachineb\beditionsp\xe9\a" + +var file_components_aksmachine_action_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_components_aksmachine_action_proto_goTypes = []any{ + (*AzureServicePrincipalCredential)(nil), // 0: aks.flex.components.aksmachine.AzureServicePrincipalCredential + (*AzureMSICredential)(nil), // 1: aks.flex.components.aksmachine.AzureMSICredential + (*AzureCredential)(nil), // 2: aks.flex.components.aksmachine.AzureCredential + (*EnsureMachine)(nil), // 3: aks.flex.components.aksmachine.EnsureMachine + (*EnsureMachineSpec)(nil), // 4: aks.flex.components.aksmachine.EnsureMachineSpec + (*MachineKubeletConfig)(nil), // 5: aks.flex.components.aksmachine.MachineKubeletConfig + (*EnsureMachineStatus)(nil), // 6: aks.flex.components.aksmachine.EnsureMachineStatus + nil, // 7: aks.flex.components.aksmachine.EnsureMachineSpec.NodeLabelsEntry + (*api.Metadata)(nil), // 8: aks.flex.components.api.Metadata +} +var file_components_aksmachine_action_proto_depIdxs = []int32{ + 0, // 0: aks.flex.components.aksmachine.AzureCredential.service_principal:type_name -> aks.flex.components.aksmachine.AzureServicePrincipalCredential + 1, // 1: aks.flex.components.aksmachine.AzureCredential.managed_identity:type_name -> aks.flex.components.aksmachine.AzureMSICredential + 8, // 2: aks.flex.components.aksmachine.EnsureMachine.metadata:type_name -> aks.flex.components.api.Metadata + 4, // 3: aks.flex.components.aksmachine.EnsureMachine.spec:type_name -> aks.flex.components.aksmachine.EnsureMachineSpec + 6, // 4: aks.flex.components.aksmachine.EnsureMachine.status:type_name -> aks.flex.components.aksmachine.EnsureMachineStatus + 7, // 5: aks.flex.components.aksmachine.EnsureMachineSpec.node_labels:type_name -> aks.flex.components.aksmachine.EnsureMachineSpec.NodeLabelsEntry + 5, // 6: aks.flex.components.aksmachine.EnsureMachineSpec.kubelet_config:type_name -> aks.flex.components.aksmachine.MachineKubeletConfig + 2, // 7: aks.flex.components.aksmachine.EnsureMachineSpec.azure_credential:type_name -> aks.flex.components.aksmachine.AzureCredential + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_components_aksmachine_action_proto_init() } +func file_components_aksmachine_action_proto_init() { + if File_components_aksmachine_action_proto != nil { + return + } + file_components_aksmachine_action_proto_msgTypes[2].OneofWrappers = []any{ + (*azureCredential_ServicePrincipal)(nil), + (*azureCredential_ManagedIdentity)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_components_aksmachine_action_proto_rawDesc), len(file_components_aksmachine_action_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_components_aksmachine_action_proto_goTypes, + DependencyIndexes: file_components_aksmachine_action_proto_depIdxs, + MessageInfos: file_components_aksmachine_action_proto_msgTypes, + }.Build() + File_components_aksmachine_action_proto = out.File + file_components_aksmachine_action_proto_goTypes = nil + file_components_aksmachine_action_proto_depIdxs = nil +} diff --git a/components/aksmachine/action.proto b/components/aksmachine/action.proto new file mode 100644 index 00000000..74c8cffa --- /dev/null +++ b/components/aksmachine/action.proto @@ -0,0 +1,80 @@ +edition = "2024"; + +package aks.flex.components.aksmachine; + +option go_package = "github.com/Azure/AKSFlexNode/components/aksmachine"; + +import "components/api/api.proto"; + +message AzureServicePrincipalCredential { + string tenant_id = 1; + string client_id = 2; + string client_secret = 3; // sensitive: must be redacted before logging +} + +message AzureMSICredential { + // Client ID of the managed identity; empty means system-assigned MI. + string client_id = 1; +} + +// AzureCredential carries the ARM authentication credential for the action. +// If neither field is set the action falls back to Azure CLI credential. +message AzureCredential { + oneof credential { + AzureServicePrincipalCredential service_principal = 1; + AzureMSICredential managed_identity = 2; + } +} + +message EnsureMachine { + api.Metadata metadata = 1; + + EnsureMachineSpec spec = 2; + + EnsureMachineStatus status = 3; +} + +message EnsureMachineSpec { + // Azure subscription ID of the target AKS cluster + string subscription_id = 1; + // Azure resource group of the target AKS cluster + string resource_group = 2; + // Name of the target AKS cluster + string cluster_name = 3; + // Name of the machine (hostname of the local machine) + string machine_name = 4; + + // Kubernetes version to join (e.g. "1.30" or "1.30.6") + string kubernetes_version = 5; + // Maximum number of pods that can run on the node + int32 max_pods = 6; + // Node labels + map node_labels = 7; + // Node taints (reconciled by AKS) + repeated string node_taints = 8; + // Node initialization taints (not reconciled by AKS, removable via kubectl) + repeated string node_initialization_taints = 9; + + // Kubelet configuration + MachineKubeletConfig kubelet_config = 10; + + // enabled controls whether the action performs Azure operations. + // Set to false when drift detection and remediation is disabled. + bool enabled = 11; + + // azure_credential is used to authenticate against the Azure ARM API. + // If unset the action falls back to Azure CLI credential. + AzureCredential azure_credential = 12; +} + +// MachineKubeletConfig holds the subset of kubelet settings sent to AKS on +// the PUT machine request. +message MachineKubeletConfig { + // Percent of disk usage above which image GC always runs (default 85) + int32 image_gc_high_threshold = 1; + // Percent of disk usage below which image GC never runs (default 80) + int32 image_gc_low_threshold = 2; +} + +message EnsureMachineStatus { +} diff --git a/components/aksmachine/redact.go b/components/aksmachine/redact.go new file mode 100644 index 00000000..788a084c --- /dev/null +++ b/components/aksmachine/redact.go @@ -0,0 +1,9 @@ +package aksmachine + +// Redact removes sensitive fields from the action. +func (x *EnsureMachine) Redact() { + // Redact the service principal client secret carried in the spec. + if sp := x.GetSpec().GetAzureCredential().GetServicePrincipal(); sp != nil { + sp.SetClientSecret("") + } +} diff --git a/components/aksmachine/v20260301/ensure_machine.go b/components/aksmachine/v20260301/ensure_machine.go new file mode 100644 index 00000000..cc0c063b --- /dev/null +++ b/components/aksmachine/v20260301/ensure_machine.go @@ -0,0 +1,284 @@ +package v20260301 + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/Azure/AKSFlexNode/components/aksmachine" + "github.com/Azure/AKSFlexNode/components/services/actions" + "github.com/Azure/AKSFlexNode/pkg/utils/utilpb" +) + +const ( + aksFlexNodePoolName = "aksflexnodes" + // flexNodeTagKey is the tag that identifies this machine as an AKS flex node. + flexNodeTagKey = "aks.azure.com/flex-node" + + // TODO: remove before merging — redirects ARM calls to a local test server. + armEndpointOverride = "http://localhost:8080" +) + +type ensureMachineAction struct { + logger *logrus.Logger +} + +func newEnsureMachineAction() (actions.Server, error) { + return &ensureMachineAction{ + logger: logrus.New(), + }, nil +} + +var _ actions.Server = (*ensureMachineAction)(nil) + +// ApplyAction runs two sequential sub-steps: +// 1. Ensure the "aksflexnodes" agent pool exists with mode "Machines". +// 2. Ensure the local machine exists in that pool tagged as a flex node. +// +// If drift detection and remediation is not enabled in the agent config, the +// action returns immediately without performing any Azure operations. +func (a *ensureMachineAction) ApplyAction( + ctx context.Context, + req *actions.ApplyActionRequest, +) (*actions.ApplyActionResponse, error) { + action, err := utilpb.AnyTo[*aksmachine.EnsureMachine](req.GetItem()) + if err != nil { + return nil, err + } + + spec := action.GetSpec() + + // Skip all Azure operations when drift detection/remediation is disabled. + if !spec.GetEnabled() { + a.logger.Info("EnsureMachine: drift detection and remediation is disabled, skipping") + item, err := anypb.New(action) + if err != nil { + return nil, err + } + return actions.ApplyActionResponse_builder{Item: item}.Build(), nil + } + + subID := spec.GetSubscriptionId() + rg := spec.GetResourceGroup() + clusterName := spec.GetClusterName() + machineName := spec.GetMachineName() + k8sVersion := spec.GetKubernetesVersion() + + if subID == "" || rg == "" || clusterName == "" || machineName == "" || k8sVersion == "" { + return nil, status.Errorf(codes.InvalidArgument, + "EnsureMachine: spec fields incomplete: subscriptionId=%q resourceGroup=%q clusterName=%q machineName=%q kubernetesVersion=%q", + subID, rg, clusterName, machineName, k8sVersion) + } + + cred, err := credentialFromSpec(spec.GetAzureCredential()) + if err != nil { + return nil, status.Errorf(codes.Internal, "EnsureMachine: resolve credential: %v", err) + } + + armOpts := buildARMClientOptions(armEndpointOverride) + + // Step 1: ensure the agent pool exists with mode "Machines". + if err := a.ensureAgentPool(ctx, cred, armOpts, subID, rg, clusterName); err != nil { + return nil, status.Errorf(codes.Internal, "EnsureMachine: ensure agent pool: %v", err) + } + + // Step 2: ensure this machine is registered in the pool as a flex node. + if err := a.ensureMachine(ctx, cred, armOpts, spec); err != nil { + return nil, status.Errorf(codes.Internal, "EnsureMachine: ensure machine: %v", err) + } + + item, err := anypb.New(action) + if err != nil { + return nil, err + } + return actions.ApplyActionResponse_builder{Item: item}.Build(), nil +} + +// ensureAgentPool calls CreateOrUpdate on the "aksflexnodes" agent pool with +// mode "Machines" and waits for the long-running operation to complete. +func (a *ensureMachineAction) ensureAgentPool(ctx context.Context, cred azcore.TokenCredential, armOpts *arm.ClientOptions, subID, rg, clusterName string) error { + client, err := armcontainerservice.NewAgentPoolsClient(subID, cred, armOpts) + if err != nil { + return fmt.Errorf("create agent pools client: %w", err) + } + + mode := armcontainerservice.AgentPoolMode("Machines") + params := armcontainerservice.AgentPool{ + Properties: &armcontainerservice.ManagedClusterAgentPoolProfileProperties{ + Mode: &mode, + }, + } + + a.logger.Infof("Ensuring agent pool %q (mode=Machines) on cluster %s/%s", aksFlexNodePoolName, rg, clusterName) + + // Check whether the agent pool already exists; if so, skip the PUT. + _, err = client.Get(ctx, rg, clusterName, aksFlexNodePoolName, nil) + if err == nil { + a.logger.Infof("Agent pool %q already exists on cluster %s/%s, skipping", aksFlexNodePoolName, rg, clusterName) + return nil + } + if !isNotFound(err) { + return fmt.Errorf("get agent pool %q: %w", aksFlexNodePoolName, err) + } + + poller, err := client.BeginCreateOrUpdate(ctx, rg, clusterName, aksFlexNodePoolName, params, nil) + if err != nil { + return fmt.Errorf("begin create or update agent pool %q: %w", aksFlexNodePoolName, err) + } + + if _, err = poller.PollUntilDone(ctx, nil); err != nil { + return fmt.Errorf("wait for agent pool %q: %w", aksFlexNodePoolName, err) + } + + a.logger.Infof("Agent pool %q ensured on cluster %s/%s", aksFlexNodePoolName, rg, clusterName) + return nil +} + +// ensureMachine registers this machine in the "aksflexnodes" agent pool as a +// flex node. It first checks whether the machine resource already exists; if so +// it skips the PUT to avoid overwriting properties managed by the AKS control plane. +func (a *ensureMachineAction) ensureMachine(ctx context.Context, cred azcore.TokenCredential, armOpts *arm.ClientOptions, spec *aksmachine.EnsureMachineSpec) error { + subID := spec.GetSubscriptionId() + rg := spec.GetResourceGroup() + clusterName := spec.GetClusterName() + machineName := spec.GetMachineName() + + client, err := armcontainerservice.NewMachinesClient(subID, cred, armOpts) + if err != nil { + return fmt.Errorf("create machines client: %w", err) + } + + // Check whether the machine is already registered; if so, skip the PUT. + _, err = client.Get(ctx, rg, clusterName, aksFlexNodePoolName, machineName, nil) + if err == nil { + a.logger.Infof("Machine %q already exists in pool %q on cluster %s/%s, skipping", machineName, aksFlexNodePoolName, rg, clusterName) + return nil + } + if !isNotFound(err) { + return fmt.Errorf("get machine %q: %w", machineName, err) + } + + params := armcontainerservice.Machine{ + Properties: &armcontainerservice.MachineProperties{ + Tags: map[string]*string{ + flexNodeTagKey: to.Ptr("true"), + }, + Kubernetes: buildK8sProfile(spec), + }, + } + + poller, err := client.BeginCreateOrUpdate(ctx, rg, clusterName, aksFlexNodePoolName, machineName, params, nil) + if err != nil { + return fmt.Errorf("begin create or update machine %q: %w", machineName, err) + } + + if _, err = poller.PollUntilDone(ctx, nil); err != nil { + return fmt.Errorf("wait for machine %q: %w", machineName, err) + } + + a.logger.Infof("Machine %q ensured in pool %q on cluster %s/%s", machineName, aksFlexNodePoolName, rg, clusterName) + return nil +} + +// buildK8sProfile constructs a MachineKubernetesProfile from the spec using +// the explicit allow-list of fields permitted for flex nodes: +// - OrchestratorVersion, MaxPods, NodeLabels, NodeTaints, +// NodeInitializationTaints, KubeletConfig (image GC thresholds). +func buildK8sProfile(spec *aksmachine.EnsureMachineSpec) *armcontainerservice.MachineKubernetesProfile { + p := &armcontainerservice.MachineKubernetesProfile{} + + if v := spec.GetKubernetesVersion(); v != "" { + p.OrchestratorVersion = to.Ptr(v) + } + if mp := spec.GetMaxPods(); mp > 0 { + p.MaxPods = to.Ptr(mp) + } + if labels := spec.GetNodeLabels(); len(labels) > 0 { + p.NodeLabels = make(map[string]*string, len(labels)) + for k, v := range labels { + p.NodeLabels[k] = to.Ptr(v) + } + } + if taints := spec.GetNodeTaints(); len(taints) > 0 { + p.NodeTaints = make([]*string, len(taints)) + for i, t := range taints { + p.NodeTaints[i] = to.Ptr(t) + } + } + if initTaints := spec.GetNodeInitializationTaints(); len(initTaints) > 0 { + p.NodeInitializationTaints = make([]*string, len(initTaints)) + for i, t := range initTaints { + p.NodeInitializationTaints[i] = to.Ptr(t) + } + } + if kc := spec.GetKubeletConfig(); kc != nil { + p.KubeletConfig = &armcontainerservice.KubeletConfig{} + if h := kc.GetImageGcHighThreshold(); h > 0 { + p.KubeletConfig.ImageGcHighThreshold = to.Ptr(h) + } + if l := kc.GetImageGcLowThreshold(); l > 0 { + p.KubeletConfig.ImageGcLowThreshold = to.Ptr(l) + } + } + + return p +} + +// credentialFromSpec resolves an Azure ARM credential from the proto AzureCredential field. +// Falls back to Azure CLI credential when the field is absent or empty. +func credentialFromSpec(cred *aksmachine.AzureCredential) (azcore.TokenCredential, error) { + if sp := cred.GetServicePrincipal(); sp != nil { + return azidentity.NewClientSecretCredential(sp.GetTenantId(), sp.GetClientId(), sp.GetClientSecret(), nil) + } + if mi := cred.GetManagedIdentity(); mi != nil { + opts := &azidentity.ManagedIdentityCredentialOptions{} + if id := mi.GetClientId(); id != "" { + opts.ID = azidentity.ClientID(id) + } + return azidentity.NewManagedIdentityCredential(opts) + } + // return azidentity.NewAzureCLICredential(nil) + return nil, nil +} + +// buildARMClientOptions returns ARM client options that redirect all calls to +// endpointOverride when non-empty (e.g. "http://localhost:8080" for local testing). +// Returns nil when the override is empty, which causes the SDK to use the default +// public Azure Resource Manager endpoint. +func buildARMClientOptions(endpointOverride string) *arm.ClientOptions { + if endpointOverride == "" { + return nil + } + return &arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Cloud: cloud.Configuration{ + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: { + Endpoint: endpointOverride, + // No audience needed for local servers that don't validate tokens. + Audience: endpointOverride, + }, + }, + }, + InsecureAllowCredentialWithHTTP: true, + }, + } +} + +// isNotFound reports whether the Azure SDK error is an HTTP 404. +func isNotFound(err error) bool { + var respErr *azcore.ResponseError + return errors.As(err, &respErr) && respErr.StatusCode == http.StatusNotFound +} diff --git a/components/aksmachine/v20260301/ensure_machine_test.go b/components/aksmachine/v20260301/ensure_machine_test.go new file mode 100644 index 00000000..8e005a96 --- /dev/null +++ b/components/aksmachine/v20260301/ensure_machine_test.go @@ -0,0 +1,223 @@ +package v20260301 + +import ( + "errors" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + + "github.com/Azure/AKSFlexNode/components/aksmachine" +) + +// ptrT is a test helper that returns a pointer to v. +func ptrT[T any](v T) *T { return &v } + +func TestBuildARMClientOptions(t *testing.T) { + t.Parallel() + + t.Run("empty override returns nil", func(t *testing.T) { + t.Parallel() + if got := buildARMClientOptions(""); got != nil { + t.Fatalf("buildARMClientOptions(\"\") = %v, want nil", got) + } + }) + + t.Run("non-empty override sets endpoint", func(t *testing.T) { + t.Parallel() + const endpoint = "http://localhost:8080" + opts := buildARMClientOptions(endpoint) + if opts == nil { + t.Fatalf("buildARMClientOptions(%q) = nil, want non-nil", endpoint) + } + if len(opts.Cloud.Services) == 0 { + t.Fatalf("no cloud services configured") + } + for _, v := range opts.Cloud.Services { + if v.Endpoint != endpoint { + t.Fatalf("endpoint=%q, want %q", v.Endpoint, endpoint) + } + } + }) +} + +func TestIsNotFound(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + err error + want bool + }{ + {name: "404 ResponseError", err: &azcore.ResponseError{StatusCode: http.StatusNotFound}, want: true}, + {name: "500 ResponseError", err: &azcore.ResponseError{StatusCode: http.StatusInternalServerError}, want: false}, + {name: "non-ResponseError", err: errors.New("something went wrong"), want: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := isNotFound(tc.err); got != tc.want { + t.Fatalf("isNotFound(%v) = %v, want %v", tc.err, got, tc.want) + } + }) + } +} + +func TestCredentialFromSpec_Nil(t *testing.T) { + t.Parallel() + cred, err := credentialFromSpec(nil) + if err != nil { + t.Fatalf("err=%v, want nil", err) + } + if cred != nil { + t.Fatalf("cred=%v, want nil", cred) + } +} + +func TestCredentialFromSpec_EmptyCredential(t *testing.T) { + t.Parallel() + empty := aksmachine.AzureCredential_builder{}.Build() + cred, err := credentialFromSpec(empty) + if err != nil { + t.Fatalf("err=%v, want nil", err) + } + if cred != nil { + t.Fatalf("cred=%v, want nil", cred) + } +} + +func TestBuildK8sProfile_EmptySpec(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{}.Build() + p := buildK8sProfile(spec) + if p == nil { + t.Fatalf("profile is nil") + } + if p.OrchestratorVersion != nil { + t.Fatalf("OrchestratorVersion should be nil for empty spec") + } + if p.MaxPods != nil { + t.Fatalf("MaxPods should be nil for empty spec") + } + if len(p.NodeLabels) != 0 { + t.Fatalf("NodeLabels should be empty for empty spec") + } + if len(p.NodeTaints) != 0 { + t.Fatalf("NodeTaints should be empty for empty spec") + } + if p.KubeletConfig != nil { + t.Fatalf("KubeletConfig should be nil for empty spec") + } +} + +func TestBuildK8sProfile_KubernetesVersion(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{KubernetesVersion: ptrT("1.30.6")}.Build() + p := buildK8sProfile(spec) + if p.OrchestratorVersion == nil || *p.OrchestratorVersion != "1.30.6" { + t.Fatalf("OrchestratorVersion=%v, want 1.30.6", p.OrchestratorVersion) + } +} + +func TestBuildK8sProfile_MaxPods(t *testing.T) { + t.Parallel() + t.Run("positive value propagated", func(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{MaxPods: ptrT(int32(110))}.Build() + p := buildK8sProfile(spec) + if p.MaxPods == nil || *p.MaxPods != 110 { + t.Fatalf("MaxPods=%v, want 110", p.MaxPods) + } + }) + t.Run("zero value not propagated", func(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{MaxPods: ptrT(int32(0))}.Build() + p := buildK8sProfile(spec) + if p.MaxPods != nil { + t.Fatalf("MaxPods=%v, want nil for zero value", p.MaxPods) + } + }) +} + +func TestBuildK8sProfile_NodeLabels(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{ + NodeLabels: map[string]string{"env": "prod", "team": "infra"}, + }.Build() + p := buildK8sProfile(spec) + if len(p.NodeLabels) != 2 { + t.Fatalf("NodeLabels len=%d, want 2", len(p.NodeLabels)) + } + if v := p.NodeLabels["env"]; v == nil || *v != "prod" { + t.Fatalf("NodeLabels[env]=%v, want prod", v) + } +} + +func TestBuildK8sProfile_NodeTaints(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{ + NodeTaints: []string{"key=value:NoSchedule"}, + }.Build() + p := buildK8sProfile(spec) + if len(p.NodeTaints) != 1 { + t.Fatalf("NodeTaints len=%d, want 1", len(p.NodeTaints)) + } + if p.NodeTaints[0] == nil || *p.NodeTaints[0] != "key=value:NoSchedule" { + t.Fatalf("NodeTaints[0]=%v, want key=value:NoSchedule", p.NodeTaints[0]) + } +} + +func TestBuildK8sProfile_NodeInitializationTaints(t *testing.T) { + t.Parallel() + spec := aksmachine.EnsureMachineSpec_builder{ + NodeInitializationTaints: []string{"init-key=init-val:NoExecute"}, + }.Build() + p := buildK8sProfile(spec) + if len(p.NodeInitializationTaints) != 1 { + t.Fatalf("NodeInitializationTaints len=%d, want 1", len(p.NodeInitializationTaints)) + } + if p.NodeInitializationTaints[0] == nil || *p.NodeInitializationTaints[0] != "init-key=init-val:NoExecute" { + t.Fatalf("NodeInitializationTaints[0]=%v, want init-key=init-val:NoExecute", p.NodeInitializationTaints[0]) + } +} + +func TestBuildK8sProfile_KubeletConfig(t *testing.T) { + t.Parallel() + t.Run("positive thresholds propagated", func(t *testing.T) { + t.Parallel() + kc := aksmachine.MachineKubeletConfig_builder{ + ImageGcHighThreshold: ptrT(int32(85)), + ImageGcLowThreshold: ptrT(int32(70)), + }.Build() + spec := aksmachine.EnsureMachineSpec_builder{KubeletConfig: kc}.Build() + p := buildK8sProfile(spec) + if p.KubeletConfig == nil { + t.Fatalf("KubeletConfig is nil") + } + if p.KubeletConfig.ImageGcHighThreshold == nil || *p.KubeletConfig.ImageGcHighThreshold != 85 { + t.Fatalf("ImageGcHighThreshold=%v, want 85", p.KubeletConfig.ImageGcHighThreshold) + } + if p.KubeletConfig.ImageGcLowThreshold == nil || *p.KubeletConfig.ImageGcLowThreshold != 70 { + t.Fatalf("ImageGcLowThreshold=%v, want 70", p.KubeletConfig.ImageGcLowThreshold) + } + }) + t.Run("zero thresholds not propagated", func(t *testing.T) { + t.Parallel() + kc := aksmachine.MachineKubeletConfig_builder{ + ImageGcHighThreshold: ptrT(int32(0)), + ImageGcLowThreshold: ptrT(int32(0)), + }.Build() + spec := aksmachine.EnsureMachineSpec_builder{KubeletConfig: kc}.Build() + p := buildK8sProfile(spec) + if p.KubeletConfig == nil { + t.Fatalf("KubeletConfig is nil") + } + if p.KubeletConfig.ImageGcHighThreshold != nil { + t.Fatalf("ImageGcHighThreshold=%v, want nil", p.KubeletConfig.ImageGcHighThreshold) + } + if p.KubeletConfig.ImageGcLowThreshold != nil { + t.Fatalf("ImageGcLowThreshold=%v, want nil", p.KubeletConfig.ImageGcLowThreshold) + } + }) +} diff --git a/components/aksmachine/v20260301/exports.go b/components/aksmachine/v20260301/exports.go new file mode 100644 index 00000000..95531684 --- /dev/null +++ b/components/aksmachine/v20260301/exports.go @@ -0,0 +1,13 @@ +package v20260301 + +import ( + "github.com/Azure/AKSFlexNode/components/aksmachine" + "github.com/Azure/AKSFlexNode/components/services/actions" +) + +func init() { + actions.MustRegister( + newEnsureMachineAction, + &aksmachine.EnsureMachine{}, + ) +} diff --git a/components/arc/action.pb.go b/components/arc/action.pb.go index a27be861..171990ae 100644 --- a/components/arc/action.pb.go +++ b/components/arc/action.pb.go @@ -148,6 +148,7 @@ type InstallArcSpec struct { xxx_hidden_MachineName *string `protobuf:"bytes,5,opt,name=machine_name,json=machineName"` xxx_hidden_Tags map[string]string `protobuf:"bytes,6,rep,name=tags" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` xxx_hidden_AksClusterName *string `protobuf:"bytes,7,opt,name=aks_cluster_name,json=aksClusterName"` + xxx_hidden_Enabled bool `protobuf:"varint,8,opt,name=enabled"` XXX_raceDetectHookData protoimpl.RaceDetectHookData XXX_presence [1]uint32 unknownFields protoimpl.UnknownFields @@ -246,29 +247,36 @@ func (x *InstallArcSpec) GetAksClusterName() string { return "" } +func (x *InstallArcSpec) GetEnabled() bool { + if x != nil { + return x.xxx_hidden_Enabled + } + return false +} + func (x *InstallArcSpec) SetSubscriptionId(v string) { x.xxx_hidden_SubscriptionId = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 0, 8) } func (x *InstallArcSpec) SetTenantId(v string) { x.xxx_hidden_TenantId = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 1, 8) } func (x *InstallArcSpec) SetResourceGroup(v string) { x.xxx_hidden_ResourceGroup = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 2, 8) } func (x *InstallArcSpec) SetLocation(v string) { x.xxx_hidden_Location = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 3, 8) } func (x *InstallArcSpec) SetMachineName(v string) { x.xxx_hidden_MachineName = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 4, 8) } func (x *InstallArcSpec) SetTags(v map[string]string) { @@ -277,7 +285,12 @@ func (x *InstallArcSpec) SetTags(v map[string]string) { func (x *InstallArcSpec) SetAksClusterName(v string) { x.xxx_hidden_AksClusterName = &v - protoimpl.X.SetPresent(&(x.XXX_presence[0]), 6, 7) + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 6, 8) +} + +func (x *InstallArcSpec) SetEnabled(v bool) { + x.xxx_hidden_Enabled = v + protoimpl.X.SetPresent(&(x.XXX_presence[0]), 7, 8) } func (x *InstallArcSpec) HasSubscriptionId() bool { @@ -322,6 +335,13 @@ func (x *InstallArcSpec) HasAksClusterName() bool { return protoimpl.X.Present(&(x.XXX_presence[0]), 6) } +func (x *InstallArcSpec) HasEnabled() bool { + if x == nil { + return false + } + return protoimpl.X.Present(&(x.XXX_presence[0]), 7) +} + func (x *InstallArcSpec) ClearSubscriptionId() { protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 0) x.xxx_hidden_SubscriptionId = nil @@ -352,6 +372,11 @@ func (x *InstallArcSpec) ClearAksClusterName() { x.xxx_hidden_AksClusterName = nil } +func (x *InstallArcSpec) ClearEnabled() { + protoimpl.X.ClearPresent(&(x.XXX_presence[0]), 7) + x.xxx_hidden_Enabled = false +} + type InstallArcSpec_builder struct { _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. @@ -369,6 +394,9 @@ type InstallArcSpec_builder struct { Tags map[string]string // Target AKS cluster name for RBAC assignments AksClusterName *string + // enabled controls whether Arc installation is performed. + // Set to false when azure.arc.enabled is false in the agent config. + Enabled *bool } func (b0 InstallArcSpec_builder) Build() *InstallArcSpec { @@ -376,30 +404,34 @@ func (b0 InstallArcSpec_builder) Build() *InstallArcSpec { b, x := &b0, m0 _, _ = b, x if b.SubscriptionId != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 0, 8) x.xxx_hidden_SubscriptionId = b.SubscriptionId } if b.TenantId != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 1, 8) x.xxx_hidden_TenantId = b.TenantId } if b.ResourceGroup != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 2, 8) x.xxx_hidden_ResourceGroup = b.ResourceGroup } if b.Location != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 3, 8) x.xxx_hidden_Location = b.Location } if b.MachineName != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 4, 8) x.xxx_hidden_MachineName = b.MachineName } x.xxx_hidden_Tags = b.Tags if b.AksClusterName != nil { - protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 6, 7) + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 6, 8) x.xxx_hidden_AksClusterName = b.AksClusterName } + if b.Enabled != nil { + protoimpl.X.SetPresentNonAtomic(&(x.XXX_presence[0]), 7, 8) + x.xxx_hidden_Enabled = *b.Enabled + } return m0 } @@ -624,7 +656,7 @@ const file_components_arc_action_proto_rawDesc = "" + "InstallArc\x12=\n" + "\bmetadata\x18\x01 \x01(\v2!.aks.flex.components.api.MetadataR\bmetadata\x12;\n" + "\x04spec\x18\x02 \x01(\v2'.aks.flex.components.arc.InstallArcSpecR\x04spec\x12A\n" + - "\x06status\x18\x03 \x01(\v2).aks.flex.components.arc.InstallArcStatusR\x06status\"\xe6\x02\n" + + "\x06status\x18\x03 \x01(\v2).aks.flex.components.arc.InstallArcStatusR\x06status\"\x80\x03\n" + "\x0eInstallArcSpec\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12\x1b\n" + "\ttenant_id\x18\x02 \x01(\tR\btenantId\x12%\n" + @@ -632,7 +664,8 @@ const file_components_arc_action_proto_rawDesc = "" + "\blocation\x18\x04 \x01(\tR\blocation\x12!\n" + "\fmachine_name\x18\x05 \x01(\tR\vmachineName\x12E\n" + "\x04tags\x18\x06 \x03(\v21.aks.flex.components.arc.InstallArcSpec.TagsEntryR\x04tags\x12(\n" + - "\x10aks_cluster_name\x18\a \x01(\tR\x0eaksClusterName\x1a7\n" + + "\x10aks_cluster_name\x18\a \x01(\tR\x0eaksClusterName\x12\x18\n" + + "\aenabled\x18\b \x01(\bR\aenabled\x1a7\n" + "\tTagsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xba\x01\n" + diff --git a/components/arc/action.proto b/components/arc/action.proto index dea3a94c..e57f494b 100644 --- a/components/arc/action.proto +++ b/components/arc/action.proto @@ -29,6 +29,9 @@ message InstallArcSpec { map tags = 6; // Target AKS cluster name for RBAC assignments string aks_cluster_name = 7; + // enabled controls whether Arc installation is performed. + // Set to false when azure.arc.enabled is false in the agent config. + bool enabled = 8; } message InstallArcStatus { diff --git a/components/arc/v20260301/arc_helpers.go b/components/arc/v20260301/arc_helpers.go index 3705d180..8ff6741f 100644 --- a/components/arc/v20260301/arc_helpers.go +++ b/components/arc/v20260301/arc_helpers.go @@ -10,7 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute" "github.com/Azure/AKSFlexNode/pkg/utils" diff --git a/components/arc/v20260301/arc_registration.go b/components/arc/v20260301/arc_registration.go index f393d144..63b92ae4 100644 --- a/components/arc/v20260301/arc_registration.go +++ b/components/arc/v20260301/arc_registration.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute" "github.com/Azure/go-autorest/autorest/to" diff --git a/components/arc/v20260301/install_arc.go b/components/arc/v20260301/install_arc.go index c404c2cf..c9ddff61 100644 --- a/components/arc/v20260301/install_arc.go +++ b/components/arc/v20260301/install_arc.go @@ -9,7 +9,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute" "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/anypb" @@ -47,6 +47,16 @@ func (a *installArcAction) ApplyAction( return nil, err } + // Skip installation when Arc is disabled in the agent config. + if !config.GetSpec().GetEnabled() { + a.logger.Info("InstallArc: Arc is disabled, skipping") + item, err := anypb.New(config) + if err != nil { + return nil, err + } + return actions.ApplyActionResponse_builder{Item: item}.Build(), nil + } + // Apply defaults and validate the configuration spec, err := api.DefaultAndValidate(config.GetSpec()) if err != nil { diff --git a/components/exports.go b/components/exports.go index 04d48b8f..1eead27d 100644 --- a/components/exports.go +++ b/components/exports.go @@ -1,6 +1,7 @@ package components import ( + _ "github.com/Azure/AKSFlexNode/components/aksmachine/v20260301" _ "github.com/Azure/AKSFlexNode/components/arc/v20260301" _ "github.com/Azure/AKSFlexNode/components/cni/v20260301" _ "github.com/Azure/AKSFlexNode/components/cri/v20260301" diff --git a/go.mod b/go.mod index a880a5f8..139fd849 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 + 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/hybridcompute/armhybridcompute v1.2.0 github.com/Azure/go-autorest/autorest/to v0.4.1 github.com/Azure/kubelogin v0.2.15 diff --git a/go.sum b/go.sum index 8f968584..8c7b0fa0 100644 --- a/go.sum +++ b/go.sum @@ -8,14 +8,14 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0 h1:5n7dPVqsWfVKw+ZiEKSd3Kzu7gwBkbEBkeXb8rgaE9Q= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5 v5.0.0/go.mod h1:HcZY0PHPo/7d75p99lB6lK0qYOP4vLRJUBpiehYXtLQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8 v8.3.0-beta.2 h1:uTV/toeMMa4Uia3It7dRli2ePtZizYpl125iuhiH6TU= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8 v8.3.0-beta.2/go.mod h1:2lUQLQklNSBVEZfdITZzWJ84eRduPBJlM9XstZW9AWg= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute v1.2.0 h1:7UuAn4ljE+H3GQ7qts3c7oAaMRvge68EgyckoNP/1Ro= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute v1.2.0/go.mod h1:F2eDq/BGK2LOEoDtoHbBOphaPqcjT0K/Y5Am8vf7+0w= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1 h1:7CBQ+Ei8SP2c6ydQTGCCrS35bDxgTMfoP2miAwK++OU= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.1.1/go.mod h1:c/wcGeGx5FUPbM/JltUYHZcKmigwyVLJlDq+4HdtXaw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= diff --git a/pkg/bootstrapper/bootstrapper.go b/pkg/bootstrapper/bootstrapper.go index 470f9c39..f489fa57 100644 --- a/pkg/bootstrapper/bootstrapper.go +++ b/pkg/bootstrapper/bootstrapper.go @@ -44,6 +44,12 @@ func (b *Bootstrapper) Bootstrap(ctx context.Context) (*ExecutionResult, error) // and startNPD which require these fields. newClusterConfigEnricher(b.logger), + // TODO: enable this step after RP machine api change is live + // // Ensure the "aksflexnodes" agent pool and register this host as a + // // machine within it. The action skips all Azure operations if drift + // // detection and remediation is disabled in the agent config. + // ensureMachine.Executor("ensure-machine", b.componentsAPIConn, b.config), + // TODO: run these steps in parallel downloadCNIBinaries.Executor("download-cni-binaries", b.componentsAPIConn, b.config), downloadCRIBinaries.Executor("download-cri-binaries", b.componentsAPIConn, b.config), diff --git a/pkg/bootstrapper/cluster_config_enricher.go b/pkg/bootstrapper/cluster_config_enricher.go index 0e7c23f2..13748ee3 100644 --- a/pkg/bootstrapper/cluster_config_enricher.go +++ b/pkg/bootstrapper/cluster_config_enricher.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" @@ -32,10 +32,11 @@ func (e *clusterConfigEnricher) GetName() string { return "enrich-cluster-config" } -// IsCompleted returns true if ServerURL is already populated (either from config -// file or from a previous execution of this step). +// IsCompleted returns true if both ServerURL and CACertData are already +// populated (either from the config file or a previous execution of this step), +// making the ARM fetch unnecessary. func (e *clusterConfigEnricher) IsCompleted(_ context.Context) bool { - return e.cfg.Node.Kubelet.ServerURL != "" + return e.cfg.Node.Kubelet.ServerURL != "" && e.cfg.Node.Kubelet.CACertData != "" } // Execute fetches cluster admin credentials from the AKS management plane and diff --git a/pkg/bootstrapper/components.go b/pkg/bootstrapper/components.go index 777e2878..fe600698 100644 --- a/pkg/bootstrapper/components.go +++ b/pkg/bootstrapper/components.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/proto" + "github.com/Azure/AKSFlexNode/components/aksmachine" "github.com/Azure/AKSFlexNode/components/api" "github.com/Azure/AKSFlexNode/components/arc" "github.com/Azure/AKSFlexNode/components/cni" @@ -324,6 +325,62 @@ var stopKubeletService resolveActionFunc[*kubelet.StopKubeletService] = func( }.Build(), nil } +var ensureMachine resolveActionFunc[*aksmachine.EnsureMachine] = func( + name string, + cfg *config.Config, +) (*aksmachine.EnsureMachine, error) { + machineName := cfg.GetArcMachineName() + if machineName == "" { + return nil, fmt.Errorf("machine name is empty: set azure.arc.machineName in config or ensure hostname is resolvable") + } + + kubeletCfg := aksmachine.MachineKubeletConfig_builder{ + ImageGcHighThreshold: ptr(int32(cfg.Node.Kubelet.ImageGCHighThreshold)), + ImageGcLowThreshold: ptr(int32(cfg.Node.Kubelet.ImageGCLowThreshold)), + }.Build() + + azureCred := buildAzureCredential(cfg) + + spec := aksmachine.EnsureMachineSpec_builder{ + SubscriptionId: ptr(cfg.GetTargetClusterSubscriptionID()), + ResourceGroup: ptr(cfg.GetTargetClusterResourceGroup()), + ClusterName: ptr(cfg.GetTargetClusterName()), + MachineName: ptr(machineName), + KubernetesVersion: ptr(cfg.Kubernetes.Version), + MaxPods: ptr(int32(cfg.Node.MaxPods)), + NodeLabels: maps.Clone(cfg.Node.Labels), + KubeletConfig: kubeletCfg, + Enabled: ptr(cfg.IsDriftDetectionAndRemediationEnabled()), + AzureCredential: azureCred, + }.Build() + + return aksmachine.EnsureMachine_builder{ + Metadata: componentAction(name), + Spec: spec, + }.Build(), nil +} + +// buildAzureCredential constructs the proto AzureCredential from the agent config. +// Returns nil when CLI credential fallback is appropriate (no SP or MI configured). +func buildAzureCredential(cfg *config.Config) *aksmachine.AzureCredential { + if cfg.IsSPConfigured() { + sp := aksmachine.AzureServicePrincipalCredential_builder{ + TenantId: ptr(cfg.Azure.ServicePrincipal.TenantID), + ClientId: ptr(cfg.Azure.ServicePrincipal.ClientID), + ClientSecret: ptr(cfg.Azure.ServicePrincipal.ClientSecret), + }.Build() + return aksmachine.AzureCredential_builder{ServicePrincipal: sp}.Build() + } + if cfg.IsMIConfigured() { + mi := aksmachine.AzureMSICredential_builder{} + if cfg.Azure.ManagedIdentity != nil && cfg.Azure.ManagedIdentity.ClientID != "" { + mi.ClientId = ptr(cfg.Azure.ManagedIdentity.ClientID) + } + return aksmachine.AzureCredential_builder{ManagedIdentity: mi.Build()}.Build() + } + return nil +} + var installArc resolveActionFunc[*arc.InstallArc] = func( name string, cfg *config.Config, @@ -336,6 +393,7 @@ var installArc resolveActionFunc[*arc.InstallArc] = func( MachineName: ptrWithDefault(cfg.GetArcMachineName(), ""), Tags: cfg.GetArcTags(), AksClusterName: ptrWithDefault(cfg.GetTargetClusterName(), ""), + Enabled: ptr(cfg.IsARCEnabled()), }.Build() return arc.InstallArc_builder{ diff --git a/pkg/components/arc/arc_base.go b/pkg/components/arc/arc_base.go index e602c201..1177eddd 100644 --- a/pkg/components/arc/arc_base.go +++ b/pkg/components/arc/arc_base.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/hybridcompute/armhybridcompute" "github.com/Azure/go-autorest/autorest/to" "github.com/sirupsen/logrus" diff --git a/pkg/kube/client.go b/pkg/kube/client.go index bf76f7fb..f4a2e541 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -6,7 +6,7 @@ import ( "fmt" "sync" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" diff --git a/pkg/spec/collector.go b/pkg/spec/collector.go index 2370d896..1cd58521 100644 --- a/pkg/spec/collector.go +++ b/pkg/spec/collector.go @@ -8,7 +8,7 @@ import ( "path/filepath" "time" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/sirupsen/logrus" "github.com/Azure/AKSFlexNode/pkg/auth" diff --git a/pkg/spec/collector_test.go b/pkg/spec/collector_test.go index 974ed2aa..09e7aa70 100644 --- a/pkg/spec/collector_test.go +++ b/pkg/spec/collector_test.go @@ -7,7 +7,7 @@ import ( "path/filepath" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v8" "github.com/sirupsen/logrus" "github.com/Azure/AKSFlexNode/pkg/config" From 40eb2ac85320d31c335cc259ce6a68802fb8c9b0 Mon Sep 17 00:00:00 2001 From: wenxuanW Date: Wed, 25 Mar 2026 11:33:24 -0700 Subject: [PATCH 2/7] Small fixes --- components/aksmachine/v20260301/ensure_machine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/aksmachine/v20260301/ensure_machine.go b/components/aksmachine/v20260301/ensure_machine.go index cc0c063b..6dada462 100644 --- a/components/aksmachine/v20260301/ensure_machine.go +++ b/components/aksmachine/v20260301/ensure_machine.go @@ -25,10 +25,10 @@ import ( const ( aksFlexNodePoolName = "aksflexnodes" // flexNodeTagKey is the tag that identifies this machine as an AKS flex node. - flexNodeTagKey = "aks.azure.com/flex-node" + flexNodeTagKey = "aks-flex-node" - // TODO: remove before merging — redirects ARM calls to a local test server. - armEndpointOverride = "http://localhost:8080" + // ARM calls to a local test server. It is for testing only and should not be set in production. + armEndpointOverride = "" ) type ensureMachineAction struct { From 8a6ba54e891bd98b3073e847a3c54849df37eebd Mon Sep 17 00:00:00 2001 From: wenxuanW Date: Wed, 25 Mar 2026 11:38:42 -0700 Subject: [PATCH 3/7] Add comments --- components/aksmachine/v20260301/ensure_machine.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/aksmachine/v20260301/ensure_machine.go b/components/aksmachine/v20260301/ensure_machine.go index 6dada462..2e13c6c6 100644 --- a/components/aksmachine/v20260301/ensure_machine.go +++ b/components/aksmachine/v20260301/ensure_machine.go @@ -184,6 +184,9 @@ func (a *ensureMachineAction) ensureMachine(ctx context.Context, cred azcore.Tok return fmt.Errorf("begin create or update machine %q: %w", machineName, err) } + // if the ARM server returns a synchronous 2xx response + // with no Azure-AsyncOperation / Operation-Location / Location header, the SDK treats it as synchronously + // complete and PollUntilDone returns right away with the response body — no looping occurs. if _, err = poller.PollUntilDone(ctx, nil); err != nil { return fmt.Errorf("wait for machine %q: %w", machineName, err) } From 5dbe13d597aed4468e349bcde51e4af4192b0828 Mon Sep 17 00:00:00 2001 From: wenxuanW Date: Wed, 25 Mar 2026 12:11:31 -0700 Subject: [PATCH 4/7] Fix golint --- pkg/bootstrapper/components.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/bootstrapper/components.go b/pkg/bootstrapper/components.go index fe600698..5f4ee223 100644 --- a/pkg/bootstrapper/components.go +++ b/pkg/bootstrapper/components.go @@ -325,6 +325,10 @@ var stopKubeletService resolveActionFunc[*kubelet.StopKubeletService] = func( }.Build(), nil } +// ensureMachine is not yet wired into the bootstrap steps; it will be enabled +// once the AKS RP Machine API change is live. See the TODO in bootstrapper.go. +// +//nolint:unused var ensureMachine resolveActionFunc[*aksmachine.EnsureMachine] = func( name string, cfg *config.Config, @@ -362,6 +366,9 @@ var ensureMachine resolveActionFunc[*aksmachine.EnsureMachine] = func( // buildAzureCredential constructs the proto AzureCredential from the agent config. // Returns nil when CLI credential fallback is appropriate (no SP or MI configured). +// Unused until ensureMachine is wired into the bootstrap steps. +// +//nolint:unused func buildAzureCredential(cfg *config.Config) *aksmachine.AzureCredential { if cfg.IsSPConfigured() { sp := aksmachine.AzureServicePrincipalCredential_builder{ From 48f3f5b56574c3ef59f5a9dcb1018da18a998e2b Mon Sep 17 00:00:00 2001 From: wenxuanW Date: Wed, 25 Mar 2026 12:19:52 -0700 Subject: [PATCH 5/7] Fix staticcheck --- pkg/bootstrapper/components.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/bootstrapper/components.go b/pkg/bootstrapper/components.go index 5f4ee223..6c6f98a4 100644 --- a/pkg/bootstrapper/components.go +++ b/pkg/bootstrapper/components.go @@ -327,8 +327,13 @@ var stopKubeletService resolveActionFunc[*kubelet.StopKubeletService] = func( // ensureMachine is not yet wired into the bootstrap steps; it will be enabled // once the AKS RP Machine API change is live. See the TODO in bootstrapper.go. -// -//nolint:unused +// The blank-identifier refs below prevent unused-symbol warnings from both +// golangci-lint and staticcheck until the step is activated. +var ( + _ = ensureMachine + _ = buildAzureCredential +) + var ensureMachine resolveActionFunc[*aksmachine.EnsureMachine] = func( name string, cfg *config.Config, @@ -367,8 +372,6 @@ var ensureMachine resolveActionFunc[*aksmachine.EnsureMachine] = func( // buildAzureCredential constructs the proto AzureCredential from the agent config. // Returns nil when CLI credential fallback is appropriate (no SP or MI configured). // Unused until ensureMachine is wired into the bootstrap steps. -// -//nolint:unused func buildAzureCredential(cfg *config.Config) *aksmachine.AzureCredential { if cfg.IsSPConfigured() { sp := aksmachine.AzureServicePrincipalCredential_builder{ From 09635603012745e0694c3e8c2e068163c3536490 Mon Sep 17 00:00:00 2001 From: Thalia Wang <58485997+wenxuan0923@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:20:56 -0700 Subject: [PATCH 6/7] Update components/aksmachine/v20260301/ensure_machine.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../aksmachine/v20260301/ensure_machine.go | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/components/aksmachine/v20260301/ensure_machine.go b/components/aksmachine/v20260301/ensure_machine.go index 2e13c6c6..87fd2531 100644 --- a/components/aksmachine/v20260301/ensure_machine.go +++ b/components/aksmachine/v20260301/ensure_machine.go @@ -242,18 +242,21 @@ func buildK8sProfile(spec *aksmachine.EnsureMachineSpec) *armcontainerservice.Ma // credentialFromSpec resolves an Azure ARM credential from the proto AzureCredential field. // Falls back to Azure CLI credential when the field is absent or empty. func credentialFromSpec(cred *aksmachine.AzureCredential) (azcore.TokenCredential, error) { - if sp := cred.GetServicePrincipal(); sp != nil { - return azidentity.NewClientSecretCredential(sp.GetTenantId(), sp.GetClientId(), sp.GetClientSecret(), nil) - } - if mi := cred.GetManagedIdentity(); mi != nil { - opts := &azidentity.ManagedIdentityCredentialOptions{} - if id := mi.GetClientId(); id != "" { - opts.ID = azidentity.ClientID(id) + // Prefer explicitly configured credentials when present. + if cred != nil { + if sp := cred.GetServicePrincipal(); sp != nil { + return azidentity.NewClientSecretCredential(sp.GetTenantId(), sp.GetClientId(), sp.GetClientSecret(), nil) + } + if mi := cred.GetManagedIdentity(); mi != nil { + opts := &azidentity.ManagedIdentityCredentialOptions{} + if id := mi.GetClientId(); id != "" { + opts.ID = azidentity.ClientID(id) + } + return azidentity.NewManagedIdentityCredential(opts) } - return azidentity.NewManagedIdentityCredential(opts) } - // return azidentity.NewAzureCLICredential(nil) - return nil, nil + // Fall back to Azure CLI credential when no explicit credential is configured. + return azidentity.NewAzureCLICredential(nil) } // buildARMClientOptions returns ARM client options that redirect all calls to From 9a309aee935edb49ea8c6895c9e03ee3b5172bb8 Mon Sep 17 00:00:00 2001 From: Thalia Wang <58485997+wenxuan0923@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:21:14 -0700 Subject: [PATCH 7/7] Update components/aksmachine/v20260301/ensure_machine_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../aksmachine/v20260301/ensure_machine_test.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/components/aksmachine/v20260301/ensure_machine_test.go b/components/aksmachine/v20260301/ensure_machine_test.go index 8e005a96..ccbc0f8a 100644 --- a/components/aksmachine/v20260301/ensure_machine_test.go +++ b/components/aksmachine/v20260301/ensure_machine_test.go @@ -67,11 +67,8 @@ func TestIsNotFound(t *testing.T) { func TestCredentialFromSpec_Nil(t *testing.T) { t.Parallel() cred, err := credentialFromSpec(nil) - if err != nil { - t.Fatalf("err=%v, want nil", err) - } - if cred != nil { - t.Fatalf("cred=%v, want nil", cred) + if err == nil && cred == nil { + t.Fatalf("expected non-nil credential or error for nil spec, got cred=nil, err=nil") } } @@ -79,11 +76,8 @@ func TestCredentialFromSpec_EmptyCredential(t *testing.T) { t.Parallel() empty := aksmachine.AzureCredential_builder{}.Build() cred, err := credentialFromSpec(empty) - if err != nil { - t.Fatalf("err=%v, want nil", err) - } - if cred != nil { - t.Fatalf("cred=%v, want nil", cred) + if err == nil && cred == nil { + t.Fatalf("expected non-nil credential or error for empty credential spec, got cred=nil, err=nil") } }