Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions data/data/install.openshift.io_installconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions pkg/asset/installconfig/gcp/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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 universeDomain != "" && universeDomain != "googleapis.com" {
options = append(options, option.WithUniverseDomain(universeDomain))
}

return options, nil
}

Expand Down
13 changes: 11 additions & 2 deletions pkg/asset/installconfig/gcp/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,13 @@ 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.
// 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{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think CredentialsFromJSON internally calls CredentialsFromJSONWithParams already, right?

Scopes: []string{compute.CloudPlatformScope},
})
}

func (f *contentLoader) String() string {
Expand All @@ -196,7 +202,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 {
Expand Down
20 changes: 13 additions & 7 deletions pkg/asset/installconfig/gcp/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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 != "" {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/asset/machines/clusterapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/asset/machines/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions pkg/asset/machines/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
}
}
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions pkg/clusterapi/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 universeDomain != "" && universeDomain != "googleapis.com" {
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)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/types/gcp/defaults/machinepool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
42 changes: 32 additions & 10 deletions pkg/types/gcp/machinepools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pkg/types/gcp/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
62 changes: 61 additions & 1 deletion pkg/types/gcp/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package gcp

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/util/sets"

"github.com/openshift/installer/pkg/types/dns"
)
Expand All @@ -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: <prefix>:<project-id>
// 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.
Expand Down Expand Up @@ -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"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you need another go generate ./pkg/types/installconfig.go :D

}

// UserLabel is a label to apply to GCP resources created for the cluster.
Expand Down Expand Up @@ -184,12 +205,51 @@ 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
// installer.
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, ":") {
Comment thread
barbacbd marked this conversation as resolved.
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 ""
}
20 changes: 20 additions & 0 deletions pkg/types/gcp/validation/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))...)
Expand Down Expand Up @@ -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
}