diff --git a/Containerfile.control-plane b/Containerfile.control-plane
index 04e8820426fa..96717965a0ea 100644
--- a/Containerfile.control-plane
+++ b/Containerfile.control-plane
@@ -28,4 +28,5 @@ LABEL io.openshift.hypershift.restricted-psa=true
LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true
LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true
LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true
+LABEL io.openshift.hypershift.control-plane-operator-supports-gcp-resource-tags=true
LABEL io.openshift.hypershift.control-plane-operator.v2-isdefault=true
diff --git a/Dockerfile.control-plane b/Dockerfile.control-plane
index fdea2e18c84a..f114edea7791 100644
--- a/Dockerfile.control-plane
+++ b/Dockerfile.control-plane
@@ -28,4 +28,5 @@ LABEL io.openshift.hypershift.restricted-psa=true
LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true
LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true
LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true
+LABEL io.openshift.hypershift.control-plane-operator-supports-gcp-resource-tags=true
LABEL io.openshift.hypershift.control-plane-operator.v2-isdefault=true
diff --git a/Dockerfile.dev b/Dockerfile.dev
index 3789aaaabe4d..3639dcfe792c 100644
--- a/Dockerfile.dev
+++ b/Dockerfile.dev
@@ -41,4 +41,5 @@ LABEL io.openshift.hypershift.restricted-psa=true
LABEL io.openshift.hypershift.control-plane-pki-operator-signs-csrs=true
LABEL io.openshift.hypershift.hosted-cluster-config-operator-reports-node-count=true
LABEL io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig=true
+LABEL io.openshift.hypershift.control-plane-operator-supports-gcp-resource-tags=true
LABEL io.openshift.hypershift.control-plane-operator.v2-isdefault=true
diff --git a/api/hypershift/v1beta1/gcp.go b/api/hypershift/v1beta1/gcp.go
index b298c7acdf91..f415f8c08656 100644
--- a/api/hypershift/v1beta1/gcp.go
+++ b/api/hypershift/v1beta1/gcp.go
@@ -47,6 +47,50 @@ type GCPResourceLabel struct {
Value *string `json:"value,omitempty"`
}
+// GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+// its parent, short key, and short value. The tag must be defined in the
+// HostedCluster project or an organization before the cluster is created.
+// See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+type GCPResourceTag struct {
+ // parentID is the ID of the project or organization that defines the TagKey.
+ // When omitted, the HostedCluster's GCP project is used.
+ // An explicit value must be the HostedCluster's GCP project ID or a numeric
+ // organization ID. A numeric value is treated as an organization ID; project
+ // numbers are not supported.
+ // An organization ID consists of 1 to 32 decimal digits and must not start with 0.
+ // A project ID is 6 to 30 characters long, contains only lowercase letters, digits
+ // and hyphens, starts with a letter and must not end with a hyphen.
+ //
+ // +optional
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=32
+ // +kubebuilder:validation:XValidation:rule="self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')",message="parentID must be a numeric organization ID or a valid GCP project ID"
+ ParentID string `json:"parentID,omitempty"`
+
+ // key is required and is the short name of the pre-existing Resource Manager TagKey.
+ // A key is 1-63 characters, begins and ends with
+ // an ASCII alphanumeric character, and may contain letters, digits, '.',
+ // '_', or '-' between them.
+ //
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')",message="key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+ Key string `json:"key,omitempty"`
+
+ // value is required and is the short name of the pre-existing Resource Manager TagValue for
+ // key. Exactly one value for a TagKey can be attached to a resource. A value
+ // is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+ // and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and
+ // spaces between them.
+ //
+ // +required
+ // +kubebuilder:validation:MinLength=1
+ // +kubebuilder:validation:MaxLength=63
+ // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\\\[\\\\]{}\\\\- ]{0,61}[a-zA-Z0-9])?$')",message="value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+ Value string `json:"value,omitempty"`
+}
+
// GCPEndpointAccessType defines the endpoint access type for GCP clusters.
// Equivalent to AWS EndpointAccessType but adapted for GCP networking model.
type GCPEndpointAccessType string
@@ -107,6 +151,8 @@ type GCPNetworkConfig struct {
// +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.storage.contains('@') && self.workloadIdentity.serviceAccountsEmails.storage.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="storage service account must belong to the same project"
// +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.imageRegistry.contains('@') && self.workloadIdentity.serviceAccountsEmails.imageRegistry.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="imageRegistry service account must belong to the same project"
// +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.network.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="network service account must belong to the same project"
+// +kubebuilder:validation:XValidation:rule="has(self.resourceTags) == has(oldSelf.resourceTags)",message="resourceTags may only be configured during installation",fieldPath=".resourceTags"
+// +kubebuilder:validation:XValidation:rule="!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID) || t.parentID.matches('^[1-9][0-9]{0,31}$') || t.parentID == self.project)",message="resourceTags parentID must be an organization ID or the GCP platform project",fieldPath=".resourceTags"
type GCPPlatformSpec struct {
// project is the GCP project ID.
// A valid project ID must satisfy the following rules:
@@ -161,6 +207,45 @@ type GCPPlatformSpec struct {
// +kubebuilder:validation:MaxItems=60
ResourceLabels []GCPResourceLabel `json:"resourceLabels,omitempty"`
+ // resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+ // GCP resources created for the hosted cluster.
+ // Currently, tags are applied to newly provisioned persistent disks and, when the
+ // image registry is enabled, to its storage bucket. Other GCP resources, such as
+ // worker VMs and their boot disks, are not tagged.
+ // The set of tagged resources is subject to change over time.
+ //
+ // When omitted, no tags are applied through this setting. When specified,
+ // the list must contain 1 to 50 entries, and each short key must be unique
+ // across the list, even when the tags have different parents. A tag's
+ // parentID defaults to the HostedCluster's GCP project if omitted.
+ // Google Cloud permits at most 50 directly attached tags per resource,
+ // including tags attached by other actors. Coordinate with external tagging
+ // automation to leave sufficient capacity; admission does not reserve tag slots.
+ //
+ // The referenced tags must already exist, and the storage and imageRegistry
+ // service accounts must have permission to use them. Neither is checked when the
+ // HostedCluster is created. When a tag cannot be applied, provisioning of the
+ // affected disk or bucket can fail, and the error is reported in the persistent
+ // volume claim's events or the image registry operator's status.
+ //
+ // resourceTags can only be set when the HostedCluster is created. It cannot be
+ // added, removed or changed afterwards. If the configured list must change,
+ // the HostedCluster must be recreated.
+ // Resource tags require a control plane operator image that supports GCP resource
+ // tag propagation. When that capability is unavailable, reconciliation fails
+ // with a message identifying resourceTags on the ReconciliationSucceeded
+ // condition, and dependent component deployments are blocked until a
+ // supporting control plane operator image is configured.
+ //
+ // +optional
+ // +listType=atomic
+ // +kubebuilder:validation:MinItems=1
+ // +kubebuilder:validation:MaxItems=50
+ // +immutable
+ // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceTags are immutable"
+ // +kubebuilder:validation:XValidation:rule="self.all(t, self.exists_one(u, u.key == t.key))",message="resourceTags keys must be unique across all parents"
+ ResourceTags []GCPResourceTag `json:"resourceTags,omitempty"`
+
// workloadIdentity configures Workload Identity Federation for the cluster.
// This enables secure, short-lived token-based authentication without storing
// long-term service account keys. These fields are immutable after cluster creation
@@ -349,6 +434,7 @@ type GCPServiceAccountsEmails struct {
// that manages GCS storage for the internal container image registry.
// This GSA requires the following IAM roles:
// - roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects)
+ // - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket)
// See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions.
// Format: service-account-name@project-id.iam.gserviceaccount.com
//
diff --git a/api/hypershift/v1beta1/hostedcluster_types.go b/api/hypershift/v1beta1/hostedcluster_types.go
index ff15d4f1719d..97cffb545fd2 100644
--- a/api/hypershift/v1beta1/hostedcluster_types.go
+++ b/api/hypershift/v1beta1/hostedcluster_types.go
@@ -1486,6 +1486,8 @@ func PlatformTypes() []PlatformType {
// PlatformSpec specifies the underlying infrastructure provider for the cluster
// and is used to configure platform specific behavior.
+//
+// +openshift:validation:FeatureGateAwareXValidation:featureGate=GCPPlatform,rule="(has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp) && has(oldSelf.gcp.resourceTags))",message="resourceTags may only be configured during installation"
type PlatformSpec struct {
// type is the type of infrastructure provider for the cluster.
//
diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go
index 4c0459433bcf..6b2522e73d84 100644
--- a/api/hypershift/v1beta1/zz_generated.deepcopy.go
+++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go
@@ -2146,6 +2146,11 @@ func (in *GCPPlatformSpec) DeepCopyInto(out *GCPPlatformSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ResourceTags != nil {
+ in, out := &in.ResourceTags, &out.ResourceTags
+ *out = make([]GCPResourceTag, len(*in))
+ copy(*out, *in)
+ }
out.WorkloadIdentity = in.WorkloadIdentity
}
@@ -2302,6 +2307,21 @@ func (in *GCPResourceReference) DeepCopy() *GCPResourceReference {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GCPResourceTag) DeepCopyInto(out *GCPResourceTag) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPResourceTag.
+func (in *GCPResourceTag) DeepCopy() *GCPResourceTag {
+ if in == nil {
+ return nil
+ }
+ out := new(GCPResourceTag)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GCPServiceAccountsEmails) DeepCopyInto(out *GCPServiceAccountsEmails) {
*out = *in
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
index cdcc4808aca4..619db6595a48 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedclusters.hypershift.openshift.io/GCPPlatform.yaml
@@ -6105,6 +6105,104 @@ spec:
x-kubernetes-list-map-keys:
- key
x-kubernetes-list-type: map
+ resourceTags:
+ description: |-
+ resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+ GCP resources created for the hosted cluster.
+ Currently, tags are applied to newly provisioned persistent disks and, when the
+ image registry is enabled, to its storage bucket. Other GCP resources, such as
+ worker VMs and their boot disks, are not tagged.
+ The set of tagged resources is subject to change over time.
+
+ When omitted, no tags are applied through this setting. When specified,
+ the list must contain 1 to 50 entries, and each short key must be unique
+ across the list, even when the tags have different parents. A tag's
+ parentID defaults to the HostedCluster's GCP project if omitted.
+ Google Cloud permits at most 50 directly attached tags per resource,
+ including tags attached by other actors. Coordinate with external tagging
+ automation to leave sufficient capacity; admission does not reserve tag slots.
+
+ The referenced tags must already exist, and the storage and imageRegistry
+ service accounts must have permission to use them. Neither is checked when the
+ HostedCluster is created. When a tag cannot be applied, provisioning of the
+ affected disk or bucket can fail, and the error is reported in the persistent
+ volume claim's events or the image registry operator's status.
+
+ resourceTags can only be set when the HostedCluster is created. It cannot be
+ added, removed or changed afterwards. If the configured list must change,
+ the HostedCluster must be recreated.
+ Resource tags require a control plane operator image that supports GCP resource
+ tag propagation. When that capability is unavailable, reconciliation fails
+ with a message identifying resourceTags on the ReconciliationSucceeded
+ condition, and dependent component deployments are blocked until a
+ supporting control plane operator image is configured.
+ items:
+ description: |-
+ GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+ its parent, short key, and short value. The tag must be defined in the
+ HostedCluster project or an organization before the cluster is created.
+ See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+ properties:
+ key:
+ description: |-
+ key is required and is the short name of the pre-existing Resource Manager TagKey.
+ A key is 1-63 characters, begins and ends with
+ an ASCII alphanumeric character, and may contain letters, digits, '.',
+ '_', or '-' between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: key must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ '.', '_' or '-'
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')
+ parentID:
+ description: |-
+ parentID is the ID of the project or organization that defines the TagKey.
+ When omitted, the HostedCluster's GCP project is used.
+ An explicit value must be the HostedCluster's GCP project ID or a numeric
+ organization ID. A numeric value is treated as an organization ID; project
+ numbers are not supported.
+ An organization ID consists of 1 to 32 decimal digits and must not start with 0.
+ A project ID is 6 to 30 characters long, contains only lowercase letters, digits
+ and hyphens, starts with a letter and must not end with a hyphen.
+ maxLength: 32
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: parentID must be a numeric organization ID
+ or a valid GCP project ID
+ rule: self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')
+ value:
+ description: |-
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+ key. Exactly one value for a TagKey can be attached to a resource. A value
+ is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+ and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and
+ spaces between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: value must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ spaces or the characters _-.@%=+:,*#&()[]{}
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\[\\]{}\\-
+ ]{0,61}[a-zA-Z0-9])?$')
+ required:
+ - key
+ - value
+ type: object
+ maxItems: 50
+ minItems: 1
+ type: array
+ x-kubernetes-list-type: atomic
+ x-kubernetes-validations:
+ - message: resourceTags are immutable
+ rule: self == oldSelf
+ - message: resourceTags keys must be unique across all parents
+ rule: self.all(t, self.exists_one(u, u.key == t.key))
workloadIdentity:
description: |-
workloadIdentity configures Workload Identity Federation for the cluster.
@@ -6245,6 +6343,7 @@ spec:
that manages GCS storage for the internal container image registry.
This GSA requires the following IAM roles:
- roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects)
+ - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket)
See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions.
Format: service-account-name@project-id.iam.gserviceaccount.com
@@ -6379,6 +6478,15 @@ spec:
- message: network service account must belong to the same project
rule: self.workloadIdentity.serviceAccountsEmails.network.endsWith('@'
+ self.project + '.iam.gserviceaccount.com')
+ - fieldPath: .resourceTags
+ message: resourceTags may only be configured during installation
+ rule: has(self.resourceTags) == has(oldSelf.resourceTags)
+ - fieldPath: .resourceTags
+ message: resourceTags parentID must be an organization ID or
+ the GCP platform project
+ rule: '!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID)
+ || t.parentID.matches(''^[1-9][0-9]{0,31}$'') || t.parentID
+ == self.project)'
ibmcloud:
description: ibmcloud defines IBMCloud specific settings for components
properties:
@@ -6860,6 +6968,10 @@ spec:
required:
- type
type: object
+ x-kubernetes-validations:
+ - message: resourceTags may only be configured during installation
+ rule: (has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp)
+ && has(oldSelf.gcp.resourceTags))
pullSecret:
description: |-
pullSecret is a local reference to a Secret that must have a ".dockerconfigjson" key whose content must be a valid Openshift pull secret JSON.
diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
index 3e87f5df92b1..5f6f8cd97efb 100644
--- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
+++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/hostedcontrolplanes.hypershift.openshift.io/GCPPlatform.yaml
@@ -5982,6 +5982,104 @@ spec:
x-kubernetes-list-map-keys:
- key
x-kubernetes-list-type: map
+ resourceTags:
+ description: |-
+ resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+ GCP resources created for the hosted cluster.
+ Currently, tags are applied to newly provisioned persistent disks and, when the
+ image registry is enabled, to its storage bucket. Other GCP resources, such as
+ worker VMs and their boot disks, are not tagged.
+ The set of tagged resources is subject to change over time.
+
+ When omitted, no tags are applied through this setting. When specified,
+ the list must contain 1 to 50 entries, and each short key must be unique
+ across the list, even when the tags have different parents. A tag's
+ parentID defaults to the HostedCluster's GCP project if omitted.
+ Google Cloud permits at most 50 directly attached tags per resource,
+ including tags attached by other actors. Coordinate with external tagging
+ automation to leave sufficient capacity; admission does not reserve tag slots.
+
+ The referenced tags must already exist, and the storage and imageRegistry
+ service accounts must have permission to use them. Neither is checked when the
+ HostedCluster is created. When a tag cannot be applied, provisioning of the
+ affected disk or bucket can fail, and the error is reported in the persistent
+ volume claim's events or the image registry operator's status.
+
+ resourceTags can only be set when the HostedCluster is created. It cannot be
+ added, removed or changed afterwards. If the configured list must change,
+ the HostedCluster must be recreated.
+ Resource tags require a control plane operator image that supports GCP resource
+ tag propagation. When that capability is unavailable, reconciliation fails
+ with a message identifying resourceTags on the ReconciliationSucceeded
+ condition, and dependent component deployments are blocked until a
+ supporting control plane operator image is configured.
+ items:
+ description: |-
+ GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+ its parent, short key, and short value. The tag must be defined in the
+ HostedCluster project or an organization before the cluster is created.
+ See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+ properties:
+ key:
+ description: |-
+ key is required and is the short name of the pre-existing Resource Manager TagKey.
+ A key is 1-63 characters, begins and ends with
+ an ASCII alphanumeric character, and may contain letters, digits, '.',
+ '_', or '-' between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: key must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ '.', '_' or '-'
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')
+ parentID:
+ description: |-
+ parentID is the ID of the project or organization that defines the TagKey.
+ When omitted, the HostedCluster's GCP project is used.
+ An explicit value must be the HostedCluster's GCP project ID or a numeric
+ organization ID. A numeric value is treated as an organization ID; project
+ numbers are not supported.
+ An organization ID consists of 1 to 32 decimal digits and must not start with 0.
+ A project ID is 6 to 30 characters long, contains only lowercase letters, digits
+ and hyphens, starts with a letter and must not end with a hyphen.
+ maxLength: 32
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: parentID must be a numeric organization ID
+ or a valid GCP project ID
+ rule: self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')
+ value:
+ description: |-
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+ key. Exactly one value for a TagKey can be attached to a resource. A value
+ is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+ and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and
+ spaces between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: value must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ spaces or the characters _-.@%=+:,*#&()[]{}
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\[\\]{}\\-
+ ]{0,61}[a-zA-Z0-9])?$')
+ required:
+ - key
+ - value
+ type: object
+ maxItems: 50
+ minItems: 1
+ type: array
+ x-kubernetes-list-type: atomic
+ x-kubernetes-validations:
+ - message: resourceTags are immutable
+ rule: self == oldSelf
+ - message: resourceTags keys must be unique across all parents
+ rule: self.all(t, self.exists_one(u, u.key == t.key))
workloadIdentity:
description: |-
workloadIdentity configures Workload Identity Federation for the cluster.
@@ -6122,6 +6220,7 @@ spec:
that manages GCS storage for the internal container image registry.
This GSA requires the following IAM roles:
- roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects)
+ - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket)
See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions.
Format: service-account-name@project-id.iam.gserviceaccount.com
@@ -6256,6 +6355,15 @@ spec:
- message: network service account must belong to the same project
rule: self.workloadIdentity.serviceAccountsEmails.network.endsWith('@'
+ self.project + '.iam.gserviceaccount.com')
+ - fieldPath: .resourceTags
+ message: resourceTags may only be configured during installation
+ rule: has(self.resourceTags) == has(oldSelf.resourceTags)
+ - fieldPath: .resourceTags
+ message: resourceTags parentID must be an organization ID or
+ the GCP platform project
+ rule: '!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID)
+ || t.parentID.matches(''^[1-9][0-9]{0,31}$'') || t.parentID
+ == self.project)'
ibmcloud:
description: ibmcloud defines IBMCloud specific settings for components
properties:
@@ -6737,6 +6845,10 @@ spec:
required:
- type
type: object
+ x-kubernetes-validations:
+ - message: resourceTags may only be configured during installation
+ rule: (has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp)
+ && has(oldSelf.gcp.resourceTags))
pullSecret:
description: pullSecret is a reference to a secret containing the
pull secret for the hosted control plane.
diff --git a/client/applyconfiguration/hypershift/v1beta1/gcpplatformspec.go b/client/applyconfiguration/hypershift/v1beta1/gcpplatformspec.go
index 254cc2660fa7..d657c0ffa917 100644
--- a/client/applyconfiguration/hypershift/v1beta1/gcpplatformspec.go
+++ b/client/applyconfiguration/hypershift/v1beta1/gcpplatformspec.go
@@ -29,6 +29,7 @@ type GCPPlatformSpecApplyConfiguration struct {
NetworkConfig *GCPNetworkConfigApplyConfiguration `json:"networkConfig,omitempty"`
EndpointAccess *hypershiftv1beta1.GCPEndpointAccessType `json:"endpointAccess,omitempty"`
ResourceLabels []GCPResourceLabelApplyConfiguration `json:"resourceLabels,omitempty"`
+ ResourceTags []GCPResourceTagApplyConfiguration `json:"resourceTags,omitempty"`
WorkloadIdentity *GCPWorkloadIdentityConfigApplyConfiguration `json:"workloadIdentity,omitempty"`
}
@@ -83,6 +84,19 @@ func (b *GCPPlatformSpecApplyConfiguration) WithResourceLabels(values ...*GCPRes
return b
}
+// WithResourceTags adds the given value to the ResourceTags field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the ResourceTags field.
+func (b *GCPPlatformSpecApplyConfiguration) WithResourceTags(values ...*GCPResourceTagApplyConfiguration) *GCPPlatformSpecApplyConfiguration {
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithResourceTags")
+ }
+ b.ResourceTags = append(b.ResourceTags, *values[i])
+ }
+ return b
+}
+
// WithWorkloadIdentity sets the WorkloadIdentity field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the WorkloadIdentity field is set to the value of the last call.
diff --git a/client/applyconfiguration/hypershift/v1beta1/gcpresourcetag.go b/client/applyconfiguration/hypershift/v1beta1/gcpresourcetag.go
new file mode 100644
index 000000000000..dc34e4783449
--- /dev/null
+++ b/client/applyconfiguration/hypershift/v1beta1/gcpresourcetag.go
@@ -0,0 +1,56 @@
+/*
+
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1beta1
+
+// GCPResourceTagApplyConfiguration represents a declarative configuration of the GCPResourceTag type for use
+// with apply.
+type GCPResourceTagApplyConfiguration struct {
+ ParentID *string `json:"parentID,omitempty"`
+ Key *string `json:"key,omitempty"`
+ Value *string `json:"value,omitempty"`
+}
+
+// GCPResourceTagApplyConfiguration constructs a declarative configuration of the GCPResourceTag type for use with
+// apply.
+func GCPResourceTag() *GCPResourceTagApplyConfiguration {
+ return &GCPResourceTagApplyConfiguration{}
+}
+
+// WithParentID sets the ParentID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ParentID field is set to the value of the last call.
+func (b *GCPResourceTagApplyConfiguration) WithParentID(value string) *GCPResourceTagApplyConfiguration {
+ b.ParentID = &value
+ return b
+}
+
+// WithKey sets the Key field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Key field is set to the value of the last call.
+func (b *GCPResourceTagApplyConfiguration) WithKey(value string) *GCPResourceTagApplyConfiguration {
+ b.Key = &value
+ return b
+}
+
+// WithValue sets the Value field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Value field is set to the value of the last call.
+func (b *GCPResourceTagApplyConfiguration) WithValue(value string) *GCPResourceTagApplyConfiguration {
+ b.Value = &value
+ return b
+}
diff --git a/client/applyconfiguration/utils.go b/client/applyconfiguration/utils.go
index a4262dc4176f..1e63e295aeaf 100644
--- a/client/applyconfiguration/utils.go
+++ b/client/applyconfiguration/utils.go
@@ -237,6 +237,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &hypershiftv1beta1.GCPResourceLabelApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("GCPResourceReference"):
return &hypershiftv1beta1.GCPResourceReferenceApplyConfiguration{}
+ case v1beta1.SchemeGroupVersion.WithKind("GCPResourceTag"):
+ return &hypershiftv1beta1.GCPResourceTagApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("GCPServiceAccountsEmails"):
return &hypershiftv1beta1.GCPServiceAccountsEmailsApplyConfiguration{}
case v1beta1.SchemeGroupVersion.WithKind("GCPWorkloadIdentityConfig"):
diff --git a/cmd/infra/gcp/iam-bindings.json b/cmd/infra/gcp/iam-bindings.json
index 7fc5df9bdc33..9986cfb4e485 100644
--- a/cmd/infra/gcp/iam-bindings.json
+++ b/cmd/infra/gcp/iam-bindings.json
@@ -58,7 +58,10 @@
"name": "image-registry",
"displayName": "HyperShift Image Registry Operator",
"description": "Service account for Image Registry Operator GCS storage operations",
- "roles": ["roles/storage.admin"],
+ "roles": [
+ "roles/storage.admin",
+ "roles/resourcemanager.tagUser"
+ ],
"k8sServiceAccounts": [
{"namespace": "openshift-image-registry", "name": "cluster-image-registry-operator"},
{"namespace": "openshift-image-registry", "name": "registry"}
diff --git a/cmd/infra/gcp/iam_test.go b/cmd/infra/gcp/iam_test.go
index f3f8d737bb5e..13c38fcf0977 100644
--- a/cmd/infra/gcp/iam_test.go
+++ b/cmd/infra/gcp/iam_test.go
@@ -400,6 +400,24 @@ func TestLoadServiceAccountDefinitions(t *testing.T) {
K8sServiceAccountRef{Namespace: "openshift-image-registry", Name: "registry"},
))
})
+
+ t.Run("When loading tag-consuming definitions, it should grant Tag User to storage and image-registry", func(t *testing.T) {
+ g := NewWithT(t)
+ definitions, err := loadServiceAccountDefinitions()
+ g.Expect(err).NotTo(HaveOccurred())
+
+ for _, name := range []string{"gcp-pd-csi", "image-registry"} {
+ var definition *ServiceAccountDefinition
+ for i := range definitions {
+ if definitions[i].Name == name {
+ definition = &definitions[i]
+ break
+ }
+ }
+ g.Expect(definition).NotTo(BeNil(), "expected to find %s service account definition", name)
+ g.Expect(definition.Roles).To(ContainElement("roles/resourcemanager.tagUser"), "%s needs Tag User for resource tags", name)
+ }
+ })
}
func TestIsTransientIAMError(t *testing.T) {
diff --git a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/techpreview.hostedclusters.gcp.testsuite.yaml b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/techpreview.hostedclusters.gcp.testsuite.yaml
index 1aba436ec5d9..907eb9396b58 100644
--- a/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/techpreview.hostedclusters.gcp.testsuite.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/tests/hostedclusters.hypershift.openshift.io/techpreview.hostedclusters.gcp.testsuite.yaml
@@ -1137,7 +1137,3250 @@ tests:
type: Route
route: {}
+ # --- GCP resourceTags validation ---
+ - name: When GCP resourceTags contain a valid tag it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "Cost.Center_123"
+ value: "prod@eu-west1"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When a GCP resourceTag key is empty it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: ""
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ expectedError: "key in body should be at least 1 chars long"
+
+ - name: When a GCP resourceTag value is empty it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: ""
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ expectedError: "value in body should be at least 1 chars long"
+
+ - name: When a GCP resourceTag key contains a forbidden character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment/invalid
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag value contains an invalid character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production/blue
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When a GCP resourceTag value does not start with a letter or number it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "-production"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When a GCP resourceTag key contains a single quote it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "environment'prod"
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag key contains a double quote it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "environment\"prod"
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag key contains a backslash it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "environment\\prod"
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag value contains a single quote it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "production'blue"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When a GCP resourceTag value contains a double quote it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "production\"blue"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When a GCP resourceTag value contains a backslash it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "production\\blue"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When GCP resourceTags contain duplicate keys it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - key: environment
+ value: development
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags keys must be unique across all parents"
+
+ - name: When GCP resourceTags contain 63-character keys and values it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
+ value: Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When a GCP resourceTag key exceeds 63 characters it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key: Too long"
+
+ - name: When a GCP resourceTag value exceeds 63 characters it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value: Too long"
+
+ - name: When a GCP resourceTag key starts with a non-alphanumeric ASCII character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "-env"
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag key ends with a non-alphanumeric ASCII character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "env."
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag value ends with a non-alphanumeric ASCII character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "prod-"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+ - name: When a GCP resourceTag key starts with a non-ASCII character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: "équipe"
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'"
+
+ - name: When a GCP resourceTag value starts with a non-ASCII character it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: "équipe"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When GCP resourceTags contain 51 entries it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: key0
+ value: production
+ - key: key1
+ value: production
+ - key: key2
+ value: production
+ - key: key3
+ value: production
+ - key: key4
+ value: production
+ - key: key5
+ value: production
+ - key: key6
+ value: production
+ - key: key7
+ value: production
+ - key: key8
+ value: production
+ - key: key9
+ value: production
+ - key: key10
+ value: production
+ - key: key11
+ value: production
+ - key: key12
+ value: production
+ - key: key13
+ value: production
+ - key: key14
+ value: production
+ - key: key15
+ value: production
+ - key: key16
+ value: production
+ - key: key17
+ value: production
+ - key: key18
+ value: production
+ - key: key19
+ value: production
+ - key: key20
+ value: production
+ - key: key21
+ value: production
+ - key: key22
+ value: production
+ - key: key23
+ value: production
+ - key: key24
+ value: production
+ - key: key25
+ value: production
+ - key: key26
+ value: production
+ - key: key27
+ value: production
+ - key: key28
+ value: production
+ - key: key29
+ value: production
+ - key: key30
+ value: production
+ - key: key31
+ value: production
+ - key: key32
+ value: production
+ - key: key33
+ value: production
+ - key: key34
+ value: production
+ - key: key35
+ value: production
+ - key: key36
+ value: production
+ - key: key37
+ value: production
+ - key: key38
+ value: production
+ - key: key39
+ value: production
+ - key: key40
+ value: production
+ - key: key41
+ value: production
+ - key: key42
+ value: production
+ - key: key43
+ value: production
+ - key: key44
+ value: production
+ - key: key45
+ value: production
+ - key: key46
+ value: production
+ - key: key47
+ value: production
+ - key: key48
+ value: production
+ - key: key49
+ value: production
+ - key: key50
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags: Too many"
+
+ - name: When GCP resourceTags are empty it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags: []
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags in body should have at least 1 items"
+
+ - name: When a GCP resourceTag value contains supported punctuation it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: ENV_TEST.1
+ value: "a_-.@%=+:,*#&()[]{} b"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When a GCP resourceTag value contains a tab it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: ENV_TEST.1
+ value: "prod\tblue"
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}"
+
+ - name: When GCP resourceTags set the own project as parentID it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: my-project-123
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When GCP resourceTags set an organization parentID it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "123456789012"
+ key: cost-center
+ value: shared
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When GCP resourceTags have distinct keys across omitted, project, and organization parents it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - parentID: my-project-123
+ key: team
+ value: platform
+ - parentID: "123456789012"
+ key: cost-center
+ value: shared
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When GCP resourceTags have distinct keys across organizations it should succeed
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "123456789012"
+ key: team
+ value: platform
+ - parentID: "987654321098"
+ key: cost-center
+ value: shared
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+
+ - name: When GCP resourceTags set a foreign project parentID it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: another-project
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags parentID must be an organization ID or the GCP platform project"
+
+ - name: When GCP resourceTags set a malformed parentID it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: Not-A-Project
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "parentID must be a numeric organization ID or a valid GCP project ID"
+
+ - name: When GCP resourceTags reference the Google system namespace it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "google"
+ key: "system:sdp-data-sensitivity"
+ value: HIGH
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags parentID must be an organization ID or the GCP platform project"
+
+ - name: When GCP resourceTags reference the Google App Hub namespace it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "google:AppHub"
+ key: environment
+ value: PRODUCTION
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "parentID must be a numeric organization ID or a valid GCP project ID"
+
+ - name: When GCP resourceTags set an organization parentID with a leading zero it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "0123456789"
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "parentID must be a numeric organization ID or a valid GCP project ID"
+
+ - name: When GCP resourceTags set an empty parentID it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: ""
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "parentID in body should be at least 1 chars long"
+
+ - name: When GCP resourceTags repeat a key with omitted and explicit project parents it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - parentID: my-project-123
+ key: environment
+ value: development
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags keys must be unique across all parents"
+
+ - name: When GCP resourceTags repeat a key across project and organization parents it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - parentID: "123456789012"
+ key: environment
+ value: shared
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags keys must be unique across all parents"
+
+ - name: When GCP resourceTags repeat a key across organizations it should fail
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "123456789012"
+ key: environment
+ value: production
+ - parentID: "987654321098"
+ key: environment
+ value: shared
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags keys must be unique across all parents"
+
onUpdate:
+ # --- GCP resourceTags installation-time immutability ---
+ - name: When GCP resourceTags are changed it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: staging
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags are immutable"
+
+ - name: When GCP resourceTag parentID changes it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - parentID: "123456789012"
+ key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags are immutable"
+
+ - name: When GCP resourceTags are reordered it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - key: team
+ value: platform
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: team
+ value: platform
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags are immutable"
+
+ - name: When GCP resourceTags are added to a nonempty list it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ - key: team
+ value: platform
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags are immutable"
+
+ - name: When GCP resourceTags are present and the entire gcp object is removed it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags may only be configured during installation"
+
+ - name: When GCP resourceTags are added with the entire gcp object it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags may only be configured during installation"
+
+ - name: When GCP resourceTags are removed it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags may only be configured during installation"
+
+ - name: When GCP resourceTags are added after creation it should reject the update
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expectedError: "resourceTags may only be configured during installation"
+
# --- GCP network service account immutability ---
- name: When GCP network service account is changed it should fail
initial: |
@@ -1427,6 +4670,194 @@ tests:
servicePublishingStrategy:
route: {}
type: Route
+ - name: When updating release image while GCP resourceTags are unchanged it should pass
+ initial: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.15.11-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ updated: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ dns:
+ baseDomain: example.com
+ platform:
+ type: GCP
+ gcp:
+ project: my-project-123
+ region: us-central1
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ workloadIdentity:
+ projectNumber: "123456789012"
+ poolID: my-wif-pool
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.16.0-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: OAuthServer
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Konnectivity
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ - service: Ignition
+ servicePublishingStrategy:
+ type: Route
+ route: {}
+ expected: |
+ apiVersion: hypershift.openshift.io/v1beta1
+ kind: HostedCluster
+ spec:
+ capabilities: {}
+ controllerAvailabilityPolicy: HighlyAvailable
+ dns:
+ baseDomain: example.com
+ etcd:
+ managed:
+ storage:
+ persistentVolume:
+ size: 8Gi
+ type: PersistentVolume
+ managementType: Managed
+ infrastructureAvailabilityPolicy: SingleReplica
+ issuerURL: https://kubernetes.default.svc
+ networking:
+ clusterNetwork:
+ - cidr: 10.132.0.0/14
+ networkType: OVNKubernetes
+ serviceNetwork:
+ - cidr: 172.31.0.0/16
+ olmCatalogPlacement: management
+ platform:
+ gcp:
+ endpointAccess: Private
+ networkConfig:
+ network:
+ name: my-network
+ privateServiceConnectSubnet:
+ name: my-psc-subnet
+ project: my-project-123
+ region: us-central1
+ workloadIdentity:
+ poolID: my-wif-pool
+ projectNumber: '123456789012'
+ providerID: my-wif-provider
+ serviceAccountsEmails:
+ cloudController: cloudcontroller@my-project-123.iam.gserviceaccount.com
+ controlPlane: controlplane@my-project-123.iam.gserviceaccount.com
+ imageRegistry: imageregistry@my-project-123.iam.gserviceaccount.com
+ network: cnccsa@my-project-123.iam.gserviceaccount.com
+ nodePool: nodepool@my-project-123.iam.gserviceaccount.com
+ storage: storage@my-project-123.iam.gserviceaccount.com
+ resourceTags:
+ - key: environment
+ value: production
+ type: GCP
+ pullSecret:
+ name: secret
+ release:
+ image: quay.io/openshift-release-dev/ocp-release:4.16.0-x86_64
+ secretEncryption:
+ aescbc:
+ activeKey:
+ name: key
+ type: aescbc
+ services:
+ - service: APIServer
+ servicePublishingStrategy:
+ route: {}
+ type: Route
+ - service: OAuthServer
+ servicePublishingStrategy:
+ route: {}
+ type: Route
+ - service: Konnectivity
+ servicePublishingStrategy:
+ route: {}
+ type: Route
+ - service: Ignition
+ servicePublishingStrategy:
+ route: {}
+ type: Route
# --- GCP network service account ratcheting ---
- name: When updating a previously invalid network SA to a new invalid value it should fail
initialCRDPatches:
@@ -2134,4 +5565,4 @@ tests:
- service: Ignition
servicePublishingStrategy:
route: {}
- type: Route
\ No newline at end of file
+ type: Route
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
index 6ffb693347ce..2b5aba8eab09 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedclusters-Hypershift-TechPreviewNoUpgrade.crd.yaml
@@ -7745,6 +7745,104 @@ spec:
x-kubernetes-list-map-keys:
- key
x-kubernetes-list-type: map
+ resourceTags:
+ description: |-
+ resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+ GCP resources created for the hosted cluster.
+ Currently, tags are applied to newly provisioned persistent disks and, when the
+ image registry is enabled, to its storage bucket. Other GCP resources, such as
+ worker VMs and their boot disks, are not tagged.
+ The set of tagged resources is subject to change over time.
+
+ When omitted, no tags are applied through this setting. When specified,
+ the list must contain 1 to 50 entries, and each short key must be unique
+ across the list, even when the tags have different parents. A tag's
+ parentID defaults to the HostedCluster's GCP project if omitted.
+ Google Cloud permits at most 50 directly attached tags per resource,
+ including tags attached by other actors. Coordinate with external tagging
+ automation to leave sufficient capacity; admission does not reserve tag slots.
+
+ The referenced tags must already exist, and the storage and imageRegistry
+ service accounts must have permission to use them. Neither is checked when the
+ HostedCluster is created. When a tag cannot be applied, provisioning of the
+ affected disk or bucket can fail, and the error is reported in the persistent
+ volume claim's events or the image registry operator's status.
+
+ resourceTags can only be set when the HostedCluster is created. It cannot be
+ added, removed or changed afterwards. If the configured list must change,
+ the HostedCluster must be recreated.
+ Resource tags require a control plane operator image that supports GCP resource
+ tag propagation. When that capability is unavailable, reconciliation fails
+ with a message identifying resourceTags on the ReconciliationSucceeded
+ condition, and dependent component deployments are blocked until a
+ supporting control plane operator image is configured.
+ items:
+ description: |-
+ GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+ its parent, short key, and short value. The tag must be defined in the
+ HostedCluster project or an organization before the cluster is created.
+ See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+ properties:
+ key:
+ description: |-
+ key is required and is the short name of the pre-existing Resource Manager TagKey.
+ A key is 1-63 characters, begins and ends with
+ an ASCII alphanumeric character, and may contain letters, digits, '.',
+ '_', or '-' between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: key must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ '.', '_' or '-'
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')
+ parentID:
+ description: |-
+ parentID is the ID of the project or organization that defines the TagKey.
+ When omitted, the HostedCluster's GCP project is used.
+ An explicit value must be the HostedCluster's GCP project ID or a numeric
+ organization ID. A numeric value is treated as an organization ID; project
+ numbers are not supported.
+ An organization ID consists of 1 to 32 decimal digits and must not start with 0.
+ A project ID is 6 to 30 characters long, contains only lowercase letters, digits
+ and hyphens, starts with a letter and must not end with a hyphen.
+ maxLength: 32
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: parentID must be a numeric organization ID
+ or a valid GCP project ID
+ rule: self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')
+ value:
+ description: |-
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+ key. Exactly one value for a TagKey can be attached to a resource. A value
+ is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+ and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and
+ spaces between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: value must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ spaces or the characters _-.@%=+:,*#&()[]{}
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\[\\]{}\\-
+ ]{0,61}[a-zA-Z0-9])?$')
+ required:
+ - key
+ - value
+ type: object
+ maxItems: 50
+ minItems: 1
+ type: array
+ x-kubernetes-list-type: atomic
+ x-kubernetes-validations:
+ - message: resourceTags are immutable
+ rule: self == oldSelf
+ - message: resourceTags keys must be unique across all parents
+ rule: self.all(t, self.exists_one(u, u.key == t.key))
workloadIdentity:
description: |-
workloadIdentity configures Workload Identity Federation for the cluster.
@@ -7885,6 +7983,7 @@ spec:
that manages GCS storage for the internal container image registry.
This GSA requires the following IAM roles:
- roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects)
+ - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket)
See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions.
Format: service-account-name@project-id.iam.gserviceaccount.com
@@ -8019,6 +8118,15 @@ spec:
- message: network service account must belong to the same project
rule: self.workloadIdentity.serviceAccountsEmails.network.endsWith('@'
+ self.project + '.iam.gserviceaccount.com')
+ - fieldPath: .resourceTags
+ message: resourceTags may only be configured during installation
+ rule: has(self.resourceTags) == has(oldSelf.resourceTags)
+ - fieldPath: .resourceTags
+ message: resourceTags parentID must be an organization ID or
+ the GCP platform project
+ rule: '!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID)
+ || t.parentID.matches(''^[1-9][0-9]{0,31}$'') || t.parentID
+ == self.project)'
ibmcloud:
description: ibmcloud defines IBMCloud specific settings for components
properties:
@@ -9041,6 +9149,10 @@ spec:
required:
- type
type: object
+ x-kubernetes-validations:
+ - message: resourceTags may only be configured during installation
+ rule: (has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp)
+ && has(oldSelf.gcp.resourceTags))
pullSecret:
description: |-
pullSecret is a local reference to a Secret that must have a ".dockerconfigjson" key whose content must be a valid Openshift pull secret JSON.
diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
index 797e8cdad415..0c07831c6dac 100644
--- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
+++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/hostedcontrolplanes-Hypershift-TechPreviewNoUpgrade.crd.yaml
@@ -7622,6 +7622,104 @@ spec:
x-kubernetes-list-map-keys:
- key
x-kubernetes-list-type: map
+ resourceTags:
+ description: |-
+ resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+ GCP resources created for the hosted cluster.
+ Currently, tags are applied to newly provisioned persistent disks and, when the
+ image registry is enabled, to its storage bucket. Other GCP resources, such as
+ worker VMs and their boot disks, are not tagged.
+ The set of tagged resources is subject to change over time.
+
+ When omitted, no tags are applied through this setting. When specified,
+ the list must contain 1 to 50 entries, and each short key must be unique
+ across the list, even when the tags have different parents. A tag's
+ parentID defaults to the HostedCluster's GCP project if omitted.
+ Google Cloud permits at most 50 directly attached tags per resource,
+ including tags attached by other actors. Coordinate with external tagging
+ automation to leave sufficient capacity; admission does not reserve tag slots.
+
+ The referenced tags must already exist, and the storage and imageRegistry
+ service accounts must have permission to use them. Neither is checked when the
+ HostedCluster is created. When a tag cannot be applied, provisioning of the
+ affected disk or bucket can fail, and the error is reported in the persistent
+ volume claim's events or the image registry operator's status.
+
+ resourceTags can only be set when the HostedCluster is created. It cannot be
+ added, removed or changed afterwards. If the configured list must change,
+ the HostedCluster must be recreated.
+ Resource tags require a control plane operator image that supports GCP resource
+ tag propagation. When that capability is unavailable, reconciliation fails
+ with a message identifying resourceTags on the ReconciliationSucceeded
+ condition, and dependent component deployments are blocked until a
+ supporting control plane operator image is configured.
+ items:
+ description: |-
+ GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+ its parent, short key, and short value. The tag must be defined in the
+ HostedCluster project or an organization before the cluster is created.
+ See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+ properties:
+ key:
+ description: |-
+ key is required and is the short name of the pre-existing Resource Manager TagKey.
+ A key is 1-63 characters, begins and ends with
+ an ASCII alphanumeric character, and may contain letters, digits, '.',
+ '_', or '-' between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: key must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ '.', '_' or '-'
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')
+ parentID:
+ description: |-
+ parentID is the ID of the project or organization that defines the TagKey.
+ When omitted, the HostedCluster's GCP project is used.
+ An explicit value must be the HostedCluster's GCP project ID or a numeric
+ organization ID. A numeric value is treated as an organization ID; project
+ numbers are not supported.
+ An organization ID consists of 1 to 32 decimal digits and must not start with 0.
+ A project ID is 6 to 30 characters long, contains only lowercase letters, digits
+ and hyphens, starts with a letter and must not end with a hyphen.
+ maxLength: 32
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: parentID must be a numeric organization ID
+ or a valid GCP project ID
+ rule: self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')
+ value:
+ description: |-
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+ key. Exactly one value for a TagKey can be attached to a resource. A value
+ is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+ and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and
+ spaces between them.
+ maxLength: 63
+ minLength: 1
+ type: string
+ x-kubernetes-validations:
+ - message: value must start and end with an ASCII letter
+ or digit and contain only ASCII letters, digits,
+ spaces or the characters _-.@%=+:,*#&()[]{}
+ rule: self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\[\\]{}\\-
+ ]{0,61}[a-zA-Z0-9])?$')
+ required:
+ - key
+ - value
+ type: object
+ maxItems: 50
+ minItems: 1
+ type: array
+ x-kubernetes-list-type: atomic
+ x-kubernetes-validations:
+ - message: resourceTags are immutable
+ rule: self == oldSelf
+ - message: resourceTags keys must be unique across all parents
+ rule: self.all(t, self.exists_one(u, u.key == t.key))
workloadIdentity:
description: |-
workloadIdentity configures Workload Identity Federation for the cluster.
@@ -7762,6 +7860,7 @@ spec:
that manages GCS storage for the internal container image registry.
This GSA requires the following IAM roles:
- roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects)
+ - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket)
See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions.
Format: service-account-name@project-id.iam.gserviceaccount.com
@@ -7896,6 +7995,15 @@ spec:
- message: network service account must belong to the same project
rule: self.workloadIdentity.serviceAccountsEmails.network.endsWith('@'
+ self.project + '.iam.gserviceaccount.com')
+ - fieldPath: .resourceTags
+ message: resourceTags may only be configured during installation
+ rule: has(self.resourceTags) == has(oldSelf.resourceTags)
+ - fieldPath: .resourceTags
+ message: resourceTags parentID must be an organization ID or
+ the GCP platform project
+ rule: '!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID)
+ || t.parentID.matches(''^[1-9][0-9]{0,31}$'') || t.parentID
+ == self.project)'
ibmcloud:
description: ibmcloud defines IBMCloud specific settings for components
properties:
@@ -8918,6 +9026,10 @@ spec:
required:
- type
type: object
+ x-kubernetes-validations:
+ - message: resourceTags may only be configured during installation
+ rule: (has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp)
+ && has(oldSelf.gcp.resourceTags))
pullSecret:
description: pullSecret is a reference to a secret containing the
pull secret for the hosted control plane.
diff --git a/docs/content/how-to/gcp-platform.md b/docs/content/how-to/gcp-platform.md
index 45665e38d74b..d259e92077da 100644
--- a/docs/content/how-to/gcp-platform.md
+++ b/docs/content/how-to/gcp-platform.md
@@ -96,6 +96,120 @@ key is later removed from the HCP, HyperShift removes it from the Service
annotation; it does not restore the Service's former value. Labels whose keys
are not managed by HyperShift are preserved.
+### Resource Manager Tags
+
+Resource Manager tags are distinct from resource labels. Define the TagKeys and
+TagValues in the HostedCluster's GCP project or an organization before
+installation, then configure their short names. If `parentID` is omitted, the
+HostedCluster project is used; for an organization-defined tag, set `parentID`
+to its numeric organization ID:
+
+```yaml
+spec:
+ platform:
+ gcp:
+ resourceTags:
+ - key: environment
+ value: production
+ - parentID: "123456789012"
+ key: cost-center
+ value: shared
+```
+
+An explicit `parentID` must be the HostedCluster project ID or a numeric
+organization ID. A different project ID is not accepted. HyperShift does not
+check whether the referenced tag exists or whether the guest identities have
+permission to use it at admission time.
+
+Google-managed system tags, such as
+`google/system:sdp-data-sensitivity` and `google:AppHub/environment`,
+cannot be configured through `resourceTags`. Their Google-managed namespaces
+are not accepted by `parentID`. Google services may independently attach
+system tags to resources. See [Google Cloud system
+tags](https://cloud.google.com/resource-manager/docs/tags/tags-creating-and-managing#system_tags).
+
+Keys and values must each be 1–63 characters long and begin and end with an
+ASCII letter or digit. Keys may also contain `.`, `_`, and `-`; values accept
+additional punctuation and spaces. When omitted, no tags are applied through
+this setting. If present, the list must contain 1–50 entries. **Each short key
+must be unique across the entire list, even when the tags have different
+parents.** Thus an organization and project tag with the same short key cannot
+both be configured with the current guest Infrastructure API. The HostedCluster
+API enforces the accepted characters.
+
+The configured tags currently apply to these resources:
+
+| Tagged resource | When tagging is attempted |
+| --- | --- |
+| GCP PD CSI persistent disks for guest PersistentVolumes | When a new disk is provisioned |
+| Image registry bucket | During bucket setup, when the image registry is enabled |
+
+They are **not** applied by this setting to worker VMs or their boot disks,
+Private Service Connect forwarding rules or IP addresses, firewall rules, DNS
+resources, or other GCP resources created by HyperShift or CAPG. Broader
+management-side resource tagging is separate work. HyperShift does not create
+TagKeys or TagValues.
+
+Google Cloud permits at most 50 directly attached Resource Manager tags per
+resource. Organization-defined and project-defined tags share this limit when
+attached to the same resource. Tags attached by administrators or external
+automation also consume these slots.
+
+Before configuring `resourceTags`, account for any other direct bindings your
+organization applies to the target disks and registry bucket. There is no fixed
+number of reserved slots: the required headroom depends on your tagging
+automation. Accepting a list at admission does not reserve capacity in Google
+Cloud or prevent another actor from attaching tags.
+
+See [Google Cloud tag
+limits](https://cloud.google.com/resource-manager/docs/limits#tag_limits).
+
+HyperShift passes these tags to the guest cluster for the two components above.
+Resource tags require OpenShift 5.1 or later, or another control plane operator
+image that supports GCP resource tag propagation. The selected image capability
+label is authoritative: when it is absent, `ReconciliationSucceeded` is
+`False` with reason `ReconciliationError`. Its message identifies
+`spec.platform.gcp.resourceTags` as requiring a supporting CPO image and lists
+blocked operations. Dependent component deployments are blocked, while core
+HostedControlPlane reconciliation and independent synchronization can continue.
+This check does not change `ValidConfiguration`.
+
+Configure tags when creating the HostedCluster: `resourceTags` cannot be added,
+removed, or changed afterward. The guest components that
+attach tags (the storage and image registry service-account identities) require
+Google Cloud Tag User on the tag value and target resource, plus the applicable
+resource-specific TagBinding permissions. HyperShift does not currently report
+a dedicated condition for missing or inaccessible TagKeys or TagValues, or for
+missing tag permissions. The `ValidGCPCredentials` and `ValidGCPWorkloadIdentity` conditions
+do not validate the guest storage and image-registry identities' tag access.
+Such failures do not block HyperShift's HostedCluster reconciliation, but they
+can prevent the guest components from creating a tagged registry bucket or
+persistent disk. Check the guest image-registry operator's status and logs, or
+the GCP PD CSI controller logs and affected PVC events, for runtime errors.
+See [Create GCP IAM Resources](gcp/create-gcp-iam.md#resource-tag-permissions)
+for the required grants and how to apply them to existing clusters.
+
+#### Recovering from tag application failures
+
+Check the guest image-registry operator status and logs, or the PD CSI
+controller logs and affected PVC events, to identify the failure.
+
+For the registry bucket, tags are attached individually after bucket creation.
+Some bindings can succeed while others fail; successfully attached tags are
+not rolled back. If external bindings exhaust the tag limit, coordinate with
+their owner to remove unnecessary bindings and prevent automation from
+immediately recreating them. The registry operator retries failed tagging
+operations during reconciliation.
+
+For missing tags or insufficient permissions, correct the referenced tag
+definitions or IAM grants. These corrections do not require changing the
+HostedCluster tag list.
+
+The HostedCluster `resourceTags` list cannot be edited after creation. If
+recovery requires changing that list rather than correcting external bindings,
+tag definitions, or permissions, recreate the HostedCluster with the corrected
+configuration.
+
## CAPG Integration
### Controller Deployment
diff --git a/docs/content/how-to/gcp/configure-image-registry.md b/docs/content/how-to/gcp/configure-image-registry.md
index a5ed4e8e64ac..fd67a92af373 100644
--- a/docs/content/how-to/gcp/configure-image-registry.md
+++ b/docs/content/how-to/gcp/configure-image-registry.md
@@ -184,6 +184,18 @@ gcloud projects add-iam-policy-binding resourceTags are pre-existing Google Cloud Resource Manager tags to apply to
+GCP resources created for the hosted cluster.
+Currently, tags are applied to newly provisioned persistent disks and, when the
+image registry is enabled, to its storage bucket. Other GCP resources, such as
+worker VMs and their boot disks, are not tagged.
+The set of tagged resources is subject to change over time. When omitted, no tags are applied through this setting. When specified,
+the list must contain 1 to 50 entries, and each short key must be unique
+across the list, even when the tags have different parents. A tag’s
+parentID defaults to the HostedCluster’s GCP project if omitted.
+Google Cloud permits at most 50 directly attached tags per resource,
+including tags attached by other actors. Coordinate with external tagging
+automation to leave sufficient capacity; admission does not reserve tag slots. The referenced tags must already exist, and the storage and imageRegistry
+service accounts must have permission to use them. Neither is checked when the
+HostedCluster is created. When a tag cannot be applied, provisioning of the
+affected disk or bucket can fail, and the error is reported in the persistent
+volume claim’s events or the image registry operator’s status. resourceTags can only be set when the HostedCluster is created. It cannot be
+added, removed or changed afterwards. If the configured list must change,
+the HostedCluster must be recreated.
+Resource tags require a control plane operator image that supports GCP resource
+tag propagation. When that capability is unavailable, reconciliation fails
+with a message identifying resourceTags on the ReconciliationSucceeded
+condition, and dependent component deployments are blocked until a
+supporting control plane operator image is configured.
+(Appears on:
+GCPPlatformSpec)
+
+ GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by
+its parent, short key, and short value. The tag must be defined in the
+HostedCluster project or an organization before the cluster is created.
+See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+(Optional)
+
+
+
+
+###GCPResourceTag { #hypershift.openshift.io/v1beta1.GCPResourceTag }
+
workloadIdentity,omitzero
@@ -51958,6 +52155,75 @@ See https://clo
| Field | +Description | +
|---|---|
+parentID
+
+string
+
+ |
+
+(Optional)
+ parentID is the ID of the project or organization that defines the TagKey. +When omitted, the HostedCluster’s GCP project is used. +An explicit value must be the HostedCluster’s GCP project ID or a numeric +organization ID. A numeric value is treated as an organization ID; project +numbers are not supported. +An organization ID consists of 1 to 32 decimal digits and must not start with 0. +A project ID is 6 to 30 characters long, contains only lowercase letters, digits +and hyphens, starts with a letter and must not end with a hyphen. + |
+
+key
+
+string
+
+ |
+
+ key is required and is the short name of the pre-existing Resource Manager TagKey. +A key is 1-63 characters, begins and ends with +an ASCII alphanumeric character, and may contain letters, digits, ‘.’, +‘_’, or ‘-’ between them. + |
+
+value
+
+string
+
+ |
+
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+key. Exactly one value for a TagKey can be attached to a resource. A value
+is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+and may contain ASCII letters and digits, |
+
(Appears on: @@ -52094,6 +52360,7 @@ GCPServiceAccountEmail that manages GCS storage for the internal container image registry. This GSA requires the following IAM roles: - roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects) +- roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket) See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions. Format: service-account-name@project-id.iam.gserviceaccount.com
This is a user-provided value referencing a pre-created Google Service Account. diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 8e9e9f34be69..ca5024edbfb2 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -8969,6 +8969,45 @@ For GCP labeling guidance, see +[]GCPResourceTag + + + +
resourceTags are pre-existing Google Cloud Resource Manager tags to apply to +GCP resources created for the hosted cluster. +Currently, tags are applied to newly provisioned persistent disks and, when the +image registry is enabled, to its storage bucket. Other GCP resources, such as +worker VMs and their boot disks, are not tagged. +The set of tagged resources is subject to change over time.
+When omitted, no tags are applied through this setting. When specified, +the list must contain 1 to 50 entries, and each short key must be unique +across the list, even when the tags have different parents. A tag’s +parentID defaults to the HostedCluster’s GCP project if omitted. +Google Cloud permits at most 50 directly attached tags per resource, +including tags attached by other actors. Coordinate with external tagging +automation to leave sufficient capacity; admission does not reserve tag slots.
+The referenced tags must already exist, and the storage and imageRegistry +service accounts must have permission to use them. Neither is checked when the +HostedCluster is created. When a tag cannot be applied, provisioning of the +affected disk or bucket can fail, and the error is reported in the persistent +volume claim’s events or the image registry operator’s status.
+resourceTags can only be set when the HostedCluster is created. It cannot be +added, removed or changed afterwards. If the configured list must change, +the HostedCluster must be recreated. +Resource tags require a control plane operator image that supports GCP resource +tag propagation. When that capability is unavailable, reconciliation fails +with a message identifying resourceTags on the ReconciliationSucceeded +condition, and dependent component deployments are blocked until a +supporting control plane operator image is configured.
+workloadIdentity,omitzero
@@ -9291,6 +9330,75 @@ See https://clo
+(Appears on: +GCPPlatformSpec) +
++
GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by +its parent, short key, and short value. The tag must be defined in the +HostedCluster project or an organization before the cluster is created. +See https://cloud.google.com/resource-manager/docs/tags/tags-overview.
+ +| Field | +Description | +
|---|---|
+parentID
+
+string
+
+ |
+
+(Optional)
+ parentID is the ID of the project or organization that defines the TagKey. +When omitted, the HostedCluster’s GCP project is used. +An explicit value must be the HostedCluster’s GCP project ID or a numeric +organization ID. A numeric value is treated as an organization ID; project +numbers are not supported. +An organization ID consists of 1 to 32 decimal digits and must not start with 0. +A project ID is 6 to 30 characters long, contains only lowercase letters, digits +and hyphens, starts with a letter and must not end with a hyphen. + |
+
+key
+
+string
+
+ |
+
+ key is required and is the short name of the pre-existing Resource Manager TagKey. +A key is 1-63 characters, begins and ends with +an ASCII alphanumeric character, and may contain letters, digits, ‘.’, +‘_’, or ‘-’ between them. + |
+
+value
+
+string
+
+ |
+
+ value is required and is the short name of the pre-existing Resource Manager TagValue for
+key. Exactly one value for a TagKey can be attached to a resource. A value
+is 1-63 characters, begins and ends with an ASCII alphanumeric character,
+and may contain ASCII letters and digits, |
+
(Appears on: @@ -9427,6 +9535,7 @@ GCPServiceAccountEmail that manages GCS storage for the internal container image registry. This GSA requires the following IAM roles: - roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects) +- roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket) See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions. Format: service-account-name@project-id.iam.gserviceaccount.com
This is a user-provided value referencing a pre-created Google Service Account. diff --git a/hypershift-operator/controllers/hostedcluster/gcp_credentials.go b/hypershift-operator/controllers/hostedcluster/gcp_conditions.go similarity index 56% rename from hypershift-operator/controllers/hostedcluster/gcp_credentials.go rename to hypershift-operator/controllers/hostedcluster/gcp_conditions.go index 6056bdaefd49..68a02f378384 100644 --- a/hypershift-operator/controllers/hostedcluster/gcp_credentials.go +++ b/hypershift-operator/controllers/hostedcluster/gcp_conditions.go @@ -23,3 +23,17 @@ func (r *HostedClusterReconciler) reconcileGCPCredentialConditions(ctx context.C } return nil } + +// validateGCPResourceTagsSupport checks whether requested tags can be propagated +// by the selected CPO image, without changing the HostedCluster. Image-resolution +// failures are reported separately, so unresolved labels are not treated as a +// missing capability. +func validateGCPResourceTagsSupport(hc *hyperv1.HostedCluster, labelsResolved bool, imageLabels map[string]string) error { + if hc.Spec.Platform.Type != hyperv1.GCPPlatform || hc.Spec.Platform.GCP == nil || len(hc.Spec.Platform.GCP.ResourceTags) == 0 || !labelsResolved { + return nil + } + if _, supported := imageLabels[controlPlaneOperatorSupportsGCPResourceTagsLabel]; !supported { + return fmt.Errorf("spec.platform.gcp.resourceTags requires a control plane operator image that supports GCP resource tag propagation") + } + return nil +} diff --git a/hypershift-operator/controllers/hostedcluster/gcp_credentials_test.go b/hypershift-operator/controllers/hostedcluster/gcp_conditions_test.go similarity index 80% rename from hypershift-operator/controllers/hostedcluster/gcp_credentials_test.go rename to hypershift-operator/controllers/hostedcluster/gcp_conditions_test.go index f80dea1d9f44..cd83a9b5d716 100644 --- a/hypershift-operator/controllers/hostedcluster/gcp_credentials_test.go +++ b/hypershift-operator/controllers/hostedcluster/gcp_conditions_test.go @@ -23,7 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/interceptor" ) -func TestReconcileGCPCredentialConditions(t *testing.T) { +func TestHostedClusterReconciler_ReconcileGCPCredentialConditions(t *testing.T) { for _, tc := range []struct { name string hcVersion, hcpVersion string @@ -267,3 +267,52 @@ func TestReconcileGCPCredentialConditions(t *testing.T) { g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) } + +func TestValidateGCPResourceTagsSupport(t *testing.T) { + for _, tc := range []struct { + name string + tags, supported, unresolved, otherPlatform, missingConfig bool + invalidConfiguration bool + expectedError string + }{ + {name: "When tags are omitted, it should not require CPO support"}, + {name: "When the GCP configuration is absent, it should not require CPO support", missingConfig: true}, + {name: "When the platform is not GCP, it should not require GCP tag support", tags: true, otherPlatform: true}, + {name: "When the CPO supports tags, it should succeed", tags: true, supported: true}, + {name: "When the CPO does not support tags, it should identify the unsupported field", tags: true, expectedError: "spec.platform.gcp.resourceTags requires a control plane operator image that supports GCP resource tag propagation"}, + {name: "When labels are unresolved, it should defer to the image-resolution error", tags: true, unresolved: true}, + {name: "When unrelated configuration is invalid and tags are supported, it should leave the condition unchanged", tags: true, supported: true, invalidConfiguration: true}, + {name: "When unrelated configuration is invalid and tags are unsupported, it should leave the condition unchanged and return the tag error", tags: true, invalidConfiguration: true, expectedError: "spec.platform.gcp.resourceTags requires a control plane operator image that supports GCP resource tag propagation"}, + } { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + hc := &hyperv1.HostedCluster{ + Spec: hyperv1.HostedClusterSpec{Platform: hyperv1.PlatformSpec{Type: hyperv1.GCPPlatform, GCP: &hyperv1.GCPPlatformSpec{}}}, + } + if tc.tags { + hc.Spec.Platform.GCP.ResourceTags = []hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}} + } + if tc.otherPlatform { + hc.Spec.Platform.Type = hyperv1.NonePlatform + } + if tc.missingConfig { + hc.Spec.Platform.GCP = nil + } + if tc.invalidConfiguration { + hc.Status.Conditions = []metav1.Condition{{Type: string(hyperv1.ValidHostedClusterConfiguration), Status: metav1.ConditionFalse, Reason: hyperv1.InvalidConfigurationReason, Message: "unrelated configuration error"}} + } + var labels map[string]string + if tc.supported { + labels = map[string]string{controlPlaneOperatorSupportsGCPResourceTagsLabel: "true"} + } + before := hc.DeepCopy() + err := validateGCPResourceTagsSupport(hc, !tc.unresolved, labels) + if tc.expectedError == "" { + g.Expect(err).NotTo(HaveOccurred()) + } else { + g.Expect(err).To(MatchError(tc.expectedError)) + } + g.Expect(hc).To(Equal(before), "validation must not mutate spec or conditions") + }) + } +} diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go index f86e6ee1eb5f..1a5c8cf84e75 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller.go @@ -138,6 +138,7 @@ const ( controlPlaneOperatorSubcommandsLabel = "io.openshift.hypershift.control-plane-operator-subcommands" controlPlaneOperatorSupportsKASCustomKubeconfigLabel = "io.openshift.hypershift.control-plane-operator-supports-kas-custom-kubeconfig" + controlPlaneOperatorSupportsGCPResourceTagsLabel = "io.openshift.hypershift.control-plane-operator-supports-gcp-resource-tags" controlPlaneOperatorAppliesManagementKASNetworkPolicyLabel = "io.openshift.hypershift.control-plane-operator-applies-management-kas-network-policy-label" controlPlanePKIOperatorSignsCSRsLabel = "io.openshift.hypershift.control-plane-pki-operator-signs-csrs" @@ -1467,6 +1468,7 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques var pullSecretBytes []byte controlPlaneOperatorImage := r.HypershiftOperatorImage var controlPlaneOperatorImageLabels map[string]string + controlPlaneOperatorImageLabelsResolved := false report.execute("CPOImageAndNamespace", critical, func() error { var err error @@ -1482,10 +1484,15 @@ func (r *HostedClusterReconciler) reconcile(ctx context.Context, req ctrl.Reques if err != nil { return fmt.Errorf("failed to get controlPlaneOperatorImageLabels: %w", err) } + controlPlaneOperatorImageLabelsResolved = true return r.reconcileControlPlaneNamespace(ctx, createOrUpdate, hcluster, controlPlaneNamespace, controlPlaneOperatorImageLabels) }) + report.execute("ValidateAndReconcileGCPTags", critical, func() error { + return validateGCPResourceTagsSupport(hcluster, controlPlaneOperatorImageLabelsResolved, controlPlaneOperatorImageLabels) + }) + cpoHasUtilities := false utilitiesImage := r.HypershiftOperatorImage if _, hasLabel := controlPlaneOperatorImageLabels[controlPlaneOperatorSubcommandsLabel]; hasLabel { @@ -4545,7 +4552,6 @@ func (r *HostedClusterReconciler) validateReleaseImage(ctx context.Context, hc * } minSupportedVersion := supportedversion.GetMinSupportedVersion(hc) - return supportedversion.IsValidReleaseVersion(&version, currentVersion, &supportedversion.LatestSupportedVersion, &minSupportedVersion, hc.Spec.Networking.NetworkType, hc.Spec.Platform.Type) } diff --git a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go index 1ab2c17994b6..bb4c10634d9a 100644 --- a/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go +++ b/hypershift-operator/controllers/hostedcluster/hostedcluster_controller_test.go @@ -51,6 +51,7 @@ import ( "github.com/openshift/hypershift/support/util/fakeimagemetadataprovider" configv1 "github.com/openshift/api/config/v1" + imagev1 "github.com/openshift/api/image/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -69,6 +70,7 @@ import ( "k8s.io/utils/ptr" capiaws "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" + capigcp "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" capibmv1 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" capov1alpha1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha1" capiv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" @@ -8966,7 +8968,152 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { expectedErrSubstring string // verifyResources asserts that specific resources were still created despite the failure. verifyResources func(t *testing.T, client crclient.Client, hcpNamespace, hcName string) + verifyRecovery func(t *testing.T, r *HostedClusterReconciler, hc *hyperv1.HostedCluster) }{ + { + name: "When GCP resource tags are unsupported, it should still sync secrets and create HCP but block deployments", + mutateHC: func(t *testing.T, hc *hyperv1.HostedCluster, client crclient.Client) { + hc.Spec.Platform = hyperv1.PlatformSpec{ + Type: hyperv1.GCPPlatform, + GCP: &hyperv1.GCPPlatformSpec{ + Project: "my-project-123", + Region: "us-central1", + ResourceTags: []hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}}, + WorkloadIdentity: hyperv1.GCPWorkloadIdentityConfig{ + ProjectNumber: "123456789012", + PoolID: "my-wif-pool", + ProviderID: "my-wif-provider", + ServiceAccountsEmails: hyperv1.GCPServiceAccountsEmails{ + NodePool: "nodepool@my-project-123.iam.gserviceaccount.com", + ControlPlane: "controlplane@my-project-123.iam.gserviceaccount.com", + CloudController: "cloudcontroller@my-project-123.iam.gserviceaccount.com", + Storage: "storage@my-project-123.iam.gserviceaccount.com", + ImageRegistry: "imageregistry@my-project-123.iam.gserviceaccount.com", + Network: "network@my-project-123.iam.gserviceaccount.com", + }, + }, + }, + } + managementNetwork := &configv1.Network{ + ObjectMeta: metav1.ObjectMeta{Name: "cluster"}, + Spec: configv1.NetworkSpec{ + NetworkType: "OVNKubernetes", + ClusterNetwork: []configv1.ClusterNetworkEntry{{CIDR: "10.128.0.0/14", HostPrefix: 23}}, + ServiceNetwork: []string{"172.30.0.0/16"}, + }, + } + if err := client.Create(t.Context(), managementNetwork); err != nil { + t.Fatalf("failed to create management network: %v", err) + } + hc.Spec.SSHKey = corev1.LocalObjectReference{Name: "test-ssh-key"} + sshKey := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Namespace: hc.Namespace, Name: hc.Spec.SSHKey.Name}, + Data: map[string][]byte{"id_rsa.pub": []byte("ssh-rsa AAAA...")}, + } + if err := client.Create(t.Context(), sshKey); err != nil { + t.Fatalf("failed to create SSH key: %v", err) + } + }, + expectedErrSubstring: "critical error: spec.platform.gcp.resourceTags requires a control plane operator image", + verifyResources: func(t *testing.T, client crclient.Client, hcpNamespace, hcName string) { + g := NewWithT(t) + hcp := &hyperv1.HostedControlPlane{} + g.Expect(client.Get(t.Context(), crclient.ObjectKey{Namespace: hcpNamespace, Name: hcName}, hcp)).To(Succeed()) + g.Expect(hcp.Spec.Platform.GCP).NotTo(BeNil()) + g.Expect(hcp.Spec.Platform.GCP.ResourceTags).To(Equal([]hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}})) + + sshKey := controlplaneoperator.SSHKey(hcpNamespace) + g.Expect(client.Get(t.Context(), crclient.ObjectKeyFromObject(sshKey), sshKey)).To(Succeed()) + g.Expect(sshKey.Data["id_rsa.pub"]).To(Equal([]byte("ssh-rsa AAAA..."))) + + deployment := &appsv1.Deployment{} + g.Expect(errors2.IsNotFound(client.Get(t.Context(), crclient.ObjectKey{Namespace: hcpNamespace, Name: "control-plane-operator"}, deployment))).To(BeTrue()) + + hc := &hyperv1.HostedCluster{} + g.Expect(client.Get(t.Context(), crclient.ObjectKey{Namespace: "test-ns", Name: hcName}, hc)).To(Succeed()) + condition := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ValidHostedClusterConfiguration)) + g.Expect(condition).NotTo(BeNil()) + g.Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(condition.Reason).To(Equal(hyperv1.AsExpectedReason)) + reconciled := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ReconciliationSucceeded)) + g.Expect(reconciled).NotTo(BeNil()) + g.Expect(reconciled.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(reconciled.Reason).To(Equal("ReconciliationError")) + g.Expect(reconciled.Message).To(ContainSubstring("critical error: spec.platform.gcp.resourceTags requires")) + g.Expect(reconciled.Message).To(ContainSubstring("blocked operations:")) + g.Expect(reconciled.Message).To(ContainSubstring("OperatorDeployments")) + }, + verifyRecovery: func(t *testing.T, r *HostedClusterReconciler, hc *hyperv1.HostedCluster) { + g := NewWithT(t) + reconcileAgain := func() error { + g.Expect(r.Client.Get(t.Context(), crclient.ObjectKeyFromObject(hc), hc)).To(Succeed()) + _, err := r.Reconcile(t.Context(), ctrl.Request{NamespacedName: crclient.ObjectKeyFromObject(hc)}) + g.Expect(r.Client.Get(t.Context(), crclient.ObjectKeyFromObject(hc), hc)).To(Succeed()) + return err + } + // A previous failure must not short-circuit the next capability check. + g.Expect(reconcileAgain()).To(MatchError(ContainSubstring("critical error: spec.platform.gcp.resourceTags"))) + + // An image-resolution outage is reported without misdiagnosing tag support. + hc.Annotations[hyperv1.ControlPlaneOperatorImageLabelsAnnotation] = "malformed-label" + g.Expect(r.Client.Update(t.Context(), hc)).To(Succeed()) + g.Expect(reconcileAgain()).To(MatchError(ContainSubstring("failed to get controlPlaneOperatorImageLabels"))) + g.Expect(r.Client.Get(t.Context(), crclient.ObjectKeyFromObject(hc), hc)).To(Succeed()) + condition := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ValidHostedClusterConfiguration)) + g.Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(condition.Reason).To(Equal(hyperv1.AsExpectedReason)) + reconciled := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ReconciliationSucceeded)) + g.Expect(reconciled).NotTo(BeNil()) + g.Expect(reconciled.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(reconciled.Message).To(ContainSubstring("failed to get controlPlaneOperatorImageLabels")) + g.Expect(reconciled.Message).NotTo(ContainSubstring("resourceTags requires")) + + // A supporting image must recover without any special reset of status. + hc.Annotations[hyperv1.ControlPlaneOperatorImageLabelsAnnotation] = controlPlaneOperatorSupportsGCPResourceTagsLabel + "=true" + g.Expect(r.Client.Update(t.Context(), hc)).To(Succeed()) + g.Expect(reconcileAgain()).To(Succeed()) + g.Expect(r.Client.Get(t.Context(), crclient.ObjectKeyFromObject(hc), hc)).To(Succeed()) + condition = meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ValidHostedClusterConfiguration)) + g.Expect(condition.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(condition.Reason).To(Equal(hyperv1.AsExpectedReason)) + reconciled = meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ReconciliationSucceeded)) + g.Expect(reconciled.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(reconciled.Message).To(Equal("Reconciliation completed successfully")) + deployment := &appsv1.Deployment{} + g.Expect(r.Client.Get(t.Context(), crclient.ObjectKey{Namespace: hcpmanifests.HostedControlPlaneNamespace(hc.Namespace, hc.Name), Name: "control-plane-operator"}, deployment)).To(Succeed()) + }, + }, + { + name: "When GCP configuration is invalid, it should block at the configuration gate without a capability exception", + mutateHC: func(t *testing.T, hc *hyperv1.HostedCluster, client crclient.Client) { + hc.Spec.ClusterID = "not-a-uuid" + hc.Spec.Platform = hyperv1.PlatformSpec{ + Type: hyperv1.GCPPlatform, + GCP: &hyperv1.GCPPlatformSpec{ + Project: "my-project-123", Region: "us-central1", + ResourceTags: []hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}}, + }, + } + }, + expectedErrSubstring: "configuration is invalid", + verifyResources: func(t *testing.T, client crclient.Client, hcpNamespace, hcName string) { + g := NewWithT(t) + hc := &hyperv1.HostedCluster{} + g.Expect(client.Get(t.Context(), crclient.ObjectKey{Namespace: "test-ns", Name: hcName}, hc)).To(Succeed()) + condition := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ValidHostedClusterConfiguration)) + g.Expect(condition.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(condition.Reason).To(Equal(hyperv1.InvalidConfigurationReason)) + g.Expect(condition.Message).To(ContainSubstring("cluster ID")) + reconciled := meta.FindStatusCondition(hc.Status.Conditions, string(hyperv1.ReconciliationSucceeded)) + g.Expect(reconciled.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(reconciled.Message).To(ContainSubstring("configuration is invalid")) + g.Expect(reconciled.Message).NotTo(ContainSubstring("resourceTags requires")) + hcp := &hyperv1.HostedControlPlane{} + g.Expect(errors2.IsNotFound(client.Get(t.Context(), crclient.ObjectKey{Namespace: hcpNamespace, Name: hcName}, hcp))).To(BeTrue()) + deployment := &appsv1.Deployment{} + g.Expect(errors2.IsNotFound(client.Get(t.Context(), crclient.ObjectKey{Namespace: hcpNamespace, Name: "control-plane-operator"}, deployment))).To(BeTrue()) + }, + }, { name: "When Phase 6 SSH key fails it should still create HCP and CPO deployment", mutateHC: func(t *testing.T, hc *hyperv1.HostedCluster, client crclient.Client) { @@ -9095,9 +9242,14 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { mockCtrl := gomock.NewController(t) mockReleaseProvider := releaseinfo.NewMockProviderWithOpenShiftImageRegistryOverrides(mockCtrl) + releaseImage := testutils.InitReleaseImageOrDie("4.16.0") + releaseImage.ImageStream.Spec.Tags = append(releaseImage.ImageStream.Spec.Tags, imagev1.TagReference{ + Name: "gcp-cluster-api-controllers", + From: &corev1.ObjectReference{Name: "quay.io/openshift/origin-gcp-cluster-api-controllers:latest"}, + }) mockReleaseProvider.EXPECT(). Lookup(gomock.Any(), gomock.Any(), gomock.Any()). - Return(testutils.InitReleaseImageOrDie("4.16.0"), nil).AnyTimes() + Return(releaseImage, nil).AnyTimes() mockReleaseProvider.EXPECT().GetRegistryOverrides().Return(nil).AnyTimes() mockReleaseProvider.EXPECT().GetOpenShiftImageRegistryOverrides().Return(nil).AnyTimes() mockReleaseProvider.EXPECT().GetMirroredReleaseImage().Return("").AnyTimes() @@ -9153,7 +9305,7 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { fakeClient := fake.NewClientBuilder(). WithScheme(api.Scheme). WithObjects(hcluster, pullSecret, ingress). - WithStatusSubresource(hcluster). + WithStatusSubresource(hcluster, &capigcp.GCPCluster{}, &capiv1.Cluster{}). Build() // Apply test-specific mutations (fault injection) @@ -9176,6 +9328,9 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { EnableOCPClusterMonitoring: true, CertRotationScale: 24 * time.Hour, HypershiftOperatorImage: "test-hso-image", + GCPOIDCStorageBucketName: "test-oidc-bucket", + GCSClient: newFakeGCSClient(), + HCPEgressBlockCIDRs: []string{"192.0.2.10/32"}, RegistryProvider: fakeReleaseProvider{ releaseProvider: mockReleaseProvider, metadataProvider: fakeMetadata, @@ -9187,7 +9342,7 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { req := ctrl.Request{NamespacedName: crclient.ObjectKeyFromObject(hcluster)} log := zap.New(zap.UseDevMode(true), zap.Level(zapcore.InfoLevel)) - _, err := r.reconcile(t.Context(), req, log, hcluster) + _, err := r.Reconcile(ctrl.LoggerInto(t.Context(), log), req) if err == nil { t.Fatalf("expected reconcile to return an error containing %q, but got nil", tc.expectedErrSubstring) @@ -9197,6 +9352,9 @@ func TestReconcileNonBlockingBehavior(t *testing.T) { } tc.verifyResources(t, fakeClient, hcpNamespace, hcName) + if tc.verifyRecovery != nil { + tc.verifyRecovery(t, r, hcluster) + } }) } } diff --git a/support/conditions/conditions_test.go b/support/conditions/conditions_test.go index 3956670dcd49..433dba714363 100644 --- a/support/conditions/conditions_test.go +++ b/support/conditions/conditions_test.go @@ -109,6 +109,14 @@ func TestExpectedHCConditions(t *testing.T) { g.Expect(expected[hyperv1.GCPEndpointAvailable]).To(Equal(metav1.ConditionTrue)) }) } + t.Run("When GCP resource tags are configured, it should require valid configuration", func(t *testing.T) { + g := NewWithT(t) + hc := &hyperv1.HostedCluster{} + hc.Spec.Platform.Type = hyperv1.GCPPlatform + hc.Spec.Platform.GCP = &hyperv1.GCPPlatformSpec{ResourceTags: []hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}}} + + g.Expect(ExpectedHCConditions(hc)[hyperv1.ValidHostedClusterConfiguration]).To(Equal(metav1.ConditionTrue)) + }) t.Run("When the control plane upgrades from 5.0 to 5.1, it should refresh credential expectations", func(t *testing.T) { g := NewWithT(t) hc := &hyperv1.HostedCluster{} diff --git a/support/globalconfig/infrastructure.go b/support/globalconfig/infrastructure.go index 5a95aeaaf3a6..c8dc9e2558ba 100644 --- a/support/globalconfig/infrastructure.go +++ b/support/globalconfig/infrastructure.go @@ -113,6 +113,7 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted IngressIPs: []string{}, } case hyperv1.GCPPlatform: + gcpStatusAlreadyExists := infra.Status.PlatformStatus.GCP != nil if infra.Status.PlatformStatus.GCP == nil { infra.Status.PlatformStatus.GCP = &configv1.GCPPlatformStatus{} } @@ -134,5 +135,23 @@ func ReconcileInfrastructure(infra *configv1.Infrastructure, hcp *hyperv1.Hosted }) } infra.Status.PlatformStatus.GCP.ResourceLabels = labels + var tags []configv1.GCPResourceTag + for _, tag := range hcp.Spec.Platform.GCP.ResourceTags { + parentID := tag.ParentID + if parentID == "" { + parentID = hcp.Spec.Platform.GCP.Project + } + tags = append(tags, configv1.GCPResourceTag{ + ParentID: parentID, + Key: tag.Key, + Value: tag.Value, + }) + } + // ResourceTags are immutable in the guest Infrastructure API. Do not + // backfill tags into an existing GCP status that was first written by an + // older control plane operator without them. + if !gcpStatusAlreadyExists || infra.Status.PlatformStatus.GCP.ResourceTags != nil || len(tags) == 0 { + infra.Status.PlatformStatus.GCP.ResourceTags = tags + } } } diff --git a/support/globalconfig/infrastructure_test.go b/support/globalconfig/infrastructure_test.go index a8bc3b484ed5..04159d75eb87 100644 --- a/support/globalconfig/infrastructure_test.go +++ b/support/globalconfig/infrastructure_test.go @@ -376,6 +376,76 @@ func TestReconcileInfrastructure(t *testing.T) { })) }, }, + { + name: "When GCP platform has resource tags, it should derive omitted parentIDs and preserve explicit parents", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.GCPPlatform) + hcp.Spec.Platform.GCP = &hyperv1.GCPPlatformSpec{ + Project: "customer-project", + Region: "us-central1", + ResourceTags: []hyperv1.GCPResourceTag{ + {Key: "Environment", Value: "production"}, + {ParentID: "customer-project", Key: "team", Value: "platform-123"}, + {ParentID: "123456789012", Key: "cost-center", Value: "shared"}, + {Key: "kubernetes-io", Value: "customer-defined"}, + }, + } + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Status.PlatformStatus.GCP.ResourceTags).To(Equal([]configv1.GCPResourceTag{ + {ParentID: "customer-project", Key: "Environment", Value: "production"}, + {ParentID: "customer-project", Key: "team", Value: "platform-123"}, + {ParentID: "123456789012", Key: "cost-center", Value: "shared"}, + {ParentID: "customer-project", Key: "kubernetes-io", Value: "customer-defined"}, + })) + }, + }, + { + name: "When GCP platform has no resource tags, it should leave guest tags omitted", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.GCPPlatform) + hcp.Spec.Platform.GCP = &hyperv1.GCPPlatformSpec{Project: "customer-project", ResourceTags: []hyperv1.GCPResourceTag{}} + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Status.PlatformStatus.GCP.ResourceTags).To(BeNil()) + }, + }, + { + name: "When GCP resource tags are omitted, it should leave guest tags omitted", + inputInfra: InfrastructureConfig(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.GCPPlatform) + hcp.Spec.Platform.GCP = &hyperv1.GCPPlatformSpec{Project: "customer-project"} + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Status.PlatformStatus.GCP.ResourceTags).To(BeNil()) + }, + }, + { + name: "When existing GCP status has no resource tags, it should not backfill them", + inputInfra: func() *configv1.Infrastructure { + infra := InfrastructureConfig() + infra.Status.PlatformStatus = &configv1.PlatformStatus{GCP: &configv1.GCPPlatformStatus{ProjectID: "customer-project", Region: "us-central1"}} + return infra + }(), + inputHCP: func() *hyperv1.HostedControlPlane { + hcp := baseHCP(hyperv1.GCPPlatform) + hcp.Spec.Platform.GCP = &hyperv1.GCPPlatformSpec{ + Project: "customer-project", + Region: "us-central1", + ResourceTags: []hyperv1.GCPResourceTag{{Key: "environment", Value: "production"}}, + } + return hcp + }(), + verify: func(g Gomega, infra *configv1.Infrastructure) { + g.Expect(infra.Status.PlatformStatus.GCP.ResourceTags).To(BeNil()) + }, + }, { name: "When GCP PlatformStatus is already initialized, it should update existing fields from HCP spec", inputInfra: func() *configv1.Infrastructure { diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go index b298c7acdf91..f415f8c08656 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/gcp.go @@ -47,6 +47,50 @@ type GCPResourceLabel struct { Value *string `json:"value,omitempty"` } +// GCPResourceTag selects a pre-existing Google Cloud Resource Manager tag by +// its parent, short key, and short value. The tag must be defined in the +// HostedCluster project or an organization before the cluster is created. +// See https://cloud.google.com/resource-manager/docs/tags/tags-overview. +type GCPResourceTag struct { + // parentID is the ID of the project or organization that defines the TagKey. + // When omitted, the HostedCluster's GCP project is used. + // An explicit value must be the HostedCluster's GCP project ID or a numeric + // organization ID. A numeric value is treated as an organization ID; project + // numbers are not supported. + // An organization ID consists of 1 to 32 decimal digits and must not start with 0. + // A project ID is 6 to 30 characters long, contains only lowercase letters, digits + // and hyphens, starts with a letter and must not end with a hyphen. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=32 + // +kubebuilder:validation:XValidation:rule="self.matches('(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)')",message="parentID must be a numeric organization ID or a valid GCP project ID" + ParentID string `json:"parentID,omitempty"` + + // key is required and is the short name of the pre-existing Resource Manager TagKey. + // A key is 1-63 characters, begins and ends with + // an ASCII alphanumeric character, and may contain letters, digits, '.', + // '_', or '-' between them. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$')",message="key must start and end with an ASCII letter or digit and contain only ASCII letters, digits, '.', '_' or '-'" + Key string `json:"key,omitempty"` + + // value is required and is the short name of the pre-existing Resource Manager TagValue for + // key. Exactly one value for a TagKey can be attached to a resource. A value + // is 1-63 characters, begins and ends with an ASCII alphanumeric character, + // and may contain ASCII letters and digits, `_-.@%=+:,*#&()[]{}`, and + // spaces between them. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:XValidation:rule="self.matches('^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\\\\[\\\\]{}\\\\- ]{0,61}[a-zA-Z0-9])?$')",message="value must start and end with an ASCII letter or digit and contain only ASCII letters, digits, spaces or the characters _-.@%=+:,*#&()[]{}" + Value string `json:"value,omitempty"` +} + // GCPEndpointAccessType defines the endpoint access type for GCP clusters. // Equivalent to AWS EndpointAccessType but adapted for GCP networking model. type GCPEndpointAccessType string @@ -107,6 +151,8 @@ type GCPNetworkConfig struct { // +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.storage.contains('@') && self.workloadIdentity.serviceAccountsEmails.storage.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="storage service account must belong to the same project" // +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.imageRegistry.contains('@') && self.workloadIdentity.serviceAccountsEmails.imageRegistry.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="imageRegistry service account must belong to the same project" // +kubebuilder:validation:XValidation:rule="self.workloadIdentity.serviceAccountsEmails.network.endsWith('@' + self.project + '.iam.gserviceaccount.com')",message="network service account must belong to the same project" +// +kubebuilder:validation:XValidation:rule="has(self.resourceTags) == has(oldSelf.resourceTags)",message="resourceTags may only be configured during installation",fieldPath=".resourceTags" +// +kubebuilder:validation:XValidation:rule="!has(self.resourceTags) || self.resourceTags.all(t, !has(t.parentID) || t.parentID.matches('^[1-9][0-9]{0,31}$') || t.parentID == self.project)",message="resourceTags parentID must be an organization ID or the GCP platform project",fieldPath=".resourceTags" type GCPPlatformSpec struct { // project is the GCP project ID. // A valid project ID must satisfy the following rules: @@ -161,6 +207,45 @@ type GCPPlatformSpec struct { // +kubebuilder:validation:MaxItems=60 ResourceLabels []GCPResourceLabel `json:"resourceLabels,omitempty"` + // resourceTags are pre-existing Google Cloud Resource Manager tags to apply to + // GCP resources created for the hosted cluster. + // Currently, tags are applied to newly provisioned persistent disks and, when the + // image registry is enabled, to its storage bucket. Other GCP resources, such as + // worker VMs and their boot disks, are not tagged. + // The set of tagged resources is subject to change over time. + // + // When omitted, no tags are applied through this setting. When specified, + // the list must contain 1 to 50 entries, and each short key must be unique + // across the list, even when the tags have different parents. A tag's + // parentID defaults to the HostedCluster's GCP project if omitted. + // Google Cloud permits at most 50 directly attached tags per resource, + // including tags attached by other actors. Coordinate with external tagging + // automation to leave sufficient capacity; admission does not reserve tag slots. + // + // The referenced tags must already exist, and the storage and imageRegistry + // service accounts must have permission to use them. Neither is checked when the + // HostedCluster is created. When a tag cannot be applied, provisioning of the + // affected disk or bucket can fail, and the error is reported in the persistent + // volume claim's events or the image registry operator's status. + // + // resourceTags can only be set when the HostedCluster is created. It cannot be + // added, removed or changed afterwards. If the configured list must change, + // the HostedCluster must be recreated. + // Resource tags require a control plane operator image that supports GCP resource + // tag propagation. When that capability is unavailable, reconciliation fails + // with a message identifying resourceTags on the ReconciliationSucceeded + // condition, and dependent component deployments are blocked until a + // supporting control plane operator image is configured. + // + // +optional + // +listType=atomic + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=50 + // +immutable + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resourceTags are immutable" + // +kubebuilder:validation:XValidation:rule="self.all(t, self.exists_one(u, u.key == t.key))",message="resourceTags keys must be unique across all parents" + ResourceTags []GCPResourceTag `json:"resourceTags,omitempty"` + // workloadIdentity configures Workload Identity Federation for the cluster. // This enables secure, short-lived token-based authentication without storing // long-term service account keys. These fields are immutable after cluster creation @@ -349,6 +434,7 @@ type GCPServiceAccountsEmails struct { // that manages GCS storage for the internal container image registry. // This GSA requires the following IAM roles: // - roles/storage.admin (Storage Admin - for creating and managing GCS buckets and objects) + // - roles/resourcemanager.tagUser (Tag User - for applying resource tags to the bucket) // See cmd/infra/gcp/iam-bindings.json for the authoritative role definitions. // Format: service-account-name@project-id.iam.gserviceaccount.com // diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go index ff15d4f1719d..97cffb545fd2 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/hostedcluster_types.go @@ -1486,6 +1486,8 @@ func PlatformTypes() []PlatformType { // PlatformSpec specifies the underlying infrastructure provider for the cluster // and is used to configure platform specific behavior. +// +// +openshift:validation:FeatureGateAwareXValidation:featureGate=GCPPlatform,rule="(has(self.gcp) && has(self.gcp.resourceTags)) == (has(oldSelf.gcp) && has(oldSelf.gcp.resourceTags))",message="resourceTags may only be configured during installation" type PlatformSpec struct { // type is the type of infrastructure provider for the cluster. // diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go index 4c0459433bcf..6b2522e73d84 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -2146,6 +2146,11 @@ func (in *GCPPlatformSpec) DeepCopyInto(out *GCPPlatformSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ResourceTags != nil { + in, out := &in.ResourceTags, &out.ResourceTags + *out = make([]GCPResourceTag, len(*in)) + copy(*out, *in) + } out.WorkloadIdentity = in.WorkloadIdentity } @@ -2302,6 +2307,21 @@ func (in *GCPResourceReference) DeepCopy() *GCPResourceReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GCPResourceTag) DeepCopyInto(out *GCPResourceTag) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPResourceTag. +func (in *GCPResourceTag) DeepCopy() *GCPResourceTag { + if in == nil { + return nil + } + out := new(GCPResourceTag) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPServiceAccountsEmails) DeepCopyInto(out *GCPServiceAccountsEmails) { *out = *in