github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/gcp/client.go (about)

     1  package gcp
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/pkg/errors"
    10  	googleoauth "golang.org/x/oauth2/google"
    11  	"google.golang.org/api/cloudresourcemanager/v3"
    12  	compute "google.golang.org/api/compute/v1"
    13  	dns "google.golang.org/api/dns/v1"
    14  	"google.golang.org/api/googleapi"
    15  	iam "google.golang.org/api/iam/v1"
    16  	"google.golang.org/api/option"
    17  	"google.golang.org/api/serviceusage/v1"
    18  	"k8s.io/apimachinery/pkg/util/sets"
    19  
    20  	gcpconsts "github.com/openshift/installer/pkg/constants/gcp"
    21  )
    22  
    23  //go:generate mockgen -source=./client.go -destination=./mock/gcpclient_generated.go -package=mock
    24  
    25  const defaultTimeout = 2 * time.Minute
    26  
    27  var (
    28  	// RequiredBasePermissions is the list of permissions required for an installation.
    29  	// A list of valid permissions can be found at https://cloud.google.com/iam/docs/understanding-roles.
    30  	RequiredBasePermissions = []string{}
    31  )
    32  
    33  // API represents the calls made to the API.
    34  type API interface {
    35  	GetNetwork(ctx context.Context, network, project string) (*compute.Network, error)
    36  	GetMachineType(ctx context.Context, project, zone, machineType string) (*compute.MachineType, error)
    37  	GetMachineTypeWithZones(ctx context.Context, project, region, machineType string) (*compute.MachineType, sets.Set[string], error)
    38  	GetPublicDomains(ctx context.Context, project string) ([]string, error)
    39  	GetDNSZone(ctx context.Context, project, baseDomain string, isPublic bool) (*dns.ManagedZone, error)
    40  	GetDNSZoneByName(ctx context.Context, project, zoneName string) (*dns.ManagedZone, error)
    41  	GetSubnetworks(ctx context.Context, network, project, region string) ([]*compute.Subnetwork, error)
    42  	GetProjects(ctx context.Context) (map[string]string, error)
    43  	GetRegions(ctx context.Context, project string) ([]string, error)
    44  	GetRecordSets(ctx context.Context, project, zone string) ([]*dns.ResourceRecordSet, error)
    45  	GetZones(ctx context.Context, project, filter string) ([]*compute.Zone, error)
    46  	GetEnabledServices(ctx context.Context, project string) ([]string, error)
    47  	GetServiceAccount(ctx context.Context, project, serviceAccount string) (string, error)
    48  	GetCredentials() *googleoauth.Credentials
    49  	GetImage(ctx context.Context, name string, project string) (*compute.Image, error)
    50  	GetProjectPermissions(ctx context.Context, project string, permissions []string) (sets.Set[string], error)
    51  	GetProjectByID(ctx context.Context, project string) (*cloudresourcemanager.Project, error)
    52  	ValidateServiceAccountHasPermissions(ctx context.Context, project string, permissions []string) (bool, error)
    53  	GetProjectTags(ctx context.Context, projectID string) (sets.Set[string], error)
    54  	GetNamespacedTagValue(ctx context.Context, tagNamespacedName string) (*cloudresourcemanager.TagValue, error)
    55  }
    56  
    57  // Client makes calls to the GCP API.
    58  type Client struct {
    59  	ssn *Session
    60  }
    61  
    62  // NewClient initializes a client with a session.
    63  func NewClient(ctx context.Context) (*Client, error) {
    64  	ssn, err := GetSession(ctx)
    65  	if err != nil {
    66  		return nil, errors.Wrap(err, "failed to get session")
    67  	}
    68  
    69  	client := &Client{
    70  		ssn: ssn,
    71  	}
    72  	return client, nil
    73  }
    74  
    75  // GetMachineType uses the GCP Compute Service API to get the specified machine type.
    76  func (c *Client) GetMachineType(ctx context.Context, project, zone, machineType string) (*compute.MachineType, error) {
    77  	svc, err := c.getComputeService(ctx)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
    83  	defer cancel()
    84  	req, err := svc.MachineTypes.Get(project, zone, machineType).Context(ctx).Do()
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	return req, nil
    90  }
    91  
    92  // GetMachineTypeList retrieves the machine type with the specified fields.
    93  func GetMachineTypeList(ctx context.Context, svc *compute.Service, project, region, machineType, fields string) ([]*compute.MachineType, error) {
    94  	var machines []*compute.MachineType
    95  
    96  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
    97  	defer cancel()
    98  
    99  	filter := fmt.Sprintf("name = \"%s\" AND zone : %s-*", machineType, region)
   100  	req := svc.MachineTypes.AggregatedList(project).Filter(filter).Context(ctx)
   101  	if len(fields) > 0 {
   102  		req.Fields(googleapi.Field(fields))
   103  	}
   104  
   105  	err := req.Pages(ctx, func(page *compute.MachineTypeAggregatedList) error {
   106  		for _, scopedList := range page.Items {
   107  			machines = append(machines, scopedList.MachineTypes...)
   108  		}
   109  		return nil
   110  	})
   111  
   112  	return machines, err
   113  }
   114  
   115  // GetMachineTypeWithZones retrieves the specified machine type and the zones in which it is available.
   116  func (c *Client) GetMachineTypeWithZones(ctx context.Context, project, region, machineType string) (*compute.MachineType, sets.Set[string], error) {
   117  	svc, err := c.getComputeService(ctx)
   118  	if err != nil {
   119  		return nil, nil, err
   120  	}
   121  
   122  	pz, err := GetZones(ctx, svc, project, fmt.Sprintf("region eq .*%s", region))
   123  	if err != nil {
   124  		return nil, nil, err
   125  	}
   126  	projZones := sets.New[string]()
   127  	for _, zone := range pz {
   128  		projZones.Insert(zone.Name)
   129  	}
   130  
   131  	machines, err := GetMachineTypeList(ctx, svc, project, region, machineType, "")
   132  	if err != nil {
   133  		return nil, nil, err
   134  	}
   135  
   136  	// Custom machine types are not included in aggregated lists, so let's try
   137  	// to get the machine type directly before returning an error. Also
   138  	// fallback to all the zones in the project
   139  	if len(machines) == 0 {
   140  		cctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   141  		defer cancel()
   142  		machine, err := svc.MachineTypes.Get(project, pz[0].Name, machineType).Context(cctx).Do()
   143  		if err != nil {
   144  			return nil, nil, fmt.Errorf("failed to fetch instance type: %w", err)
   145  		}
   146  		return machine, projZones, nil
   147  	}
   148  
   149  	zones := sets.New[string]()
   150  	for _, machine := range machines {
   151  		zones.Insert(machine.Zone)
   152  	}
   153  	// Restrict to zones avaialable in the project
   154  	zones = zones.Intersection(projZones)
   155  
   156  	return machines[0], zones, nil
   157  }
   158  
   159  // GetNetwork uses the GCP Compute Service API to get a network by name from a project.
   160  func (c *Client) GetNetwork(ctx context.Context, network, project string) (*compute.Network, error) {
   161  	svc, err := c.getComputeService(ctx)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   167  	defer cancel()
   168  	res, err := svc.Networks.Get(project, network).Context(ctx).Do()
   169  	if err != nil {
   170  		return nil, errors.Wrapf(err, "failed to get network %s", network)
   171  	}
   172  	return res, nil
   173  }
   174  
   175  // GetPublicDomains returns all of the domains from among the project's public DNS zones.
   176  func (c *Client) GetPublicDomains(ctx context.Context, project string) ([]string, error) {
   177  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   178  	defer cancel()
   179  
   180  	svc, err := c.getDNSService(ctx)
   181  	if err != nil {
   182  		return []string{}, err
   183  	}
   184  
   185  	var publicZones []string
   186  	req := svc.ManagedZones.List(project).Context(ctx)
   187  	if err := req.Pages(ctx, func(page *dns.ManagedZonesListResponse) error {
   188  		for _, v := range page.ManagedZones {
   189  			if v.Visibility != "private" {
   190  				publicZones = append(publicZones, strings.TrimSuffix(v.DnsName, "."))
   191  			}
   192  		}
   193  		return nil
   194  	}); err != nil {
   195  		return publicZones, err
   196  	}
   197  	return publicZones, nil
   198  }
   199  
   200  // GetDNSZoneByName returns a DNS zone matching the `zoneName` if the DNS zone exists
   201  // and can be seen (correct permissions for a private zone) in the project.
   202  func (c *Client) GetDNSZoneByName(ctx context.Context, project, zoneName string) (*dns.ManagedZone, error) {
   203  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   204  	defer cancel()
   205  
   206  	svc, err := c.getDNSService(ctx)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	returnedZone, err := svc.ManagedZones.Get(project, zoneName).Context(ctx).Do()
   211  	if err != nil {
   212  		return nil, errors.Wrap(err, "failed to get DNS Zones")
   213  	}
   214  	return returnedZone, nil
   215  }
   216  
   217  // GetDNSZone returns a DNS zone for a basedomain.
   218  func (c *Client) GetDNSZone(ctx context.Context, project, baseDomain string, isPublic bool) (*dns.ManagedZone, error) {
   219  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   220  	defer cancel()
   221  
   222  	svc, err := c.getDNSService(ctx)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	if !strings.HasSuffix(baseDomain, ".") {
   227  		baseDomain = fmt.Sprintf("%s.", baseDomain)
   228  	}
   229  	req := svc.ManagedZones.List(project).DnsName(baseDomain).Context(ctx)
   230  	var res *dns.ManagedZone
   231  	if err := req.Pages(ctx, func(page *dns.ManagedZonesListResponse) error {
   232  		for idx, v := range page.ManagedZones {
   233  			if v.Visibility != "private" && isPublic {
   234  				res = page.ManagedZones[idx]
   235  			} else if v.Visibility == "private" && !isPublic {
   236  				res = page.ManagedZones[idx]
   237  			}
   238  		}
   239  		return nil
   240  	}); err != nil {
   241  		return nil, errors.Wrap(err, "failed to list DNS Zones")
   242  	}
   243  	if res == nil {
   244  		if isPublic {
   245  			return nil, errors.New("no matching public DNS Zone found")
   246  		}
   247  		// A Private DNS Zone may be created (if the correct permissions exist)
   248  		return nil, nil
   249  	}
   250  	return res, nil
   251  }
   252  
   253  // GetRecordSets returns all the records for a DNS zone.
   254  func (c *Client) GetRecordSets(ctx context.Context, project, zone string) ([]*dns.ResourceRecordSet, error) {
   255  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   256  	defer cancel()
   257  
   258  	svc, err := c.getDNSService(ctx)
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	req := svc.ResourceRecordSets.List(project, zone).Context(ctx)
   264  	var rrSets []*dns.ResourceRecordSet
   265  	if err := req.Pages(ctx, func(page *dns.ResourceRecordSetsListResponse) error {
   266  		rrSets = append(rrSets, page.Rrsets...)
   267  		return nil
   268  	}); err != nil {
   269  		return nil, err
   270  	}
   271  	return rrSets, nil
   272  }
   273  
   274  // GetSubnetworks uses the GCP Compute Service API to retrieve all subnetworks in a given network.
   275  func (c *Client) GetSubnetworks(ctx context.Context, network, project, region string) ([]*compute.Subnetwork, error) {
   276  	svc, err := c.getComputeService(ctx)
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  
   281  	filter := fmt.Sprintf("network eq .*%s", network)
   282  	req := svc.Subnetworks.List(project, region).Filter(filter)
   283  	var res []*compute.Subnetwork
   284  
   285  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   286  	defer cancel()
   287  
   288  	if err := req.Pages(ctx, func(page *compute.SubnetworkList) error {
   289  		res = append(res, page.Items...)
   290  		return nil
   291  	}); err != nil {
   292  		return nil, err
   293  	}
   294  	return res, nil
   295  }
   296  
   297  func (c *Client) getComputeService(ctx context.Context) (*compute.Service, error) {
   298  	svc, err := compute.NewService(ctx, option.WithCredentials(c.ssn.Credentials))
   299  	if err != nil {
   300  		return nil, errors.Wrap(err, "failed to create compute service")
   301  	}
   302  	return svc, nil
   303  }
   304  
   305  func (c *Client) getDNSService(ctx context.Context) (*dns.Service, error) {
   306  	svc, err := dns.NewService(ctx, option.WithCredentials(c.ssn.Credentials))
   307  	if err != nil {
   308  		return nil, errors.Wrap(err, "failed to create dns service")
   309  	}
   310  	return svc, nil
   311  }
   312  
   313  // GetProjects gets the list of project names and ids associated with the current user in the form
   314  // of a map whose keys are ids and values are names.
   315  func (c *Client) GetProjects(ctx context.Context) (map[string]string, error) {
   316  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   317  	defer cancel()
   318  
   319  	svc, err := c.getCloudResourceService(ctx)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	req := svc.Projects.Search()
   325  	projects := make(map[string]string)
   326  	if err := req.Pages(ctx, func(page *cloudresourcemanager.SearchProjectsResponse) error {
   327  		for _, project := range page.Projects {
   328  			projects[project.ProjectId] = project.Name
   329  		}
   330  		return nil
   331  	}); err != nil {
   332  		return nil, err
   333  	}
   334  	return projects, nil
   335  }
   336  
   337  // GetProjectByID retrieves the project specified by its ID.
   338  func (c *Client) GetProjectByID(ctx context.Context, project string) (*cloudresourcemanager.Project, error) {
   339  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   340  	defer cancel()
   341  
   342  	svc, err := c.getCloudResourceService(ctx)
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  
   347  	return svc.Projects.Get(fmt.Sprintf(gcpconsts.ProjectNameFmt, project)).Context(ctx).Do()
   348  }
   349  
   350  // GetRegions gets the regions that are valid for the project. An error is returned when unsuccessful
   351  func (c *Client) GetRegions(ctx context.Context, project string) ([]string, error) {
   352  	svc, err := c.getComputeService(ctx)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  
   357  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   358  	defer cancel()
   359  	gcpRegionsList, err := svc.Regions.List(project).Context(ctx).Do()
   360  	if err != nil {
   361  		return nil, errors.Wrapf(err, "failed to get regions for project")
   362  	}
   363  
   364  	computeRegions := make([]string, len(gcpRegionsList.Items))
   365  	for _, region := range gcpRegionsList.Items {
   366  		computeRegions = append(computeRegions, region.Name)
   367  	}
   368  
   369  	return computeRegions, nil
   370  }
   371  
   372  // GetZones uses the GCP Compute Service API to get a list of zones from a project.
   373  func GetZones(ctx context.Context, svc *compute.Service, project, filter string) ([]*compute.Zone, error) {
   374  	req := svc.Zones.List(project)
   375  	if filter != "" {
   376  		req = req.Filter(filter)
   377  	}
   378  
   379  	zones := []*compute.Zone{}
   380  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   381  	defer cancel()
   382  	if err := req.Pages(ctx, func(page *compute.ZoneList) error {
   383  		zones = append(zones, page.Items...)
   384  		return nil
   385  	}); err != nil {
   386  		return nil, errors.Wrapf(err, "failed to get zones from project %s", project)
   387  	}
   388  
   389  	return zones, nil
   390  }
   391  
   392  // GetZones uses the GCP Compute Service API to get a list of zones from a project.
   393  func (c *Client) GetZones(ctx context.Context, project, filter string) ([]*compute.Zone, error) {
   394  	svc, err := c.getComputeService(ctx)
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  
   399  	return GetZones(ctx, svc, project, filter)
   400  }
   401  
   402  func (c *Client) getCloudResourceService(ctx context.Context) (*cloudresourcemanager.Service, error) {
   403  	svc, err := cloudresourcemanager.NewService(ctx, option.WithCredentials(c.ssn.Credentials))
   404  	if err != nil {
   405  		return nil, errors.Wrap(err, "failed to create cloud resource service")
   406  	}
   407  	return svc, nil
   408  }
   409  
   410  // GetEnabledServices gets the list of enabled services for a project.
   411  func (c *Client) GetEnabledServices(ctx context.Context, project string) ([]string, error) {
   412  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   413  	defer cancel()
   414  
   415  	svc, err := c.getServiceUsageService(ctx)
   416  	if err != nil {
   417  		return nil, err
   418  	}
   419  
   420  	// List accepts a parent, which includes the type of resource with the id.
   421  	parent := fmt.Sprintf("projects/%s", project)
   422  	req := svc.Services.List(parent).Filter("state:ENABLED")
   423  	var services []string
   424  	if err := req.Pages(ctx, func(page *serviceusage.ListServicesResponse) error {
   425  		for _, service := range page.Services {
   426  			//services are listed in the form of project/services/serviceName
   427  			index := strings.LastIndex(service.Name, "/")
   428  			services = append(services, service.Name[index+1:])
   429  		}
   430  		return nil
   431  	}); err != nil {
   432  		return nil, err
   433  	}
   434  	return services, nil
   435  }
   436  
   437  func (c *Client) getServiceUsageService(ctx context.Context) (*serviceusage.Service, error) {
   438  	svc, err := serviceusage.NewService(ctx, option.WithCredentials(c.ssn.Credentials))
   439  	if err != nil {
   440  		return nil, errors.Wrap(err, "failed to create service usage service")
   441  	}
   442  	return svc, nil
   443  }
   444  
   445  // GetServiceAccount retrieves a service account from a project if it exists.
   446  func (c *Client) GetServiceAccount(ctx context.Context, project, serviceAccount string) (string, error) {
   447  	svc, err := iam.NewService(ctx)
   448  	if err != nil {
   449  		return "", errors.Wrapf(err, "failed create IAM service")
   450  	}
   451  
   452  	ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
   453  	defer cancel()
   454  
   455  	fullServiceAccountPath := fmt.Sprintf("projects/%s/serviceAccounts/%s", project, serviceAccount)
   456  	rsp, err := svc.Projects.ServiceAccounts.Get(fullServiceAccountPath).Context(ctx).Do()
   457  	if err != nil {
   458  		return "", errors.Wrapf(err, fmt.Sprintf("failed to find resource %s", fullServiceAccountPath))
   459  	}
   460  	return rsp.Name, nil
   461  }
   462  
   463  // GetCredentials returns the credentials used to authenticate the GCP session.
   464  func (c *Client) GetCredentials() *googleoauth.Credentials {
   465  	return c.ssn.Credentials
   466  }
   467  
   468  // GetImage returns the marketplace image specified by the user.
   469  func (c *Client) GetImage(ctx context.Context, name string, project string) (*compute.Image, error) {
   470  	svc, err := c.getComputeService(ctx)
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  
   475  	ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
   476  	defer cancel()
   477  
   478  	return svc.Images.Get(project, name).Context(ctx).Do()
   479  }
   480  
   481  func (c *Client) getPermissions(ctx context.Context, project string, permissions []string) ([]string, error) {
   482  	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
   483  	defer cancel()
   484  
   485  	service, err := c.getCloudResourceService(ctx)
   486  	if err != nil {
   487  		return nil, errors.Wrapf(err, "failed to get cloud resource manager service")
   488  	}
   489  
   490  	projectsService := cloudresourcemanager.NewProjectsService(service)
   491  	rb := &cloudresourcemanager.TestIamPermissionsRequest{Permissions: permissions}
   492  	response, err := projectsService.TestIamPermissions(fmt.Sprintf(gcpconsts.ProjectNameFmt, project), rb).Context(ctx).Do()
   493  	if err != nil {
   494  		return nil, errors.Wrapf(err, "failed to get Iam permissions")
   495  	}
   496  
   497  	return response.Permissions, nil
   498  }
   499  
   500  // GetProjectPermissions consumes a set of permissions and returns the set of found permissions for the service
   501  // account (in the provided project). A list of valid permissions can be found at
   502  // https://cloud.google.com/iam/docs/understanding-roles.
   503  func (c *Client) GetProjectPermissions(ctx context.Context, project string, permissions []string) (sets.Set[string], error) {
   504  	validPermissions, err := c.getPermissions(ctx, project, permissions)
   505  	if err != nil {
   506  		return nil, err
   507  	}
   508  	return sets.New[string](validPermissions...), nil
   509  }
   510  
   511  // ValidateServiceAccountHasPermissions compares the permissions to the set returned from the GCP API. Returns true
   512  // if all permissions are available to the service account in the project.
   513  func (c *Client) ValidateServiceAccountHasPermissions(ctx context.Context, project string, permissions []string) (bool, error) {
   514  	validPermissions, err := c.GetProjectPermissions(ctx, project, permissions)
   515  	if err != nil {
   516  		return false, err
   517  	}
   518  	return validPermissions.Len() == len(permissions), nil
   519  }
   520  
   521  // GetProjectTags returns the list of effective tags attached to the provided project resource.
   522  func (c *Client) GetProjectTags(ctx context.Context, projectID string) (sets.Set[string], error) {
   523  	service, err := c.getCloudResourceService(ctx)
   524  	if err != nil {
   525  		return nil, fmt.Errorf("failed to create cloud resource service: %w", err)
   526  	}
   527  
   528  	effectiveTags := sets.New[string]()
   529  	effectiveTagsService := cloudresourcemanager.NewEffectiveTagsService(service)
   530  	effectiveTagsRequest := effectiveTagsService.List().
   531  		Context(ctx).
   532  		Parent(fmt.Sprintf(gcpconsts.ProjectParentPathFmt, projectID))
   533  
   534  	if err := effectiveTagsRequest.Pages(ctx, func(page *cloudresourcemanager.ListEffectiveTagsResponse) error {
   535  		for _, effectiveTag := range page.EffectiveTags {
   536  			effectiveTags.Insert(effectiveTag.NamespacedTagValue)
   537  		}
   538  		return nil
   539  	}); err != nil {
   540  		return nil, fmt.Errorf("failed to fetch tags attached to %s project: %w", projectID, err)
   541  	}
   542  
   543  	return effectiveTags, nil
   544  }
   545  
   546  // GetNamespacedTagValue returns the Tag Value metadata fetched using the tag's NamespacedName.
   547  func (c *Client) GetNamespacedTagValue(ctx context.Context, tagNamespacedName string) (*cloudresourcemanager.TagValue, error) {
   548  	service, err := c.getCloudResourceService(ctx)
   549  	if err != nil {
   550  		return nil, fmt.Errorf("failed to create cloud resource service: %w", err)
   551  	}
   552  
   553  	tagValuesService := cloudresourcemanager.NewTagValuesService(service)
   554  
   555  	tagValue, err := tagValuesService.GetNamespaced().
   556  		Context(ctx).
   557  		Name(tagNamespacedName).
   558  		Do()
   559  
   560  	if err != nil {
   561  		return nil, fmt.Errorf("failed to fetch %s tag value: %w", tagNamespacedName, err)
   562  	}
   563  
   564  	return tagValue, nil
   565  }