...

Source file src/github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/livestate/fetchlivestate.go

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

     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 livestate
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strings"
    21  
    22  	corekccv1alpha1 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/apis/core/v1alpha1"
    23  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl"
    24  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/conversion"
    25  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/extension"
    26  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/dcl/kcclite"
    27  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/deepcopy"
    28  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/gcp"
    29  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    30  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/servicemapping/servicemappingloader"
    31  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util"
    32  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/pathslice"
    33  
    34  	mmdcl "github.com/GoogleCloudPlatform/declarative-resource-client-library/dcl"
    35  	dclunstruct "github.com/GoogleCloudPlatform/declarative-resource-client-library/unstructured"
    36  	"github.com/nasa9084/go-openapi"
    37  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  )
    40  
    41  // FetchLiveState returns the live state of the underlying resource.
    42  
    43  // It invokes DCL Get() to read on the underlying resource and then convert response to KCC lite format.
    44  func FetchLiveState(ctx context.Context, resource *dcl.Resource, dclConfig *mmdcl.Config, converter *conversion.Converter, serviceMappingLoader *servicemappingloader.ServiceMappingLoader, kubeClient client.Client) (*unstructured.Unstructured, error) {
    45  	serverGeneratedIDNotConfigured, err := resource.HasServerGeneratedIDButNotConfigured()
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	if serverGeneratedIDNotConfigured {
    50  		// If the resource ID cannot be determined because it requires a server-
    51  		// generated ID that has not been set, this means the resource has not
    52  		// yet been created. Return as if the read returned a non-existent
    53  		// resource.
    54  		return nil, nil
    55  	}
    56  
    57  	var lite *unstructured.Unstructured
    58  	secretVersions := make(map[string]string)
    59  	if k8s.IsDeleted(&resource.ObjectMeta) {
    60  		lite, err = kcclite.ToKCCLiteBestEffort(resource, converter.MetadataLoader, converter.SchemaLoader, serviceMappingLoader, kubeClient)
    61  	} else {
    62  		lite, secretVersions, err = kcclite.ToKCCLiteAndSecretVersions(resource, converter.MetadataLoader, converter.SchemaLoader, serviceMappingLoader, kubeClient)
    63  	}
    64  	if err != nil {
    65  		return nil, fmt.Errorf("error converting to lite state: %w", err)
    66  	}
    67  
    68  	dclResource, err := converter.KRMObjectToDCLObject(lite)
    69  	if err != nil {
    70  		return nil, fmt.Errorf("error converting to DCL resource object: %w", err)
    71  	}
    72  
    73  	liveState, err := dclunstruct.Get(ctx, dclConfig, dclResource)
    74  	if err != nil {
    75  		if gcp.IsNotFoundError(err) {
    76  			return nil, nil
    77  		}
    78  		return nil, fmt.Errorf("error reading underlying resource: %w", err)
    79  	}
    80  	liveLite, err := converter.DCLObjectToKRMObject(liveState)
    81  	if err != nil {
    82  		return nil, fmt.Errorf("error converting DCL object to KRM resource obj: %w", err)
    83  	}
    84  
    85  	liveLite, err = withMutableButUnreadableFields(liveLite, resource, secretVersions, kubeClient)
    86  	if err != nil {
    87  		return nil, fmt.Errorf("error setting mutable-but-unreadable fields for live state: %w", err)
    88  	}
    89  
    90  	return liveLite, nil
    91  }
    92  
    93  func SetMutableButUnreadableFields(kccLite *unstructured.Unstructured, mutableButUnreadableSpec map[string]interface{}, path []string, schema *openapi.Schema, secretVersions map[string]string, namespace string, kubeClient client.Client) (*unstructured.Unstructured, error) {
    94  	if len(mutableButUnreadableSpec) == 0 {
    95  		return kccLite, nil
    96  	}
    97  
    98  	if schema.Type != "object" {
    99  		return nil, fmt.Errorf("wrong type for provided schema: %s, expect to have object", schema.Type)
   100  	}
   101  
   102  	for k, v := range mutableButUnreadableSpec {
   103  		path := append(path, k)
   104  
   105  		subSchema, ok := schema.Properties[k]
   106  		if !ok {
   107  			return nil, fmt.Errorf("unknown mutable-but-unreadable path '%v'", pathslice.ToString(path))
   108  		}
   109  		switch subSchema.Type {
   110  		case "integer":
   111  			v, err := dcl.CanonicalizeIntegerValue(v)
   112  			if err != nil {
   113  				return nil, fmt.Errorf("error canonicalizing the integer value for path '%v': %w", pathslice.ToString(path), err)
   114  			}
   115  
   116  			if err := unstructured.SetNestedField(kccLite.Object, v, path...); err != nil {
   117  				return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   118  			}
   119  		case "number":
   120  			v, err := dcl.CanonicalizeNumberValue(v)
   121  			if err != nil {
   122  				return nil, fmt.Errorf("error canonicalizing the number value for path '%v': %w", pathslice.ToString(path), err)
   123  			}
   124  
   125  			if err := unstructured.SetNestedField(kccLite.Object, v, path...); err != nil {
   126  				return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   127  			}
   128  
   129  		case "string":
   130  			isSensitive, err := extension.IsSensitiveField(subSchema)
   131  			if err != nil {
   132  				return nil, fmt.Errorf("error checking sensitivity for mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   133  			}
   134  
   135  			if !isSensitive {
   136  				if err := unstructured.SetNestedField(kccLite.Object, v, path...); err != nil {
   137  					return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   138  				}
   139  				continue
   140  			}
   141  
   142  			val, err := resolveSensitiveValueForLiveState(v, secretVersions, namespace, kubeClient)
   143  			if err != nil {
   144  				return nil, fmt.Errorf("error parsing the secret for mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   145  			}
   146  			valMap := make(map[string]interface{})
   147  			valMap["value"] = val
   148  			if err := unstructured.SetNestedField(kccLite.Object, valMap, path...); err != nil {
   149  				return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   150  			}
   151  		case "boolean":
   152  			if err := unstructured.SetNestedField(kccLite.Object, v, path...); err != nil {
   153  				return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   154  			}
   155  		case "array":
   156  			v, ok := v.([]interface{})
   157  			if !ok {
   158  				return nil, fmt.Errorf("wrong type for path '%v': %T, expect to have []interface{}", pathslice.ToString(path), v)
   159  			}
   160  
   161  			itemSchema := subSchema.Items
   162  			switch itemSchema.Type {
   163  			case "string", "boolean", "number", "integer":
   164  				// List/set of primitives
   165  				path := strings.Split(pathslice.ToString(path), ".")
   166  				if err := unstructured.SetNestedField(kccLite.Object, deepcopy.DeepCopy(v), path...); err != nil {
   167  					return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   168  				}
   169  			case "array", "object":
   170  				// List/set of non-primitives
   171  				return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': mutable-but-unreadable list/set of non-primitive types is not yet supported", pathslice.ToString(path))
   172  			default:
   173  				// List of unknown types
   174  				return nil, fmt.Errorf("unknown list/set type %T for mutable-but-unreadable path '%v'", itemSchema, pathslice.ToString(path))
   175  			}
   176  		case "object":
   177  			v, ok := v.(map[string]interface{})
   178  			if !ok {
   179  				return nil, fmt.Errorf("wrong type for path '%v': %T, expect to have map[string]interface{}", pathslice.ToString(path), v)
   180  			}
   181  
   182  			if subSchema.AdditionalProperties != nil {
   183  				// Map
   184  				// Currently KCC doesn't support the nested path in the value
   185  				// of the map to be mutable-but-unreadable, so the map is copied
   186  				// as a whole.
   187  				if err := unstructured.SetNestedField(kccLite.Object, deepcopy.DeepCopy(v), path...); err != nil {
   188  					return nil, fmt.Errorf("error setting mutable-but-unreadable path '%v': %w", pathslice.ToString(path), err)
   189  				}
   190  			} else {
   191  				// Object
   192  				var err error
   193  				kccLite, err = SetMutableButUnreadableFields(kccLite, v, path, subSchema, secretVersions, namespace, kubeClient)
   194  				if err != nil {
   195  					return nil, fmt.Errorf("error setting nested mutable-but-unreadable path: %w", err)
   196  				}
   197  			}
   198  		default:
   199  			// Unknown types
   200  			return nil, fmt.Errorf("unknown type %T for mutable-but-unreadable path '%v'", subSchema, pathslice.ToString(path))
   201  		}
   202  	}
   203  
   204  	return kccLite, nil
   205  }
   206  
   207  func resolveSensitiveValueForLiveState(value interface{}, secretVersions map[string]string, namespace string, kubeClient client.Client) (string, error) {
   208  	sensitiveField := corekccv1alpha1.SensitiveField{}
   209  	if err := util.Marshal(value, &sensitiveField); err != nil {
   210  		return "", fmt.Errorf("error parsing %v onto a SensitiveField struct: %v", value, err)
   211  	}
   212  
   213  	if sensitiveField.Value != nil {
   214  		return *sensitiveField.Value, nil
   215  	}
   216  
   217  	secretKeyRef := sensitiveField.ValueFrom.SecretKeyRef
   218  	secretVal, secretVer, err := k8s.GetSecretVal(secretKeyRef, namespace, kubeClient)
   219  	if err != nil {
   220  		// If a previously referenced Secret cannot be resolved, it is possible
   221  		// it simply no longer exists. Don't error out in this case; just skip
   222  		// the presetting of the path in the live state so that a diff is
   223  		// generated if the path had been updated in the spec. If the path
   224  		// still points to the same Secret in the spec, then the DCL KCClite
   225  		// conversion will appropriately error out due to the non-existent
   226  		// Secret.
   227  		return "", nil
   228  	}
   229  	// Preset sensitive path only if we can be sure that the
   230  	// referenced Secret had not been changed.
   231  	prevSecretVer, ok := secretVersions[secretKeyRef.Name]
   232  	if ok && secretVer == prevSecretVer {
   233  		return secretVal, nil
   234  	}
   235  
   236  	// If the referenced Secret couldn't be found in the secretVersions map, or
   237  	// the referenced Secret has a different Secret version, then we need to
   238  	// explicitly set the secret value to an empty string ("") to trigger the diff.
   239  	return "", nil
   240  }
   241  
   242  func withMutableButUnreadableFields(kccLite *unstructured.Unstructured, resource *dcl.Resource, currSecretVersions map[string]string, kubeClient client.Client) (*unstructured.Unstructured, error) {
   243  	hasMutableButUnreadableFields, err := resource.HasMutableButUnreadableFields()
   244  	if err != nil {
   245  		return nil, fmt.Errorf("error checking if resource has mutable-but-unreadable fields: %w", err)
   246  	}
   247  	if !hasMutableButUnreadableFields {
   248  		return kccLite, nil
   249  	}
   250  
   251  	lastSeenValues, err := dcl.GetMutableButUnreadableFieldsFromAnnotations(resource)
   252  	if err != nil {
   253  		return nil, fmt.Errorf("error getting last-seen values of mutable-but-unreadable fields from annotations: %w", err)
   254  	}
   255  
   256  	updatedLite := kccLite.DeepCopy()
   257  	if len(lastSeenValues) == 0 { // if len(lastSeenValues) == 0, the mutable-but-unreadable field is probably optional and unset
   258  		return updatedLite, nil
   259  	}
   260  
   261  	secretVersions, err := k8s.GetSecretVersionsFromAnnotations(&resource.Resource)
   262  	if err != nil {
   263  		return nil, fmt.Errorf("error getting secret versions from annotations: %w", err)
   264  	}
   265  	// When secretVersions are not found in the annotations, there is a possibility
   266  	// that this is either (1) a resource acquisition or (2) a resource created
   267  	// before it supported the annotation. To avoid unnecessarily updating the
   268  	// resource in both cases, use the current Secret versions.
   269  	if secretVersions == nil {
   270  		secretVersions = currSecretVersions
   271  	}
   272  
   273  	updatedLite, err = SetMutableButUnreadableFields(updatedLite, lastSeenValues["spec"].(map[string]interface{}), []string{"spec"}, resource.Schema, secretVersions, resource.GetNamespace(), kubeClient)
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	return updatedLite, nil
   278  }
   279  

View as plain text