...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient/tfiamclient.go

Documentation: github.com/GoogleCloudPlatform/k8s-config-connector/pkg/controller/iam/iamclient

     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 iamclient
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  
    24  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/iam/v1beta1"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gvks/externalonlygvks"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/krmtotf"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    31  	tfresource "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/tf/resource"
    32  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    33  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    34  	"k8s.io/apimachinery/pkg/api/errors"
    35  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    36  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    37  	"k8s.io/apimachinery/pkg/runtime/schema"
    38  	"k8s.io/apimachinery/pkg/types"
    39  
    40  	tfschema "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    41  	"sigs.k8s.io/controller-runtime/pkg/client"
    42  )
    43  
    44  type TFIAMClient struct {
    45  	kubeClient client.Client
    46  	provider   *tfschema.Provider
    47  	smLoader   *servicemappingloader.ServiceMappingLoader
    48  }
    49  
    50  func (t *TFIAMClient) SetPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
    51  	rc, err := t.getResourceConfigForReferencedResource(ctx, policyMember)
    52  	if err != nil {
    53  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
    54  	}
    55  	if !resourceSupportsIAMPolicyMember(rc) {
    56  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
    57  	}
    58  	resource, err := t.newResource(ctx, policyMember, rc)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("error fetching live state for resource: %w", err)
    65  	}
    66  	cfg, _, err := krmtotf.KRMResourceToTFResourceConfig(resource, t.kubeClient, t.smLoader)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("error creating resource config: %w", err)
    69  	}
    70  	diff, err := resource.TFResource.Diff(ctx, liveState, cfg, t.provider.Meta())
    71  	if err != nil {
    72  		return nil, fmt.Errorf("error calculating diff: %w", err)
    73  	}
    74  	if !liveState.Empty() && diff.RequiresNew() {
    75  		return nil, k8s.NewImmutableFieldsMutationError(tfresource.ImmutableFieldsFromDiff(diff))
    76  	}
    77  	if diff.Empty() {
    78  		logger.Info("underlying resource is already up to date", "resource", k8s.GetNamespacedName(policyMember))
    79  		return policyMember, nil
    80  	}
    81  	newState, diagnostics := resource.TFResource.Apply(ctx, liveState, diff, t.provider.Meta())
    82  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
    83  		return nil, fmt.Errorf("error applying changes: %w", err)
    84  	}
    85  	return newIAMPolicyMemberFromTFState(resource, newState, policyMember)
    86  }
    87  
    88  func (t *TFIAMClient) GetPolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
    89  	rc, err := t.getResourceConfigForReferencedResource(ctx, policyMember)
    90  	if err != nil {
    91  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
    92  	}
    93  	if !resourceSupportsIAMPolicyMember(rc) {
    94  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
    95  	}
    96  	resource, err := t.newResourceSkeleton(ctx, policyMember, rc)
    97  	if err != nil {
    98  		return nil, fmt.Errorf("error building resource skeleton for getting IAM resource: %w", err)
    99  	}
   100  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   101  	if err != nil {
   102  		return nil, fmt.Errorf("error fetching live state for resource: %w", err)
   103  	}
   104  	if liveState.Empty() {
   105  		return nil, NotFoundError
   106  	}
   107  	return newIAMPolicyMemberFromTFState(resource, liveState, policyMember)
   108  }
   109  
   110  func (t *TFIAMClient) DeletePolicyMember(ctx context.Context, policyMember *v1beta1.IAMPolicyMember) error {
   111  	rc, err := t.getResourceConfigForReferencedResource(ctx, policyMember)
   112  	if err != nil {
   113  		return fmt.Errorf("error getting resource config for referenced resource: %w", err)
   114  	}
   115  	if !resourceSupportsIAMPolicyMember(rc) {
   116  		return fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
   117  	}
   118  	resource, err := t.newResource(ctx, policyMember, rc)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   123  	if err != nil {
   124  		return fmt.Errorf("error fetching live state for resource: %w", err)
   125  	}
   126  	if liveState.Empty() {
   127  		return NotFoundError
   128  	}
   129  	_, diagnostics := resource.TFResource.Apply(ctx, liveState, &terraform.InstanceDiff{Destroy: true}, t.provider.Meta())
   130  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
   131  		return fmt.Errorf("error deleting IAMPolicyMember: %w", err)
   132  	}
   133  	return nil
   134  }
   135  
   136  func (t *TFIAMClient) SetPolicy(ctx context.Context, policy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   137  	rc, err := t.getResourceConfigForReferencedResource(ctx, policy)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
   140  	}
   141  	if !resourceSupportsIAMPolicy(rc) {
   142  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
   143  	}
   144  	if len(policy.Spec.AuditConfigs) > 0 && !resourceSupportsIAMAuditConfigs(rc) {
   145  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   146  	}
   147  	resource, err := t.newResource(ctx, policy, rc)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   152  	if err != nil {
   153  		return nil, fmt.Errorf("error fetching live state for resource")
   154  	}
   155  	cfg, _, err := krmtotf.KRMResourceToTFResourceConfig(resource, t.kubeClient, t.smLoader)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("error creating resource config: %w", err)
   158  	}
   159  	diff, err := resource.TFResource.Diff(ctx, liveState, cfg, t.provider.Meta())
   160  	if err != nil {
   161  		return nil, fmt.Errorf("error calculating diff: %w", err)
   162  	}
   163  	if !liveState.Empty() && diff.RequiresNew() {
   164  		return nil, k8s.NewImmutableFieldsMutationError(tfresource.ImmutableFieldsFromDiff(diff))
   165  	}
   166  	if diff.Empty() {
   167  		logger.Info("underlying resource is already up to date", "resource", k8s.GetNamespacedName(policy))
   168  		return policy, nil
   169  	}
   170  	newState, diagnostics := resource.TFResource.Apply(ctx, liveState, diff, t.provider.Meta())
   171  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
   172  		return nil, fmt.Errorf("error applying changes: %w", err)
   173  	}
   174  	return newIAMPolicyFromTFState(resource, newState, policy)
   175  }
   176  
   177  func (t *TFIAMClient) GetPolicy(ctx context.Context, policy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   178  	rc, err := t.getResourceConfigForReferencedResource(ctx, policy)
   179  	if err != nil {
   180  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
   181  	}
   182  	if !resourceSupportsIAMPolicy(rc) {
   183  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
   184  	}
   185  	if len(policy.Spec.AuditConfigs) > 0 && !resourceSupportsIAMAuditConfigs(rc) {
   186  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   187  	}
   188  	resource, err := t.newResourceSkeleton(ctx, policy, rc)
   189  	if err != nil {
   190  		return nil, fmt.Errorf("error building resource skeleton for getting IAM resource: %w", err)
   191  	}
   192  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("error fetching live state for resource: %w", err)
   195  	}
   196  	if liveState.Empty() {
   197  		return nil, NotFoundError
   198  	}
   199  	return newIAMPolicyFromTFState(resource, liveState, policy)
   200  }
   201  
   202  func (t *TFIAMClient) DeletePolicy(ctx context.Context, policy *v1beta1.IAMPolicy) error {
   203  	rc, err := t.getResourceConfigForReferencedResource(ctx, policy)
   204  	if err != nil {
   205  		return fmt.Errorf("error getting resource config for referenced resource: %w", err)
   206  	}
   207  	if !resourceSupportsIAMPolicy(rc) {
   208  		return fmt.Errorf("invalid resource reference: kind %v does not support IAM policies", rc.Kind)
   209  	}
   210  	if len(policy.Spec.AuditConfigs) > 0 && !resourceSupportsIAMAuditConfigs(rc) {
   211  		return fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   212  	}
   213  	resource, err := t.newResource(ctx, policy, rc)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   218  	if err != nil {
   219  		return fmt.Errorf("error fetching live state for resource: %w", err)
   220  	}
   221  	if liveState.Empty() {
   222  		return NotFoundError
   223  	}
   224  	_, diagnostics := resource.TFResource.Apply(ctx, liveState, &terraform.InstanceDiff{Destroy: true}, t.provider.Meta())
   225  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
   226  		return fmt.Errorf("error deleting IAMPolicy: %w", err)
   227  	}
   228  	return nil
   229  }
   230  
   231  func (t *TFIAMClient) SetAuditConfig(ctx context.Context, auditConfig *v1beta1.IAMAuditConfig) (*v1beta1.IAMAuditConfig, error) {
   232  	rc, err := t.getResourceConfigForReferencedResource(ctx, auditConfig)
   233  	if err != nil {
   234  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
   235  	}
   236  	if !resourceSupportsIAMAuditConfigs(rc) {
   237  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   238  	}
   239  	resource, err := t.newResource(ctx, auditConfig, rc)
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   244  	if err != nil {
   245  		return nil, fmt.Errorf("error fetching live state for resource: %w", err)
   246  	}
   247  	cfg, _, err := krmtotf.KRMResourceToTFResourceConfig(resource, t.kubeClient, t.smLoader)
   248  	if err != nil {
   249  		return nil, fmt.Errorf("error creating resource config: %w", err)
   250  	}
   251  	diff, err := resource.TFResource.Diff(ctx, liveState, cfg, t.provider.Meta())
   252  	if err != nil {
   253  		return nil, fmt.Errorf("error calculating diff: %w", err)
   254  	}
   255  	if !liveState.Empty() && diff.RequiresNew() {
   256  		return nil, k8s.NewImmutableFieldsMutationError(tfresource.ImmutableFieldsFromDiff(diff))
   257  	}
   258  	if diff.Empty() {
   259  		logger.Info("underlying resource is already up to date", "resource", k8s.GetNamespacedName(auditConfig))
   260  		return auditConfig, nil
   261  	}
   262  	newState, diagnostics := resource.TFResource.Apply(ctx, liveState, diff, t.provider.Meta())
   263  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
   264  		return nil, fmt.Errorf("error applying changes: %w", err)
   265  	}
   266  	return newIAMAuditConfigFromTFState(resource, newState, auditConfig)
   267  }
   268  
   269  func (t *TFIAMClient) GetAuditConfig(ctx context.Context, auditConfig *v1beta1.IAMAuditConfig) (*v1beta1.IAMAuditConfig, error) {
   270  	rc, err := t.getResourceConfigForReferencedResource(ctx, auditConfig)
   271  	if err != nil {
   272  		return nil, fmt.Errorf("error getting resource config for referenced resource: %w", err)
   273  	}
   274  	if !resourceSupportsIAMAuditConfigs(rc) {
   275  		return nil, fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   276  	}
   277  	resource, err := t.newResourceSkeleton(ctx, auditConfig, rc)
   278  	if err != nil {
   279  		return nil, fmt.Errorf("error building resource skeleton for getting IAM resource: %w", err)
   280  	}
   281  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   282  	if err != nil {
   283  		return nil, fmt.Errorf("error fetching live state for resource: %w", err)
   284  	}
   285  	if liveState.Empty() {
   286  		return nil, NotFoundError
   287  	}
   288  	return newIAMAuditConfigFromTFState(resource, liveState, auditConfig)
   289  }
   290  
   291  func (t *TFIAMClient) DeleteAuditConfig(ctx context.Context, auditConfig *v1beta1.IAMAuditConfig) error {
   292  	rc, err := t.getResourceConfigForReferencedResource(ctx, auditConfig)
   293  	if err != nil {
   294  		return fmt.Errorf("error getting resource config for referenced resource: %w", err)
   295  	}
   296  	if !resourceSupportsIAMAuditConfigs(rc) {
   297  		return fmt.Errorf("invalid resource reference: kind %v does not support IAM audit configs", rc.Kind)
   298  	}
   299  	resource, err := t.newResource(ctx, auditConfig, rc)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	liveState, err := krmtotf.FetchLiveState(ctx, resource, t.provider, t.kubeClient, t.smLoader)
   304  	if err != nil {
   305  		return fmt.Errorf("error fetching live state for resource: %w", err)
   306  	}
   307  	if liveState.Empty() {
   308  		return NotFoundError
   309  	}
   310  	_, diagnostics := resource.TFResource.Apply(ctx, liveState, &terraform.InstanceDiff{Destroy: true}, t.provider.Meta())
   311  	if err := krmtotf.NewErrorFromDiagnostics(diagnostics); err != nil {
   312  		return fmt.Errorf("error deleting IAMAuditConfig: %w", err)
   313  	}
   314  	return nil
   315  }
   316  
   317  // newResource creates a Resource which represents the given IAM object.
   318  func (t *TFIAMClient) newResource(ctx context.Context, iamInterface interface{}, rc *corekccv1alpha1.ResourceConfig) (*krmtotf.Resource, error) {
   319  	SetGVK(iamInterface)
   320  	unstructSkeleton, err := t.newUnstructuredSkeleton(ctx, iamInterface, rc)
   321  	if err != nil {
   322  		return nil, fmt.Errorf("error building a minimal unstructured skeleton for the IAM resource: %w", err)
   323  	}
   324  
   325  	// Add any extra fields from the IAM object not included in the unstructured skeleton
   326  	switch iamObject := iamInterface.(type) {
   327  	case *v1beta1.IAMPolicy:
   328  		krmPolicyUnstruct, err := k8s.MarshalObjectAsUnstructured(iamObject)
   329  		if err != nil {
   330  			return nil, err
   331  		}
   332  		spec := krmPolicyUnstruct.Object["spec"].(map[string]interface{})
   333  		policyDataMap := map[string]interface{}{
   334  			"bindings":     spec["bindings"],
   335  			"auditConfigs": spec["auditConfigs"],
   336  		}
   337  		if iamObject.Spec.Etag != "" {
   338  			policyDataMap["etag"] = iamObject.Spec.Etag
   339  		}
   340  		b, err := json.Marshal(policyDataMap)
   341  		if err != nil {
   342  			return nil, fmt.Errorf("error marshalling policy data to JSON: %w", err)
   343  		}
   344  		unstructured.SetNestedField(unstructSkeleton.Object, string(b), "spec", "policyData")
   345  	case *v1beta1.IAMPolicyMember:
   346  		// An unstructured skeleton for getting an IAM policy member already
   347  		// fully represents the IAM policy member.
   348  		break
   349  	case *v1beta1.IAMAuditConfig:
   350  		auditLogConfigs := iamObject.Spec.AuditLogConfigs
   351  		var auditLogConfigSlice []interface{}
   352  		if err := util.Marshal(auditLogConfigs, &auditLogConfigSlice); err != nil {
   353  			return nil, fmt.Errorf("unable to marshal %v to slice: %w", reflect.TypeOf(auditLogConfigs).Name(), err)
   354  		}
   355  		unstructured.SetNestedSlice(unstructSkeleton.Object, auditLogConfigSlice, "spec", "auditLogConfig")
   356  	default:
   357  		panic(fmt.Errorf("unknown type: %v", reflect.TypeOf(iamInterface).Name()))
   358  	}
   359  
   360  	iamSM, err := t.newServiceMappingForAssociatedIAMInterface(ctx, iamInterface, rc.IAMConfig)
   361  	if err != nil {
   362  		return nil, fmt.Errorf("error building service mapping for IAM resource: %w", err)
   363  	}
   364  	return krmtotf.NewResource(unstructSkeleton, iamSM, t.provider)
   365  }
   366  
   367  // newResourceSkeleton creates a Resource for the given IAM object which
   368  // contains just enough fields to perform a Terraform read.
   369  func (t *TFIAMClient) newResourceSkeleton(ctx context.Context, iamInterface interface{}, rc *corekccv1alpha1.ResourceConfig) (*krmtotf.Resource, error) {
   370  	SetGVK(iamInterface)
   371  	unstructSkeleton, err := t.newUnstructuredSkeleton(ctx, iamInterface, rc)
   372  	if err != nil {
   373  		return nil, fmt.Errorf("error building unstructured skeleton for getting IAM resource: %w", err)
   374  	}
   375  	iamSM, err := t.newServiceMappingForAssociatedIAMInterface(ctx, iamInterface, rc.IAMConfig)
   376  	if err != nil {
   377  		return nil, fmt.Errorf("error building service mapping for IAM resource: %w", err)
   378  	}
   379  	return krmtotf.NewResource(unstructSkeleton, iamSM, t.provider)
   380  }
   381  
   382  // newUnstructuredSkeleton creates an Unstructured for the given IAM
   383  // object which contains just enough fields to perform a Terraform read.
   384  func (t *TFIAMClient) newUnstructuredSkeleton(ctx context.Context, iamInterface interface{}, rc *corekccv1alpha1.ResourceConfig) (*unstructured.Unstructured, error) {
   385  	namespace, resourceRef := extractNamespaceAndResourceReference(iamInterface)
   386  	unstructSkeleton, err := t.buildUnstructuredIAMSkeletonFromReference(ctx, iamInterface, namespace, resourceRef, rc)
   387  	if err != nil {
   388  		return nil, fmt.Errorf("error building unstructured skeleton from referenced resource: %w", err)
   389  	}
   390  	switch iamObject := iamInterface.(type) {
   391  	case *v1beta1.IAMPolicy:
   392  		// IAM policies are uniquely identified by the resource they are referencing.
   393  		// No extra information is required to be able to get a policy from GCP.
   394  		return unstructSkeleton, nil
   395  	case *v1beta1.IAMPolicyMember:
   396  		// IAM policy members are uniquely identified by the tuple of (member, role, conditions).
   397  		member, err := ResolveMemberIdentity(ctx, iamObject.Spec.Member, iamObject.Spec.MemberFrom, iamObject.Namespace, t)
   398  		if err != nil {
   399  			return nil, fmt.Errorf("could not resolve member identity for IAMPolicyMember: %w", err)
   400  		}
   401  		unstructured.SetNestedField(unstructSkeleton.Object, member, "spec", "member")
   402  		unstructured.SetNestedField(unstructSkeleton.Object, iamObject.Spec.Role, "spec", "role")
   403  		if iamObject.Spec.Condition != nil {
   404  			condition := iamObject.Spec.Condition
   405  			var conditionMap map[string]interface{}
   406  			if err := util.Marshal(condition, &conditionMap); err != nil {
   407  				return nil, fmt.Errorf("unable to marshal %v to map: %w", reflect.TypeOf(condition).Name(), err)
   408  			}
   409  			unstructured.SetNestedMap(unstructSkeleton.Object, conditionMap, "spec", "condition")
   410  		}
   411  		return unstructSkeleton, nil
   412  	case *v1beta1.IAMAuditConfig:
   413  		// IAM audit configs are uniquely identified by their service field.
   414  		unstructured.SetNestedField(unstructSkeleton.Object, iamObject.Spec.Service, "spec", "service")
   415  		return unstructSkeleton, nil
   416  	default:
   417  		panic(fmt.Errorf("unknown type: %v", reflect.TypeOf(iamInterface).Name()))
   418  	}
   419  }
   420  
   421  func (t *TFIAMClient) buildUnstructuredIAMSkeletonFromReference(ctx context.Context, iamInterface interface{}, namespace string,
   422  	resourceRef v1beta1.ResourceReference, rc *corekccv1alpha1.ResourceConfig) (*unstructured.Unstructured, error) {
   423  	id, err := t.getResourceID(ctx, resourceRef, namespace)
   424  	if err != nil {
   425  		return nil, fmt.Errorf("couldn't get resource id for resource reference: %w", err)
   426  	}
   427  
   428  	iamObject := t.newIAMObjectFromInterface(iamInterface)
   429  	unstruct, err := k8s.MarshalObjectAsUnstructured(iamObject)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  
   434  	if externalonlygvks.IsExternalOnlyGVK(resourceRef.GroupVersionKind()) {
   435  		return unstructuredIAMSkeletonForExternalOnlyRef(resourceRef, unstruct)
   436  	}
   437  
   438  	tfResourceName := rc.Name
   439  	tfInfo := &terraform.InstanceInfo{
   440  		Type: tfResourceName,
   441  	}
   442  	tfStateParsedFromId, err := krmtotf.ImportState(ctx, id, tfInfo, t.provider)
   443  	if err != nil {
   444  		return nil, fmt.Errorf("failed to import state given id %v: %w", id, err)
   445  	}
   446  
   447  	refFieldName := text.SnakeCaseToLowerCamelCase(rc.IAMConfig.ReferenceField.Name)
   448  	refFieldVal, err := getReferenceFieldValue(id, rc)
   449  	if err != nil {
   450  		return nil, fmt.Errorf("failed to get reference field value given id %v: %w", id, err)
   451  	}
   452  	spec := map[string]interface{}{
   453  		refFieldName: refFieldVal,
   454  	}
   455  	for k, v := range tfStateParsedFromId.Attributes {
   456  		spec[k] = v
   457  	}
   458  
   459  	unstruct.Object["spec"] = spec
   460  	return unstruct, nil
   461  }
   462  
   463  func (t *TFIAMClient) getResourceID(ctx context.Context, resourceRef v1beta1.ResourceReference, namespace string) (string, error) {
   464  	if resourceRef.External != "" {
   465  		return resourceRef.External, nil
   466  	}
   467  
   468  	nn := types.NamespacedName{
   469  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   470  		Name:      resourceRef.Name,
   471  	}
   472  	refResource, err := t.getResource(ctx, resourceRef.GroupVersionKind(), nn)
   473  	if err != nil {
   474  		return "", err
   475  	}
   476  	id, err := refResource.GetImportID(t.kubeClient, t.smLoader)
   477  	if err != nil {
   478  		return "", fmt.Errorf("error getting import ID for referenced resource: %w", err)
   479  	}
   480  	if id != "" {
   481  		return id, nil
   482  	}
   483  	return "", fmt.Errorf("couldn't construct id for referenced resource")
   484  }
   485  
   486  func (t *TFIAMClient) getResourceConfigForReferencedResource(ctx context.Context, iamInterface interface{}) (*corekccv1alpha1.ResourceConfig, error) {
   487  	namespace, resourceRef := extractNamespaceAndResourceReference(iamInterface)
   488  	if resourceRef.External != "" {
   489  		return t.getResourceConfigForExternalRef(resourceRef)
   490  	}
   491  	nn := types.NamespacedName{
   492  		Namespace: useIfNonEmptyElseDefaultTo(resourceRef.Namespace, namespace),
   493  		Name:      resourceRef.Name,
   494  	}
   495  	refResource, err := t.getResource(ctx, resourceRef.GroupVersionKind(), nn)
   496  	if err != nil {
   497  		return nil, err
   498  	}
   499  	return &refResource.ResourceConfig, nil
   500  }
   501  
   502  func (t *TFIAMClient) getResourceConfigForExternalRef(resourceRef v1beta1.ResourceReference) (*corekccv1alpha1.ResourceConfig, error) {
   503  	if resourceRef.External == "" {
   504  		return nil, fmt.Errorf("external field is empty")
   505  	}
   506  	gvk := resourceRef.GroupVersionKind()
   507  	if externalonlygvks.IsExternalOnlyGVK(gvk) {
   508  		return GetResourceConfigForExternalOnlyGVK(gvk)
   509  	}
   510  	sm, err := t.smLoader.GetServiceMapping(gvk.Group)
   511  	if err != nil {
   512  		return nil, fmt.Errorf("failed to load ServiceMapping for GroupVersionKind %v: %w", gvk, err)
   513  	}
   514  	rcs := servicemappingloader.GetResourceConfigsForKind(sm, gvk.Kind)
   515  	switch len(rcs) {
   516  	case 0:
   517  		return nil, fmt.Errorf("couldn't find any ResourceConfig defined for GroupVersionKind %v", gvk)
   518  	case 1:
   519  		return rcs[0], nil
   520  	// It is possible for a kind to have multiple ResourceConfigs (e.g.
   521  	// ResourceConfigs with different locationalities). In this case, use the
   522  	// resource ID specified by the external reference to distinguish between
   523  	// different ResourceConfigs since different ResourceConfigs will have
   524  	// different ID templates.
   525  	default:
   526  		id := resourceRef.External
   527  		for _, rc := range rcs {
   528  			if idMatchesTemplate(id, rc.IDTemplate) {
   529  				return rc, nil
   530  			}
   531  		}
   532  		return nil, fmt.Errorf("no ResourceConfig found for GroupVersionKind %v with an IDTemplate that matches the id %v", gvk, id)
   533  	}
   534  }
   535  
   536  func (t *TFIAMClient) getResource(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*krmtotf.Resource, error) {
   537  	if nn.Name == "" {
   538  		return t.getResourceForHeadlessKind(ctx, gvk, nn)
   539  	}
   540  	u := &unstructured.Unstructured{}
   541  	u.SetGroupVersionKind(gvk)
   542  	if err := t.kubeClient.Get(ctx, nn, u); err != nil {
   543  		if errors.IsNotFound(err) {
   544  			return nil, k8s.NewReferenceNotFoundError(gvk, nn)
   545  		}
   546  		return nil, fmt.Errorf("error retrieving resource '%v' with GroupVersionKind '%v': %w", nn, gvk, err)
   547  	}
   548  	sm, err := t.smLoader.GetServiceMapping(gvk.Group)
   549  	if err != nil {
   550  		return nil, err
   551  	}
   552  	resource, err := krmtotf.NewResource(u, sm, t.provider)
   553  	if err != nil {
   554  		return nil, fmt.Errorf("error unmarshalling '%v' to resource: %w", nn, err)
   555  	}
   556  	if !k8s.IsResourceReady(&resource.Resource) {
   557  		return nil, k8s.NewReferenceNotReadyErrorForResource(&resource.Resource)
   558  	}
   559  	return resource, nil
   560  }
   561  
   562  func (t *TFIAMClient) getResourceForHeadlessKind(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*krmtotf.Resource, error) {
   563  	switch gvk.Kind {
   564  	case ProjectKind:
   565  		return t.getProjectResource(ctx, gvk, nn)
   566  	default:
   567  		return nil, fmt.Errorf("unrecognized IAM kind '%v'", gvk.Kind)
   568  	}
   569  }
   570  
   571  func (t *TFIAMClient) getProjectResource(ctx context.Context, gvk schema.GroupVersionKind, nn types.NamespacedName) (*krmtotf.Resource, error) {
   572  	projectID, err := k8s.GetProjectIDForNamespace(t.kubeClient, ctx, nn.Namespace)
   573  	if err != nil {
   574  		return nil, fmt.Errorf("error getting project ID for namespace: %w", err)
   575  	}
   576  	u := unstructured.Unstructured{
   577  		Object: map[string]interface{}{
   578  			"kind": ProjectKind,
   579  			"metadata": map[string]interface{}{
   580  				"name": projectID,
   581  			},
   582  		},
   583  	}
   584  	sm, err := t.smLoader.GetServiceMapping(ResourceManagerGroup)
   585  	if err != nil {
   586  		return nil, fmt.Errorf("error getting service mapping for kind 'Project': %w", err)
   587  	}
   588  	resource, err := krmtotf.NewResource(&u, sm, t.provider)
   589  	if err != nil {
   590  		return nil, fmt.Errorf("error unmarshalling '%v' to resource: %w", nn, err)
   591  	}
   592  	return resource, nil
   593  }
   594  
   595  func (t *TFIAMClient) resolveMemberReference(ctx context.Context, ref *v1beta1.MemberReference,
   596  	gvk schema.GroupVersionKind, resourceNamespace string) (string, error) {
   597  
   598  	nn := types.NamespacedName{
   599  		Namespace: useIfNonEmptyElseDefaultTo(ref.Namespace, resourceNamespace),
   600  		Name:      ref.Name,
   601  	}
   602  	refResource, err := t.getResource(ctx, gvk, nn)
   603  	if err != nil {
   604  		return "", err
   605  	}
   606  	memberRefConfig := refResource.ResourceConfig.IAMMemberReferenceConfig
   607  	val, err := resolveTargetFieldValue(refResource, memberRefConfig.TargetField)
   608  	if err != nil {
   609  		return "", err
   610  	}
   611  	if memberRefConfig.ValueTemplate == "" {
   612  		return val, nil
   613  	}
   614  	return krmtotf.ResolveValueTemplate(memberRefConfig.ValueTemplate, val, refResource, t.kubeClient, t.smLoader)
   615  }
   616  
   617  func (t *TFIAMClient) newIAMObjectFromInterface(iamInterface interface{}) metav1.Object {
   618  	switch iamInterface.(type) {
   619  	case *v1beta1.IAMPolicy:
   620  		return &v1beta1.IAMPolicy{
   621  			TypeMeta: metav1.TypeMeta{
   622  				APIVersion: v1beta1.IAMPolicyGVK.GroupVersion().String(),
   623  				Kind:       v1beta1.IAMPolicyGVK.Kind,
   624  			},
   625  		}
   626  	case *v1beta1.IAMPolicyMember:
   627  		return &v1beta1.IAMPolicyMember{
   628  			TypeMeta: metav1.TypeMeta{
   629  				APIVersion: v1beta1.IAMPolicyMemberGVK.GroupVersion().String(),
   630  				Kind:       v1beta1.IAMPolicyMemberGVK.Kind,
   631  			},
   632  		}
   633  	case *v1beta1.IAMAuditConfig:
   634  		return &v1beta1.IAMAuditConfig{
   635  			TypeMeta: metav1.TypeMeta{
   636  				APIVersion: v1beta1.IAMAuditConfigGVK.GroupVersion().String(),
   637  				Kind:       v1beta1.IAMAuditConfigGVK.Kind,
   638  			},
   639  		}
   640  	}
   641  	panic(fmt.Errorf("unknown type: %v", reflect.TypeOf(iamInterface).Name()))
   642  }
   643  
   644  func (t *TFIAMClient) newServiceMappingForAssociatedIAMInterface(ctx context.Context, iamInterface interface{}, iamConfig corekccv1alpha1.IAMConfig) (*corekccv1alpha1.ServiceMapping, error) {
   645  	switch iamInterface.(type) {
   646  	case *v1beta1.IAMPolicy:
   647  		return newServiceMappingForGVKAndTFResourceName(v1beta1.IAMPolicyGVK, iamConfig.PolicyName), nil
   648  	case *v1beta1.IAMPolicyMember:
   649  		return newServiceMappingForGVKAndTFResourceName(v1beta1.IAMPolicyMemberGVK, iamConfig.PolicyMemberName), nil
   650  	case *v1beta1.IAMAuditConfig:
   651  		return newServiceMappingForGVKAndTFResourceName(v1beta1.IAMAuditConfigGVK, iamConfig.AuditConfigName), nil
   652  	}
   653  	panic(fmt.Errorf("unknown type: %v", reflect.TypeOf(iamInterface).Name()))
   654  }
   655  
   656  func getReferenceFieldValue(id string, rc *corekccv1alpha1.ResourceConfig) (string, error) {
   657  	switch rc.IAMConfig.ReferenceField.Type {
   658  	case corekccv1alpha1.IAMReferenceTypeId:
   659  		return id, nil
   660  	case corekccv1alpha1.IAMReferenceTypeName:
   661  		return parseNameFromId(id)
   662  	default:
   663  		panic(fmt.Errorf("unknown value type: %v", rc.IAMConfig.ReferenceField.Type))
   664  	}
   665  }
   666  
   667  func resolveTargetFieldValue(r *krmtotf.Resource, targetField string) (string, error) {
   668  	key := text.SnakeCaseToLowerCamelCase(targetField)
   669  	switch key {
   670  	case "":
   671  		panic(fmt.Errorf("empty target field specified"))
   672  	default:
   673  		if val, exists, _ := unstructured.NestedString(r.Spec, strings.Split(key, ".")...); exists {
   674  			return val, nil
   675  		}
   676  		if val, exists, _ := unstructured.NestedString(r.Status, strings.Split(key, ".")...); exists {
   677  			return val, nil
   678  		}
   679  		return "", fmt.Errorf("couldn't resolve the value for target field %v from the referenced resource %v", targetField, r.GetNamespacedName())
   680  	}
   681  }
   682  
   683  func newIAMPolicyFromTFState(resource *krmtotf.Resource, state *terraform.InstanceState, origPolicy *v1beta1.IAMPolicy) (*v1beta1.IAMPolicy, error) {
   684  	resource.Spec, resource.Status = krmtotf.GetSpecAndStatusFromState(resource, state)
   685  	if err := embedPolicyData(resource.Spec); err != nil {
   686  		return nil, err
   687  	}
   688  	u, err := resource.MarshalAsUnstructured()
   689  	if err != nil {
   690  		return nil, fmt.Errorf("error marshalling resource to unstructured: %w", err)
   691  	}
   692  	iamPolicy := v1beta1.IAMPolicy{}
   693  	if err := util.Marshal(u, &iamPolicy); err != nil {
   694  		return nil, fmt.Errorf("error marshalling unstructured to iampolicy: %w", err)
   695  	}
   696  	iamPolicy.Spec.ResourceReference = origPolicy.Spec.ResourceReference
   697  	iamPolicy.ObjectMeta = origPolicy.ObjectMeta
   698  	etag := krmtotf.GetEtagFromState(resource, state)
   699  	if etag == "" {
   700  		return nil, fmt.Errorf("unexpected empty etag read from the IAM Policy resource")
   701  	}
   702  	iamPolicy.Spec.Etag = etag
   703  	return &iamPolicy, nil
   704  }
   705  
   706  func newIAMPolicyMemberFromTFState(resource *krmtotf.Resource, state *terraform.InstanceState, origPolicyMember *v1beta1.IAMPolicyMember) (*v1beta1.IAMPolicyMember, error) {
   707  	resource.Spec, resource.Status = krmtotf.GetSpecAndStatusFromState(resource, state)
   708  	u, err := resource.MarshalAsUnstructured()
   709  	if err != nil {
   710  		return nil, fmt.Errorf("error marshalling resource to unstructured: %w", err)
   711  	}
   712  	iamPolicyMember := v1beta1.IAMPolicyMember{}
   713  	if err := util.Marshal(u, &iamPolicyMember); err != nil {
   714  		return nil, fmt.Errorf("error marshalling unstructured to IAMPolicyMember: %w", err)
   715  	}
   716  	if origPolicyMember.Spec.Member == "" {
   717  		iamPolicyMember.Spec.Member = ""
   718  		iamPolicyMember.Spec.MemberFrom = origPolicyMember.Spec.MemberFrom
   719  	}
   720  	iamPolicyMember.Spec.ResourceReference = origPolicyMember.Spec.ResourceReference
   721  	iamPolicyMember.ObjectMeta = origPolicyMember.ObjectMeta
   722  	return &iamPolicyMember, nil
   723  }
   724  
   725  func newIAMAuditConfigFromTFState(resource *krmtotf.Resource, state *terraform.InstanceState, origAuditConfig *v1beta1.IAMAuditConfig) (*v1beta1.IAMAuditConfig, error) {
   726  	resource.Spec, resource.Status = krmtotf.GetSpecAndStatusFromState(resource, state)
   727  	if auditLogConfigs, ok := resource.Spec["auditLogConfig"]; ok {
   728  		resource.Spec["auditLogConfigs"] = auditLogConfigs
   729  		delete(resource.Spec, "auditLogConfig")
   730  	}
   731  	u, err := resource.MarshalAsUnstructured()
   732  	if err != nil {
   733  		return nil, fmt.Errorf("error marshalling resource to unstructured: %w", err)
   734  	}
   735  	iamAuditConfig := v1beta1.IAMAuditConfig{}
   736  	if err := util.Marshal(u, &iamAuditConfig); err != nil {
   737  		return nil, fmt.Errorf("error marshalling unstructured to IAMAuditConfig: %w", err)
   738  	}
   739  	iamAuditConfig.Spec.ResourceReference = origAuditConfig.Spec.ResourceReference
   740  	iamAuditConfig.ObjectMeta = origAuditConfig.ObjectMeta
   741  	return &iamAuditConfig, nil
   742  }
   743  
   744  func newServiceMappingForGVKAndTFResourceName(iamGVK schema.GroupVersionKind, tfResourceName string) *corekccv1alpha1.
   745  	ServiceMapping {
   746  	return &corekccv1alpha1.ServiceMapping{
   747  		TypeMeta: metav1.TypeMeta{
   748  			APIVersion: iamGVK.GroupVersion().String(),
   749  			Kind:       reflect.TypeOf(corekccv1alpha1.ServiceMapping{}).Name(),
   750  		},
   751  		ObjectMeta: metav1.ObjectMeta{},
   752  		Spec: corekccv1alpha1.ServiceMappingSpec{
   753  			Name:    iamGVK.Kind,
   754  			Version: iamGVK.Version,
   755  			Resources: []corekccv1alpha1.ResourceConfig{
   756  				{
   757  					Name:       tfResourceName,
   758  					Kind:       iamGVK.Kind,
   759  					SkipImport: true,
   760  					ResourceReferences: []corekccv1alpha1.ReferenceConfig{
   761  						{
   762  							TypeConfig: corekccv1alpha1.TypeConfig{
   763  								Key: "resourceRef",
   764  							},
   765  						},
   766  					},
   767  				},
   768  			},
   769  		},
   770  	}
   771  }
   772  

View as plain text