Skip to content

Commit

Permalink
CFE-684 : Add user defined tags to the created gcp resource
Browse files Browse the repository at this point in the history
  • Loading branch information
bharath-b-rh committed Jun 27, 2023
1 parent 6e3a7df commit 6f394f3
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 1 deletion.
1 change: 1 addition & 0 deletions pkg/cloud/gcp/actuators/machine/actuator.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (a *Actuator) Create(ctx context.Context, machine *machinev1.Machine) error
coreClient: a.coreClient,
machine: machine,
computeClientBuilder: a.computeClientBuilder,
eventRecorder: a.eventRecorder,
})
if err != nil {
fmtErr := fmt.Errorf(scopeFailFmt, machine.GetName(), err)
Expand Down
21 changes: 21 additions & 0 deletions pkg/cloud/gcp/actuators/machine/machine_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
machineapierros "github.com/openshift/machine-api-operator/pkg/controller/machine"
computeservice "github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/services/compute"
"github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util"

"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
controllerclient "sigs.k8s.io/controller-runtime/pkg/client"
)
Expand All @@ -21,6 +23,7 @@ type machineScopeParams struct {
coreClient controllerclient.Client
machine *machinev1.Machine
computeClientBuilder computeservice.BuilderFuncType
eventRecorder record.EventRecorder
}

// machineScope defines a scope defined around a machine and its cluster.
Expand All @@ -43,6 +46,12 @@ type machineScope struct {
origProviderStatus *machinev1.GCPMachineProviderStatus

machineToBePatched controllerclient.Patch

eventRecorder record.EventRecorder

// Tags is a list of user-defined tags to apply to the resources created
// for the cluster
Tags []string
}

// newMachineScope creates a new MachineScope from the supplied parameters.
Expand Down Expand Up @@ -75,6 +84,16 @@ func newMachineScope(params machineScopeParams) (*machineScope, error) {
}
}

infra, err := util.GetInfrastructure(params.coreClient)
if err != nil {
return nil, fmt.Errorf("failed to get cluster infrastructure: %w", err)
}

tags, err := util.GetTagsList(infra.Status.PlatformStatus, providerSpec)
if err != nil {
return nil, fmt.Errorf("error getting list of user-defined tags: %w", err)
}

computeService, err := params.computeClientBuilder(serviceAccountJSON)
if err != nil {
return nil, machineapierros.InvalidMachineConfiguration("error creating compute service: %v", err)
Expand All @@ -97,6 +116,8 @@ func newMachineScope(params machineScopeParams) (*machineScope, error) {
origMachine: params.machine.DeepCopy(),
origProviderStatus: providerStatus.DeepCopy(),
machineToBePatched: controllerclient.MergeFrom(params.machine.DeepCopy()),
eventRecorder: params.eventRecorder,
Tags: tags,
}, nil
}

Expand Down
19 changes: 18 additions & 1 deletion pkg/cloud/gcp/actuators/machine/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import (
machinecontroller "github.com/openshift/machine-api-operator/pkg/controller/machine"
"github.com/openshift/machine-api-operator/pkg/metrics"
"github.com/openshift/machine-api-operator/pkg/util/windows"
"github.com/openshift/machine-api-provider-gcp/pkg/cloud/gcp/actuators/util"

"google.golang.org/api/compute/v1"
googleapi "google.golang.org/api/googleapi"
"google.golang.org/api/googleapi"
corev1 "k8s.io/api/core/v1"
apimachineryerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -345,6 +347,21 @@ func (r *Reconciler) create() error {
}
return fmt.Errorf("failed to create instance via compute service: %v", err)
}

tags := &util.GCPTags{
CoreClient: r.coreClient,
Namespace: r.machine.GetNamespace(),
ProviderSpec: *r.providerSpec,
ProjectID: r.projectID,
InstanceID: instance.Id,
InstanceZone: instance.Zone,
TagValues: r.Tags,
}
if err := tags.ApplyTagsToComputeInstance(r.Context); err != nil {
r.eventRecorder.Eventf(r.machine, corev1.EventTypeWarning, createEventAction,
"failed to add user-defined tags to %s compute instance: %v", r.machine.Name, err)
}

return r.reconcileMachineWithCloudState(nil)
}

Expand Down
235 changes: 235 additions & 0 deletions pkg/cloud/gcp/actuators/util/gcp_tags_labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*
Copyright 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 util

import (
"context"
"fmt"
"time"

rscmgr "cloud.google.com/go/resourcemanager/apiv3"
rscmgrpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"golang.org/x/time/rate"
"google.golang.org/api/iterator"
"google.golang.org/api/option"

configv1 "github.com/openshift/api/config/v1"
machinev1 "github.com/openshift/api/machine/v1beta1"

"k8s.io/klog/v2"
controllerclient "sigs.k8s.io/controller-runtime/pkg/client"
)

const (
// globalInfrastructureName is the default name of the Infrastructure object
globalInfrastructureName = "cluster"

// resourceManagerEndPoint is the host name to be used for binding tag values
// to the VM resources.
resourceManagerEndPoint = "cloudresourcemanager.googleapis.com"

// computeAPIEndPoint is the endpoint for identifying the VM resource
computeAPIEndPoint = "//compute.googleapis.com"
)

type GCPTags struct {
CoreClient controllerclient.Client
Namespace string
ProviderSpec machinev1.GCPMachineProviderSpec
ProjectID string
InstanceID uint64
InstanceZone string
TagValues []string
}

func GetInfrastructure(client controllerclient.Client) (*configv1.Infrastructure, error) {
infra := &configv1.Infrastructure{}
infraName := controllerclient.ObjectKey{Name: globalInfrastructureName}

if err := client.Get(context.Background(), infraName, infra); err != nil {
return nil, fmt.Errorf("failed to get infrastructure: %w", err)
}

return infra, nil
}

