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..7ceb3655e7 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() } + for _, restoredTag := range restored.Spec.ResourceManagerTags { + dst.Spec.ResourceManagerTags = append(dst.Spec.ResourceManagerTags, *restoredTag.DeepCopy()) + } + return nil } diff --git a/api/v1alpha4/gcpclustertemplate_conversion.go b/api/v1alpha4/gcpclustertemplate_conversion.go index 9c946103b7..a4fae1a343 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() } + for _, restoredTag := range restored.Spec.Template.Spec.ResourceManagerTags { + dst.Spec.Template.Spec.ResourceManagerTags = append(dst.Spec.Template.Spec.ResourceManagerTags, *restoredTag.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..b3025d0136 100644 --- a/api/v1beta1/gcpcluster_types.go +++ b/api/v1beta1/gcpcluster_types.go @@ -54,6 +54,12 @@ 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. + // +maxItems=50 + // +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 diff --git a/api/v1beta1/gcpmachine_types.go b/api/v1beta1/gcpmachine_types.go index 4fff0c10e7..8463855584 100644 --- a/api/v1beta1/gcpmachine_types.go +++ b/api/v1beta1/gcpmachine_types.go @@ -197,6 +197,12 @@ 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. + // +maxItems=50 + // +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..424d672f34 --- /dev/null +++ b/api/v1beta1/tags.go @@ -0,0 +1,64 @@ +/* +Copyright 2023 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 + +// ResourceManagerTags is an slice of ResourceManagerTag structs. +type ResourceManagerTags []ResourceManagerTag + +// ResourceManagerTagsMap defines a map of key value pairs as expected by compute.InstanceParams.ResourceManagerTags. +type ResourceManagerTagsMap map[string]string + +// 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"` +} + +// Merge merges resource manager tags in receiver and other. +func (t *ResourceManagerTags) Merge(other ResourceManagerTags) { + *t = append(*t, other...) +} 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/clients.go b/cloud/scope/clients.go index e70db2a74c..e92c3fa3c8 100644 --- a/cloud/scope/clients.go +++ b/cloud/scope/clients.go @@ -24,6 +24,7 @@ import ( computerest "cloud.google.com/go/compute/apiv1" container "cloud.google.com/go/container/apiv1" credentials "cloud.google.com/go/iam/credentials/apiv1" + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud" "github.com/pkg/errors" "google.golang.org/api/compute/v1" @@ -143,3 +144,19 @@ func newInstanceGroupManagerClient(ctx context.Context, credentialsRef *infrav1. return instanceGroupManagersClient, nil } + +func newTagBindingsClient(ctx context.Context, credentialsRef *infrav1.ObjectReference, crClient client.Client, location string) (*resourcemanager.TagBindingsClient, error) { + opts, err := defaultClientOptions(ctx, credentialsRef, crClient) + endpoint := fmt.Sprintf("%s-cloudresourcemanager.googleapis.com:443", location) + opts = append(opts, option.WithEndpoint(endpoint)) + if err != nil { + return nil, fmt.Errorf("getting default gcp client options: %w", err) + } + + client, err := resourcemanager.NewTagBindingsClient(ctx, opts...) + if err != nil { + return nil, errors.Errorf("failed to create gcp tag binding client: %v", err) + } + + return client, nil +} diff --git a/cloud/scope/cluster.go b/cloud/scope/cluster.go index d981e7e075..47621ac4fd 100644 --- a/cloud/scope/cluster.go +++ b/cloud/scope/cluster.go @@ -130,6 +130,15 @@ func (s *ClusterScope) AdditionalLabels() infrav1.Labels { return s.GCPCluster.Spec.AdditionalLabels } +// ResourceManagerTags returns ResourceManagerTags from the scope's GCPCluster. The returned value will never be nil. +func (s *ClusterScope) ResourceManagerTags() infrav1.ResourceManagerTags { + if len(s.GCPCluster.Spec.ResourceManagerTags) == 0 { + s.GCPCluster.Spec.ResourceManagerTags = infrav1.ResourceManagerTags{} + } + + return s.GCPCluster.Spec.ResourceManagerTags.DeepCopy() +} + // 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..b83b9eb4aa 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -35,6 +35,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" "sigs.k8s.io/cluster-api-provider-gcp/cloud" "sigs.k8s.io/cluster-api-provider-gcp/cloud/providerid" + "sigs.k8s.io/cluster-api-provider-gcp/cloud/services/shared" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" "sigs.k8s.io/cluster-api/controllers/noderefutil" capierrors "sigs.k8s.io/cluster-api/errors" @@ -239,9 +240,10 @@ func (m *MachineScope) InstanceImageSpec() *compute.AttachedDisk { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskSizeGb: m.GCPMachine.Spec.RootDeviceSize, - DiskType: path.Join("zones", m.Zone(), "diskTypes", string(diskType)), - SourceImage: sourceImage, + DiskSizeGb: m.GCPMachine.Spec.RootDeviceSize, + DiskType: path.Join("zones", m.Zone(), "diskTypes", string(diskType)), + ResourceManagerTags: shared.ResourceTagConvert(context.TODO(), m.GCPMachine.Spec.ResourceManagerTags), + SourceImage: sourceImage, }, } } @@ -253,8 +255,9 @@ func (m *MachineScope) InstanceAdditionalDiskSpec() []*compute.AttachedDisk { additionalDisk := &compute.AttachedDisk{ AutoDelete: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskSizeGb: pointer.Int64Deref(disk.Size, 30), - DiskType: path.Join("zones", m.Zone(), "diskTypes", string(*disk.DeviceType)), + DiskSizeGb: pointer.Int64Deref(disk.Size, 30), + DiskType: path.Join("zones", m.Zone(), "diskTypes", string(*disk.DeviceType)), + ResourceManagerTags: shared.ResourceTagConvert(context.TODO(), m.GCPMachine.Spec.ResourceManagerTags), }, } if strings.HasSuffix(additionalDisk.InitializeParams.DiskType, string(infrav1.LocalSsdDiskType)) { @@ -338,6 +341,9 @@ func (m *MachineScope) InstanceSpec(log logr.Logger) *compute.Instance { m.ClusterGetter.Name(), ), }, + Params: &compute.InstanceParams{ + ResourceManagerTags: shared.ResourceTagConvert(context.TODO(), m.ResourceManagerTags()), + }, Labels: infrav1.Build(infrav1.BuildParams{ ClusterName: m.ClusterGetter.Name(), Lifecycle: infrav1.ResourceLifecycleOwned, @@ -428,3 +434,16 @@ func (m *MachineScope) PatchObject() error { func (m *MachineScope) Close() error { return m.PatchObject() } + +// ResourceManagerTags merges ResourceManagerTags from the scope's GCPCluster and GCPMachine. If the same key is present in both, +// the value from GCPMachine takes precedence. The returned ResourceManagerTags will never be nil. +func (m *MachineScope) ResourceManagerTags() infrav1.ResourceManagerTags { + tags := infrav1.ResourceManagerTags{} + + // Start with the cluster-wide tags... + tags.Merge(m.ClusterGetter.ResourceManagerTags()) + // ... and merge in the Machine's + tags.Merge(m.GCPMachine.Spec.ResourceManagerTags) + + return tags +} diff --git a/cloud/scope/managedcluster.go b/cloud/scope/managedcluster.go index 1ace53d878..967fa7b720 100644 --- a/cloud/scope/managedcluster.go +++ b/cloud/scope/managedcluster.go @@ -133,6 +133,15 @@ func (s *ManagedClusterScope) AdditionalLabels() infrav1.Labels { return s.GCPManagedCluster.Spec.AdditionalLabels } +// ResourceManagerTags returns ResourceManagerTags from cluster. The returned value will never be nil. +func (s *ManagedClusterScope) ResourceManagerTags() infrav1.ResourceManagerTags { + if len(s.GCPManagedCluster.Spec.ResourceManagerTags) == 0 { + s.GCPManagedCluster.Spec.ResourceManagerTags = infrav1.ResourceManagerTags{} + } + + return s.GCPManagedCluster.Spec.ResourceManagerTags.DeepCopy() +} + // ControlPlaneEndpoint returns the cluster control-plane endpoint. func (s *ManagedClusterScope) ControlPlaneEndpoint() clusterv1.APIEndpoint { endpoint := s.GCPManagedCluster.Spec.ControlPlaneEndpoint diff --git a/cloud/scope/managedcontrolplane.go b/cloud/scope/managedcontrolplane.go index f0c022f246..8e730bd908 100644 --- a/cloud/scope/managedcontrolplane.go +++ b/cloud/scope/managedcontrolplane.go @@ -26,6 +26,7 @@ import ( container "cloud.google.com/go/container/apiv1" credentials "cloud.google.com/go/iam/credentials/apiv1" + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" "github.com/pkg/errors" infrav1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/api/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" @@ -43,6 +44,7 @@ const ( type ManagedControlPlaneScopeParams struct { CredentialsClient *credentials.IamCredentialsClient ManagedClusterClient *container.ClusterManagerClient + TagBindingsClient *resourcemanager.TagBindingsClient Client client.Client Cluster *clusterv1.Cluster GCPManagedCluster *infrav1exp.GCPManagedCluster @@ -74,6 +76,13 @@ func NewManagedControlPlaneScope(ctx context.Context, params ManagedControlPlane } params.ManagedClusterClient = managedClusterClient } + if params.TagBindingsClient == nil { + tagBindingsClient, err := newTagBindingsClient(ctx, params.GCPManagedCluster.Spec.CredentialsRef, params.Client, params.GCPManagedCluster.Spec.Region) + if err != nil { + return nil, errors.Errorf("failed to create gcp tag bindings client: %v", err) + } + params.TagBindingsClient = tagBindingsClient + } if params.CredentialsClient == nil { var credentialsClient *credentials.IamCredentialsClient credentialsClient, err = newIamCredentialsClient(ctx, params.GCPManagedCluster.Spec.CredentialsRef, params.Client) @@ -94,6 +103,7 @@ func NewManagedControlPlaneScope(ctx context.Context, params ManagedControlPlane GCPManagedCluster: params.GCPManagedCluster, GCPManagedControlPlane: params.GCPManagedControlPlane, mcClient: params.ManagedClusterClient, + tagBindingsClient: params.TagBindingsClient, credentialsClient: params.CredentialsClient, credential: credential, patchHelper: helper, @@ -109,6 +119,7 @@ type ManagedControlPlaneScope struct { GCPManagedCluster *infrav1exp.GCPManagedCluster GCPManagedControlPlane *infrav1exp.GCPManagedControlPlane mcClient *container.ClusterManagerClient + tagBindingsClient *resourcemanager.TagBindingsClient credentialsClient *credentials.IamCredentialsClient credential *Credential @@ -132,6 +143,7 @@ func (s *ManagedControlPlaneScope) PatchObject() error { // Close closes the current scope persisting the managed control plane configuration and status. func (s *ManagedControlPlaneScope) Close() error { s.mcClient.Close() + s.tagBindingsClient.Close() s.credentialsClient.Close() return s.PatchObject() } @@ -151,6 +163,11 @@ func (s *ManagedControlPlaneScope) ManagedControlPlaneClient() *container.Cluste return s.mcClient } +// TagBindingsClient returns a client used to interact with resource manager tags. +func (s *ManagedControlPlaneScope) TagBindingsClient() *resourcemanager.TagBindingsClient { + return s.tagBindingsClient +} + // CredentialsClient returns a client used to interact with IAM. func (s *ManagedControlPlaneScope) CredentialsClient() *credentials.IamCredentialsClient { return s.credentialsClient diff --git a/cloud/services/compute/instances/reconcile_test.go b/cloud/services/compute/instances/reconcile_test.go index 3e3bf734df..bf7487d3c9 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{}, }, } } @@ -231,8 +232,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-c/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -255,6 +257,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{ @@ -292,8 +297,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-c/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -316,6 +322,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{ @@ -355,8 +364,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-c/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -379,6 +389,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{ @@ -418,8 +431,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-c/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -442,6 +456,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)), @@ -484,8 +501,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-c/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-c/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -508,6 +526,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)), @@ -543,8 +564,9 @@ func TestService_createOrGetInstance(t *testing.T) { AutoDelete: true, Boot: true, InitializeParams: &compute.AttachedDiskInitializeParams{ - DiskType: "zones/us-central1-a/diskTypes/pd-standard", - SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + DiskType: "zones/us-central1-a/diskTypes/pd-standard", + SourceImage: "projects/my-proj/global/images/family/capi-ubuntu-1804-k8s-v1-19", + ResourceManagerTags: map[string]string{}, }, }, }, @@ -567,6 +589,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/cloud/services/container/clusters/reconcile.go b/cloud/services/container/clusters/reconcile.go index 89894f50f2..88f42f4821 100644 --- a/cloud/services/container/clusters/reconcile.go +++ b/cloud/services/container/clusters/reconcile.go @@ -280,6 +280,17 @@ func (s *Service) createCluster(ctx context.Context, log *logr.Logger) error { return err } + err = shared.ResourceTagBinding( + ctx, + s.scope.TagBindingsClient(), + s.scope.GCPManagedCluster.Spec, + s.scope.ClusterName(), + ) + if err != nil { + log.Error(err, "Error binding tags to cluster resources", "name", s.scope.ClusterName()) + return err + } + return nil } diff --git a/cloud/services/shared/tags.go b/cloud/services/shared/tags.go new file mode 100644 index 0000000000..12b7fd5f35 --- /dev/null +++ b/cloud/services/shared/tags.go @@ -0,0 +1,98 @@ +/* +Copyright 2023 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 shared + +import ( + "context" + "fmt" + + resourcemanager "cloud.google.com/go/resourcemanager/apiv3" + rmpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb" + infrav1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" + infrav1exp "sigs.k8s.io/cluster-api-provider-gcp/exp/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// ResourceTagBinding creates a TagBinding between a TagValue and a Google Cloud resource. +// If any of the SDK calls fail, the error is logged and no action is taken. +func ResourceTagBinding(ctx context.Context, client *resourcemanager.TagBindingsClient, spec infrav1exp.GCPManagedClusterSpec, name string) error { + for _, tag := range spec.ResourceManagerTags { + tagValue, err := getTagValues(ctx, tag) + if err != nil { + return fmt.Errorf("failed to retrieve tag value: %w", err) + } + req := &rmpb.CreateTagBindingRequest{ + TagBinding: &rmpb.TagBinding{ + Parent: fmt.Sprintf("//container.googleapis.com/projects/%s/locations/%s/clusters/%s", tag.ParentID, spec.Region, name), + TagValue: tagValue.Name, + }, + } + op, err := client.CreateTagBinding(ctx, req) + if err != nil { + return fmt.Errorf("failed to create tag binding: %w", err) + } + + _, err = op.Wait(ctx) + if err != nil { + return fmt.Errorf("tag binding operation failed: %w", err) + } + } + + return nil +} + +// ResourceTagConvert converts the passed resource-manager tags to a GCP API valid format. +// Tag keys and Tag Values will be created by the user and only the Tag bindings to the Compute Instance will be +// handled by CAPG. 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 ResourceTagConvert(ctx context.Context, t infrav1.ResourceManagerTags) infrav1.ResourceManagerTagsMap { + tagValueList := make(infrav1.ResourceManagerTagsMap, len(t)) + log := log.FromContext(ctx) + if len(t) == 0 { + return tagValueList + } + + for _, tag := range t { + tagValue, err := getTagValues(ctx, tag) + if err != nil { + log.Error(err, "failed to retrieve tag value") + continue + } + tagValueList[tagValue.Parent] = tagValue.Name + } + + return tagValueList +} + +func getTagValues(ctx context.Context, tag infrav1.ResourceManagerTag) (*rmpb.TagValue, error) { + log := log.FromContext(ctx) + client, err := resourcemanager.NewTagValuesClient(ctx) + if err != nil { + log.Error(err, "failed to create tag values client") + return &rmpb.TagValue{}, err + } + defer client.Close() + + req := &rmpb.GetNamespacedTagValueRequest{ + Name: fmt.Sprintf("%s/%s/%s", tag.ParentID, tag.Key, tag.Value), + } + tagValue, err := client.GetNamespacedTagValue(ctx, req) + if err != nil { + return &rmpb.TagValue{}, err + } + + return tagValue, nil +} 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..a7572cbcbf 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,56 @@ spec: region: description: The GCP Region the cluster lives in. type: string + 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 + type: array 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..e9a0520e11 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,57 @@ spec: region: description: The GCP Region the cluster lives in. type: string + 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 + type: array 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..67e62cf31c 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,56 @@ 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 + type: array 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..c2b9e98cbd 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,57 @@ 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 + type: array 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..6b38297da8 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,56 @@ spec: region: description: The GCP Region the cluster lives in. type: string + 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 + type: array required: - project - region diff --git a/exp/api/v1beta1/gcpmanagedcluster_types.go b/exp/api/v1beta1/gcpmanagedcluster_types.go index cc0921ff0b..851acd9968 100644 --- a/exp/api/v1beta1/gcpmanagedcluster_types.go +++ b/exp/api/v1beta1/gcpmanagedcluster_types.go @@ -49,6 +49,12 @@ 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. + // +maxItems=50 + // +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 1e2b33ec72..a5df513335 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 23bda9dc9c..8184aa81ad 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( cloud.google.com/go/compute v1.23.1 cloud.google.com/go/container v1.26.1 cloud.google.com/go/iam v1.1.3 + 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.6.0 @@ -33,7 +34,11 @@ require ( sigs.k8s.io/controller-runtime v0.15.0 ) -require golang.org/x/sync v0.4.0 // indirect +require ( + cloud.google.com/go v0.110.8 // indirect + cloud.google.com/go/longrunning v0.5.1 // indirect + golang.org/x/sync v0.4.0 // indirect +) require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect diff --git a/go.sum b/go.sum index 11e2d51cad..fbb70dd173 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.110.8 h1:tyNdfIxjzaWctIiLYOTalaLKZ17SI44SKFW26QbOhME= +cloud.google.com/go v0.110.8/go.mod h1:Iz8AkXJf1qmxC3Oxoep8R1T36w8B92yU29PcBhHO5fk= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -35,10 +36,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.3 h1:18tKG7DzydKWUnLjonWcJO6wjSCAtzh4GcRKlH/Hrzc= cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE= +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=