From 2fdfa16d95de7e8463db88c3df452b9a516df51f Mon Sep 17 00:00:00 2001 From: barbacbd Date: Wed, 24 Jun 2026 15:57:44 -0400 Subject: [PATCH 1/4] gcp: Add universe domain field for sovereign cloud support Add UniverseDomain field to the GCP platform configuration to support sovereign cloud environments that use different API endpoints. The universe domain specifies which GCP universe the cluster operates in. Public GCP uses googleapis.com (the default), while sovereign clouds use their own universe domains (e.g., apis-berlin-build0.goog for the German sovereign cloud). This field is validated to ensure it's only set when using sovereign cloud project IDs (those with a colon prefix like eu0:project-id). --- .../install.openshift.io_installconfigs.yaml | 6 ++ pkg/asset/installconfig/gcp/validation.go | 20 +++--- pkg/types/gcp/metadata.go | 1 + pkg/types/gcp/platform.go | 62 ++++++++++++++++++- 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index 37c2e9c4021..fdc503f2a72 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -6640,6 +6640,12 @@ spec: description: Region specifies the GCP region where the cluster will be created. type: string + universeDomain: + description: |- + UniverseDomain is the Google Cloud universe domain for the cluster. + When no value is set, GCP APIs use a default value: googleapis.com. + Universe Domain may be required to configure Sovereign Cloud environments. + type: string userLabels: description: |- userLabels has additional keys and values that the installer will add as diff --git a/pkg/asset/installconfig/gcp/validation.go b/pkg/asset/installconfig/gcp/validation.go index 12c268b9a17..204f8968d12 100644 --- a/pkg/asset/installconfig/gcp/validation.go +++ b/pkg/asset/installconfig/gcp/validation.go @@ -201,8 +201,14 @@ func validateServiceAccountPresent(client API, ic *types.InstallConfig) field.Er return allErrs } -// DefaultInstanceTypeForArch returns the appropriate instance type based on the target architecture. -func DefaultInstanceTypeForArch(arch types.Architecture) string { +// DefaultInstanceTypeForArch returns the appropriate instance type based on project ID, region, and architecture. +// The cloud environment is automatically detected from the project ID and region. +func DefaultInstanceTypeForArch(projectID, region string, arch types.Architecture) string { + if cloudEnv := gcp.GetCloudEnvironment(projectID); cloudEnv == gcp.CloudEnvironmentSovereign { + // c3-standard-4 is the default for sovereign clouds + // Note: ARM64 is not currently supported in sovereign clouds + return "c3-standard-4" + } if arch == types.ArchitectureARM64 { return "t2a-standard-4" } @@ -232,7 +238,7 @@ func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList if ic.GCP.DefaultMachinePlatform.DiskType != "" { defaultDiskType = ic.GCP.DefaultMachinePlatform.DiskType } else { - defaultDiskType = gcp.DefaultDiskTypeForInstance(defaultInstanceType) + defaultDiskType = gcp.DefaultDiskTypeForInstanceAndProjectID(defaultInstanceType, ic.GCP.ProjectID, ic.GCP.Region) } if ic.GCP.DefaultMachinePlatform.OnHostMaintenance != "" { @@ -270,7 +276,7 @@ func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList if ic.ControlPlane != nil { arch = string(ic.ControlPlane.Architecture) if instanceType == "" { - instanceType = DefaultInstanceTypeForArch(ic.ControlPlane.Architecture) + instanceType = DefaultInstanceTypeForArch(ic.GCP.ProjectID, ic.GCP.Region, ic.ControlPlane.Architecture) } if ic.ControlPlane.Platform.GCP != nil { if ic.ControlPlane.Platform.GCP.InstanceType != "" { @@ -291,7 +297,7 @@ func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList fmt.Sprintf("instance type %s requires a disk type to be set", instanceType), )) } - cpDiskType = gcp.DefaultDiskTypeForInstance(instanceType) + cpDiskType = gcp.DefaultDiskTypeForInstanceAndProjectID(instanceType, ic.GCP.ProjectID, ic.GCP.Region) } if ic.ControlPlane.Platform.GCP.OnHostMaintenance != "" { cpOnHostMaintenance = ic.ControlPlane.Platform.GCP.OnHostMaintenance @@ -334,7 +340,7 @@ func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList onHostMaintenance := defaultOnHostMaintenance confidentialCompute := defaultConfidentialCompute if instanceType == "" { - instanceType = DefaultInstanceTypeForArch(compute.Architecture) + instanceType = DefaultInstanceTypeForArch(ic.GCP.ProjectID, ic.GCP.Region, compute.Architecture) } if diskType == "" { diskType = gcp.PDSSD @@ -365,7 +371,7 @@ func validateInstanceTypes(client API, ic *types.InstallConfig) field.ErrorList fmt.Sprintf("instance type %s requires a disk type to be set", instanceType), )) } - diskType = gcp.DefaultDiskTypeForInstance(instanceType) + diskType = gcp.DefaultDiskTypeForInstanceAndProjectID(instanceType, ic.GCP.ProjectID, ic.GCP.Region) } } diff --git a/pkg/types/gcp/metadata.go b/pkg/types/gcp/metadata.go index dc33be89d2c..c8a61246b9b 100644 --- a/pkg/types/gcp/metadata.go +++ b/pkg/types/gcp/metadata.go @@ -9,4 +9,5 @@ type Metadata struct { PrivateZoneProjectID string `json:"privateZoneProjectID,omitempty"` Endpoint *PSCEndpoint `json:"endpoint,omitempty"` FirewallRulesManagement FirewallRulesManagementPolicy `json:"firewallRulesManagement,omitempty"` + UniverseDomain string `json:"universeDomain,omitempty"` } diff --git a/pkg/types/gcp/platform.go b/pkg/types/gcp/platform.go index 7fbb9ca53cc..c1b40d11312 100644 --- a/pkg/types/gcp/platform.go +++ b/pkg/types/gcp/platform.go @@ -2,6 +2,9 @@ package gcp import ( "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" "github.com/openshift/installer/pkg/types/dns" ) @@ -17,6 +20,18 @@ const ( // UnmanagedFirewallRules indicates that the firewall rules should be managed by the user. The // firewall rules should exist prior to the installation occurs. UnmanagedFirewallRules FirewallRulesManagementPolicy = "Unmanaged" + + // CloudEnvironmentSovereign is the cloud environment identifier for GCP sovereign clouds. + CloudEnvironmentSovereign = "sovereign" +) + +var ( + // sovereignCloudProjectPrefixes contains known project ID prefixes for sovereign clouds. + // Project IDs in sovereign clouds use the format: : + // This list helps distinguish from organization-scoped public GCP projects (orgname:project-id). + sovereignCloudProjectPrefixes = []string{ + "eu0", // European sovereign cloud (Germany) + } ) // DNS contains the gcp dns zone information for the cluster. @@ -126,6 +141,12 @@ type Platform struct { // and the firewall rules before the installation. // +optional FirewallRulesManagement FirewallRulesManagementPolicy `json:"firewallRulesManagement,omitempty"` + + // UniverseDomain is the Google Cloud universe domain for the cluster. + // When no value is set, GCP APIs use a default value: googleapis.com. + // Universe Domain may be required to configure Sovereign Cloud environments. + // +optional + UniverseDomain string `json:"universeDomain,omitempty"` } // UserLabel is a label to apply to GCP resources created for the cluster. @@ -184,8 +205,29 @@ func GetConfiguredServiceAccount(platform *Platform, mpool *MachinePool) string // GetDefaultServiceAccount returns the default service account email to use based on role. // The default should be used when an existing service account is not configured. +// For sovereign cloud project IDs (e.g., eu0:project-id), the service account email format is: +// +// service-account-name@project-id.eu0.iam.gserviceaccount.com +// +// For standard project IDs, the format is: +// +// service-account-name@project-id.iam.gserviceaccount.com func GetDefaultServiceAccount(platform *Platform, clusterID string, role string) string { - return fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", clusterID, role[0:1], platform.ProjectID) + projectID := platform.ProjectID + + // For sovereign cloud project IDs in format "prefix:project-id", swap to "project-id.prefix" + if strings.Contains(projectID, ":") { + parts := strings.SplitN(projectID, ":", 2) + if len(parts) == 2 && sets.New(sovereignCloudProjectPrefixes...).Has(parts[0]) { + // Sovereign cloud: swap to project-id.prefix format + // Example: "eu0:openshift" becomes "openshift.eu0" + projectID = fmt.Sprintf("%s.%s", parts[1], parts[0]) + } + // For organization-scoped projects (e.g., "google.com:project-id"), + // keep the full projectID unchanged and let GCP handle the format + } + + return fmt.Sprintf("%s-%s@%s.iam.gserviceaccount.com", clusterID, role[0:1], projectID) } // ShouldUseEndpointForInstaller returns true when the endpoint should be used for GCP api endpoint overrides in the @@ -193,3 +235,21 @@ func GetDefaultServiceAccount(platform *Platform, clusterID string, role string) func ShouldUseEndpointForInstaller(endpoint *PSCEndpoint) bool { return endpoint != nil && endpoint.ClusterUseOnly != nil && !(*endpoint.ClusterUseOnly) } + +// GetCloudEnvironment determines the cloud environment from the project ID format. +// Returns CloudEnvironmentSovereign for sovereign cloud environments, empty string for public GCP. +// Uses known sovereign cloud project ID prefixes to distinguish from organization-scoped +// public GCP projects (orgname:project-id). +func GetCloudEnvironment(projectID string) string { + // Check if project ID has a known sovereign cloud prefix + if strings.Contains(projectID, ":") { + parts := strings.SplitN(projectID, ":", 2) + if len(parts) == 2 && sets.New(sovereignCloudProjectPrefixes...).Has(parts[0]) { + // Known sovereign prefix is definitive - this IS a sovereign cloud project + return CloudEnvironmentSovereign + } + } + + // No known sovereign prefix found + return "" +} From 0d7d068ec53219d9d556430ac606c91b444034b9 Mon Sep 17 00:00:00 2001 From: barbacbd Date: Wed, 24 Jun 2026 15:57:52 -0400 Subject: [PATCH 2/4] gcp: Apply universe domain from credentials to GCP API clients Update credential loading to use CredentialsFromJSONWithParams and FindDefaultCredentialsWithParams, which automatically extract and apply the universe domain from the credentials JSON. Explicitly set the universe domain on all GCP service clients when it differs from the default googleapis.com. This ensures all GCP API calls use the correct endpoints for sovereign cloud environments. Without this, API calls would attempt to use googleapis.com endpoints even when credentials specify a different universe domain, resulting in authentication failures. --- pkg/asset/installconfig/gcp/services.go | 13 +++++++++++++ pkg/asset/installconfig/gcp/session.go | 10 ++++++++-- pkg/types/gcp/platform.go | 7 +++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/pkg/asset/installconfig/gcp/services.go b/pkg/asset/installconfig/gcp/services.go index 38185f135e6..3354bf037bd 100644 --- a/pkg/asset/installconfig/gcp/services.go +++ b/pkg/asset/installconfig/gcp/services.go @@ -12,6 +12,8 @@ import ( "google.golang.org/api/iam/v1" "google.golang.org/api/option" serviceusage "google.golang.org/api/serviceusage/v1beta1" + + gcptypes "github.com/openshift/installer/pkg/types/gcp" ) // ServiceNameGCP is the name of the GCP Service. @@ -69,6 +71,17 @@ func getOptions(ctx context.Context) ([]option.ClientOption, error) { options := []option.ClientOption{ option.WithCredentials(ssn.Credentials), } + + // Explicitly set universe domain from credentials if present + // This is required for sovereign clouds which use a different universe domain + universeDomain, err := ssn.Credentials.GetUniverseDomain() + if err != nil { + return nil, fmt.Errorf("failed to get universe domain from credentials: %w", err) + } + if gcptypes.IsNonDefaultUniverseDomain(universeDomain) { + options = append(options, option.WithUniverseDomain(universeDomain)) + } + return options, nil } diff --git a/pkg/asset/installconfig/gcp/session.go b/pkg/asset/installconfig/gcp/session.go index 431e99fcd70..33ef66c62fc 100644 --- a/pkg/asset/installconfig/gcp/session.go +++ b/pkg/asset/installconfig/gcp/session.go @@ -182,7 +182,10 @@ type contentLoader struct { } func (f *contentLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) { - return googleoauth.CredentialsFromJSON(ctx, []byte(f.content), compute.CloudPlatformScope) + // Use CredentialsFromJSONWithParams to ensure universe domain from credentials is applied + return googleoauth.CredentialsFromJSONWithParams(ctx, []byte(f.content), googleoauth.CredentialsParams{ + Scopes: []string{compute.CloudPlatformScope}, + }) } func (f *contentLoader) String() string { @@ -196,7 +199,10 @@ func (f *contentLoader) Content() string { type cliLoader struct{} func (c *cliLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) { - return googleoauth.FindDefaultCredentials(ctx, compute.CloudPlatformScope) + // Use FindDefaultCredentialsWithParams to ensure universe domain from credentials is applied + return googleoauth.FindDefaultCredentialsWithParams(ctx, googleoauth.CredentialsParams{ + Scopes: []string{compute.CloudPlatformScope}, + }) } func (c *cliLoader) String() string { diff --git a/pkg/types/gcp/platform.go b/pkg/types/gcp/platform.go index c1b40d11312..7cdfa670555 100644 --- a/pkg/types/gcp/platform.go +++ b/pkg/types/gcp/platform.go @@ -253,3 +253,10 @@ func GetCloudEnvironment(projectID string) string { // No known sovereign prefix found return "" } + +// IsNonDefaultUniverseDomain returns true if the universe domain is non-empty +// and not the default "googleapis.com" value. +// This indicates a sovereign cloud or custom universe domain configuration. +func IsNonDefaultUniverseDomain(universeDomain string) bool { + return universeDomain != "" && universeDomain != "googleapis.com" +} From 98b26805b50d5745528cbe58ba52b1bba6b3808d Mon Sep 17 00:00:00 2001 From: barbacbd Date: Wed, 24 Jun 2026 15:57:58 -0400 Subject: [PATCH 3/4] gcp: Set sovereign cloud defaults for machine types Configure default instance and disk types for sovereign cloud regions. Sovereign clouds have limited machine type availability compared to public GCP. Use c3-standard-4 instances and hyperdisk-balanced disks as defaults for sovereign cloud regions (those with 'u-' prefix), as these types are available in sovereign clouds while the standard defaults (n2-standard-4 and pd-ssd) are not. --- pkg/asset/machines/clusterapi.go | 2 +- pkg/asset/machines/master.go | 2 +- pkg/asset/machines/worker.go | 8 ++--- pkg/types/gcp/defaults/machinepool.go | 2 +- pkg/types/gcp/machinepools.go | 42 ++++++++++++++++++++------- 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/pkg/asset/machines/clusterapi.go b/pkg/asset/machines/clusterapi.go index 4a2b0f8a1ab..3a4b04102bf 100644 --- a/pkg/asset/machines/clusterapi.go +++ b/pkg/asset/machines/clusterapi.go @@ -326,7 +326,7 @@ func (c *ClusterAPI) Generate(ctx context.Context, dependencies asset.Parents) e c.FileList = append(c.FileList, azureMachines...) case gcptypes.Name: // Generate GCP master machines using ControPlane machinepool - mpool := defaultGCPMachinePoolPlatform(pool.Architecture) + mpool := defaultGCPMachinePoolPlatform(pool.Architecture, ic.Platform.GCP.ProjectID, ic.Platform.GCP.Region) mpool.Set(ic.Platform.GCP.DefaultMachinePlatform) mpool.Set(pool.Platform.GCP) if len(mpool.Zones) == 0 { diff --git a/pkg/asset/machines/master.go b/pkg/asset/machines/master.go index 9a46ef858f3..6bb68e8f077 100644 --- a/pkg/asset/machines/master.go +++ b/pkg/asset/machines/master.go @@ -278,7 +278,7 @@ func (m *Master) Generate(ctx context.Context, dependencies asset.Parents) error } aws.ConfigMasters(machines, controlPlaneMachineSet, clusterID.InfraID, ic.Publish) case gcptypes.Name: - mpool := defaultGCPMachinePoolPlatform(pool.Architecture) + mpool := defaultGCPMachinePoolPlatform(pool.Architecture, ic.Platform.GCP.ProjectID, ic.Platform.GCP.Region) mpool.Set(ic.Platform.GCP.DefaultMachinePlatform) mpool.Set(pool.Platform.GCP) if len(mpool.Zones) == 0 { diff --git a/pkg/asset/machines/worker.go b/pkg/asset/machines/worker.go index 57b2432f39a..58701eba348 100644 --- a/pkg/asset/machines/worker.go +++ b/pkg/asset/machines/worker.go @@ -141,13 +141,13 @@ func defaultAzureMachinePoolPlatform(env azuretypes.CloudEnvironment) azuretypes } } -func defaultGCPMachinePoolPlatform(arch types.Architecture) gcptypes.MachinePool { - instanceType := icgcp.DefaultInstanceTypeForArch(arch) +func defaultGCPMachinePoolPlatform(arch types.Architecture, projectID, region string) gcptypes.MachinePool { + instanceType := icgcp.DefaultInstanceTypeForArch(projectID, region, arch) return gcptypes.MachinePool{ InstanceType: instanceType, OSDisk: gcptypes.OSDisk{ DiskSizeGB: powerOfTwoRootVolumeSize, - DiskType: gcptypes.DefaultDiskTypeForInstance(instanceType), + DiskType: gcptypes.DefaultDiskTypeForInstanceAndProjectID(instanceType, projectID, region), }, } } @@ -684,7 +684,7 @@ func (w *Worker) Generate(ctx context.Context, dependencies asset.Parents) error } } case gcptypes.Name: - mpool := defaultGCPMachinePoolPlatform(pool.Architecture) + mpool := defaultGCPMachinePoolPlatform(pool.Architecture, ic.Platform.GCP.ProjectID, ic.Platform.GCP.Region) mpool.Set(ic.Platform.GCP.DefaultMachinePlatform) mpool.Set(pool.Platform.GCP) if len(mpool.Zones) == 0 { diff --git a/pkg/types/gcp/defaults/machinepool.go b/pkg/types/gcp/defaults/machinepool.go index 2cfda74ef07..58cae3712e3 100644 --- a/pkg/types/gcp/defaults/machinepool.go +++ b/pkg/types/gcp/defaults/machinepool.go @@ -27,7 +27,7 @@ func SetMachinePoolDefaults(platform *types.Platform, pool *gcp.MachinePool) { if pool.InstanceType != "" && pool.OSDisk.DiskType == "" { family := gcp.GetGCPInstanceFamily(pool.InstanceType) if _, ok := gcp.InstanceTypeToDiskTypeMap[family]; ok { - pool.OSDisk.DiskType = gcp.DefaultDiskTypeForInstance(pool.InstanceType) + pool.OSDisk.DiskType = gcp.DefaultDiskTypeForInstanceAndProjectID(pool.InstanceType, platform.GCP.ProjectID, platform.GCP.Region) } } } diff --git a/pkg/types/gcp/machinepools.go b/pkg/types/gcp/machinepools.go index e7203eac087..001094c886c 100644 --- a/pkg/types/gcp/machinepools.go +++ b/pkg/types/gcp/machinepools.go @@ -372,21 +372,43 @@ func GetGCPInstanceFamily(instanceType string) string { } // DefaultDiskTypeForInstance returns the default disk type for a GCP instance type. If instance type is not -// recognized, pd-ssd is return. +// recognized, pd-ssd is returned. For sovereign cloud instances, hyperdisk-balanced is preferred when available. func DefaultDiskTypeForInstance(instanceType string) string { + return DefaultDiskTypeForInstanceAndProjectID(instanceType, "", "") +} + +// DefaultDiskTypeForInstanceAndProjectID returns the default disk type for a GCP instance type, project ID, and region. +// The cloud environment is automatically detected from the project ID and region. +// For sovereign cloud, hyperdisk-balanced is preferred. For public GCP, pd-ssd is preferred. +func DefaultDiskTypeForInstanceAndProjectID(instanceType, projectID, region string) string { defaultDiskType := PDSSD diskTypes, ok := GetDiskTypes(instanceType) if ok { supportedDiskTypes := sets.New(diskTypes...) - switch { - case supportedDiskTypes.Has(PDSSD): - defaultDiskType = PDSSD - case supportedDiskTypes.Has(HyperDiskBalanced): - defaultDiskType = HyperDiskBalanced - default: - // this shouldn't happen because all supported instance types - // have either pd-ssd or hyperdisk balanced - defaultDiskType = diskTypes[0] + cloudEnv := GetCloudEnvironment(projectID) + + // Sovereign cloud prefers hyperdisk-balanced + if cloudEnv == CloudEnvironmentSovereign { + switch { + case supportedDiskTypes.Has(HyperDiskBalanced): + defaultDiskType = HyperDiskBalanced + case supportedDiskTypes.Has(PDSSD): + defaultDiskType = PDSSD + default: + defaultDiskType = diskTypes[0] + } + } else { + // Public GCP prefers pd-ssd + switch { + case supportedDiskTypes.Has(PDSSD): + defaultDiskType = PDSSD + case supportedDiskTypes.Has(HyperDiskBalanced): + defaultDiskType = HyperDiskBalanced + default: + // this shouldn't happen because all supported instance types + // have either pd-ssd or hyperdisk balanced + defaultDiskType = diskTypes[0] + } } } return defaultDiskType From 87ade03f4e942bf3d1d05b65c4bd4aaf8fd17b72 Mon Sep 17 00:00:00 2001 From: barbacbd Date: Wed, 24 Jun 2026 20:21:10 -0400 Subject: [PATCH 4/4] gcp: Set universe domain environment variable for CAPI provider The GCP Cluster API provider needs the GOOGLE_CLOUD_UNIVERSE_DOMAIN environment variable set to use the correct API endpoints in sovereign cloud environments. Without this, the CAPI provider attempts to use the default googleapis.com endpoints even when credentials contain a different universe domain, resulting in errors like: the configured universe domain ("googleapis.com") does not match the universe domain found in the credentials ("apis-berlin-build0.goog") Extract the universe domain from the GCP session credentials and set it as an environment variable for the CAPG infrastructure controller when it differs from the default googleapis.com. Also update universe domain validation to make it optional - users can provide it explicitly in install-config, or let the GCP SDK extract it from credentials automatically. --- pkg/asset/installconfig/gcp/session.go | 5 ++++- pkg/clusterapi/system.go | 12 ++++++++++++ pkg/types/gcp/validation/platform.go | 20 ++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pkg/asset/installconfig/gcp/session.go b/pkg/asset/installconfig/gcp/session.go index 33ef66c62fc..6aa3aa42484 100644 --- a/pkg/asset/installconfig/gcp/session.go +++ b/pkg/asset/installconfig/gcp/session.go @@ -182,7 +182,10 @@ type contentLoader struct { } func (f *contentLoader) Load(ctx context.Context) (*googleoauth.Credentials, error) { - // Use CredentialsFromJSONWithParams to ensure universe domain from credentials is applied + // Use CredentialsFromJSONWithParams to ensure universe domain from credentials is applied. + // This function automatically extracts the universe domain from the credentials JSON. + // Note: CredentialsFromJSONWithTypeAndParams would be preferred but is not available + // in the current vendored version of golang.org/x/oauth2/google. return googleoauth.CredentialsFromJSONWithParams(ctx, []byte(f.content), googleoauth.CredentialsParams{ Scopes: []string{compute.CloudPlatformScope}, }) diff --git a/pkg/clusterapi/system.go b/pkg/clusterapi/system.go index f991d0f54ab..32321ade4f5 100644 --- a/pkg/clusterapi/system.go +++ b/pkg/clusterapi/system.go @@ -307,6 +307,18 @@ func (c *system) Run(ctx context.Context) error { //nolint:gocyclo gAppCredEnvVar: session.Path, } + // Set universe domain for sovereign cloud environments + // The universe domain is extracted from the credentials and must be explicitly + // set for the CAPI provider to use the correct GCP API endpoints + universeDomain, err := session.Credentials.GetUniverseDomain() + if err != nil { + return fmt.Errorf("failed to get universe domain from GCP credentials: %w", err) + } + if gcp.IsNonDefaultUniverseDomain(universeDomain) { + capgEnvVars["GOOGLE_CLOUD_UNIVERSE_DOMAIN"] = universeDomain + logrus.Infof("setting GOOGLE_CLOUD_UNIVERSE_DOMAIN to %s for capg infrastructure controller", universeDomain) + } + if v, ok := capgEnvVars[gAppCredEnvVar]; ok { logrus.Infof("setting %q to %s for capg infrastructure controller", gAppCredEnvVar, v) } diff --git a/pkg/types/gcp/validation/platform.go b/pkg/types/gcp/validation/platform.go index 6ad20026b68..1ba5c8c0ffb 100644 --- a/pkg/types/gcp/validation/platform.go +++ b/pkg/types/gcp/validation/platform.go @@ -92,6 +92,8 @@ func ValidatePlatform(p *gcp.Platform, fldPath *field.Path, ic *types.InstallCon if p.Region == "" { allErrs = append(allErrs, field.Required(fldPath.Child("region"), "must provide a region")) } + cloudEnv := gcp.GetCloudEnvironment(p.ProjectID) + allErrs = append(allErrs, validateUniverseDomain(p.UniverseDomain, cloudEnv, fldPath.Child("universeDomain"))...) if p.DefaultMachinePlatform != nil { allErrs = append(allErrs, ValidateMachinePool(p, p.DefaultMachinePlatform, fldPath.Child("defaultMachinePlatform"))...) allErrs = append(allErrs, ValidateDefaultDiskType(p.DefaultMachinePlatform, fldPath.Child("defaultMachinePlatform"))...) @@ -194,3 +196,21 @@ func validateLabel(key, value string) error { } return nil } + +// validateUniverseDomain validates universe domain usage. +// Universe domain is optional - it can be set for sovereign clouds when explicitly +// provided by the user. If not provided, the GCP SDK will extract it from credentials. +// Note: Format validation is intentionally not performed here. The GCP SDK will +// validate the actual domain format when making API calls. +func validateUniverseDomain(universeDomain, cloudEnvironment string, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // Universe domain is optional. If provided for non-sovereign cloud, warn the user + // that this is unusual but don't prevent it (they may know something we don't). + if universeDomain != "" && cloudEnvironment != gcp.CloudEnvironmentSovereign { + allErrs = append(allErrs, field.Invalid(fldPath, universeDomain, + "universe domain is typically only needed for sovereign cloud environments (project IDs with colon prefix)")) + } + + return allErrs +}