diff --git a/pkg/types/gcp/platform.go b/pkg/types/gcp/platform.go index 5ce97983e1a..6db3e8845eb 100644 --- a/pkg/types/gcp/platform.go +++ b/pkg/types/gcp/platform.go @@ -6,6 +6,23 @@ import ( "github.com/openshift/installer/pkg/types/dns" ) +const ( + // CloudResourceManagerServiceName is the name and internal key for the cloud resource manager API endpoint + CloudResourceManagerServiceName = "cloudresourcemanager" + // ComputeServiceName is the name and internal key for the compute API endpoint + ComputeServiceName = "compute" + // DNSServiceName is the name and internal key for the DNS API endpoint + DNSServiceName = "dns" + // FileServiceName is the name and internal key for the file API endpoint + FileServiceName = "file" + // IAMServiceName is the name and internal key for the IAM API endpoint + IAMServiceName = "iam" + // ServiceUsageServiceName is the name and internal key for the service usage API endpoint + ServiceUsageServiceName = "serviceusage" + // StorageServiceName is the name and internal key for the storage API endpoint + StorageServiceName = "storage" +) + // Platform stores all the global configuration that all machinesets // use. type Platform struct { @@ -58,6 +75,27 @@ type Platform struct { // +default="Disabled" // +kubebuilder:validation:Enum="Enabled";"Disabled" UserProvisionedDNS dns.UserProvisionedDNS `json:"userProvisionedDNS,omitempty"` + + // ServiceEndpoints list contains custom endpoints which will override default + // service endpoint of GCP Services. + // There must be only one ServiceEndpoint for a service. + // +optional + ServiceEndpoints []ServiceEndpoint `json:"serviceEndpoints,omitempty"` +} + +// ServiceEndpoint store the configuration for services to +// override existing defaults of GCP Services. +type ServiceEndpoint struct { + // Name is the name of the GCP service. + // This must be provided and cannot be empty. + Name string `json:"name"` + + // URL is fully qualified URI with scheme https, that overrides the default generated + // endpoint for a client. + // This must be provided and cannot be empty. + // + // +kubebuilder:validation:Pattern=`^https://` + URL string `json:"url"` } // UserLabel is a label to apply to GCP resources created for the cluster. diff --git a/pkg/types/gcp/validation/platform.go b/pkg/types/gcp/validation/platform.go index 2ef83c77e58..bdaa4b97921 100644 --- a/pkg/types/gcp/validation/platform.go +++ b/pkg/types/gcp/validation/platform.go @@ -2,9 +2,11 @@ package validation import ( "fmt" + "net/url" "regexp" "sort" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" "github.com/openshift/installer/pkg/types" @@ -76,6 +78,16 @@ var ( // userLabelKeyPrefixRegex is for verifying that the label key does not contain restricted prefixes. userLabelKeyPrefixRegex = regexp.MustCompile(`^(?i)(kubernetes\-io|openshift\-io)`) + + supportedEndpointNames = sets.New( + gcp.CloudResourceManagerServiceName, + gcp.ComputeServiceName, + gcp.DNSServiceName, + gcp.FileServiceName, + gcp.IAMServiceName, + gcp.ServiceUsageServiceName, + gcp.StorageServiceName, + ) ) const ( @@ -118,6 +130,7 @@ func ValidatePlatform(p *gcp.Platform, fldPath *field.Path, ic *types.InstallCon // check if configured userLabels are valid. allErrs = append(allErrs, validateUserLabels(p.UserLabels, fldPath.Child("userLabels"))...) + allErrs = append(allErrs, validateServiceEndpoints(p.ServiceEndpoints, fldPath.Child("serviceEndpoints"))...) return allErrs } @@ -161,3 +174,49 @@ func validateLabel(key, value string) error { } return nil } + +func validateServiceEndpoints(endpoints []gcp.ServiceEndpoint, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + tracker := map[string]int{} + for idx, e := range endpoints { + fldp := fldPath.Index(idx) + if !supportedEndpointNames.Has(e.Name) { + allErrs = append(allErrs, field.NotSupported(fldp.Child("name"), e.Name, sets.List(supportedEndpointNames))) + } + if _, ok := tracker[e.Name]; ok { + allErrs = append(allErrs, field.Duplicate(fldp.Child("name"), e.Name)) + } else { + tracker[e.Name] = idx + } + + if err := validateServiceURL(e.URL); err != nil { + allErrs = append(allErrs, field.Invalid(fldp.Child("url"), e.URL, err.Error())) + } + } + return allErrs +} + +var schemeRE = regexp.MustCompile("^([^:]+)://") + +func validateServiceURL(uri string) error { + endpoint := uri + if !schemeRE.MatchString(endpoint) { + scheme := "https" + endpoint = fmt.Sprintf("%s://%s", scheme, endpoint) + } + + u, err := url.Parse(endpoint) + if err != nil { + return err + } + if u.Hostname() == "" { + return fmt.Errorf("host cannot be empty, empty host provided") + } + if s := u.Scheme; s != "https" { + return fmt.Errorf("invalid scheme %s, only https allowed", s) + } + // Unlike AWS, the format can include a path without request parameters see + // https://cloud.google.com/storage/docs/request-endpoints as an example. + + return nil +} diff --git a/pkg/types/gcp/validation/platform_test.go b/pkg/types/gcp/validation/platform_test.go index 88fde768b05..4359c8da2c1 100644 --- a/pkg/types/gcp/validation/platform_test.go +++ b/pkg/types/gcp/validation/platform_test.go @@ -153,6 +153,88 @@ func TestValidatePlatform(t *testing.T) { credentialsMode: types.MintCredentialsMode, valid: false, }, + { + name: "invalid gcp endpoint blank name", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "", + URL: "https://my-custom-endpoint.example.com/copmute/v1/", + }, + }, + }, + valid: false, + }, + { + name: "invalid gcp endpoint invalid name", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "badname", + URL: "https://my-custom-endpoint.example.com/copmute/v1/", + }, + }, + }, + valid: false, + }, + { + name: "invalid gcp endpoint duplicate name", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "compute", + URL: "https://my-custom-endpoint.example.com/compute/v1/", + }, + { + Name: "compute", + URL: "https://my-custom-endpoint.example.com/compute/v2/", + }, + }, + }, + valid: false, + }, + { + name: "invalid gcp endpoint url blank", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "compute", + URL: "", + }, + }, + }, + valid: false, + }, + { + name: "invalid scheme gcp endpoint url", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "compute", + URL: "http://my-custom-endpoint.example.com/compute/v1/", + }, + }, + }, + valid: false, + }, + { + name: "valid gcp endpoint", + platform: &gcp.Platform{ + Region: "us-east1", + ServiceEndpoints: []gcp.ServiceEndpoint{ + { + Name: "compute", + URL: "https://my-custom-endpoint.example.com/compute/v1/", + }, + }, + }, + valid: true, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) {