diff --git a/api/v1alpha3/zz_generated.conversion.go b/api/v1alpha3/zz_generated.conversion.go index 9214688742..dd84220291 100644 --- a/api/v1alpha3/zz_generated.conversion.go +++ b/api/v1alpha3/zz_generated.conversion.go @@ -414,6 +414,7 @@ func autoConvert_v1beta1_GCPClusterSpec_To_v1alpha3_GCPClusterSpec(in *v1beta1.G } out.FailureDomains = *(*[]string)(unsafe.Pointer(&in.FailureDomains)) out.AdditionalLabels = *(*Labels)(unsafe.Pointer(&in.AdditionalLabels)) + // WARNING: in.ResourceManagerTags requires manual conversion: does not exist in peer-type // WARNING: in.CredentialsRef requires manual conversion: does not exist in peer-type return nil } @@ -567,6 +568,7 @@ func autoConvert_v1beta1_GCPMachineSpec_To_v1alpha3_GCPMachineSpec(in *v1beta1.G out.AdditionalMetadata = *(*[]MetadataItem)(unsafe.Pointer(&in.AdditionalMetadata)) out.PublicIP = (*bool)(unsafe.Pointer(in.PublicIP)) out.AdditionalNetworkTags = *(*[]string)(unsafe.Pointer(&in.AdditionalNetworkTags)) + // WARNING: in.ResourceManagerTags requires manual conversion: does not exist in peer-type out.RootDeviceSize = in.RootDeviceSize out.RootDeviceType = (*DiskType)(unsafe.Pointer(in.RootDeviceType)) out.AdditionalDisks = *(*[]AttachedDiskSpec)(unsafe.Pointer(&in.AdditionalDisks)) diff --git a/api/v1alpha4/gcpcluster_conversion.go b/api/v1alpha4/gcpcluster_conversion.go index f55cc12df5..953a24b938 100644 --- a/api/v1alpha4/gcpcluster_conversion.go +++ b/api/v1alpha4/gcpcluster_conversion.go @@ -52,6 +52,10 @@ func (src *GCPCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint dst.Spec.CredentialsRef = restored.Spec.CredentialsRef.DeepCopy() } + if restored.Spec.ResourceManagerTags != nil { + dst.Spec.ResourceManagerTags = restored.Spec.ResourceManagerTags.DeepCopy() + } + return nil } diff --git a/api/v1alpha4/gcpclustertemplate_conversion.go b/api/v1alpha4/gcpclustertemplate_conversion.go index 9c946103b7..7d21d46154 100644 --- a/api/v1alpha4/gcpclustertemplate_conversion.go +++ b/api/v1alpha4/gcpclustertemplate_conversion.go @@ -54,6 +54,10 @@ func (src *GCPClusterTemplate) ConvertTo(dstRaw conversion.Hub) error { // nolin dst.Spec.Template.Spec.CredentialsRef = restored.Spec.Template.Spec.CredentialsRef.DeepCopy() } + if restored.Spec.Template.Spec.ResourceManagerTags != nil { + dst.Spec.Template.Spec.ResourceManagerTags = restored.Spec.Template.Spec.ResourceManagerTags.DeepCopy() + } + return nil } diff --git a/api/v1alpha4/gcpmachine_conversion.go b/api/v1alpha4/gcpmachine_conversion.go index 3388f94bd0..24c4c237d3 100644 --- a/api/v1alpha4/gcpmachine_conversion.go +++ b/api/v1alpha4/gcpmachine_conversion.go @@ -53,6 +53,10 @@ func (src *GCPMachine) ConvertTo(dstRaw conversion.Hub) error { // nolint dst.Spec.ConfidentialCompute = restored.Spec.ConfidentialCompute } + if restored.Spec.ResourceManagerTags != nil { + dst.Spec.ResourceManagerTags = restored.Spec.ResourceManagerTags + } + return nil } diff --git a/api/v1alpha4/gcpmachinetemplate_conversion.go b/api/v1alpha4/gcpmachinetemplate_conversion.go index 2c5fa5e2a5..a12cf51701 100644 --- a/api/v1alpha4/gcpmachinetemplate_conversion.go +++ b/api/v1alpha4/gcpmachinetemplate_conversion.go @@ -54,6 +54,10 @@ func (src *GCPMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { // nolin dst.Spec.Template.Spec.ConfidentialCompute = restored.Spec.Template.Spec.ConfidentialCompute } + if restored.Spec.Template.Spec.ResourceManagerTags != nil { + dst.Spec.Template.Spec.ResourceManagerTags = restored.Spec.Template.Spec.ResourceManagerTags + } + return nil } diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index 3d6b0f6132..ccc324965d 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -459,6 +459,7 @@ func autoConvert_v1beta1_GCPClusterSpec_To_v1alpha4_GCPClusterSpec(in *v1beta1.G } out.FailureDomains = *(*[]string)(unsafe.Pointer(&in.FailureDomains)) out.AdditionalLabels = *(*Labels)(unsafe.Pointer(&in.AdditionalLabels)) + // WARNING: in.ResourceManagerTags requires manual conversion: does not exist in peer-type // WARNING: in.CredentialsRef requires manual conversion: does not exist in peer-type return nil } @@ -734,6 +735,7 @@ func autoConvert_v1beta1_GCPMachineSpec_To_v1alpha4_GCPMachineSpec(in *v1beta1.G out.AdditionalMetadata = *(*[]MetadataItem)(unsafe.Pointer(&in.AdditionalMetadata)) out.PublicIP = (*bool)(unsafe.Pointer(in.PublicIP)) out.AdditionalNetworkTags = *(*[]string)(unsafe.Pointer(&in.AdditionalNetworkTags)) + // WARNING: in.ResourceManagerTags requires manual conversion: does not exist in peer-type out.RootDeviceSize = in.RootDeviceSize out.RootDeviceType = (*DiskType)(unsafe.Pointer(in.RootDeviceType)) out.AdditionalDisks = *(*[]AttachedDiskSpec)(unsafe.Pointer(&in.AdditionalDisks)) diff --git a/api/v1beta1/gcpcluster_types.go b/api/v1beta1/gcpcluster_types.go index 920cda0e36..61dd570371 100644 --- a/api/v1beta1/gcpcluster_types.go +++ b/api/v1beta1/gcpcluster_types.go @@ -54,12 +54,56 @@ type GCPClusterSpec struct { // +optional AdditionalLabels Labels `json:"additionalLabels,omitempty"` + // resourceManagerTags is an optional set of tags to apply to GCP resources managed + // by the GCP provider. GCP supports a maximum of 50 tags per resource. + // +kubebuilder:validation:MaxItems=50 + // +listType=map + // +listMapKey=key + // +optional + ResourceManagerTags ResourceManagerTags `json:"resourceManagerTags,omitempty"` + // CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this cluster. If not // supplied then the credentials of the controller will be used. // +optional CredentialsRef *ObjectReference `json:"credentialsRef,omitempty"` } +// ResourceManagerTag is a tag to apply to GCP resources managed by the GCP provider. +type ResourceManagerTag struct { + // parentID is the ID of the hierarchical resource where the tags are defined + // e.g. at the Organization or the Project level. To find the Organization or Project ID ref + // https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id + // https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects + // An OrganizationID must consist of decimal numbers, and cannot have leading zeroes. + // A ProjectID must be 6 to 30 characters in length, can only contain lowercase letters, + // numbers, and hyphens, and must start with a letter, and cannot end with a hyphen. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=32 + // +kubebuilder:validation:Pattern=`(^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$)` + ParentID string `json:"parentID"` + + // key is the key part of the tag. A tag key can have a maximum of 63 characters and cannot + // be empty. Tag key must begin and end with an alphanumeric character, and must contain + // only uppercase, lowercase alphanumeric characters, and the following special + // characters `._-`. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$` + Key string `json:"key"` + + // value is the value part of the tag. A tag value can have a maximum of 63 characters and + // cannot be empty. Tag value must begin and end with an alphanumeric character, and must + // contain only uppercase, lowercase alphanumeric characters, and the following special + // characters `_-.@%=+:,*#&(){}[]` and spaces. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$` + Value string `json:"value"` +} + // GCPClusterStatus defines the observed state of GCPCluster. type GCPClusterStatus struct { FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"` diff --git a/api/v1beta1/gcpmachine_types.go b/api/v1beta1/gcpmachine_types.go index 4fff0c10e7..f574a6bbc3 100644 --- a/api/v1beta1/gcpmachine_types.go +++ b/api/v1beta1/gcpmachine_types.go @@ -197,6 +197,14 @@ type GCPMachineSpec struct { // +optional AdditionalNetworkTags []string `json:"additionalNetworkTags,omitempty"` + // resourceManagerTags is an optional set of tags to apply to GCP resources managed + // by the GCP provider. GCP supports a maximum of 50 tags per resource. + // +kubebuilder:validation:MaxItems=50 + // +listType=map + // +listMapKey=key + // +optional + ResourceManagerTags ResourceManagerTags `json:"resourceManagerTags,omitempty"` + // RootDeviceSize is the size of the root volume in GB. // Defaults to 30. // +optional diff --git a/api/v1beta1/tags.go b/api/v1beta1/tags.go new file mode 100644 index 0000000000..f4e0f08d10 --- /dev/null +++ b/api/v1beta1/tags.go @@ -0,0 +1,62 @@ +/* +Copyright 2021 The Kubernetes Authors. + +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. +*/ + +package v1beta1 + +import ( + "context" + "fmt" + + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" + rmpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// ResourceManagerTags defines a list of tags. +type ResourceManagerTags []ResourceManagerTag + +// ResourceManagerTagsMap defines a map of key value pairs as expected by compute.InstanceParams.ResourceManagerTags. +type ResourceManagerTagsMap map[string]string + +// AddResourceManagerTags binds the passed resource-manager tags to the resource. Tag keys and Tag Values +// will be created by the user and only the Tag bindings to the Compute Instance will be created. +// If the Tag Key/Tag Value cannot be retrieved or no tags are provided, this will be empty and no tags will be added. +func AddResourceManagerTags(ctx context.Context, tagList ResourceManagerTags) ResourceManagerTagsMap { + tagValueList := make(ResourceManagerTagsMap, len(tagList)) + log := log.FromContext(ctx) + if len(tagList) == 0 { + return tagValueList + } + + client, err := resourcemanager.NewTagValuesClient(ctx) + if err != nil { + log.Error(err, "failed to create tag values client") + return tagValueList + } + + getTagValuesReq := &rmpb.GetNamespacedTagValueRequest{} + for _, tag := range tagList { + getTagValuesReq.Name = fmt.Sprintf("%s/%s/%s", tag.ParentID, tag.Key, tag.Value) + value, err := client.GetNamespacedTagValue(ctx, getTagValuesReq) + if err != nil { + log.Error(err, "failed to retrieve tag value") + return tagValueList + } + tagValueList[value.Parent] = value.Name + } + + return tagValueList +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 21c85dce9a..d00707bbfc 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -176,6 +176,11 @@ func (in *GCPClusterSpec) DeepCopyInto(out *GCPClusterSpec) { (*out)[key] = val } } + if in.ResourceManagerTags != nil { + in, out := &in.ResourceManagerTags, &out.ResourceManagerTags + *out = make(ResourceManagerTags, len(*in)) + copy(*out, *in) + } if in.CredentialsRef != nil { in, out := &in.CredentialsRef, &out.CredentialsRef *out = new(ObjectReference) @@ -413,6 +418,11 @@ func (in *GCPMachineSpec) DeepCopyInto(out *GCPMachineSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.ResourceManagerTags != nil { + in, out := &in.ResourceManagerTags, &out.ResourceManagerTags + *out = make(ResourceManagerTags, len(*in)) + copy(*out, *in) + } if in.RootDeviceType != nil { in, out := &in.RootDeviceType, &out.RootDeviceType *out = new(DiskType) @@ -760,6 +770,61 @@ func (in *ObjectReference) DeepCopy() *ObjectReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceManagerTag) DeepCopyInto(out *ResourceManagerTag) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceManagerTag. +func (in *ResourceManagerTag) DeepCopy() *ResourceManagerTag { + if in == nil { + return nil + } + out := new(ResourceManagerTag) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ResourceManagerTags) DeepCopyInto(out *ResourceManagerTags) { + { + in := &in + *out = make(ResourceManagerTags, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceManagerTags. +func (in ResourceManagerTags) DeepCopy() ResourceManagerTags { + if in == nil { + return nil + } + out := new(ResourceManagerTags) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ResourceManagerTagsMap) DeepCopyInto(out *ResourceManagerTagsMap) { + { + in := &in + *out = make(ResourceManagerTagsMap, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceManagerTagsMap. +func (in ResourceManagerTagsMap) DeepCopy() ResourceManagerTagsMap { + if in == nil { + return nil + } + out := new(ResourceManagerTagsMap) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccount) DeepCopyInto(out *ServiceAccount) { *out = *in diff --git a/cloud/interfaces.go b/cloud/interfaces.go index 1ed41d2ec2..12dfe6f08e 100644 --- a/cloud/interfaces.go +++ b/cloud/interfaces.go @@ -60,6 +60,7 @@ type ClusterGetter interface { AdditionalLabels() infrav1.Labels FailureDomains() clusterv1.FailureDomains ControlPlaneEndpoint() clusterv1.APIEndpoint + ResourceManagerTags() infrav1.ResourceManagerTags } // ClusterSetter is an interface which can set cluster information. diff --git a/cloud/scope/cluster.go b/cloud/scope/cluster.go index d981e7e075..d47a82b37c 100644 --- a/cloud/scope/cluster.go +++ b/cloud/scope/cluster.go @@ -130,6 +130,11 @@ func (s *ClusterScope) AdditionalLabels() infrav1.Labels { return s.GCPCluster.Spec.AdditionalLabels } +// ResourceManagerTags returns the cluster resource tags. +func (s *ClusterScope) ResourceManagerTags() infrav1.ResourceManagerTags { + return s.GCPCluster.Spec.ResourceManagerTags +} + // ControlPlaneEndpoint returns the cluster control-plane endpoint. func (s *ClusterScope) ControlPlaneEndpoint() clusterv1.APIEndpoint { endpoint := s.GCPCluster.Spec.ControlPlaneEndpoint diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 24ec9dcb99..b230a832d6 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -338,6 +338,9 @@ func (m *MachineScope) InstanceSpec(log logr.Logger) *compute.Instance { m.ClusterGetter.Name(), ), }, + Params: &compute.InstanceParams{ + ResourceManagerTags: infrav1.AddResourceManagerTags(context.TODO(), m.GCPMachine.Spec.ResourceManagerTags), + }, Labels: infrav1.Build(infrav1.BuildParams{ ClusterName: m.ClusterGetter.Name(), Lifecycle: infrav1.ResourceLifecycleOwned, diff --git a/cloud/scope/managedcluster.go b/cloud/scope/managedcluster.go index 1ace53d878..e4838d5921 100644 --- a/cloud/scope/managedcluster.go +++ b/cloud/scope/managedcluster.go @@ -133,6 +133,11 @@ func (s *ManagedClusterScope) AdditionalLabels() infrav1.Labels { return s.GCPManagedCluster.Spec.AdditionalLabels } +// ResourceManagerTags returns the cluster resource tags. +func (s *ManagedClusterScope) ResourceManagerTags() infrav1.ResourceManagerTags { + return s.GCPManagedCluster.Spec.ResourceManagerTags +} + // ControlPlaneEndpoint returns the cluster control-plane endpoint. func (s *ManagedClusterScope) ControlPlaneEndpoint() clusterv1.APIEndpoint { endpoint := s.GCPManagedCluster.Spec.ControlPlaneEndpoint diff --git a/cloud/services/compute/instances/reconcile_test.go b/cloud/services/compute/instances/reconcile_test.go index 3e3bf734df..0f59c5e96d 100644 --- a/cloud/services/compute/instances/reconcile_test.go +++ b/cloud/services/compute/instances/reconcile_test.go @@ -126,6 +126,7 @@ func getFakeGCPMachine() *infrav1.GCPMachine { AdditionalLabels: map[string]string{ "foo": "bar", }, + ResourceManagerTags: []infrav1.ResourceManagerTag{}, }, } } @@ -255,6 +256,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", Scheduling: &compute.Scheduling{}, ServiceAccounts: []*compute.ServiceAccount{ @@ -316,6 +320,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", Scheduling: &compute.Scheduling{}, ServiceAccounts: []*compute.ServiceAccount{ @@ -379,6 +386,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", Scheduling: &compute.Scheduling{}, ServiceAccounts: []*compute.ServiceAccount{ @@ -442,6 +452,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", Scheduling: &compute.Scheduling{ OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyTerminate)), @@ -508,6 +521,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-c/instances/my-machine", Scheduling: &compute.Scheduling{ OnHostMaintenance: strings.ToUpper(string(infrav1.HostMaintenancePolicyMigrate)), @@ -567,6 +583,9 @@ func TestService_createOrGetInstance(t *testing.T) { Network: "projects/my-proj/global/networks/default", }, }, + Params: &compute.InstanceParams{ + ResourceManagerTags: map[string]string{}, + }, SelfLink: "https://www.googleapis.com/compute/v1/projects/proj-id/zones/us-central1-a/instances/my-machine", Scheduling: &compute.Scheduling{}, ServiceAccounts: []*compute.ServiceAccount{ diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclusters.yaml index 12931b622a..303cddc5fc 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclusters.yaml @@ -636,6 +636,13 @@ spec: region: description: The GCP Region the cluster lives in. type: string + resourceManagerTags: + additionalProperties: + type: string + description: resourceManagerTags is an optional set of tags to apply + to GCP resources managed by the GCP provider. GCP supports a maximum + of 50 tags per resource. + type: object required: - project - region diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclustertemplates.yaml index 2c261b371d..c5ce280b75 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpclustertemplates.yaml @@ -357,6 +357,13 @@ spec: region: description: The GCP Region the cluster lives in. type: string + resourceManagerTags: + additionalProperties: + type: string + description: resourceManagerTags is an optional set of tags + to apply to GCP resources managed by the GCP provider. GCP + supports a maximum of 50 tags per resource. + type: object required: - project - region diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml index 7543659c2f..a8d12549f4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachines.yaml @@ -601,6 +601,60 @@ spec: public IP. Set this to true if you don't have a NAT instances or Cloud Nat setup. type: boolean + resourceManagerTags: + description: resourceManagerTags is an optional set of tags to apply + to GCP resources managed by the GCP provider. GCP supports a maximum + of 50 tags per resource. + items: + description: ResourceManagerTag is a tag to apply to GCP resources + managed by the GCP provider. + properties: + key: + description: key is the key part of the tag. A tag key can have + a maximum of 63 characters and cannot be empty. Tag key must + begin and end with an alphanumeric character, and must contain + only uppercase, lowercase alphanumeric characters, and the + following special characters `._-`. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$ + type: string + parentID: + description: parentID is the ID of the hierarchical resource + where the tags are defined e.g. at the Organization or the + Project level. To find the Organization or Project ID ref + https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id + https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects + An OrganizationID must consist of decimal numbers, and cannot + have leading zeroes. A ProjectID must be 6 to 30 characters + in length, can only contain lowercase letters, numbers, and + hyphens, and must start with a letter, and cannot end with + a hyphen. + maxLength: 32 + minLength: 1 + pattern: (^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) + type: string + value: + description: value is the value part of the tag. A tag value + can have a maximum of 63 characters and cannot be empty. Tag + value must begin and end with an alphanumeric character, and + must contain only uppercase, lowercase alphanumeric characters, + and the following special characters `_-.@%=+:,*#&(){}[]` + and spaces. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$ + type: string + required: + - key + - parentID + - value + type: object + maxItems: 50 + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map rootDeviceSize: description: RootDeviceSize is the size of the root volume in GB. Defaults to 30. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml index 43a9712b5b..38d89f1619 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmachinetemplates.yaml @@ -493,6 +493,61 @@ spec: get a public IP. Set this to true if you don't have a NAT instances or Cloud Nat setup. type: boolean + resourceManagerTags: + description: resourceManagerTags is an optional set of tags + to apply to GCP resources managed by the GCP provider. GCP + supports a maximum of 50 tags per resource. + items: + description: ResourceManagerTag is a tag to apply to GCP + resources managed by the GCP provider. + properties: + key: + description: key is the key part of the tag. A tag key + can have a maximum of 63 characters and cannot be + empty. Tag key must begin and end with an alphanumeric + character, and must contain only uppercase, lowercase + alphanumeric characters, and the following special + characters `._-`. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.-]{0,61}[a-zA-Z0-9])?$ + type: string + parentID: + description: parentID is the ID of the hierarchical + resource where the tags are defined e.g. at the Organization + or the Project level. To find the Organization or + Project ID ref https://cloud.google.com/resource-manager/docs/creating-managing-organization#retrieving_your_organization_id + https://cloud.google.com/resource-manager/docs/creating-managing-projects#identifying_projects + An OrganizationID must consist of decimal numbers, + and cannot have leading zeroes. A ProjectID must be + 6 to 30 characters in length, can only contain lowercase + letters, numbers, and hyphens, and must start with + a letter, and cannot end with a hyphen. + maxLength: 32 + minLength: 1 + pattern: (^[1-9][0-9]{0,31}$)|(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) + type: string + value: + description: value is the value part of the tag. A tag + value can have a maximum of 63 characters and cannot + be empty. Tag value must begin and end with an alphanumeric + character, and must contain only uppercase, lowercase + alphanumeric characters, and the following special + characters `_-.@%=+:,*#&(){}[]` and spaces. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z0-9]([0-9A-Za-z_.@%=+:,*#&()\[\]{}\-\s]{0,61}[a-zA-Z0-9])?$ + type: string + required: + - key + - parentID + - value + type: object + maxItems: 50 + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map rootDeviceSize: description: RootDeviceSize is the size of the root volume in GB. Defaults to 30. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclusters.yaml index c62a9eb448..096b4dd62f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_gcpmanagedclusters.yaml @@ -187,6 +187,13 @@ spec: region: description: The GCP Region the cluster lives in. type: string + resourceManagerTags: + additionalProperties: + type: string + description: resourceManagerTags is an optional set of tags to apply + to GCP resources managed by the GCP provider. GCP supports a maximum + of 50 tags per resource. + type: object required: - project - region diff --git a/exp/api/v1beta1/gcpmanagedcluster_types.go b/exp/api/v1beta1/gcpmanagedcluster_types.go index cc0921ff0b..9328f62859 100644 --- a/exp/api/v1beta1/gcpmanagedcluster_types.go +++ b/exp/api/v1beta1/gcpmanagedcluster_types.go @@ -49,6 +49,14 @@ type GCPManagedClusterSpec struct { // +optional AdditionalLabels infrav1.Labels `json:"additionalLabels,omitempty"` + // resourceManagerTags is an optional set of tags to apply to GCP resources managed + // by the GCP provider. GCP supports a maximum of 50 tags per resource. + // +kubebuilder:validation:MaxItems=50 + // +listType=map + // +listMapKey=key + // +optional + ResourceManagerTags infrav1.ResourceManagerTags `json:"resourceManagerTags,omitempty"` + // CredentialsRef is a reference to a Secret that contains the credentials to use for provisioning this cluster. If not // supplied then the credentials of the controller will be used. // +optional diff --git a/exp/api/v1beta1/zz_generated.deepcopy.go b/exp/api/v1beta1/zz_generated.deepcopy.go index 1e0e020beb..0ea132b028 100644 --- a/exp/api/v1beta1/zz_generated.deepcopy.go +++ b/exp/api/v1beta1/zz_generated.deepcopy.go @@ -98,6 +98,11 @@ func (in *GCPManagedClusterSpec) DeepCopyInto(out *GCPManagedClusterSpec) { (*out)[key] = val } } + if in.ResourceManagerTags != nil { + in, out := &in.ResourceManagerTags, &out.ResourceManagerTags + *out = make(apiv1beta1.ResourceManagerTags, len(*in)) + copy(*out, *in) + } if in.CredentialsRef != nil { in, out := &in.CredentialsRef, &out.CredentialsRef *out = new(apiv1beta1.ObjectReference) diff --git a/exp/controllers/suite_test.go b/exp/controllers/suite_test.go index 47befcf416..07d16df777 100644 --- a/exp/controllers/suite_test.go +++ b/exp/controllers/suite_test.go @@ -34,9 +34,11 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment +) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -67,7 +69,6 @@ var _ = BeforeSuite(func() { k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) - }) var _ = AfterSuite(func() { diff --git a/go.mod b/go.mod index 8d01c47ab7..a9774be9a1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cloud.google.com/go/compute v1.23.0 cloud.google.com/go/container v1.25.0 cloud.google.com/go/iam v1.1.2 + cloud.google.com/go/resourcemanager v1.9.1 github.com/GoogleCloudPlatform/k8s-cloud-provider v1.24.0 github.com/go-logr/logr v1.2.4 github.com/google/go-cmp v0.5.9 @@ -33,7 +34,10 @@ require ( sigs.k8s.io/controller-runtime v0.15.0 ) -require golang.org/x/sync v0.3.0 // indirect +require ( + cloud.google.com/go/longrunning v0.5.1 // indirect + golang.org/x/sync v0.3.0 // indirect +) require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect diff --git a/go.sum b/go.sum index 42650e56a7..9d1d9f1c12 100644 --- a/go.sum +++ b/go.sum @@ -35,10 +35,14 @@ cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4= cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/resourcemanager v1.9.1 h1:QIAMfndPOHR6yTmMUB0ZN+HSeRmPjR/21Smq5/xwghI= +cloud.google.com/go/resourcemanager v1.9.1/go.mod h1:dVCuosgrh1tINZ/RwBufr8lULmWGOkPS8gL5gqyjdT8= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=