func newLimiter(limit, burst int, emptyBucket bool) *rate.Limiter {
limiter := rate.NewLimiter(rate.Every(time.Second/time.Duration(limit)), burst)

if emptyBucket {
// Start with empty bucket to exert control
// over token renewal rate during the first burst.
limiter.AllowN(time.Now(), burst)
}

return limiter
}

func getInfraResourceTagsList(platformStatus *configv1.PlatformStatus) (tags map[string]string) {
if platformStatus != nil && platformStatus.GCP != nil && platformStatus.GCP.ResourceTags != nil {
tags = make(map[string]string, len(platformStatus.GCP.ResourceTags))
for _, tag := range platformStatus.GCP.ResourceTags {
tags[tag.Key] = tag.Value
}
}
return
}

func GetTagsList(platformStatus *configv1.PlatformStatus, providerSpec *machinev1.GCPMachineProviderSpec) ([]string, error) {
tags := getInfraResourceTagsList(platformStatus)

if len(tags) < 0 && len(providerSpec.UserTags) < 0 {
return nil, nil
}

if platformStatus.GCP.OrganizationID == "" &&
providerSpec.OrganizationID == "" {
return nil, fmt.Errorf("organizationID must be defined either in" +
"infrstructure.status or Machine.Spec.ProviderSpec")
}
orgID := getOrganizationID(platformStatus.GCP.OrganizationID, providerSpec.OrganizationID)

if tags == nil {
return toTagValueList(orgID, providerSpec.UserTags), nil
}

// merge tags present in Infrastructure.Status with
// the tags configured in GCPMachineProviderSpec, with
// precedence given to those in GCPMachineProviderSpec
// for new or updated tags.
for k, v := range providerSpec.UserTags {
tags[k] = v
}

if len(tags) > 50 {
return nil, fmt.Errorf("maximum of 50 tags can be added to a VM instance, "+
"infrstructure.status.resourceTags Machine.Spec.ProviderSpec.UserTags put together configured tag count is %d", len(tags))
}

return toTagValueList(orgID, tags), nil
}

func (tags *GCPTags) ApplyTagsToComputeInstance(ctx context.Context) error {
if len(tags.TagValues) < 0 {
return nil
}

parent := fmt.Sprintf("%s/projects/%s/zones/%s/instances/%d",
computeAPIEndPoint, tags.ProjectID, tags.InstanceZone, tags.InstanceID)

client, err := tags.getTagBindingsClient(ctx)
if err != nil || client == nil {
return fmt.Errorf("failed to create tag binding client for adding tags to %d machine: %w", tags.InstanceID, err)
}
defer client.Close()

filteredTags := getFilteredTagList(ctx, client, parent, tags.TagValues)

// GCP has a rate limit of 10 requests per second. we will
// restrict to 8.
limiter := newLimiter(8, 8, true)

tagBindingReq := &rscmgrpb.CreateTagBindingRequest{
TagBinding: &rscmgrpb.TagBinding{
Parent: parent,
},
}
errFlag := false
for _, value := range filteredTags {
if err := limiter.Wait(ctx); err != nil {
errFlag = true
klog.Errorf("rate limiting request to add %s tag to %d VM failed: %w", value, tags.InstanceID, err)
}

tagBindingReq.TagBinding.TagValueNamespacedName = value
result, err := client.CreateTagBinding(ctx, tagBindingReq)
if err != nil {
errFlag = true
klog.Errorf("request to add %s tag to %d VM failed", value, tags.InstanceID)
}

if _, err = result.Wait(ctx); err != nil {
errFlag = true
klog.Errorf("failed to add %s tag to %d VM", value, tags.InstanceID)
}
}
if errFlag {
return fmt.Errorf("failed to add tags to %d VM", tags.InstanceID)
}
return nil
}

func (tags *GCPTags) getTagBindingsClient(ctx context.Context) (*rscmgr.TagBindingsClient, error) {
cred, err := GetCredentialsSecret(tags.CoreClient, tags.Namespace, tags.ProviderSpec)
if err != nil {
return nil, err
}

endpoint := fmt.Sprintf("https://%s-%s", tags.InstanceZone, resourceManagerEndPoint)
opts := []option.ClientOption{
option.WithCredentialsJSON([]byte(cred)),
option.WithEndpoint(endpoint),
}
return rscmgr.NewTagBindingsRESTClient(ctx, opts...)
}

func getFilteredTagList(ctx context.Context, client *rscmgr.TagBindingsClient, parent string, tagList []string) []string {
dupTags := make(map[string]bool, len(tagList))
for _, k := range tagList {
dupTags[k] = false
}

listBindingsReq := &rscmgrpb.ListEffectiveTagsRequest{
Parent: parent,
}
bindings := client.ListEffectiveTags(ctx, listBindingsReq)
for {
binding, read := bindings.Next()
if read == iterator.Done {
break
}
if _, exist := dupTags[binding.GetNamespacedTagValue()]; exist {
dupTags[binding.GetNamespacedTagValue()] = true
}
}

filteredTags := make([]string, 0, len(tagList))
for tagValue, dup := range dupTags {
if !dup {
filteredTags = append(filteredTags, tagValue)
}
}

return filteredTags
}

func getOrganizationID(source1, source2 string) string {
if source1 != "" {
return source1
}
return source2
}

func toTagValueList(orgID string, tags map[string]string) []string {
if len(tags) < 0 {
return nil
}

list := make([]string, 0, len(tags))
for k, v := range tags {
tag := fmt.Sprintf("%s/%s/%s", orgID, k, v)
list = append(list, tag)
}
return list
}

0 comments on commit 6f394f3

Please sign in to comment.