Skip to content

Commit e5a8201

Browse files
committed
Add support for user-assigned managed identities in AzureMachine
- new allowed value for VMIdentity - new field in AzureMachine - validation to ensure list of identities is not empty if type is user assigned - user assgined identity template flavor
1 parent 1cf1ec4 commit e5a8201

21 files changed

+572
-38
lines changed

api/v1alpha2/azuremachine_conversion.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ func (src *AzureMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint
3939
if restored.Spec.Identity != "" {
4040
dst.Spec.Identity = restored.Spec.Identity
4141
}
42+
if len(restored.Spec.UserAssignedIdentities) > 0 {
43+
dst.Spec.UserAssignedIdentities = restored.Spec.UserAssignedIdentities
44+
}
4245

4346
dst.Spec.FailureDomain = restored.Spec.FailureDomain
4447

api/v1alpha2/azuremachinetemplate_conversion.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ func (src *AzureMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nol
3737
if restored.Spec.Template.Spec.Identity != "" {
3838
dst.Spec.Template.Spec.Identity = restored.Spec.Template.Spec.Identity
3939
}
40-
40+
if len(restored.Spec.Template.Spec.UserAssignedIdentities) > 0 {
41+
dst.Spec.Template.Spec.UserAssignedIdentities = restored.Spec.Template.Spec.UserAssignedIdentities
42+
}
4143
dst.Spec.Template.Spec.FailureDomain = restored.Spec.Template.Spec.FailureDomain
4244

4345
return nil

api/v1alpha2/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha3/azuremachine_default_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,22 @@ func createMachineWithSSHPublicKey(t *testing.T, sshPublicKey string) *AzureMach
5858
},
5959
}
6060
}
61+
62+
func createMachineWithUserAssignedIdentities(t *testing.T, identitiesList []UserAssignedIdentity) *AzureMachine {
63+
return &AzureMachine{
64+
Spec: AzureMachineSpec{
65+
SSHPublicKey: generateSSHPublicKey(),
66+
Image: &Image{
67+
SharedGallery: &AzureSharedGalleryImage{
68+
SubscriptionID: "SUB123",
69+
ResourceGroup: "RG123",
70+
Name: "NAME123",
71+
Gallery: "GALLERY1",
72+
Version: "1.0.0",
73+
},
74+
},
75+
Identity: VMIdentityUserAssigned,
76+
UserAssignedIdentities: identitiesList,
77+
},
78+
}
79+
}

