diff --git a/api/v1beta1/gcpmachine_types.go b/api/v1beta1/gcpmachine_types.go index dc7c3c012..996caa8d2 100644 --- a/api/v1beta1/gcpmachine_types.go +++ b/api/v1beta1/gcpmachine_types.go @@ -137,9 +137,22 @@ const ( ConfidentialComputePolicyDisabled ConfidentialComputePolicy = "Disabled" ) +// ConfidentialVMTechnology represents the confidential computing technology used by a GCP confidential machine. +type ConfidentialVMTechnology string + +const ( + // ConfidentialVMTechSEV sets AMD SEV as the VM instance's confidential computing technology of choice. + ConfidentialVMTechSEV ConfidentialVMTechnology = "sev" + // ConfidentialVMTechSEVSNP sets AMD SEV-SNP as the VM instance's confidential computing technology of choice. + ConfidentialVMTechSEVSNP ConfidentialVMTechnology = "sev-snp" +) + // Confidential VM supports Compute Engine machine types in the following series: // reference: https://cloud.google.com/compute/confidential-vm/docs/os-and-machine-type#machine-type -var confidentialComputeSupportedMachineSeries = []string{"n2d", "c2d"} +var confidentialComputeSupportedMachineSeries = map[ConfidentialVMTechnology][]string{ + ConfidentialVMTechSEV: {"n2d", "c2d", "c3d"}, + ConfidentialVMTechSEVSNP: {"n2d"}, +} // HostMaintenancePolicy represents the desired behavior ase of a host maintenance event. type HostMaintenancePolicy string @@ -339,10 +352,18 @@ type GCPMachineSpec struct { // ConfidentialCompute Defines whether the instance should have confidential compute enabled. // If enabled OnHostMaintenance is required to be set to "Terminate". // If omitted, the platform chooses a default, which is subject to change over time, currently that default is false. + // If ConfidentialInstanceType is configured, even if ConfidentialCompute is Disabled, a confidential compute instance will be configured. // +kubebuilder:validation:Enum=Enabled;Disabled // +optional ConfidentialCompute *ConfidentialComputePolicy `json:"confidentialCompute,omitempty"` + // confidentialInstanceType determines the required type of confidential computing technology. + // confidentialInstanceType will precede confidentialCompute. That is, if confidentialCompute is "Disabled" but a valid confidentialInstanceType is specified, a confidential instance will be configured. + // If confidentialInstanceType isn't set and confidentialCompute is "Enabled" the platform will set the default, which is subject to change over time. Currently the default is "sev" for "c2d", "c3d", and "n2d" machineTypes. For the other machine cases, a valid confidentialInstanceType must be specified. + // +kubebuilder:validation:Enum=sev;sev-snp; + // +optional + ConfidentialInstanceType *ConfidentialVMTechnology `json:"confidentialInstanceType,omitempty"` + // RootDiskEncryptionKey defines the KMS key to be used to encrypt the root disk. // +optional RootDiskEncryptionKey *CustomerEncryptionKey `json:"rootDiskEncryptionKey,omitempty"` diff --git a/api/v1beta1/gcpmachine_webhook.go b/api/v1beta1/gcpmachine_webhook.go index 423befd5b..5a8eaa75d 100644 --- a/api/v1beta1/gcpmachine_webhook.go +++ b/api/v1beta1/gcpmachine_webhook.go @@ -108,15 +108,32 @@ func (m *GCPMachine) Default() { clusterlog.Info("default", "name", m.Name) } +func targetConfidentialType(tech *ConfidentialVMTechnology) (ConfidentialVMTechnology, error) { + if tech == nil || tech != nil && *tech == "" { + return ConfidentialVMTechSEV, nil + } + _, ok := confidentialComputeSupportedMachineSeries[*tech] + if !ok { + return "", fmt.Errorf("Invalid ConfidentialInstanceType %s", *tech) + } + return *tech, nil +} + func validateConfidentialCompute(spec GCPMachineSpec) error { - if spec.ConfidentialCompute != nil && *spec.ConfidentialCompute == ConfidentialComputePolicyEnabled { + if spec.ConfidentialCompute != nil && *spec.ConfidentialCompute == ConfidentialComputePolicyEnabled || spec.ConfidentialInstanceType != nil && *spec.ConfidentialInstanceType != "" { if spec.OnHostMaintenance == nil || *spec.OnHostMaintenance == HostMaintenancePolicyMigrate { return fmt.Errorf("ConfidentialCompute require OnHostMaintenance to be set to %s, the current value is: %s", HostMaintenancePolicyTerminate, HostMaintenancePolicyMigrate) } + confidentialType, err := targetConfidentialType(spec.ConfidentialInstanceType) + if err != nil { + return err + } + supportedMachines := confidentialComputeSupportedMachineSeries[confidentialType] + machineSeries := strings.Split(spec.InstanceType, "-")[0] - if !slices.Contains(confidentialComputeSupportedMachineSeries, machineSeries) { - return fmt.Errorf("ConfidentialCompute require instance type in the following series: %s", confidentialComputeSupportedMachineSeries) + if !slices.Contains(supportedMachines, machineSeries) { + return fmt.Errorf("ConfidentialInstanceType %s requires an instance type in the following series: %s", confidentialType, supportedMachines) } } return nil diff --git a/api/v1beta1/gcpmachine_webhook_test.go b/api/v1beta1/gcpmachine_webhook_test.go index 28b236121..a50417a7e 100644 --- a/api/v1beta1/gcpmachine_webhook_test.go +++ b/api/v1beta1/gcpmachine_webhook_test.go @@ -27,6 +27,8 @@ func TestGCPMachine_ValidateCreate(t *testing.T) { confidentialComputeEnabled := ConfidentialComputePolicyEnabled onHostMaintenanceTerminate := HostMaintenancePolicyTerminate onHostMaintenanceMigrate := HostMaintenancePolicyMigrate + confidentialInstanceTypeSEV := ConfidentialVMTechSEV + confidentialInstanceTypeSEVSNP := ConfidentialVMTechSEVSNP tests := []struct { name string *GCPMachine @@ -85,6 +87,50 @@ func TestGCPMachine_ValidateCreate(t *testing.T) { }, wantErr: true, }, + { + name: "GCPMachine with explicit ConfidentialInstanceType and OnHostMaintenance Migrate - invalid", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + InstanceType: "n2d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceMigrate, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with SEVSNP ConfidentialInstanceType and unsupported machine type - invalid", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + InstanceType: "c2d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with SEVSNP ConfidentialInstanceType and supported machine type - valid", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + InstanceType: "n2d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + wantErr: false, + }, + { + name: "GCPMachine with explicit SEV ConfidentialInstanceType and supported machine type - valid", + GCPMachine: &GCPMachine{ + Spec: GCPMachineSpec{ + InstanceType: "c3d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEV, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + wantErr: false, + }, { name: "GCPMachine with RootDiskEncryptionKey KeyType Managed and Managed field set", GCPMachine: &GCPMachine{ diff --git a/api/v1beta1/gcpmachinetemplate_webhook_test.go b/api/v1beta1/gcpmachinetemplate_webhook_test.go index d42defb27..a9d5c5df4 100644 --- a/api/v1beta1/gcpmachinetemplate_webhook_test.go +++ b/api/v1beta1/gcpmachinetemplate_webhook_test.go @@ -27,6 +27,8 @@ func TestGCPMachineTemplate_ValidateCreate(t *testing.T) { confidentialComputeEnabled := ConfidentialComputePolicyEnabled onHostMaintenanceTerminate := HostMaintenancePolicyTerminate onHostMaintenanceMigrate := HostMaintenancePolicyMigrate + confidentialInstanceTypeSEV := ConfidentialVMTechSEV + confidentialInstanceTypeSEVSNP := ConfidentialVMTechSEVSNP tests := []struct { name string template *GCPMachineTemplate @@ -105,6 +107,66 @@ func TestGCPMachineTemplate_ValidateCreate(t *testing.T) { }, wantErr: true, }, + { + name: "GCPMachine with explicit ConfidentialInstanceType and OnHostMaintenance Migrate - invalid", + template: &GCPMachineTemplate{ + Spec: GCPMachineTemplateSpec{ + Template: GCPMachineTemplateResource{ + Spec: GCPMachineSpec{ + InstanceType: "n2d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceMigrate, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with SEVSNP ConfidentialInstanceType and unsupported machine type - invalid", + template: &GCPMachineTemplate{ + Spec: GCPMachineTemplateSpec{ + Template: GCPMachineTemplateResource{ + Spec: GCPMachineSpec{ + InstanceType: "c3d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "GCPMachine with SEVSNP ConfidentialInstanceType and supported machine type - valid", + template: &GCPMachineTemplate{ + Spec: GCPMachineTemplateSpec{ + Template: GCPMachineTemplateResource{ + Spec: GCPMachineSpec{ + InstanceType: "n2d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEVSNP, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "GCPMachine with explicit SEV ConfidentialInstanceType and supported machine type - valid", + template: &GCPMachineTemplate{ + Spec: GCPMachineTemplateSpec{ + Template: GCPMachineTemplateResource{ + Spec: GCPMachineSpec{ + InstanceType: "c3d-standard-4", + ConfidentialInstanceType: &confidentialInstanceTypeSEV, + OnHostMaintenance: &onHostMaintenanceTerminate, + }, + }, + }, + }, + wantErr: false, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 3c8560ad9..51fe30659 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -500,6 +500,11 @@ func (in *GCPMachineSpec) DeepCopyInto(out *GCPMachineSpec) { *out = new(ConfidentialComputePolicy) **out = **in } + if in.ConfidentialInstanceType != nil { + in, out := &in.ConfidentialInstanceType, &out.ConfidentialInstanceType + *out = new(ConfidentialVMTechnology) + **out = **in + } if in.RootDiskEncryptionKey != nil { in, out := &in.RootDiskEncryptionKey, &out.RootDiskEncryptionKey *out = new(CustomerEncryptionKey) diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index ce5d02d55..813e1e1f9 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -451,6 +451,19 @@ func (m *MachineScope) InstanceSpec(log logr.Logger) *compute.Instance { EnableConfidentialCompute: enabled, } } + if m.GCPMachine.Spec.ConfidentialInstanceType != nil { + if instance.ConfidentialInstanceConfig == nil { + instance.ConfidentialInstanceConfig = &compute.ConfidentialInstanceConfig{} + } + switch *m.GCPMachine.Spec.ConfidentialInstanceType { + case infrav1.ConfidentialVMTechSEV: + instance.ConfidentialInstanceConfig.ConfidentialInstanceType = "SEV" + case infrav1.ConfidentialVMTechSEVSNP: + instance.ConfidentialInstanceConfig.ConfidentialInstanceType = "SEV_SNP" + default: + log.Error(errors.New("Invalid value"), "Unknown ConfidentialInstanceType value", "Spec.ConfidentialInstanceType", *m.GCPMachine.Spec.ConfidentialInstanceType) + } + } instance.Disks = append(instance.Disks, m.InstanceImageSpec()) instance.Disks = append(instance.Disks, m.InstanceAdditionalDiskSpec()...) diff --git a/cloud/services/compute/instances/reconcile_test.go b/cloud/services/compute/instances/reconcile_test.go index 74b33ba8b..576c50659 100644 --- a/cloud/services/compute/instances/reconcile_test.go +++ b/cloud/services/compute/instances/reconcile_test.go @@ -495,6 +495,159 @@ func TestService_createOrGetInstance(t *testing.T) { Zone: "us-central1-c", }, }, + { + name: "instance does not exist (should create instance) with confidential compute disabled and any confidential instance type specified", + scope: func() Scope { + machineScope.GCPMachine = getFakeGCPMachine() + hostMaintenancePolicyTerminate := infrav1.HostMaintenancePolicyTerminate + machineScope.GCPMachine.Spec.OnHostMaintenance = &hostMaintenancePolicyTerminate + confidentialInstTypeSEV := infrav1.ConfidentialVMTechSEV + machineScope.GCPMachine.Spec.ConfidentialInstanceType = &confidentialInstTypeSEV + return machineScope + }, + mockInstance: &cloud.MockInstances{ + ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"}, + Objects: map[meta.Key]*cloud.MockInstancesObj{}, + }, + want: &compute.Instance{ + Name: "my-machine", + CanIpForward: true, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + InitializeParams: &compute.AttachedDiskInitializeParams{ + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + Labels: map[string]string{ + "capg-role": "node", + "capg-cluster-my-cluster": "owned", + "foo": "bar", + }, + MachineType: "zones/us-central1-c/machineTypes", + Metadata: &compute.Metadata{ + Items: []*compute.MetadataItems{ + { + Key: "user-data", + Value: ptr.To[string]("Zm9vCg=="), + }, + }, + }, + NetworkInterfaces: []*compute.NetworkInterface{ + { + Network: "projects/my-proj/global/networks/default", + }, + }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, + SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", + ConfidentialInstanceConfig: &compute.ConfidentialInstanceConfig{ + ConfidentialInstanceType: "SEV", + }, + Scheduling: &compute.Scheduling{ + OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyTerminate)), + }, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "default", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + }, + }, + Tags: &compute.Tags{ + Items: []string{ + "my-cluster-node", + "my-cluster", + }, + }, + Zone: "us-central1-c", + }, + }, + { + name: "instance does not exist (should create instance) with confidential compute enabled and any confidential instance type specified", + scope: func() Scope { + machineScope.GCPMachine = getFakeGCPMachine() + hostMaintenancePolicyTerminate := infrav1.HostMaintenancePolicyTerminate + machineScope.GCPMachine.Spec.OnHostMaintenance = &hostMaintenancePolicyTerminate + confidentialInstTypeSEVSNP := infrav1.ConfidentialVMTechSEVSNP + machineScope.GCPMachine.Spec.ConfidentialInstanceType = &confidentialInstTypeSEVSNP + confidentialComputePolicyEnabled := infrav1.ConfidentialComputePolicyEnabled + machineScope.GCPMachine.Spec.ConfidentialCompute = &confidentialComputePolicyEnabled + return machineScope + }, + mockInstance: &cloud.MockInstances{ + ProjectRouter: &cloud.SingleProjectRouter{ID: "proj-id"}, + Objects: map[meta.Key]*cloud.MockInstancesObj{}, + }, + want: &compute.Instance{ + Name: "my-machine", + CanIpForward: true, + Disks: []*compute.AttachedDisk{ + { + AutoDelete: true, + Boot: true, + InitializeParams: &compute.AttachedDiskInitializeParams{ + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + Labels: map[string]string{ + "capg-role": "node", + "capg-cluster-my-cluster": "owned", + "foo": "bar", + }, + MachineType: "zones/us-central1-c/machineTypes", + Metadata: &compute.Metadata{ + Items: []*compute.MetadataItems{ + { + Key: "user-data", + Value: ptr.To[string]("Zm9vCg=="), + }, + }, + }, + NetworkInterfaces: []*compute.NetworkInterface{ + { + Network: "projects/my-proj/global/networks/default", + }, + }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, + SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", + ConfidentialInstanceConfig: &compute.ConfidentialInstanceConfig{ + EnableConfidentialCompute: true, + ConfidentialInstanceType: "SEV_SNP", + }, + Scheduling: &compute.Scheduling{ + OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyTerminate)), + }, + ServiceAccounts: []*compute.ServiceAccount{ + { + Email: "default", + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + }, + }, + Tags: &compute.Tags{ + Items: []string{ + "my-cluster-node", + "my-cluster", + }, + }, + Zone: "us-central1-c", + }, + }, { name: "instance does not exist (should create instance) with MIGRATE OnHostMaintenance", scope: func() Scope { diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml index 5dbe18c8a..a9dc068b7 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml @@ -196,10 +196,20 @@ spec: ConfidentialCompute Defines whether the instance should have confidential compute enabled. If enabled OnHostMaintenance is required to be set to "Terminate". If omitted, the platform chooses a default, which is subject to change over time, currently that default is false. + If ConfidentialInstanceType is configured, even if ConfidentialCompute is Disabled, a confidential compute instance will be configured. enum: - Enabled - Disabled type: string + confidentialInstanceType: + description: |- + confidentialInstanceType determines the required type of confidential computing technology. + confidentialInstanceType will precede confidentialCompute. That is, if confidentialCompute is "Disabled" but a valid confidentialInstanceType is specified, a confidential instance will be configured. + If confidentialInstanceType isn't set and confidentialCompute is "Enabled" the platform will set the default, which is subject to change over time. Currently the default is "sev" for "c2d", "c3d", and "n2d" machineTypes. For the other machine cases, a valid confidentialInstanceType must be specified. + enum: + - sev + - sev-snp + type: string image: description: |- Image is the full reference to a valid image to be used for this machine. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml index ece5bc8d8..fed86870a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml @@ -211,10 +211,20 @@ spec: ConfidentialCompute Defines whether the instance should have confidential compute enabled. If enabled OnHostMaintenance is required to be set to "Terminate". If omitted, the platform chooses a default, which is subject to change over time, currently that default is false. + If ConfidentialInstanceType is configured, even if ConfidentialCompute is Disabled, a confidential compute instance will be configured. enum: - Enabled - Disabled type: string + confidentialInstanceType: + description: |- + confidentialInstanceType determines the required type of confidential computing technology. + confidentialInstanceType will precede confidentialCompute. That is, if confidentialCompute is "Disabled" but a valid confidentialInstanceType is specified, a confidential instance will be configured. + If confidentialInstanceType isn't set and confidentialCompute is "Enabled" the platform will set the default, which is subject to change over time. Currently the default is "sev" for "c2d", "c3d", and "n2d" machineTypes. For the other machine cases, a valid confidentialInstanceType must be specified. + enum: + - sev + - sev-snp + type: string image: description: |- Image is the full reference to a valid image to be used for this machine.