diff --git a/data/data/install.openshift.io_installconfigs.yaml b/data/data/install.openshift.io_installconfigs.yaml index b2c3c29a989..ed3435b9e27 100644 --- a/data/data/install.openshift.io_installconfigs.yaml +++ b/data/data/install.openshift.io_installconfigs.yaml @@ -3199,6 +3199,81 @@ spec: description: Region specifies the GCP region where the cluster will be created. type: string + userLabels: + description: userLabels has additional keys and values that the + installer will add as labels to all resources that it creates + on GCP. Resources created by the cluster itself may not include + these labels. This is a TechPreview feature and requires setting + CustomNoUpgrade featureSet with GCPLabelsTags featureGate enabled + or TechPreviewNoUpgrade featureSet to configure labels. + items: + description: UserLabel is a label to apply to GCP resources + created for the cluster. + properties: + key: + description: key is the key part of the label. A label key + can have a maximum of 63 characters and cannot be empty. + Label must begin with a lowercase letter, and must contain + only lowercase letters, numeric characters, and the following + special characters `_-`. + type: string + value: + description: value is the value part of the label. A label + value can have a maximum of 63 characters and cannot be + empty. Value must contain only lowercase letters, numeric + characters, and the following special characters `_-`. + type: string + required: + - key + - value + type: object + type: array + userTags: + description: userTags has additional keys and values that the + installer will add as tags to all resources that it creates + on GCP. Resources created by the cluster itself may not include + these tags. Tag key and tag value should be the shortnames of + the tag key and tag value resource. This is a TechPreview feature + and requires setting CustomNoUpgrade featureSet with GCPLabelsTags + featureGate enabled or TechPreviewNoUpgrade featureSet to configure + tags. + items: + description: UserTag is a tag to apply to GCP resources created + for the cluster. + properties: + key: + description: key is the key part of the tag. A tag key can + have a maximum of 63 characters and cannot be empty. Tag + key must begin and end with an alphanumeric character, + and must contain only uppercase, lowercase alphanumeric + characters, and the following special characters `._-`. + type: string + parentID: + description: 'parentID is the ID of the hierarchical resource + where the tags are defined, e.g. at the Organization or + the Project level. To find the Organization ID or Project + ID refer to the following pages: https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id, + https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects. + An OrganizationID must consist of decimal numbers, and + cannot have leading zeroes. A ProjectID must be 6 to 30 + characters in length, can only contain lowercase letters, + numbers, and hyphens, and must start with a letter, and + cannot end with a hyphen.' + type: string + value: + description: value is the value part of the tag. A tag value + can have a maximum of 63 characters and cannot be empty. + Tag value must begin and end with an alphanumeric character, + and must contain only uppercase, lowercase alphanumeric + characters, and the following special characters `_-.@%=+:,*#&(){}[]` + and spaces. + type: string + required: + - key + - parentID + - value + type: object + type: array required: - projectID - region diff --git a/pkg/types/gcp/platform.go b/pkg/types/gcp/platform.go index 06829d4724e..319b0fda682 100644 --- a/pkg/types/gcp/platform.go +++ b/pkg/types/gcp/platform.go @@ -42,4 +42,56 @@ type Platform struct { // such as the current env OPENSHIFT_INSTALL_OS_IMAGE_OVERRIDE // +optional Licenses []string `json:"licenses,omitempty"` + + // userLabels has additional keys and values that the installer will add as + // labels to all resources that it creates on GCP. Resources created by the + // cluster itself may not include these labels. This is a TechPreview feature + // and requires setting CustomNoUpgrade featureSet with GCPLabelsTags featureGate + // enabled or TechPreviewNoUpgrade featureSet to configure labels. + UserLabels []UserLabel `json:"userLabels,omitempty"` + + // userTags has additional keys and values that the installer will add as + // tags to all resources that it creates on GCP. Resources created by the + // cluster itself may not include these tags. Tag key and tag value should + // be the shortnames of the tag key and tag value resource. This is a TechPreview + // feature and requires setting CustomNoUpgrade featureSet with GCPLabelsTags + // featureGate enabled or TechPreviewNoUpgrade featureSet to configure tags. + UserTags []UserTag `json:"userTags,omitempty"` +} + +// UserLabel is a label to apply to GCP resources created for the cluster. +type UserLabel struct { + // key is the key part of the label. A label key can have a maximum of 63 characters + // and cannot be empty. Label must begin with a lowercase letter, and must contain + // only lowercase letters, numeric characters, and the following special characters `_-`. + Key string `json:"key"` + + // value is the value part of the label. A label value can have a maximum of 63 characters + // and cannot be empty. Value must contain only lowercase letters, numeric characters, and + // the following special characters `_-`. + Value string `json:"value"` +} + +// UserTag is a tag to apply to GCP resources created for the cluster. +type UserTag struct { + // parentID is the ID of the hierarchical resource where the tags are defined, + // e.g. at the Organization or the Project level. To find the Organization ID or Project ID refer to the following pages: + // https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id, + // https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects. + // An OrganizationID must consist of decimal numbers, and cannot have leading zeroes. + // A ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, + // numbers, and hyphens, and must start with a letter, and cannot end with a hyphen. + ParentID string `json:"parentID"` + + // key is the key part of the tag. A tag key can have a maximum of 63 characters and + // cannot be empty. Tag key must begin and end with an alphanumeric character, and + // must contain only uppercase, lowercase alphanumeric characters, and the following + // special characters `._-`. + Key string `json:"key"` + + // value is the value part of the tag. A tag value can have a maximum of 63 characters + // and cannot be empty. Tag value must begin and end with an alphanumeric character, and + // must contain only uppercase, lowercase alphanumeric characters, and the following + // special characters `_-.@%=+:,*#&(){}[]` and spaces. + Value string `json:"value"` } diff --git a/pkg/types/gcp/validation/platform.go b/pkg/types/gcp/validation/platform.go index 4a3faf9d828..3a7a3a689fd 100644 --- a/pkg/types/gcp/validation/platform.go +++ b/pkg/types/gcp/validation/platform.go @@ -1,7 +1,9 @@ package validation import ( + "fmt" "os" + "regexp" "sort" "k8s.io/apimachinery/pkg/util/validation/field" @@ -65,8 +67,34 @@ var ( sort.Strings(validValues) return validValues }() + + // userLabelKeyRegex is for verifying that the label key contains only allowed characters. + userLabelKeyRegex = regexp.MustCompile(`^[a-z][0-9a-z_-]{1,62}$`) + + // userLabelValueRegex is for verifying that the label value contains only allowed characters. + userLabelValueRegex = regexp.MustCompile(`^[0-9a-z_-]{1,63}$`) + + // userLabelKeyPrefixRegex is for verifying that the label key does not contain restricted prefixes. + userLabelKeyPrefixRegex = regexp.MustCompile(`^(?i)(kubernetes\-io|openshift\-io)`) + + // userTagKeyRegex is for verifying that the tag key contains only allowed characters. + userTagKeyRegex = regexp.MustCompile(`^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$`) + + // userTagValueRegex is for verifying that the tag value contains only allowed characters. + userTagValueRegex = regexp.MustCompile(`^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$`) + + // userTagParentIDRegex is for verifying that the tag parentID contains only allowed characters. + userTagParentIDRegex = regexp.MustCompile(`(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)`) ) +// maxUserLabelLimit is the maximum userLabels that can be configured as defined in openshift/api. +// https://github.com/openshift/api/commit/ae73a19d05c35068af16c9aeff375d0b7c936a8a#diff-07b264a49084976b670fb699badaca1795027d6ea732a99226a5388104f6174fR592-R602 +const maxUserLabelLimit = 32 + +// maxUserTagLimit is the maximum userTags that can be configured as defined in openshift/api. +// https://github.com/openshift/api/commit/ae73a19d05c35068af16c9aeff375d0b7c936a8a#diff-07b264a49084976b670fb699badaca1795027d6ea732a99226a5388104f6174fR604-R613 +const maxUserTagLimit = 50 + // ValidatePlatform checks that the specified platform is valid. func ValidatePlatform(p *gcp.Platform, fldPath *field.Path, ic *types.InstallConfig) field.ErrorList { allErrs := field.ErrorList{} @@ -109,5 +137,94 @@ 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"))...) + + // check if configured userTags are valid. + allErrs = append(allErrs, validateUserTags(p.UserTags, fldPath.Child("userTags"))...) + return allErrs } + +// validateUserLabels verifies if configured number of UserLabels is not more than +// allowed limit and the label keys and values are valid. +func validateUserLabels(labels []gcp.UserLabel, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(labels) == 0 { + return allErrs + } + + if len(labels) > maxUserLabelLimit { + allErrs = append(allErrs, field.TooMany(fldPath, len(labels), maxUserLabelLimit)) + } + + for _, label := range labels { + if err := validateLabel(label.Key, label.Value); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Key(label.Key), label.Value, err.Error())) + } + } + return allErrs +} + +// validateLabel checks the following to ensure that the label configured is acceptable. +// - The key and value contain only allowed characters. +// - The key is not empty and at most 63 characters and starts with a lowercase letter. +// - The value is not empty and at most 63 characters. +// - The key and value must contain only lowercase letters, numeric characters, +// underscores, and dashes. +// - The key cannot be Name or have kubernetes.io, openshift.io prefixes. +func validateLabel(key, value string) error { + if !userLabelKeyRegex.MatchString(key) { + return fmt.Errorf("label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`") + } + if !userLabelValueRegex.MatchString(value) { + return fmt.Errorf("label value is invalid or contains invalid characters. Label value can have a maximum of 63 characters and cannot be empty. Value must contain only lowercase letters, numeric characters, and the following special characters `_-`") + } + if userLabelKeyPrefixRegex.MatchString(key) { + return fmt.Errorf("label key contains restricted prefix. Label key cannot have `kubernetes-io`, `openshift-io` prefixes") + } + return nil +} + +// validateUserTags verifies if configured number of UserTags is not more than +// allowed limit and the tag keys and values are valid. +func validateUserTags(tags []gcp.UserTag, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(tags) == 0 { + return allErrs + } + + if len(tags) > maxUserTagLimit { + allErrs = append(allErrs, field.TooMany(fldPath, len(tags), maxUserTagLimit)) + } + + for _, tag := range tags { + if err := validateTag(tag.ParentID, tag.Key, tag.Value); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Key(tag.Key), tag.Value, err.Error())) + } + } + + return allErrs +} + +// validateTag checks the following to ensure that the tag configured is acceptable. Though +// the criteria is for tag resources to pre-exist, tags will be validated to catch the +// error much earlier. +// - The key and value contain only allowed characters. +// - The key and value is not empty and can have at most 63 characters. +// - The ParentID can be either OrganizationID or ProjectID. +// - OrganizationID must consist of decimal numbers, and cannot have leading zeroes. +// - ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, +// and hyphens, and must start with a letter, and cannot end with a hyphen. +func validateTag(parentID, key, value string) error { + if !userTagParentIDRegex.MatchString(parentID) { + return fmt.Errorf("tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen") + } + if !userTagKeyRegex.MatchString(key) { + return fmt.Errorf("tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`") + } + if !userTagValueRegex.MatchString(value) { + return fmt.Errorf("tag value is invalid or contains invalid characters. Tag value can have a maximum of 63 characters and cannot be empty. Tag value must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `_-.@%%=+:,*#&(){}[]` and spaces") + } + return nil +} diff --git a/pkg/types/gcp/validation/platform_test.go b/pkg/types/gcp/validation/platform_test.go index 269876f1271..a9ac80ec263 100644 --- a/pkg/types/gcp/validation/platform_test.go +++ b/pkg/types/gcp/validation/platform_test.go @@ -1,6 +1,7 @@ package validation import ( + "fmt" "testing" "github.com/stretchr/testify/assert" @@ -175,3 +176,301 @@ func TestValidatePlatform(t *testing.T) { }) } } + +func TestValidateUserLabels(t *testing.T) { + fieldPath := "spec.platform.gcp.UserLabels" + cases := []struct { + name string + userLabels []gcp.UserLabel + expectedErr string + }{ + { + name: "userLabels not configured", + userLabels: nil, + expectedErr: "[]", + }, + { + name: "userLabels configured", + userLabels: []gcp.UserLabel{ + {Key: "key1", Value: "value1"}, + {Key: "key_2", Value: "value_2"}, + {Key: "key-3", Value: "value-3"}, + {Key: "key4_", Value: "value4_"}, + {Key: "key5-", Value: "value5-"}, + }, + expectedErr: "[]", + }, + { + name: "userLabels configured is more than max limit", + userLabels: []gcp.UserLabel{ + {Key: "key11", Value: "value11"}, {Key: "key18", Value: "value18"}, + {Key: "key19", Value: "value19"}, {Key: "key21", Value: "value21"}, + {Key: "key14", Value: "value14"}, {Key: "key22", Value: "value22"}, + {Key: "key25", Value: "value25"}, {Key: "key27", Value: "value27"}, + {Key: "key31", Value: "value31"}, {Key: "key9", Value: "value9"}, + {Key: "key10", Value: "value10"}, {Key: "key15", Value: "value15"}, + {Key: "key28", Value: "value28"}, {Key: "key29", Value: "value29"}, + {Key: "key32", Value: "value32"}, {Key: "key3", Value: "value3"}, + {Key: "key7", Value: "value7"}, {Key: "key17", Value: "value17"}, + {Key: "key20", Value: "value20"}, {Key: "key4", Value: "value4"}, + {Key: "key23", Value: "value23"}, {Key: "key26", Value: "value26"}, + {Key: "key12", Value: "value12"}, {Key: "key33", Value: "value33"}, + {Key: "key1", Value: "value1"}, {Key: "key2", Value: "value2"}, + {Key: "key5", Value: "value5"}, {Key: "key8", Value: "value8"}, + {Key: "key30", Value: "value30"}, {Key: "key6", Value: "value6"}, + {Key: "key13", Value: "value13"}, {Key: "key16", Value: "value16"}, + {Key: "key24", Value: "value24"}, + }, + expectedErr: "[spec.platform.gcp.UserLabels: Too many: 33: must have at most 32 items]", + }, + { + name: "userLabels contains key starting a number", + userLabels: []gcp.UserLabel{{Key: "1key", Value: "1value"}}, + expectedErr: "[spec.platform.gcp.UserLabels[1key]: Invalid value: \"1value\": label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains key starting a uppercase letter", + userLabels: []gcp.UserLabel{{Key: "Key", Value: "1value"}}, + expectedErr: "[spec.platform.gcp.UserLabels[Key]: Invalid value: \"1value\": label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains empty key", + userLabels: []gcp.UserLabel{{Key: "", Value: "value"}}, + expectedErr: "[spec.platform.gcp.UserLabels[]: Invalid value: \"value\": label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains key length greater than 63", + userLabels: []gcp.UserLabel{ + { + Key: "thisisaverylongkeywithmorethan63characterswhichisnotallowedforgcpresourcelabelkey", + Value: "value", + }, + }, + expectedErr: "[spec.platform.gcp.UserLabels[thisisaverylongkeywithmorethan63characterswhichisnotallowedforgcpresourcelabelkey]: Invalid value: \"value\": label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains key with invalid character", + userLabels: []gcp.UserLabel{{Key: "key/test", Value: "value"}}, + expectedErr: "[spec.platform.gcp.UserLabels[key/test]: Invalid value: \"value\": label key is invalid or contains invalid characters. Label key can have a maximum of 63 characters and cannot be empty. Label key must begin with a lowercase letter, and must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains value length greater than 63", + userLabels: []gcp.UserLabel{ + { + Key: "key", + Value: "thisisaverylongvaluewithmorethan63characterswhichisnotallowedforgcpresourcelabelvalue", + }, + }, + expectedErr: "[spec.platform.gcp.UserLabels[key]: Invalid value: \"thisisaverylongvaluewithmorethan63characterswhichisnotallowedforgcpresourcelabelvalue\": label value is invalid or contains invalid characters. Label value can have a maximum of 63 characters and cannot be empty. Value must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains empty value", + userLabels: []gcp.UserLabel{{Key: "key", Value: ""}}, + expectedErr: "[spec.platform.gcp.UserLabels[key]: Invalid value: \"\": label value is invalid or contains invalid characters. Label value can have a maximum of 63 characters and cannot be empty. Value must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains value with invalid character", + userLabels: []gcp.UserLabel{{Key: "key", Value: "value*^%"}}, + expectedErr: "[spec.platform.gcp.UserLabels[key]: Invalid value: \"value*^%\": label value is invalid or contains invalid characters. Label value can have a maximum of 63 characters and cannot be empty. Value must contain only lowercase letters, numeric characters, and the following special characters `_-`]", + }, + { + name: "userLabels contains key with prefix kubernetes-io", + userLabels: []gcp.UserLabel{{Key: "kubernetes-io_cluster", Value: "value"}}, + expectedErr: "[spec.platform.gcp.UserLabels[kubernetes-io_cluster]: Invalid value: \"value\": label key contains restricted prefix. Label key cannot have `kubernetes-io`, `openshift-io` prefixes]", + }, + { + name: "userLabels contains allowed key prefix for_openshift-io", + userLabels: []gcp.UserLabel{{Key: "for_openshift-io", Value: "gcp"}}, + expectedErr: "[]", + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + err := validateUserLabels(tt.userLabels, field.NewPath(fieldPath)) + if fmt.Sprintf("%v", err) != tt.expectedErr { + t.Errorf("Got: %+v Want: %+v", err, tt.expectedErr) + } + }) + } +} + +func TestValidateUserTags(t *testing.T) { + fieldPath := "spec.platform.gcp.userTags" + cases := []struct { + name string + userTags []gcp.UserTag + expectedErr string + }{ + { + name: "userTags not configured", + userTags: []gcp.UserTag{}, + expectedErr: "[]", + }, + { + name: "userTags configured", + userTags: []gcp.UserTag{ + {ParentID: "1234567890", Key: "key_2", Value: "value_2"}, + {ParentID: "test-project-123", Key: "key.gcp", Value: "value.3"}, + {ParentID: "1234567890", Key: "keY", Value: "value"}, + {ParentID: "test-project-123", Key: "thisisalongkeywithinlimitof63_characters-whichisallowedfortags", Value: "value"}, + {ParentID: "1234567890", Key: "KEY4", Value: "hisisavaluewithin-63characters_{[(.@%=+: ,*#&)]}forgcptagvalue"}, + {ParentID: "test-project-123", Key: "key1", Value: "value1"}, + }, + expectedErr: "[]", + }, + { + name: "userTags configured is more than max limit", + userTags: []gcp.UserTag{ + {ParentID: "1234567890", Key: "key29", Value: "value29"}, + {ParentID: "test-project-123", Key: "key33", Value: "value33"}, + {ParentID: "1234567890", Key: "key39", Value: "value39"}, + {ParentID: "test-project-123", Key: "key43", Value: "value43"}, + {ParentID: "1234567890", Key: "key5", Value: "value5"}, + {ParentID: "test-project-123", Key: "key6", Value: "value6"}, + {ParentID: "1234567890", Key: "key14", Value: "value14"}, + {ParentID: "test-project-123", Key: "key25", Value: "value25"}, + {ParentID: "1234567890", Key: "key20", Value: "value20"}, + {ParentID: "test-project-123", Key: "key24", Value: "value24"}, + {ParentID: "1234567890", Key: "key40", Value: "value40"}, + {ParentID: "test-project-123", Key: "key46", Value: "value46"}, + {ParentID: "1234567890", Key: "key1", Value: "value1"}, + {ParentID: "test-project-123", Key: "key2", Value: "value2"}, + {ParentID: "1234567890", Key: "key4", Value: "value4"}, + {ParentID: "test-project-123", Key: "key10", Value: "value10"}, + {ParentID: "1234567890", Key: "key51", Value: "value51"}, + {ParentID: "test-project-123", Key: "key8", Value: "value8"}, + {ParentID: "1234567890", Key: "key13", Value: "value13"}, + {ParentID: "test-project-123", Key: "key44", Value: "value44"}, + {ParentID: "1234567890", Key: "key48", Value: "value48"}, + {ParentID: "test-project-123", Key: "key9", Value: "value9"}, + {ParentID: "1234567890", Key: "key17", Value: "value17"}, + {ParentID: "test-project-123", Key: "key18", Value: "value18"}, + {ParentID: "1234567890", Key: "key30", Value: "value30"}, + {ParentID: "test-project-123", Key: "key36", Value: "value36"}, + {ParentID: "1234567890", Key: "key49", Value: "value49"}, + {ParentID: "test-project-123", Key: "key7", Value: "value7"}, + {ParentID: "1234567890", Key: "key15", Value: "value15"}, + {ParentID: "test-project-123", Key: "key22", Value: "value22"}, + {ParentID: "1234567890", Key: "key34", Value: "value34"}, + {ParentID: "test-project-123", Key: "key37", Value: "value37"}, + {ParentID: "1234567890", Key: "key38", Value: "value38"}, + {ParentID: "test-project-123", Key: "key47", Value: "value47"}, + {ParentID: "1234567890", Key: "key12", Value: "value12"}, + {ParentID: "test-project-123", Key: "key16", Value: "value16"}, + {ParentID: "1234567890", Key: "key23", Value: "value23"}, + {ParentID: "test-project-123", Key: "key28", Value: "value28"}, + {ParentID: "1234567890", Key: "key50", Value: "value50"}, + {ParentID: "test-project-123", Key: "key21", Value: "value21"}, + {ParentID: "1234567890", Key: "key26", Value: "value26"}, + {ParentID: "test-project-123", Key: "key35", Value: "value35"}, + {ParentID: "1234567890", Key: "key42", Value: "value42"}, + {ParentID: "test-project-123", Key: "key31", Value: "value31"}, + {ParentID: "1234567890", Key: "key32", Value: "value32"}, + {ParentID: "test-project-123", Key: "key41", Value: "value41"}, + {ParentID: "1234567890", Key: "key45", Value: "value45"}, + {ParentID: "test-project-123", Key: "key3", Value: "value3"}, + {ParentID: "1234567890", Key: "key11", Value: "value11"}, + {ParentID: "test-project-123", Key: "key19", Value: "value19"}, + {ParentID: "1234567890", Key: "key27", Value: "value27"}, + }, + expectedErr: "[spec.platform.gcp.userTags: Too many: 51: must have at most 50 items]", + }, + { + name: "userTags contains key starting with a special character", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "_key", Value: "1value"}}, + expectedErr: "[spec.platform.gcp.userTags[_key]: Invalid value: \"1value\": tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`]", + }, + { + name: "userTags contains key ending with a special character", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "key@", Value: "1value"}}, + expectedErr: "[spec.platform.gcp.userTags[key@]: Invalid value: \"1value\": tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`]", + }, + { + name: "userTags contains empty key", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "", Value: "value"}}, + expectedErr: "[spec.platform.gcp.userTags[]: Invalid value: \"value\": tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`]", + }, + { + name: "userTags contains key length greater than 63", + userTags: []gcp.UserTag{ + { + ParentID: "1234567890", + Key: "thisisalongkeyforlimitof63_characters-whichisnotallowedfortagkey", + Value: "value", + }, + }, + expectedErr: "[spec.platform.gcp.userTags[thisisalongkeyforlimitof63_characters-whichisnotallowedfortagkey]: Invalid value: \"value\": tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`]", + }, + { + name: "userTags contains key with invalid character", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "key/test", Value: "value"}}, + expectedErr: "[spec.platform.gcp.userTags[key/test]: Invalid value: \"value\": tag key is invalid or contains invalid characters. Tag key can have a maximum of 63 characters and cannot be empty. Tag key must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `._-`]", + }, + { + name: "userTags contains value length greater than 63", + userTags: []gcp.UserTag{ + { + ParentID: "1234567890", + Key: "key", + Value: "hisisavaluewith-63characters_{[(.@%=+: ,*#&)]}allowedforgcptagvalue", + }, + }, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"hisisavaluewith-63characters_{[(.@%=+: ,*#&)]}allowedforgcptagvalue\": tag value is invalid or contains invalid characters. Tag value can have a maximum of 63 characters and cannot be empty. Tag value must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `_-.@%=+:,*#&(){}[]` and spaces]", + }, + { + name: "userTags contains empty value", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "key", Value: ""}}, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"\": tag value is invalid or contains invalid characters. Tag value can have a maximum of 63 characters and cannot be empty. Tag value must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `_-.@%=+:,*#&(){}[]` and spaces]", + }, + { + name: "userTags contains value with invalid character", + userTags: []gcp.UserTag{{ParentID: "1234567890", Key: "key", Value: "value*^%"}}, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value*^%\": tag value is invalid or contains invalid characters. Tag value can have a maximum of 63 characters and cannot be empty. Tag value must begin and end with an alphanumeric character, and must contain only uppercase, lowercase alphanumeric characters, and the following special characters `_-.@%=+:,*#&(){}[]` and spaces]", + }, + { + name: "userTags contains empty ParentID", + userTags: []gcp.UserTag{{Key: "key", Value: "value*^%"}}, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value*^%\": tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen]", + }, + { + name: "userTags contains ParentID configured with invalid OrganizationID", + userTags: []gcp.UserTag{{ParentID: "00001234567890", Key: "key", Value: "value"}}, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value\": tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen]", + }, + { + name: "userTags contains ParentID configured with invalid ProjectID", + userTags: []gcp.UserTag{{ParentID: "test-project-123-", Key: "key", Value: "value"}}, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value\": tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen]", + }, + { + name: "userTags contains ParentID configured with invalid OrganizationID length", + userTags: []gcp.UserTag{ + { + ParentID: "123456789012345678901234567890123", + Key: "key", + Value: "value", + }, + }, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value\": tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen]", + }, + { + name: "userTags contains ParentID configured with invalid ProjectID length", + userTags: []gcp.UserTag{ + { + ParentID: "test-project-123-test-project-123-test-project-123-test-project-123", + Key: "key", + Value: "value", + }, + }, + expectedErr: "[spec.platform.gcp.userTags[key]: Invalid value: \"value\": tag parentID is invalid or contains invalid characters. ParentID can have a maximum of 32 characters and cannot be empty. ParentID can be either OrganizationID or ProjectID. OrganizationID must consist of decimal numbers, and cannot have leading zeroes and ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, numbers, and hyphens, and must start with a letter, and cannot end with a hyphen]", + }, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + err := validateUserTags(tt.userTags, field.NewPath(fieldPath)) + if fmt.Sprintf("%v", err) != tt.expectedErr { + t.Errorf("Got: %+v Want: %+v", err, tt.expectedErr) + } + }) + } +} diff --git a/pkg/types/validation/installconfig.go b/pkg/types/validation/installconfig.go index a7155e5d62e..d0d413f6642 100644 --- a/pkg/types/validation/installconfig.go +++ b/pkg/types/validation/installconfig.go @@ -1067,6 +1067,15 @@ func validateFeatureSet(c *types.InstallConfig) field.ErrorList { allErrs = append(allErrs, field.Forbidden(field.NewPath("platform", "vsphere", "hosts"), errMsg)) } } + + if c.GCP != nil { + if len(c.GCP.UserTags) > 0 { + allErrs = append(allErrs, field.Forbidden(field.NewPath("platform", "gcp", "userTags"), errMsg)) + } + if len(c.GCP.UserLabels) > 0 { + allErrs = append(allErrs, field.Forbidden(field.NewPath("platform", "gcp", "userLabels"), errMsg)) + } + } } return allErrs