api/v1alpha3/azuremachine_types.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,20 @@ type AzureMachineSpec struct {
5151
Image *Image `json:"image,omitempty"`
5252

5353
// Identity is the type of identity used for the virtual machine.
54-
// The type 'SystemAssigned' is an implicitly created identity
55-
// The generated identity will be assigned a Subscription contributor role
54+
// The type 'SystemAssigned' is an implicitly created identity.
55+
// The generated identity will be assigned a Subscription contributor role.
56+
// The type 'UserAssigned' is a standalone Azure resource provided by the user
57+
// and assigned to the VM
5658
// +kubebuilder:default=None
5759
// +optional
5860
Identity VMIdentity `json:"identity,omitempty"`
5961

62+
// UserAssignedIdentities is a list of standalone Azure identities provided by the user
63+
// The lifecycle of a user-assigned identity is managed separately from the lifecycle of
64+
// the AzureMachine.
65+
// +optional
66+
UserAssignedIdentities []UserAssignedIdentity `json:"userAssignedIdentities,omitempty"`
67+
6068
OSDisk OSDisk `json:"osDisk"`
6169

6270
Location string `json:"location"`

api/v1alpha3/azuremachine_validation.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,14 @@ func ValidateSSHKey(sshKey string, fldPath *field.Path) field.ErrorList {
4040

4141
return allErrs
4242
}
43+
44+
// ValidateUserAssignedIdentity validates the user-assigned identities list
45+
func ValidateUserAssignedIdentity(identityType VMIdentity, userAssignedIdenteties []UserAssignedIdentity, fldPath *field.Path) field.ErrorList {
46+
allErrs := field.ErrorList{}
47+
48+
if identityType == VMIdentityUserAssigned && len(userAssignedIdenteties) == 0 {
49+
allErrs = append(allErrs, field.Required(fldPath, "must be specified for the 'UserAssigned' identity type"))
50+
}
51+
52+
return allErrs
53+
}

api/v1alpha3/azuremachine_webhook.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,33 +42,43 @@ var _ webhook.Validator = &AzureMachine{}
4242
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
4343
func (m *AzureMachine) ValidateCreate() error {
4444
machinelog.Info("validate create", "name", m.Name)
45+
var allErrs field.ErrorList
4546

4647
if errs := ValidateImage(m.Spec.Image, field.NewPath("image")); len(errs) > 0 {
47-
return apierrors.NewInvalid(
48-
GroupVersion.WithKind("AzureMachine").GroupKind(),
49-
m.Name, errs)
48+
allErrs = append(allErrs, errs...)
5049
}
5150

5251
if errs := ValidateSSHKey(m.Spec.SSHPublicKey, field.NewPath("sshPublicKey")); len(errs) > 0 {
53-
return apierrors.NewInvalid(
54-
GroupVersion.WithKind("AzureMachine").GroupKind(),
55-
m.Name, errs)
52+
allErrs = append(allErrs, errs...)
5653
}
5754

58-
return nil
55+
if errs := ValidateUserAssignedIdentity(m.Spec.Identity, m.Spec.UserAssignedIdentities, field.NewPath("userAssignedIdentities")); len(errs) > 0 {
56+
allErrs = append(allErrs, errs...)
57+
}
58+
59+
if len(allErrs) == 0 {
60+
return nil
61+
}
62+
return apierrors.NewInvalid(GroupVersion.WithKind("AzureMachine").GroupKind(), m.Name, allErrs)
5963
}
6064

6165
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
6266
func (m *AzureMachine) ValidateUpdate(old runtime.Object) error {
6367
machinelog.Info("validate update", "name", m.Name)
68+
var allErrs field.ErrorList
6469

6570
if errs := ValidateSSHKey(m.Spec.SSHPublicKey, field.NewPath("sshPublicKey")); len(errs) > 0 {
66-
return apierrors.NewInvalid(
67-
GroupVersion.WithKind("AzureMachine").GroupKind(),
68-
m.Name, errs)
71+
allErrs = append(allErrs, errs...)
6972
}
7073

71-
return nil
74+
if errs := ValidateUserAssignedIdentity(m.Spec.Identity, m.Spec.UserAssignedIdentities, field.NewPath("userAssignedIdentities")); len(errs) > 0 {
75+
allErrs = append(allErrs, errs...)
76+
}
77+
78+
if len(allErrs) == 0 {
79+
return nil
80+
}
81+
return apierrors.NewInvalid(GroupVersion.WithKind("AzureMachine").GroupKind(), m.Name, allErrs)
7282
}
7383

7484
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type

api/v1alpha3/azuremachine_webhook_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ func TestAzureMachine_ValidateCreate(t *testing.T) {
7777
machine: createMachineWithSSHPublicKey(t, "invalid ssh key"),
7878
wantErr: true,
7979
},
80+
{
81+
name: "azuremachine with list of user-assigned identities",
82+
machine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{{ProviderID: "azure:////123"}, {ProviderID: "azure:////456"}}),
83+
wantErr: false,
84+
},
85+
{
86+
name: "azuremachine with empty list of user-assigned identities",
87+
machine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{}),
88+
wantErr: true,
89+
},
8090
}
8191
for _, tc := range tests {
8292
t.Run(tc.name, func(t *testing.T) {
@@ -117,6 +127,18 @@ func TestAzureMachine_ValidateUpdate(t *testing.T) {
117127
machine: createMachineWithSSHPublicKey(t, "invalid ssh key"),
118128
wantErr: true,
119129
},
130+
{
131+
name: "azuremachine with user assigned identities",
132+
oldMachine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{{ProviderID: "azure:////123"}}),
133+
machine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{{ProviderID: "azure:////123"}, {ProviderID: "azure:////456"}}),
134+
wantErr: false,
135+
},
136+
{
137+
name: "azuremachine with empty user assigned identities",
138+
oldMachine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{{ProviderID: "azure:////123"}}),
139+
machine: createMachineWithUserAssignedIdentities(t, []UserAssignedIdentity{}),
140+
wantErr: true,
141+
},
120142
}
121143
for _, tc := range tests {
122144
t.Run(tc.name, func(t *testing.T) {

api/v1alpha3/types.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,26 @@ type AvailabilityZone struct {
278278
}
279279

280280
// VMIdentity defines the identity of the virtual machine, if configured.
281-
// +kubebuilder:validation:Enum=None;SystemAssigned
281+
// +kubebuilder:validation:Enum=None;SystemAssigned;UserAssigned
282282
type VMIdentity string
283283

284284
const (
285285
// VMIdentityNone ...
286286
VMIdentityNone VMIdentity = "None"
287287
// VMIdentitySystemAssigned ...
288288
VMIdentitySystemAssigned VMIdentity = "SystemAssigned"
289+
// VMIdentityUserAssigned ...
290+
VMIdentityUserAssigned VMIdentity = "UserAssigned"
289291
)
290292

293+
// UserAssignedIdentity defines the user-assigned identities provided
294+
// by the user to be assigned to Azure resources.
295+
type UserAssignedIdentity struct {
296+
// ProviderID is the identification ID of the user-assigned Identity, the format of an identity is:
297+
// 'azure:////subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{identityName}'
298+
ProviderID string `json:"providerID"`
299+
}
300+
291301
// OSDisk defines the operating system disk for a VM.
292302
type OSDisk struct {
293303
OSType string `json:"osType"`

api/v1alpha3/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)