Skip to content

Commit

Permalink
CFE-684: Add user defined tags to the created gcp resources
Browse files Browse the repository at this point in the history
  • Loading branch information
bharath-b-rh committed Nov 27, 2023
1 parent 9ad4bc2 commit 9f7fd2a
Show file tree
Hide file tree
Showing 6 changed files with 571 additions and 12 deletions.
10 changes: 10 additions & 0 deletions pkg/cloud/gcp/actuators/machine/machine_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type machineScope struct {

// gcpLabelsTagsFeatureEnabled indicates whether FeatureGateGCPLabelsTags is enabled
gcpLabelsTagsFeatureEnabled bool

// resourceTagManager is for handling resource manager tags related operations.
resourceTagManager util.TagManager
}

// newMachineScope creates a new MachineScope from the supplied parameters.
Expand Down Expand Up @@ -93,6 +96,12 @@ func newMachineScope(params machineScopeParams) (*machineScope, error) {
klog.V(1).Infof("status of %v feature is %t", configv1.FeatureGateGCPLabelsTags, gcpLabelsTagsFeature)
}

tagManager, err := util.NewTagManager(params.coreClient, gcpLabelsTagsFeature,
serviceAccountJSON, providerSpec.ResourceManagerTags)
if err != nil {
return nil, err
}

return &machineScope{
Context: params.Context,
coreClient: params.coreClient,
Expand All @@ -112,6 +121,7 @@ func newMachineScope(params machineScopeParams) (*machineScope, error) {
origProviderStatus: providerStatus.DeepCopy(),
machineToBePatched: controllerclient.MergeFrom(params.machine.DeepCopy()),
gcpLabelsTagsFeatureEnabled: gcpLabelsTagsFeature,
resourceTagManager: tagManager,
}, nil
}

Expand Down
17 changes: 13 additions & 4 deletions pkg/cloud/gcp/actuators/machine/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ func (r *Reconciler) create() error {
},
}

userTags, err := r.resourceTagManager.GetResourceManagerTags(r.Context)
if err != nil {
return machinecontroller.CreateMachine("failed to fetch user-defined tags for %s: %v", r.machine.Name, err)
}
instance.Params = &compute.InstanceParams{
ResourceManagerTags: userTags,
}

if automaticRestart, err := restartPolicyToBool(r.providerSpec.RestartPolicy, r.providerSpec.Preemptible); err != nil {
return machinecontroller.InvalidMachineConfiguration("failed to determine restart policy: %v", err)
} else {
Expand Down Expand Up @@ -247,10 +255,11 @@ func (r *Reconciler) create() error {
AutoDelete: disk.AutoDelete,
Boot: disk.Boot,
InitializeParams: &compute.AttachedDiskInitializeParams{
DiskSizeGb: disk.SizeGB,
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone, disk.Type),
Labels: labels,
SourceImage: srcImage,
DiskSizeGb: disk.SizeGB,
DiskType: fmt.Sprintf("zones/%s/diskTypes/%s", zone, disk.Type),
SourceImage: srcImage,
Labels: labels,
ResourceManagerTags: userTags,
},
DiskEncryptionKey: generateDiskEncryptionKey(disk.EncryptionKey, r.projectID),
})
Expand Down
26 changes: 21 additions & 5 deletions pkg/cloud/gcp/actuators/machine/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
machinev1 "github.com/openshift/api/machine/v1beta1"
machinecontroller "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"
compute "google.golang.org/api/compute/v1"
googleapi "google.golang.org/api/googleapi"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -442,13 +443,27 @@ func TestCreate(t *testing.T) {
Zone: "test-zone",
MachineType: "n2d-standard-4",
ConfidentialCompute: machinev1.ConfidentialComputePolicyEnabled,
ResourceManagerTags: []machinev1.ResourceManagerTag{
{ParentID: "openshift", Key: "key1", Value: "value1"},
},
},
validateInstance: func(t *testing.T, instance *compute.Instance) {
if instance.ConfidentialInstanceConfig.EnableConfidentialCompute != true {
t.Errorf("Expected EnableConfidentialCompute to be true, Got: %t", instance.ConfidentialInstanceConfig.EnableConfidentialCompute)
}
},
},
{
name: "failed to fetch resource manager tags",
providerSpec: &machinev1.GCPMachineProviderSpec{
Region: "test-region",
Zone: "test-zone",
ResourceManagerTags: []machinev1.ResourceManagerTag{
{ParentID: "openshift", Key: "key2", Value: "value2"},
},
},
expectedError: errors.New("failed to fetch user-defined tags for : fail to fetch resource manager {openshift key2 value2} tag"),
},
}

