From 3fee99b19d07470066198128d35b235c1d2a8ee3 Mon Sep 17 00:00:00 2001 From: Burak Sekili <32663655+buraksekili@users.noreply.github.com> Date: Sun, 10 May 2026 22:45:01 +0300 Subject: [PATCH 1/5] add securityProfile support for TrustedLaunch VMs Azure enforces TrustedLaunch on Gen2 VMs using gallery images, causing VM creation to fail with a 400 BadRequest. Add a `securityProfile` field to the Azure provider spec so users can set `securityType` to TrustedLaunch (with optional secure boot and vTPM) or Standard (to opt out). Fixes #2013 Signed-off-by: Burak Sekili <32663655+buraksekili@users.noreply.github.com> --- examples/azure-machinedeployment.yaml | 13 ++ pkg/cloudprovider/provider/azure/provider.go | 55 ++++++ .../provider/azure/provider_test.go | 158 ++++++++++++++++++ sdk/cloudprovider/azure/types.go | 9 + 4 files changed, 235 insertions(+) diff --git a/examples/azure-machinedeployment.yaml b/examples/azure-machinedeployment.yaml index abfe62ba8..6d950657e 100644 --- a/examples/azure-machinedeployment.yaml +++ b/examples/azure-machinedeployment.yaml @@ -75,6 +75,19 @@ spec: imageID: "myImageID" assignPublicIP: false securityGroupName: my-security-group + # securityProfile configures security features like TrustedLaunch for the VM. + # TrustedLaunch requires a Gen2 VM size. Use with Azure Compute Gallery images + # that have SecurityType set to TrustedLaunch. + # securityProfile: + # securityType: TrustedLaunch + # secureBootEnabled: true + # vTpmEnabled: true + # + # To opt out of TrustedLaunch on subscriptions where Azure auto-enables it + # (TrustedLaunchByDefaultPreview registered), set securityType to Standard. + # Requires the UseStandardSecurityType feature flag on the subscription. + # securityProfile: + # securityType: Standard # zones is an optional field and it represents Availability Zones is a high-availability offering # that protects your applications and data from datacenter failures. zones: diff --git a/pkg/cloudprovider/provider/azure/provider.go b/pkg/cloudprovider/provider/azure/provider.go index 143be9a8f..176fadb1d 100644 --- a/pkg/cloudprovider/provider/azure/provider.go +++ b/pkg/cloudprovider/provider/azure/provider.go @@ -108,6 +108,7 @@ type config struct { EnableAcceleratedNetworking *bool EnableBootDiagnostics bool Tags map[string]string + SecurityProfile *compute.SecurityProfile } type azureVM struct { @@ -364,6 +365,23 @@ func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*config, *p c.EnableBootDiagnostics = *rawCfg.EnableBootDiagnostics } + if rawCfg.SecurityProfile != nil && + (rawCfg.SecurityProfile.SecurityType != "" || + rawCfg.SecurityProfile.SecureBootEnabled != nil || + rawCfg.SecurityProfile.VTpmEnabled != nil) { + sp := &compute.SecurityProfile{} + if rawCfg.SecurityProfile.SecurityType != "" { + sp.SecurityType = compute.SecurityTypes(rawCfg.SecurityProfile.SecurityType) + } + if rawCfg.SecurityProfile.SecureBootEnabled != nil || rawCfg.SecurityProfile.VTpmEnabled != nil { + sp.UefiSettings = &compute.UefiSettings{ + SecureBootEnabled: rawCfg.SecurityProfile.SecureBootEnabled, + VTpmEnabled: rawCfg.SecurityProfile.VTpmEnabled, + } + } + c.SecurityProfile = sp + } + return &c, pConfig, nil } @@ -761,6 +779,10 @@ func (p *provider) Create(ctx context.Context, log *zap.SugaredLogger, machine * } } + if config.SecurityProfile != nil { + vmSpec.SecurityProfile = config.SecurityProfile + } + log.Info("Creating machine") if err := data.Update(machine, func(updatedMachine *clusterv1alpha1.Machine) { if !kuberneteshelper.HasFinalizer(updatedMachine, finalizerDisks) { @@ -1023,6 +1045,35 @@ func validateSKUCapabilities(_ context.Context, c *config, sku compute.ResourceS return nil } +func validateSecurityProfile(_ context.Context, c *config, sku compute.ResourceSku) error { + if c.SecurityProfile == nil { + return nil + } + + // the legacy Track-1 SDK only exports SecurityTypesTrustedLaunch and SecurityTypesConfidentialVM. + // compute.SecurityTypes is a string alias so casting the literal is safe. + const securityTypeStandard = compute.SecurityTypes("Standard") + + st := c.SecurityProfile.SecurityType + switch st { + case compute.SecurityTypesTrustedLaunch: + if !skuSupportsGen2(sku) { + return fmt.Errorf("securityType %q requires a Gen2 VM size, but %q does not support Gen2", st, c.VMSize) + } + case securityTypeStandard: + if c.SecurityProfile.UefiSettings != nil && + (c.SecurityProfile.UefiSettings.SecureBootEnabled != nil || c.SecurityProfile.UefiSettings.VTpmEnabled != nil) { + return fmt.Errorf("securityType %q cannot be combined with secureBootEnabled or vTpmEnabled", st) + } + case "": + return errors.New("securityProfile.securityType must be set when securityProfile is specified") + default: + return fmt.Errorf("unsupported securityType %q; supported values: TrustedLaunch, Standard", st) + } + + return nil +} + func (p *provider) Validate(ctx context.Context, log *zap.SugaredLogger, spec clusterv1alpha1.MachineSpec) error { c, providerConfig, err := p.getConfig(spec.ProviderSpec) if err != nil { @@ -1120,6 +1171,10 @@ func (p *provider) Validate(ctx context.Context, log *zap.SugaredLogger, spec cl return fmt.Errorf("failed to validate SKU capabilities: %w", err) } + if err := validateSecurityProfile(ctx, c, sku); err != nil { + return fmt.Errorf("failed to validate security profile: %w", err) + } + _, err = getOSImageReference(c, providerConfig.OperatingSystem) return err } diff --git a/pkg/cloudprovider/provider/azure/provider_test.go b/pkg/cloudprovider/provider/azure/provider_test.go index 535c0e650..c41cbe65d 100644 --- a/pkg/cloudprovider/provider/azure/provider_test.go +++ b/pkg/cloudprovider/provider/azure/provider_test.go @@ -17,7 +17,11 @@ limitations under the License. package azure import ( + "context" "testing" + + "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute" + "github.com/Azure/go-autorest/autorest/to" ) func TestVMSizeSupportsGen2(t *testing.T) { @@ -87,3 +91,157 @@ func TestVMSizeSupportsGen2(t *testing.T) { }) } } + +func gen2SKU() compute.ResourceSku { + return compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ + {Name: to.StringPtr("HyperVGenerations"), Value: to.StringPtr("V1,V2")}, + }, + } +} + +func gen1OnlySKU() compute.ResourceSku { + return compute.ResourceSku{ + Capabilities: &[]compute.ResourceSkuCapabilities{ + {Name: to.StringPtr("HyperVGenerations"), Value: to.StringPtr("V1")}, + }, + } +} + +func TestValidateSecurityProfile(t *testing.T) { + boolTrue := true + + tests := []struct { + name string + config *config + sku compute.ResourceSku + expectError bool + }{ + { + name: "nil SecurityProfile passes", + config: &config{VMSize: "Standard_D2s_v3"}, + sku: gen2SKU(), + expectError: false, + }, + { + name: "UEFI settings without securityType fails", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: &boolTrue, + }, + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "invalid securityType ConfidentialVM fails", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesConfidentialVM, + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "garbage securityType fails", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("Nonsense"), + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "TrustedLaunch on non-Gen2 SKU fails", + config: &config{ + VMSize: "Standard_A2", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + }, + }, + sku: gen1OnlySKU(), + expectError: true, + }, + { + name: "TrustedLaunch on Gen2 SKU passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: &boolTrue, + VTpmEnabled: &boolTrue, + }, + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "Standard on Gen2 SKU passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("Standard"), + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "Standard on Gen1 SKU passes", + config: &config{ + VMSize: "Standard_A2", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("Standard"), + }, + }, + sku: gen1OnlySKU(), + expectError: false, + }, + { + name: "Standard with secureBootEnabled fails", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("Standard"), + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: &boolTrue, + }, + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "Standard with vTpmEnabled fails", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("Standard"), + UefiSettings: &compute.UefiSettings{ + VTpmEnabled: &boolTrue, + }, + }, + }, + sku: gen2SKU(), + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSecurityProfile(context.Background(), tt.config, tt.sku) + if (err != nil) != tt.expectError { + t.Errorf("validateSecurityProfile() error = %v, expectError %v", err, tt.expectError) + } + }) + } +} diff --git a/sdk/cloudprovider/azure/types.go b/sdk/cloudprovider/azure/types.go index ae3266ab1..ebdb4e670 100644 --- a/sdk/cloudprovider/azure/types.go +++ b/sdk/cloudprovider/azure/types.go @@ -21,6 +21,13 @@ import ( "k8c.io/machine-controller/sdk/providerconfig" ) +// SecurityProfile specifies security settings like TrustedLaunch for the VM. +type SecurityProfile struct { + SecurityType string `json:"securityType,omitempty"` + SecureBootEnabled *bool `json:"secureBootEnabled,omitempty"` + VTpmEnabled *bool `json:"vTpmEnabled,omitempty"` +} + // RawConfig is a direct representation of an Azure machine object's configuration. type RawConfig struct { SubscriptionID providerconfig.ConfigVarString `json:"subscriptionID,omitempty"` @@ -53,6 +60,8 @@ type RawConfig struct { AssignPublicIP providerconfig.ConfigVarBool `json:"assignPublicIP"` PublicIPSKU *string `json:"publicIPSKU,omitempty"` Tags map[string]string `json:"tags,omitempty"` + + SecurityProfile *SecurityProfile `json:"securityProfile,omitempty"` } // ImagePlan contains azure OS Plan fields for the marketplace images. From 3550974ce41d1f1dc4edb4f55338acb2e2b5de64 Mon Sep 17 00:00:00 2001 From: Burak Sekili <32663655+buraksekili@users.noreply.github.com> Date: Mon, 11 May 2026 14:23:46 +0300 Subject: [PATCH 2/5] extract `securityTypeStandard` as a package-level constant shared between provider and tests. Signed-off-by: Burak Sekili <32663655+buraksekili@users.noreply.github.com> --- examples/azure-machinedeployment.yaml | 2 + pkg/cloudprovider/provider/azure/provider.go | 60 ++-- .../provider/azure/provider_test.go | 267 ++++++++++++++++-- 3 files changed, 287 insertions(+), 42 deletions(-) diff --git a/examples/azure-machinedeployment.yaml b/examples/azure-machinedeployment.yaml index 6d950657e..62365ca78 100644 --- a/examples/azure-machinedeployment.yaml +++ b/examples/azure-machinedeployment.yaml @@ -76,6 +76,7 @@ spec: assignPublicIP: false securityGroupName: my-security-group # securityProfile configures security features like TrustedLaunch for the VM. + # securityType is case-sensitive. Valid values: TrustedLaunch, Standard. # TrustedLaunch requires a Gen2 VM size. Use with Azure Compute Gallery images # that have SecurityType set to TrustedLaunch. # securityProfile: @@ -86,6 +87,7 @@ spec: # To opt out of TrustedLaunch on subscriptions where Azure auto-enables it # (TrustedLaunchByDefaultPreview registered), set securityType to Standard. # Requires the UseStandardSecurityType feature flag on the subscription. + # See: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch#preview-trusted-launch-as-default # securityProfile: # securityType: Standard # zones is an optional field and it represents Availability Zones is a high-availability offering diff --git a/pkg/cloudprovider/provider/azure/provider.go b/pkg/cloudprovider/provider/azure/provider.go index 176fadb1d..b73848721 100644 --- a/pkg/cloudprovider/provider/azure/provider.go +++ b/pkg/cloudprovider/provider/azure/provider.go @@ -56,6 +56,10 @@ const ( CapabilityValueTrue = "True" capabilityAcceleratedNetworking = "AcceleratedNetworkingEnabled" + // the legacy Track-1 SDK only exports SecurityTypesTrustedLaunch and SecurityTypesConfidentialVM. + // compute.SecurityTypes is a string alias so casting the literal is safe. + securityTypeStandard = compute.SecurityTypes("Standard") + machineUIDTag = "Machine-UID" finalizerPublicIP = "kubermatic.io/cleanup-azure-public-ip" @@ -365,22 +369,7 @@ func (p *provider) getConfig(provSpec clusterv1alpha1.ProviderSpec) (*config, *p c.EnableBootDiagnostics = *rawCfg.EnableBootDiagnostics } - if rawCfg.SecurityProfile != nil && - (rawCfg.SecurityProfile.SecurityType != "" || - rawCfg.SecurityProfile.SecureBootEnabled != nil || - rawCfg.SecurityProfile.VTpmEnabled != nil) { - sp := &compute.SecurityProfile{} - if rawCfg.SecurityProfile.SecurityType != "" { - sp.SecurityType = compute.SecurityTypes(rawCfg.SecurityProfile.SecurityType) - } - if rawCfg.SecurityProfile.SecureBootEnabled != nil || rawCfg.SecurityProfile.VTpmEnabled != nil { - sp.UefiSettings = &compute.UefiSettings{ - SecureBootEnabled: rawCfg.SecurityProfile.SecureBootEnabled, - VTpmEnabled: rawCfg.SecurityProfile.VTpmEnabled, - } - } - c.SecurityProfile = sp - } + c.SecurityProfile = buildSecurityProfile(rawCfg.SecurityProfile) return &c, pConfig, nil } @@ -1045,30 +1034,49 @@ func validateSKUCapabilities(_ context.Context, c *config, sku compute.ResourceS return nil } +// buildSecurityProfile converts the raw provider-spec SecurityProfile into the +// Azure SDK shape. Returns nil when no SecurityProfile is configured so the +// VM CreateOrUpdate call lets Azure apply its subscription defaults. +func buildSecurityProfile(raw *azuretypes.SecurityProfile) *compute.SecurityProfile { + if raw == nil { + return nil + } + sp := &compute.SecurityProfile{} + if raw.SecurityType != "" { + sp.SecurityType = compute.SecurityTypes(raw.SecurityType) + } + if raw.SecureBootEnabled != nil || raw.VTpmEnabled != nil { + sp.UefiSettings = &compute.UefiSettings{ + SecureBootEnabled: raw.SecureBootEnabled, + VTpmEnabled: raw.VTpmEnabled, + } + } + return sp +} + func validateSecurityProfile(_ context.Context, c *config, sku compute.ResourceSku) error { if c.SecurityProfile == nil { return nil } - // the legacy Track-1 SDK only exports SecurityTypesTrustedLaunch and SecurityTypesConfidentialVM. - // compute.SecurityTypes is a string alias so casting the literal is safe. - const securityTypeStandard = compute.SecurityTypes("Standard") + secProfile := c.SecurityProfile + secType := secProfile.SecurityType - st := c.SecurityProfile.SecurityType - switch st { + switch secProfile.SecurityType { case compute.SecurityTypesTrustedLaunch: if !skuSupportsGen2(sku) { - return fmt.Errorf("securityType %q requires a Gen2 VM size, but %q does not support Gen2", st, c.VMSize) + return fmt.Errorf("securityType %q requires a Gen2 VM size, but %q does not support Gen2", secType, c.VMSize) } case securityTypeStandard: - if c.SecurityProfile.UefiSettings != nil && - (c.SecurityProfile.UefiSettings.SecureBootEnabled != nil || c.SecurityProfile.UefiSettings.VTpmEnabled != nil) { - return fmt.Errorf("securityType %q cannot be combined with secureBootEnabled or vTpmEnabled", st) + uefiSettings := secProfile.UefiSettings + if uefiSettings != nil && (uefiSettings.SecureBootEnabled != nil || uefiSettings.VTpmEnabled != nil) { + // vTPM can't be used with "Standard" security type + return fmt.Errorf("securityType %q cannot be combined with secureBootEnabled or vTpmEnabled", secType) } case "": return errors.New("securityProfile.securityType must be set when securityProfile is specified") default: - return fmt.Errorf("unsupported securityType %q; supported values: TrustedLaunch, Standard", st) + return fmt.Errorf("unsupported securityType %q; supported values (case-sensitive): TrustedLaunch, Standard", secType) } return nil diff --git a/pkg/cloudprovider/provider/azure/provider_test.go b/pkg/cloudprovider/provider/azure/provider_test.go index c41cbe65d..1dda8a038 100644 --- a/pkg/cloudprovider/provider/azure/provider_test.go +++ b/pkg/cloudprovider/provider/azure/provider_test.go @@ -22,6 +22,11 @@ import ( "github.com/Azure/azure-sdk-for-go/profiles/latest/compute/mgmt/compute" "github.com/Azure/go-autorest/autorest/to" + "github.com/google/go-cmp/cmp" + + "k8s.io/utils/ptr" + + azuretypes "k8c.io/machine-controller/sdk/cloudprovider/azure" ) func TestVMSizeSupportsGen2(t *testing.T) { @@ -92,25 +97,26 @@ func TestVMSizeSupportsGen2(t *testing.T) { } } -func gen2SKU() compute.ResourceSku { +func skuWithHyperVGenerations(generations string) compute.ResourceSku { return compute.ResourceSku{ Capabilities: &[]compute.ResourceSkuCapabilities{ - {Name: to.StringPtr("HyperVGenerations"), Value: to.StringPtr("V1,V2")}, + {Name: to.StringPtr("HyperVGenerations"), Value: to.StringPtr(generations)}, }, } } -func gen1OnlySKU() compute.ResourceSku { +func gen2SKU() compute.ResourceSku { return skuWithHyperVGenerations("V1,V2") } +func gen2OnlySKU() compute.ResourceSku { return skuWithHyperVGenerations("V2") } +func gen1OnlySKU() compute.ResourceSku { return skuWithHyperVGenerations("V1") } +func skuWithoutGenCap() compute.ResourceSku { return compute.ResourceSku{ Capabilities: &[]compute.ResourceSkuCapabilities{ - {Name: to.StringPtr("HyperVGenerations"), Value: to.StringPtr("V1")}, + {Name: to.StringPtr("vCPUs"), Value: to.StringPtr("2")}, }, } } func TestValidateSecurityProfile(t *testing.T) { - boolTrue := true - tests := []struct { name string config *config @@ -123,13 +129,22 @@ func TestValidateSecurityProfile(t *testing.T) { sku: gen2SKU(), expectError: false, }, + { + name: "empty SecurityProfile (zero value) fails with empty securityType error", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{}, + }, + sku: gen2SKU(), + expectError: true, + }, { name: "UEFI settings without securityType fails", config: &config{ VMSize: "Standard_D2s_v3", SecurityProfile: &compute.SecurityProfile{ UefiSettings: &compute.UefiSettings{ - SecureBootEnabled: &boolTrue, + SecureBootEnabled: ptr.To(true), }, }, }, @@ -158,6 +173,28 @@ func TestValidateSecurityProfile(t *testing.T) { sku: gen2SKU(), expectError: true, }, + { + name: "lowercase trustedlaunch fails (case-sensitive)", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("trustedlaunch"), + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "lowercase standard fails (case-sensitive)", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("standard"), + }, + }, + sku: gen2SKU(), + expectError: true, + }, { name: "TrustedLaunch on non-Gen2 SKU fails", config: &config{ @@ -170,14 +207,90 @@ func TestValidateSecurityProfile(t *testing.T) { expectError: true, }, { - name: "TrustedLaunch on Gen2 SKU passes", + name: "TrustedLaunch on SKU without HyperVGenerations capability fails", + config: &config{ + VMSize: "Standard_A2", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + }, + }, + sku: skuWithoutGenCap(), + expectError: true, + }, + { + name: "TrustedLaunch on Gen2-only SKU passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + }, + }, + sku: gen2OnlySKU(), + expectError: false, + }, + { + name: "TrustedLaunch on Gen2 SKU with no UefiSettings passes (Azure applies defaults)", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "TrustedLaunch with only secureBootEnabled set passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(true), + }, + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "TrustedLaunch with only vTpmEnabled set passes", config: &config{ VMSize: "Standard_D2s_v3", SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, UefiSettings: &compute.UefiSettings{ - SecureBootEnabled: &boolTrue, - VTpmEnabled: &boolTrue, + VTpmEnabled: ptr.To(true), + }, + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "TrustedLaunch with secureBoot and vTpm both false passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(false), + VTpmEnabled: ptr.To(false), + }, + }, + }, + sku: gen2SKU(), + expectError: false, + }, + { + name: "TrustedLaunch with both UEFI settings true passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypesTrustedLaunch, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(true), + VTpmEnabled: ptr.To(true), }, }, }, @@ -189,7 +302,7 @@ func TestValidateSecurityProfile(t *testing.T) { config: &config{ VMSize: "Standard_D2s_v3", SecurityProfile: &compute.SecurityProfile{ - SecurityType: compute.SecurityTypes("Standard"), + SecurityType: securityTypeStandard, }, }, sku: gen2SKU(), @@ -200,20 +313,32 @@ func TestValidateSecurityProfile(t *testing.T) { config: &config{ VMSize: "Standard_A2", SecurityProfile: &compute.SecurityProfile{ - SecurityType: compute.SecurityTypes("Standard"), + SecurityType: securityTypeStandard, }, }, sku: gen1OnlySKU(), expectError: false, }, + { + name: "Standard with empty UefiSettings (both nil) passes", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: securityTypeStandard, + UefiSettings: &compute.UefiSettings{}, + }, + }, + sku: gen2SKU(), + expectError: false, + }, { name: "Standard with secureBootEnabled fails", config: &config{ VMSize: "Standard_D2s_v3", SecurityProfile: &compute.SecurityProfile{ - SecurityType: compute.SecurityTypes("Standard"), + SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{ - SecureBootEnabled: &boolTrue, + SecureBootEnabled: ptr.To(true), }, }, }, @@ -225,9 +350,23 @@ func TestValidateSecurityProfile(t *testing.T) { config: &config{ VMSize: "Standard_D2s_v3", SecurityProfile: &compute.SecurityProfile{ - SecurityType: compute.SecurityTypes("Standard"), + SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{ - VTpmEnabled: &boolTrue, + VTpmEnabled: ptr.To(true), + }, + }, + }, + sku: gen2SKU(), + expectError: true, + }, + { + name: "Standard with secureBootEnabled false fails (any non-nil pointer)", + config: &config{ + VMSize: "Standard_D2s_v3", + SecurityProfile: &compute.SecurityProfile{ + SecurityType: securityTypeStandard, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(false), }, }, }, @@ -245,3 +384,99 @@ func TestValidateSecurityProfile(t *testing.T) { }) } } + +func TestBuildSecurityProfile(t *testing.T) { + tests := []struct { + name string + raw *azuretypes.SecurityProfile + expected *compute.SecurityProfile + }{ + { + name: "nil raw returns nil", + raw: nil, + expected: nil, + }, + { + name: "empty raw returns empty SecurityProfile (caught by validator)", + raw: &azuretypes.SecurityProfile{}, + expected: &compute.SecurityProfile{}, + }, + { + name: "only securityType set", + raw: &azuretypes.SecurityProfile{SecurityType: "TrustedLaunch"}, + expected: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("TrustedLaunch"), + }, + }, + { + name: "only secureBootEnabled set (no securityType) constructs UefiSettings, validator rejects later", + raw: &azuretypes.SecurityProfile{SecureBootEnabled: ptr.To(true)}, + expected: &compute.SecurityProfile{ + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(true), + }, + }, + }, + { + name: "only vTpmEnabled set constructs UefiSettings", + raw: &azuretypes.SecurityProfile{VTpmEnabled: ptr.To(true)}, + expected: &compute.SecurityProfile{ + UefiSettings: &compute.UefiSettings{ + VTpmEnabled: ptr.To(true), + }, + }, + }, + { + name: "both UEFI flags set, no securityType", + raw: &azuretypes.SecurityProfile{ + SecureBootEnabled: ptr.To(true), + VTpmEnabled: ptr.To(false), + }, + expected: &compute.SecurityProfile{ + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(true), + VTpmEnabled: ptr.To(false), + }, + }, + }, + { + name: "fully populated TrustedLaunch", + raw: &azuretypes.SecurityProfile{ + SecurityType: "TrustedLaunch", + SecureBootEnabled: ptr.To(true), + VTpmEnabled: ptr.To(true), + }, + expected: &compute.SecurityProfile{ + SecurityType: compute.SecurityTypes("TrustedLaunch"), + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(true), + VTpmEnabled: ptr.To(true), + }, + }, + }, + { + name: "Standard with both UEFI flags (validator will reject) is still constructed", + raw: &azuretypes.SecurityProfile{ + SecurityType: "Standard", + SecureBootEnabled: ptr.To(false), + VTpmEnabled: ptr.To(false), + }, + expected: &compute.SecurityProfile{ + SecurityType: securityTypeStandard, + UefiSettings: &compute.UefiSettings{ + SecureBootEnabled: ptr.To(false), + VTpmEnabled: ptr.To(false), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := buildSecurityProfile(tt.raw) + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("buildSecurityProfile() mismatch (-want +got):\n%s", diff) + } + }) + } +} From 4f9138578e0d4985cf7d88450f8764338e85fa41 Mon Sep 17 00:00:00 2001 From: Burak Sekili <32663655+buraksekili@users.noreply.github.com> Date: Mon, 11 May 2026 14:24:36 +0300 Subject: [PATCH 3/5] gimps Signed-off-by: Burak Sekili <32663655+buraksekili@users.noreply.github.com> --- pkg/cloudprovider/provider/azure/provider_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cloudprovider/provider/azure/provider_test.go b/pkg/cloudprovider/provider/azure/provider_test.go index 1dda8a038..e992a4ba5 100644 --- a/pkg/cloudprovider/provider/azure/provider_test.go +++ b/pkg/cloudprovider/provider/azure/provider_test.go @@ -24,9 +24,9 @@ import ( "github.com/Azure/go-autorest/autorest/to" "github.com/google/go-cmp/cmp" - "k8s.io/utils/ptr" - azuretypes "k8c.io/machine-controller/sdk/cloudprovider/azure" + + "k8s.io/utils/ptr" ) func TestVMSizeSupportsGen2(t *testing.T) { From 08e215dd2c9778c8f038077fc23bfcb2fd328702 Mon Sep 17 00:00:00 2001 From: Burak Sekili <32663655+buraksekili@users.noreply.github.com> Date: Mon, 11 May 2026 14:26:25 +0300 Subject: [PATCH 4/5] fmt Signed-off-by: Burak Sekili <32663655+buraksekili@users.noreply.github.com> --- pkg/cloudprovider/provider/azure/provider.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/cloudprovider/provider/azure/provider.go b/pkg/cloudprovider/provider/azure/provider.go index b73848721..9d2d0ca53 100644 --- a/pkg/cloudprovider/provider/azure/provider.go +++ b/pkg/cloudprovider/provider/azure/provider.go @@ -1041,16 +1041,19 @@ func buildSecurityProfile(raw *azuretypes.SecurityProfile) *compute.SecurityProf if raw == nil { return nil } + sp := &compute.SecurityProfile{} if raw.SecurityType != "" { sp.SecurityType = compute.SecurityTypes(raw.SecurityType) } + if raw.SecureBootEnabled != nil || raw.VTpmEnabled != nil { sp.UefiSettings = &compute.UefiSettings{ SecureBootEnabled: raw.SecureBootEnabled, VTpmEnabled: raw.VTpmEnabled, } } + return sp } From 45dbe387288daa61b8c83e09e0e722107c2625d4 Mon Sep 17 00:00:00 2001 From: Burak Sekili <32663655+buraksekili@users.noreply.github.com> Date: Mon, 11 May 2026 14:33:01 +0300 Subject: [PATCH 5/5] extract VM size literals to test constants replace repeated "Standard_D2s_v3" and "Standard_A2" strings with `testVMSizeGen2` and `testVMSizeGen1` constants. Signed-off-by: Burak Sekili <32663655+buraksekili@users.noreply.github.com> --- .../provider/azure/provider_test.go | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/pkg/cloudprovider/provider/azure/provider_test.go b/pkg/cloudprovider/provider/azure/provider_test.go index e992a4ba5..2c359d227 100644 --- a/pkg/cloudprovider/provider/azure/provider_test.go +++ b/pkg/cloudprovider/provider/azure/provider_test.go @@ -29,6 +29,11 @@ import ( "k8s.io/utils/ptr" ) +const ( + testVMSizeGen2 = "Standard_D2s_v3" + testVMSizeGen1 = "Standard_A2" +) + func TestVMSizeSupportsGen2(t *testing.T) { tests := []struct { name string @@ -42,7 +47,7 @@ func TestVMSizeSupportsGen2(t *testing.T) { }, { name: "Standard_D2s_v3 should support Gen2", - vmSize: "Standard_D2s_v3", + vmSize: testVMSizeGen2, expected: true, }, { @@ -62,7 +67,7 @@ func TestVMSizeSupportsGen2(t *testing.T) { }, { name: "Standard_A2 should not support Gen2", - vmSize: "Standard_A2", + vmSize: testVMSizeGen1, expected: false, }, { @@ -125,14 +130,14 @@ func TestValidateSecurityProfile(t *testing.T) { }{ { name: "nil SecurityProfile passes", - config: &config{VMSize: "Standard_D2s_v3"}, + config: &config{VMSize: testVMSizeGen2}, sku: gen2SKU(), expectError: false, }, { name: "empty SecurityProfile (zero value) fails with empty securityType error", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{}, }, sku: gen2SKU(), @@ -141,7 +146,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "UEFI settings without securityType fails", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ UefiSettings: &compute.UefiSettings{ SecureBootEnabled: ptr.To(true), @@ -154,7 +159,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "invalid securityType ConfidentialVM fails", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesConfidentialVM, }, @@ -165,7 +170,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "garbage securityType fails", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypes("Nonsense"), }, @@ -176,7 +181,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "lowercase trustedlaunch fails (case-sensitive)", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypes("trustedlaunch"), }, @@ -187,7 +192,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "lowercase standard fails (case-sensitive)", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypes("standard"), }, @@ -198,7 +203,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch on non-Gen2 SKU fails", config: &config{ - VMSize: "Standard_A2", + VMSize: testVMSizeGen1, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, }, @@ -209,7 +214,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch on SKU without HyperVGenerations capability fails", config: &config{ - VMSize: "Standard_A2", + VMSize: testVMSizeGen1, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, }, @@ -220,7 +225,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch on Gen2-only SKU passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, }, @@ -231,7 +236,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch on Gen2 SKU with no UefiSettings passes (Azure applies defaults)", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, }, @@ -242,7 +247,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch with only secureBootEnabled set passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, UefiSettings: &compute.UefiSettings{ @@ -256,7 +261,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch with only vTpmEnabled set passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, UefiSettings: &compute.UefiSettings{ @@ -270,7 +275,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch with secureBoot and vTpm both false passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, UefiSettings: &compute.UefiSettings{ @@ -285,7 +290,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "TrustedLaunch with both UEFI settings true passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: compute.SecurityTypesTrustedLaunch, UefiSettings: &compute.UefiSettings{ @@ -300,7 +305,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard on Gen2 SKU passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, }, @@ -311,7 +316,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard on Gen1 SKU passes", config: &config{ - VMSize: "Standard_A2", + VMSize: testVMSizeGen1, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, }, @@ -322,7 +327,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard with empty UefiSettings (both nil) passes", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{}, @@ -334,7 +339,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard with secureBootEnabled fails", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{ @@ -348,7 +353,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard with vTpmEnabled fails", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{ @@ -362,7 +367,7 @@ func TestValidateSecurityProfile(t *testing.T) { { name: "Standard with secureBootEnabled false fails (any non-nil pointer)", config: &config{ - VMSize: "Standard_D2s_v3", + VMSize: testVMSizeGen2, SecurityProfile: &compute.SecurityProfile{ SecurityType: securityTypeStandard, UefiSettings: &compute.UefiSettings{