Repository navigation
Expand file tree
/
Copy pathazure.go
More file actions
268 lines (244 loc) · 9.52 KB
/
Copy pathazure.go
File metadata and controls
268 lines (244 loc) · 9.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package main
import (
"context"
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"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/compute/armcompute/v8"
)
// azureVMResource identifies the Azure compute resource backing a Kubernetes
// node, parsed from the node's spec.ProviderID. AKS nodes are either:
//
// - standalone VMs:
// azure:///subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm>
// - Virtual Machine Scale Set instances (the AKS default for node pools):
// azure:///subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachineScaleSets/<vmss>/virtualMachines/<instance>
type azureVMResource struct {
SubscriptionID string
ResourceGroup string
// Provider is "virtualMachines" or "virtualMachineScaleSets".
Provider string
// VMName is set for standalone VMs (Provider == "virtualMachines").
VMName string
// VMSSName is set for VMSS instances (Provider == "virtualMachineScaleSets").
VMSSName string
// InstanceID is the VMSS instance index (Provider == "virtualMachineScaleSets").
InstanceID string
}
func (r azureVMResource) isVMSS() bool { return r.Provider == "virtualMachineScaleSets" }
// azureComputeClient is the minimum interface we need for reading and writing
// Azure VM tags. The production implementation wraps the armcompute
// VirtualMachinesClient and VirtualMachineScaleSetVMsClient; tests use a fake.
type azureComputeClient interface {
// GetInstanceTags returns the current tags on the Azure VM (or VMSS VM
// instance) identified by res.
GetInstanceTags(ctx context.Context, res azureVMResource) (map[string]string, error)
// SetInstanceTags replaces the resource's entire tag set with desiredTags.
// Callers are responsible for preserving any unmanaged tags by including
// them in desiredTags.
SetInstanceTags(ctx context.Context, res azureVMResource, desiredTags map[string]string) error
}
// azureComputeClientImpl is the production azureComputeClient. It holds the
// Azure credential resolved at setup and lazily builds the per-subscription
// armcompute clients, because the subscription ID is not known until a node's
// provider ID is parsed in Reconcile.
type azureComputeClientImpl struct {
cred azcore.TokenCredential
}
var _ azureComputeClient = (*azureComputeClientImpl)(nil)
// newAzureComputeClient builds an azureComputeClient using ambient Azure
// credentials. This mirrors how the AWS provider uses
// awsconfig.LoadDefaultConfig(ctx) and the GCP provider uses gce.NewService(ctx),
// both of which pick up workload-identity / ambient creds from the environment.
//
// On AKS with workload identity federation enabled, azidentity's
// NewDefaultAzureCredential resolves the WorkloadIdentityCredential from the
// AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_FEDERATED_TOKEN_FILE env vars
// projected onto the pod by the Azure workload-identity mutating admission
// webhook. No client secret is required.
func newAzureComputeClient(ctx context.Context) (azureComputeClient, error) {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, fmt.Errorf("unable to load Azure credentials: %v", err)
}
return &azureComputeClientImpl{cred: cred}, nil
}
func (c *azureComputeClientImpl) vmClient(subscriptionID string) (*armcompute.VirtualMachinesClient, error) {
return armcompute.NewVirtualMachinesClient(subscriptionID, c.cred, nil)
}
func (c *azureComputeClientImpl) vmssVMClient(subscriptionID string) (*armcompute.VirtualMachineScaleSetVMsClient, error) {
return armcompute.NewVirtualMachineScaleSetVMsClient(subscriptionID, c.cred, nil)
}
// GetInstanceTags fetches the current tags on the VM (or VMSS VM instance).
func (c *azureComputeClientImpl) GetInstanceTags(ctx context.Context, res azureVMResource) (map[string]string, error) {
if res.isVMSS() {
client, err := c.vmssVMClient(res.SubscriptionID)
if err != nil {
return nil, err
}
resp, err := client.Get(ctx, res.ResourceGroup, res.VMSSName, res.InstanceID, nil)
if err != nil {
return nil, err
}
return derefAzureTags(resp.Tags), nil
}
client, err := c.vmClient(res.SubscriptionID)
if err != nil {
return nil, err
}
resp, err := client.Get(ctx, res.ResourceGroup, res.VMName, nil)
if err != nil {
return nil, err
}
return derefAzureTags(resp.Tags), nil
}
// SetInstanceTags replaces the resource's tags with desiredTags. It GETs the
// resource first (to preserve required fields like Location and Properties),
// overwrites Tags, and PUTs/PATCHes it back, polling the long-running
// operation to completion. Unmanaged tags must already be preserved by the
// caller in desiredTags.
func (c *azureComputeClientImpl) SetInstanceTags(ctx context.Context, res azureVMResource, desiredTags map[string]string) error {
tagMap := toAzureTags(desiredTags)
if res.isVMSS() {
client, err := c.vmssVMClient(res.SubscriptionID)
if err != nil {
return err
}
resp, err := client.Get(ctx, res.ResourceGroup, res.VMSSName, res.InstanceID, nil)
if err != nil {
return err
}
vm := resp.VirtualMachineScaleSetVM
vm.Tags = tagMap
poller, err := client.BeginUpdate(ctx, res.ResourceGroup, res.VMSSName, res.InstanceID, vm, nil)
if err != nil {
return err
}
_, err = poller.PollUntilDone(ctx, nil)
return err
}
client, err := c.vmClient(res.SubscriptionID)
if err != nil {
return err
}
resp, err := client.Get(ctx, res.ResourceGroup, res.VMName, nil)
if err != nil {
return err
}
vm := resp.VirtualMachine
vm.Tags = tagMap
poller, err := client.BeginCreateOrUpdate(ctx, res.ResourceGroup, res.VMName, vm, nil)
if err != nil {
return err
}
_, err = poller.PollUntilDone(ctx, nil)
return err
}
// derefAzureTags converts the SDK's map[string]*string into a plain
// map[string]string, dropping nil-valued entries.
func derefAzureTags(tags map[string]*string) map[string]string {
out := make(map[string]string, len(tags))
for k, v := range tags {
if v == nil {
continue
}
out[k] = *v
}
return out
}
// toAzureTags converts a plain map[string]string into the SDK's
// map[string]*string.
func toAzureTags(tags map[string]string) map[string]*string {
out := make(map[string]*string, len(tags))
for k, v := range tags {
kv := v
out[k] = to.Ptr(kv)
}
return out
}
// parseAzureProviderID parses an Azure provider ID emitted by the Azure cloud
// provider integration for a Kubernetes node. Both standalone VM and VMSS
// instance forms are accepted:
//
// azure:///subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm>
// azure:///subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachineScaleSets/<vmss>/virtualMachines/<instance>
//
// The leading "azure://" prefix and any immediately-following slash (the
// cloud provider emits three slashes) are both trimmed.
func parseAzureProviderID(providerID string) (azureVMResource, error) {
const prefix = "azure://"
if !strings.HasPrefix(providerID, prefix) {
return azureVMResource{}, fmt.Errorf("providerID missing %q prefix, this might not be an Azure node? %q", prefix, providerID)
}
trimmed := strings.TrimPrefix(providerID, prefix)
// The Azure cloud provider emits a third slash ("azure:///..."); trim any
// leading slash so the path starts at "subscriptions/...".
trimmed = strings.TrimLeft(trimmed, "/")
parts := strings.Split(trimmed, "/")
// Expected: subscriptions <sub> resourceGroups <rg> providers Microsoft.Compute <provider> <name> [virtualMachines <instance>]
const minLen = 8
if len(parts) < minLen {
return azureVMResource{}, fmt.Errorf("invalid Azure provider ID format: %q", providerID)
}
if parts[0] != "subscriptions" || parts[2] != "resourceGroups" || parts[4] != "providers" || parts[5] != "Microsoft.Compute" {
return azureVMResource{}, fmt.Errorf("invalid Azure provider ID format: %q", providerID)
}
res := azureVMResource{
SubscriptionID: parts[1],
ResourceGroup: parts[3],
Provider: parts[6],
}
switch res.Provider {
case "virtualMachines":
res.VMName = parts[7]
case "virtualMachineScaleSets":
// .../virtualMachineScaleSets/<vmss>/virtualMachines/<instance>
if len(parts) < 10 || parts[8] != "virtualMachines" {
return azureVMResource{}, fmt.Errorf("invalid Azure VMSS provider ID format: %q", providerID)
}
res.VMSSName = parts[7]
res.InstanceID = parts[9]
default:
return azureVMResource{}, fmt.Errorf("unsupported Azure compute provider type %q in provider ID: %q", res.Provider, providerID)
}
return res, nil
}
// sanitizeKeyForAzure sanitizes a Kubernetes label/annotation key to fit Azure's
// tag key constraints. Azure tag keys disallow <, >, %, &, \, ?, / and have a
// maximum length of 512 characters. Kubernetes label keys frequently contain
// "/" (e.g. "kubernetes.io/hostname", "topology.kubernetes.io/region",
// "node-role.kubernetes.io/control-plane"), which Azure would otherwise reject.
func sanitizeKeyForAzure(key string) string {
key = strings.NewReplacer(
"/", "_",
"<", "_",
">", "_",
"%", "_",
"&", "_",
"\\", "_",
"?", "_",
).Replace(key)
if len(key) > 512 {
key = key[:512]
}
return key
}
// sanitizeValueForAzure sanitizes a Kubernetes label/annotation value to fit
// Azure's tag value constraints. Azure tag values disallow <, >, %, &, \, ?
// and have a maximum length of 256 characters.
func sanitizeValueForAzure(value string) string {
value = strings.NewReplacer(
"<", "_",
">", "_",
"%", "_",
"&", "_",
"\\", "_",
"?", "_",
).Replace(value)
if len(value) > 256 {
value = value[:256]
}
return value
}