for _, tc := range cases {
Expand All @@ -475,11 +490,12 @@ func TestCreate(t *testing.T) {
Labels: labels,
},
},
coreClient: controllerfake.NewFakeClient(),
providerSpec: providerSpec,
providerStatus: &machinev1.GCPMachineProviderStatus{},
computeService: mockComputeService,
projectID: providerSpec.ProjectID,
coreClient: controllerfake.NewFakeClient(),
providerSpec: providerSpec,
providerStatus: &machinev1.GCPMachineProviderStatus{},
computeService: mockComputeService,
projectID: providerSpec.ProjectID,
resourceTagManager: util.NewFakeTagManager(providerSpec.ResourceManagerTags),
}

reconciler := newReconciler(&machineScope)
Expand Down
151 changes: 151 additions & 0 deletions pkg/cloud/gcp/actuators/util/gcp_tags_labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ import (
"context"
"fmt"

rscmgr "cloud.google.com/go/resourcemanager/apiv3"
rscmgrpb "cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"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"
)

Expand All @@ -28,8 +35,41 @@ const (
// ocpDefaultLabelFmt is the format string for the default label
// added to the OpenShift created GCP resources.
ocpDefaultLabelFmt = "kubernetes-io-cluster-%s"

// resourceManagerHostSubPath is the endpoint for tag requests.
resourceManagerHostSubPath = "cloudresourcemanager.googleapis.com"
)

// tagManager handles resource tagging.
type tagManager struct {
tagsFeatureEnabled bool
machineProviderSpecTags []machinev1.ResourceManagerTag

client controllerclient.Client
tagClientOptions []option.ClientOption
}

// tagValuesClient handles operations on tag value resources.
type tagValuesClient struct {
*rscmgr.TagValuesClient
}

// TagManager is the interface that wraps methods for resource tag operations.
type TagManager interface {
GetResourceManagerTags(context.Context) (map[string]string, error)
}

// NewTagManager creates a tagManager instance.
func NewTagManager(client controllerclient.Client, tagsFeatureEnabled bool,
credentials string, machineProviderSpecTags []machinev1.ResourceManagerTag) (TagManager, error) {
return &tagManager{
client: client,
tagsFeatureEnabled: tagsFeatureEnabled,
machineProviderSpecTags: machineProviderSpecTags,
tagClientOptions: getTagClientOptions(credentials),
}, nil
}

