diff --git a/azure/const.go b/azure/const.go index 9c4ac179ab2..17c523da0a1 100644 --- a/azure/const.go +++ b/azure/const.go @@ -28,4 +28,7 @@ const ( // See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ // for annotation formatting rules. RGTagsLastAppliedAnnotation = "sigs.k8s.io/cluster-api-provider-azure-last-applied-tags-rg" + + // Cloudinit represents cloudinit instance initializer. + Cloudinit InstanceInitializer = "Cloudinit" ) diff --git a/azure/scope/machine.go b/azure/scope/machine.go index ebffd647d98..376636befaf 100644 --- a/azure/scope/machine.go +++ b/azure/scope/machine.go @@ -170,11 +170,15 @@ func (m *MachineScope) VMSpec() azure.ResourceSpecGetter { SecurityProfile: m.AzureMachine.Spec.SecurityProfile, AdditionalTags: m.AdditionalTags(), ProviderID: m.ProviderID(), + SubscriptionID: m.SubscriptionID(), + SecureBootstrapEnabled: m.SecureBootstrapEnabled(), + Initializer: azure.Cloudinit, } if m.cache != nil { spec.SKU = m.cache.VMSKU spec.Image = m.cache.VMImage spec.BootstrapData = m.cache.BootstrapData + spec.BootstrapDataCompressed = m.cache.BootstrapDataCompressed } return spec } diff --git a/azure/services/virtualmachines/spec.go b/azure/services/virtualmachines/spec.go index 5cf4caca8f9..224a0aa8ae0 100644 --- a/azure/services/virtualmachines/spec.go +++ b/azure/services/virtualmachines/spec.go @@ -26,33 +26,38 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" "sigs.k8s.io/cluster-api-provider-azure/azure/converters" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/cloudinit" "sigs.k8s.io/cluster-api-provider-azure/azure/services/resourceskus" "sigs.k8s.io/cluster-api-provider-azure/util/generators" ) // VMSpec defines the specification for a Virtual Machine. type VMSpec struct { - Name string - ResourceGroup string - Location string - ClusterName string - Role string - NICIDs []string - SSHKeyData string - Size string - AvailabilitySetID string - Zone string - Identity infrav1.VMIdentity - OSDisk infrav1.OSDisk - DataDisks []infrav1.DataDisk - UserAssignedIdentities []infrav1.UserAssignedIdentity - SpotVMOptions *infrav1.SpotVMOptions - SecurityProfile *infrav1.SecurityProfile - AdditionalTags infrav1.Tags - SKU resourceskus.SKU - Image *infrav1.Image - BootstrapData string - ProviderID string + Name string + ResourceGroup string + Location string + ClusterName string + Role string + NICIDs []string + SSHKeyData string + Size string + AvailabilitySetID string + Zone string + Identity infrav1.VMIdentity + OSDisk infrav1.OSDisk + DataDisks []infrav1.DataDisk + UserAssignedIdentities []infrav1.UserAssignedIdentity + SpotVMOptions *infrav1.SpotVMOptions + SecurityProfile *infrav1.SecurityProfile + AdditionalTags infrav1.Tags + SKU resourceskus.SKU + Image *infrav1.Image + BootstrapData string + BootstrapDataCompressed []byte + ProviderID string + SubscriptionID string + SecureBootstrapEnabled bool + Initializer azure.InstanceInitializer } // ResourceName returns the name of the virtual machine. @@ -240,10 +245,19 @@ func (s *VMSpec) generateOSProfile() (*compute.OSProfile, error) { return nil, errors.Wrap(err, "failed to decode ssh public key") } + resolver, err := s.getUserdataResolver() + if err != nil { + return nil, err + } + userData, err := resolver.ResolveUserData() + if err != nil { + return nil, err + } + osProfile := &compute.OSProfile{ ComputerName: to.StringPtr(s.Name), AdminUsername: to.StringPtr(azure.DefaultUserName), - CustomData: to.StringPtr(s.BootstrapData), + CustomData: to.StringPtr(userData), } switch s.OSDisk.OSType { @@ -276,6 +290,31 @@ func (s *VMSpec) generateOSProfile() (*compute.OSProfile, error) { return osProfile, nil } +func (s *VMSpec) getUserdataResolver() (azure.UserDataResolver, error) { + if !s.SecureBootstrapEnabled { + return cloudinit.SimpleUserDataResolver{ + Data: s.BootstrapData, + }, nil + } + + if s.Identity != infrav1.VMIdentityUserAssigned || len(s.UserAssignedIdentities) == 0 { + return nil, errors.Errorf("secure bootstrap cannot be used with identity of type %q, "+ + "only UserAssigned identity is allowed", s.Identity) + } + + switch s.Initializer { + case azure.Cloudinit: + return cloudinit.SecureUserDataResolver{ + Data: s.BootstrapDataCompressed, + Identity: s.UserAssignedIdentities[0].ProviderID, + ClusterName: s.ClusterName, + MachineName: s.Name, + }, nil + default: + return nil, errors.Errorf("unsupported cloud instance initializer %s", s.Initializer) + } +} + func (s *VMSpec) generateSecurityProfile() (*compute.SecurityProfile, error) { if s.SecurityProfile == nil { return nil, nil diff --git a/azure/services/virtualmachines/spec_test.go b/azure/services/virtualmachines/spec_test.go index 8a47a7a4e80..c9361f64426 100644 --- a/azure/services/virtualmachines/spec_test.go +++ b/azure/services/virtualmachines/spec_test.go @@ -659,6 +659,53 @@ func TestParameters(t *testing.T) { }, expectedError: "reconcile error that cannot be recovered occurred: vm size Standard_D2v3 does not support ultra disks in location test-location. select a different vm size or disable ultra disks. Object will not be requeued", }, + { + name: "can create a vm with secure bootstrap enabled", + spec: &VMSpec{ + Name: "my-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + Identity: infrav1.VMIdentityUserAssigned, + UserAssignedIdentities: []infrav1.UserAssignedIdentity{{ProviderID: "my-user-id"}}, + SKU: validSKU, + SecureBootstrapEnabled: true, + BootstrapDataCompressed: []byte("bootstrap data compressed which will be stored in a secret"), + Initializer: azure.Cloudinit, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeAssignableToTypeOf(compute.VirtualMachine{})) + g.Expect(result.(compute.VirtualMachine).Identity.Type).To(Equal(compute.ResourceIdentityTypeUserAssigned)) + g.Expect(result.(compute.VirtualMachine).Identity.UserAssignedIdentities).To(Equal(map[string]*compute.VirtualMachineIdentityUserAssignedIdentitiesValue{"my-user-id": {}})) + }, + expectedError: "", + }, + { + name: "cannot create a vm with secure bootstrap enabled if user identity is not used", + spec: &VMSpec{ + Name: "my-vm", + Role: infrav1.Node, + NICIDs: []string{"my-nic"}, + SSHKeyData: "fakesshpublickey", + Size: "Standard_D2v3", + Zone: "1", + Image: &infrav1.Image{ID: to.StringPtr("fake-image-id")}, + Identity: infrav1.VMIdentitySystemAssigned, + SKU: validSKU, + SecureBootstrapEnabled: true, + BootstrapDataCompressed: []byte("bootstrap data compressed which will be stored in a secret"), + Initializer: azure.Cloudinit, + }, + existing: nil, + expect: func(g *WithT, result interface{}) { + g.Expect(result).To(BeNil()) + }, + expectedError: "failed to generate OS Profile: secure bootstrap cannot be used with identity of type \"SystemAssigned\", only UserAssigned identity is allowed", + }, } for _, tc := range testcases { tc := tc diff --git a/azure/types.go b/azure/types.go index a134ae2615c..c27d0648ed7 100644 --- a/azure/types.go +++ b/azure/types.go @@ -365,3 +365,6 @@ type AgentPoolSpec struct { // OsDiskType specifies the OS disk type for each node in the pool. Allowed values are 'Ephemeral' and 'Managed'. OsDiskType *string `json:"osDiskType,omitempty"` } + +// InstanceInitializer represents the type of initializer used to initialize VMs. +type InstanceInitializer string