...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceskeleton/resourceskeleton.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceskeleton

     1  // Copyright 2022 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package resourceskeleton
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/url"
    21  	"strings"
    22  
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/asset"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/cli/serviceclient"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdgeneration"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
    29  	uri2 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/resourceskeleton/uri"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    31  
    32  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    33  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    34  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  )
    37  
    38  const ProjectKind = "Project"
    39  
    40  var ResourceManagerAPIGroupName = fmt.Sprintf("resourcemanager.%v", crdgeneration.ApiDomain)
    41  
    42  func NewProject(projectId string, smLoader *servicemappingloader.ServiceMappingLoader) (*unstructured.Unstructured, error) {
    43  	sm, err := smLoader.GetServiceMapping(ResourceManagerAPIGroupName)
    44  	if err != nil {
    45  		return nil, fmt.Errorf("error getting service mapping for '%v': %v", ResourceManagerAPIGroupName, err)
    46  	}
    47  	u := &unstructured.Unstructured{}
    48  	gvk := schema.GroupVersionKind{
    49  		Group:   sm.Name,
    50  		Version: sm.Spec.Version,
    51  		Kind:    ProjectKind,
    52  	}
    53  	u.SetGroupVersionKind(gvk)
    54  	u.SetName(projectId)
    55  	annotations := make(map[string]string, 1)
    56  	annotations[k8s.FolderIDAnnotation] = "skeleton-folder"
    57  	u.SetAnnotations(annotations)
    58  	return u, nil
    59  }
    60  
    61  func NewFromURI(uri string, smLoader *servicemappingloader.ServiceMappingLoader, tfProvider *tfschema.Provider) (*unstructured.Unstructured, error) {
    62  	parsedUrl, err := url.Parse(uri)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("error parsing '%v' as url: %w", uri, err)
    65  	}
    66  	sm, rc, err := uri2.GetServiceMappingAndResourceConfig(smLoader, parsedUrl.Host, parsedUrl.Path)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("error getting service mapping and resource config for url '%v': %w", uri, err)
    69  	}
    70  	tfInfo := terraform.InstanceInfo{
    71  		Type: rc.Name,
    72  	}
    73  	state, err := krmtotf.ImportState(context.Background(), strings.TrimPrefix(parsedUrl.Path, "/"), &tfInfo, tfProvider)
    74  	if err != nil {
    75  		return nil, fmt.Errorf("error importing resource name to TF state: %w", err)
    76  	}
    77  	resource, err := tfStateToResource(state, sm, rc, tfProvider)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("error creating new resource: %w", err)
    80  	}
    81  	return resource.MarshalAsUnstructured()
    82  }
    83  
    84  func NewFromAsset(a *asset.Asset, smLoader *servicemappingloader.ServiceMappingLoader, tfProvider *tfschema.Provider, serviceClient serviceclient.ServiceClient) (*unstructured.Unstructured, error) {
    85  	sm, rc, err := asset.GetServiceMappingAndResourceConfig(smLoader, a)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	tfInfo := terraform.InstanceInfo{
    90  		Type: rc.Name,
    91  	}
    92  	name := trimServiceHostName(a, sm)
    93  	importID, err := convertAssetNameToImportID(a, rc, name)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("error coverting cloud asset inventory name '%v' to resource id: %v", name, err)
    96  	}
    97  	state, err := krmtotf.ImportState(context.Background(), importID, &tfInfo, tfProvider)
    98  	if err != nil {
    99  		return nil, fmt.Errorf("error importing resource name to TF state: %v", err)
   100  	}
   101  	resource, err := tfStateToResource(state, sm, rc, tfProvider)
   102  	if err != nil {
   103  		return nil, fmt.Errorf("error creating new resource: %v", err)
   104  	}
   105  	err = applyAssetKRMResourceHacks(resource, a, serviceClient, state)
   106  	if err != nil {
   107  		return nil, fmt.Errorf("unable to apply asset KRM hacks on asset %v: %v", a, err)
   108  	}
   109  	return resource.MarshalAsUnstructured()
   110  }
   111  
   112  func tfStateToResource(state *terraform.InstanceState, sm *v1alpha1.ServiceMapping, rc *v1alpha1.ResourceConfig, tfProvider *tfschema.Provider) (*krmtotf.Resource, error) {
   113  	resource, err := krmtotf.NewResourceFromResourceConfig(rc, tfProvider)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("error creating new resource: %v", err)
   116  	}
   117  	gvk := schema.GroupVersionKind{
   118  		Group:   sm.Name,
   119  		Version: sm.GetVersionFor(rc),
   120  		Kind:    rc.Kind,
   121  	}
   122  
   123  	resource.SetGroupVersionKind(gvk)
   124  	resource.Spec, resource.Status = krmtotf.GetSpecAndStatusFromState(resource, state)
   125  	resource.Labels = krmtotf.GetLabelsFromState(resource, state)
   126  	resource.Annotations = krmtotf.GetAnnotationsFromState(resource, state)
   127  	resource.Name = krmtotf.GetNameFromState(resource, state)
   128  	return resource, nil
   129  }
   130  
   131  func trimServiceHostName(a *asset.Asset, sm *v1alpha1.ServiceMapping) string {
   132  	return strings.TrimPrefix(a.Name, fmt.Sprintf("//%v/", sm.Spec.ServiceHostName))
   133  }
   134  
   135  // convertAssetNameToImportID converts the name of the resource in Asset Inventory into
   136  // the import ID of the resource in KCC.
   137  func convertAssetNameToImportID(a *asset.Asset, rc *v1alpha1.ResourceConfig, name string) (string, error) {
   138  	// IAMCustomRole is a custom resource, and has a bespoke ID format.
   139  	if rc.Kind == "IAMCustomRole" {
   140  		id, err := parseIAMCustomRoleID(name)
   141  		if err != nil {
   142  			return "", fmt.Errorf("unable to parse IAMCustomRole id: %v", err)
   143  		}
   144  		switch id.parentType {
   145  		case Project:
   146  			return fmt.Sprintf("%v##%v", id.parentID, id.roleID), nil
   147  		case Organization:
   148  			return fmt.Sprintf("#%v#%v", id.parentID, id.roleID), nil
   149  		}
   150  	}
   151  	if rc.Kind == "MonitoringAlertPolicy" {
   152  		partitions := strings.Split(name, "/")
   153  		if len(partitions) != 4 {
   154  			return "", fmt.Errorf("expected 4 partitions split by '/' for '%v'", name)
   155  		}
   156  		return fmt.Sprintf("%v projects/%v/alertPolicies/%v", partitions[1], partitions[1], partitions[3]), nil
   157  	}
   158  
   159  	return name, nil
   160  }
   161  
   162  // Apply any hacks that need to be made because we have not come up with the appropriate abstraction in service mappings (yet)
   163  func applyAssetKRMResourceHacks(resource *krmtotf.Resource, a *asset.Asset, client serviceclient.ServiceClient, state *terraform.InstanceState) error {
   164  	if resource.Kind == "StorageBucket" {
   165  		// the storage bucket properly uses the container annotation of 'project', however, it is only used and
   166  		// verified on creation. The project id is not part of the ResourceName in the asset, but the resource
   167  		// skeleton is not useable without it as krmtotf, etc, need the project-id annotation since StorageBucket requires
   168  		// the project id annotation. For that reason, use the project number in its place as it is mostly correct and
   169  		// will allow the resource to be useable by the rest of the system
   170  		for _, ancestor := range a.Ancestors {
   171  			if strings.HasPrefix(ancestor, "projects/") {
   172  				projectNumber := strings.Replace(ancestor, "projects/", "", 1)
   173  				project, err := client.GetProjectFromProjectIDOrNumber(projectNumber)
   174  				if err != nil {
   175  					return err
   176  				}
   177  				resource.Annotations[k8s.ProjectIDAnnotation] = project.ProjectId
   178  			}
   179  		}
   180  	} else if resource.Kind == "IAMCustomRole" {
   181  		id, err := parseIAMCustomRoleID(state.ID)
   182  		if err != nil {
   183  			return fmt.Errorf("unable to parse IAMCustomRole id: %v", err)
   184  		}
   185  		if resource.Spec == nil {
   186  			resource.Spec = make(map[string]interface{})
   187  		}
   188  		resource.Spec[k8s.ResourceIDFieldName] = id.roleID
   189  		switch id.parentType {
   190  		case Project:
   191  			resource.Annotations[k8s.ProjectIDAnnotation] = id.parentID
   192  		case Organization:
   193  			resource.Annotations[k8s.OrgIDAnnotation] = id.parentID
   194  		}
   195  	}
   196  	return nil
   197  }
   198  
   199  type parentType int32
   200  
   201  const (
   202  	Project parentType = iota
   203  	Organization
   204  )
   205  
   206  type iamCustomRoleID struct {
   207  	parentType parentType
   208  	parentID   string
   209  	roleID     string
   210  }
   211  
   212  // parseIAMCustomRoleID parses an asset inventory ID for a Custom Role
   213  // and returns its components.
   214  func parseIAMCustomRoleID(id string) (*iamCustomRoleID, error) {
   215  	partitions := strings.Split(id, "/")
   216  	if len(partitions) != 4 {
   217  		return nil, fmt.Errorf("expected 4 partitions split by '/' for for '%v'", id)
   218  	}
   219  	value := iamCustomRoleID{
   220  		parentID: partitions[1],
   221  		roleID:   partitions[3],
   222  	}
   223  	switch partitions[0] {
   224  	case "projects":
   225  		value.parentType = Project
   226  	case "organizations":
   227  		value.parentType = Organization
   228  	default:
   229  		return nil, fmt.Errorf("expected 'projects' or 'organizations' for first partition, got '%v'", partitions[0])
   230  	}
   231  	return &value, nil
   232  }
   233  

View as plain text