// GetInfrastructure returns the Infrastructure object infrastructure/cluster or empty
// on encountering any error.
func GetInfrastructure(client controllerclient.Client) (*configv1.Infrastructure, error) {
Expand Down Expand Up @@ -119,3 +159,114 @@ func GetLabelsList(userLabelsAllowed bool, client controllerclient.Client, machi

return labels, nil
}

// getInfraResourceTagsList returns the user-defined tags present in the
// status sub-resource of Infrastructure.
func getInfraResourceTagsList(platformStatus *configv1.PlatformStatus) (tags []machinev1.ResourceManagerTag) {
if platformStatus != nil && platformStatus.GCP != nil && platformStatus.GCP.ResourceTags != nil {
tags = make([]machinev1.ResourceManagerTag, len(platformStatus.GCP.ResourceTags))
for i, tag := range platformStatus.GCP.ResourceTags {
tags[i] = machinev1.ResourceManagerTag{
ParentID: tag.ParentID,
Key: tag.Key,
Value: tag.Value,
}
}
}
return
}

// getTagClientOptions returns the tag client options adding the credentials and
// the endpoint which will be used by the client.
func getTagClientOptions(cred string) []option.ClientOption {
return []option.ClientOption{
option.WithCredentialsJSON([]byte(cred)),
option.WithEndpoint(fmt.Sprintf("https://%s", resourceManagerHostSubPath)),
}
}

// getTagValuesClient returns the client to be used for reading tag values to
// the resources.
func (t *tagManager) getTagValuesClient(ctx context.Context) (*tagValuesClient, error) {
client, err := rscmgr.NewTagValuesRESTClient(ctx, t.tagClientOptions...)
return &tagValuesClient{client}, err
}

func (c *tagValuesClient) getTagValuesNames(ctx context.Context, tagList []machinev1.ResourceManagerTag) (map[string]string, error) {
tagValueList := make(map[string]string, len(tagList))
getTagValuesReq := &rscmgrpb.GetNamespacedTagValueRequest{}
for _, tag := range tagList {
getTagValuesReq.Name = fmt.Sprintf("%s/%s/%s", tag.ParentID, tag.Key, tag.Value)
value, err := c.GetNamespacedTagValue(ctx, getTagValuesReq)
if err != nil {
return nil, err
}
tagValueList[value.Parent] = value.Name
}

return tagValueList, nil
}

// mergeInfraProviderSpecTags merges user-defined tags in Infrastructure.Status and
// GCPMachineProviderSpec, with precedence given to those in GCPMachineProviderSpec
// for new or updated tags. infraTags arg will contain the merged list.
func mergeInfraProviderSpecTags(infraTags *[]machinev1.ResourceManagerTag, providerSpecTags []machinev1.ResourceManagerTag) {
for _, pTag := range providerSpecTags {
appendTag := true
for i, iTag := range *infraTags {
if pTag.ParentID == iTag.ParentID && pTag.Key == iTag.Key && pTag.Value == iTag.Value {
appendTag = false
break
}
if pTag.ParentID == iTag.ParentID && pTag.Key == iTag.Key && pTag.Value != iTag.Value {
appendTag = false
(*infraTags)[i].Value = pTag.Value
break
}
}
if appendTag {
*infraTags = append(*infraTags, pTag)
}
}
}

// GetResourceManagerTags returns the merged list of user-defined tags in Infrastructure.Status
// and GCPMachineProviderSpec to apply on the resources.
func (t *tagManager) GetResourceManagerTags(ctx context.Context) (map[string]string, error) {
if !t.tagsFeatureEnabled {
klog.V(1).Infof("getResourceManagerTags: GCP tags featureGate is not enabled, skip adding tags")
return nil, nil
}

infra, err := GetInfrastructure(t.client)
if err != nil {
return nil, fmt.Errorf("failed to get cluster infrastructure: %w", err)
}
userTags := getInfraResourceTagsList(infra.Status.PlatformStatus)

if len(userTags) <= 0 && len(t.machineProviderSpecTags) <= 0 {
klog.V(1).Infof("getResourceManagerTags: user-defined tags in Infrastructure and MachineProviderSpec is empty")
return nil, nil
}

client, err := t.getTagValuesClient(ctx)
if err != nil {
return nil, err
}
defer client.Close()

if len(userTags) <= 0 {
klog.V(1).Infof("getResourceManagerTags: user-defined tags in Infrastructure is empty")
return client.getTagValuesNames(ctx, t.machineProviderSpecTags)
}

mergeInfraProviderSpecTags(&userTags, t.machineProviderSpecTags)

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

return client.getTagValuesNames(ctx, userTags)
}
29 changes: 29 additions & 0 deletions pkg/cloud/gcp/actuators/util/gcp_tags_labels_fakes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package util

import (
"context"
"fmt"

machinev1 "github.com/openshift/api/machine/v1beta1"
)

type FakeTagManager struct {
tags []machinev1.ResourceManagerTag
}

func NewFakeTagManager(tags []machinev1.ResourceManagerTag) *FakeTagManager {
return &FakeTagManager{tags: tags}
}

func (f *FakeTagManager) GetResourceManagerTags(ctx context.Context) (map[string]string, error) {
retTags := make(map[string]string)
if f.tags != nil {
for _, tag := range f.tags {
if tag.ParentID == "openshift" && tag.Key == "key2" && tag.Value == "value2" {
return nil, fmt.Errorf("fail to fetch resource manager %v tag", tag)
}
retTags[tag.Key] = tag.Value
}
}
return retTags, nil
}

0 comments on commit 9f7fd2a

Please sign in to comment.