...

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

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

     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 krmtotf
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  
    21  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
    22  	"github.com/GoogleCloudPlatform/k8s-config-connector/pkg/text"
    23  
    24  	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  )
    27  
    28  func ResolveLegacyGCPManagedFields(r *Resource, liveState *terraform.InstanceState, config map[string]interface{}) error {
    29  	// TODO(kcc-eng): This is a temporary workaround for the well-known cases of
    30  	//  autoscaling/auto-updating fields. When the full GCP-managed fields story
    31  	//  is in place, this will be made fully generic.
    32  	if liveState.Empty() {
    33  		// If the resource is being created, then there is nothing from GCP to
    34  		// manage yet. Use the values explicitly from the customer.
    35  		return nil
    36  	}
    37  	switch r.GroupVersionKind().Kind {
    38  	case "SQLInstance":
    39  		return resolveSQLInstanceDiskSize(r, config)
    40  	case "ContainerCluster":
    41  		if err := resolveContainerClusterNodeVersion(r, config); err != nil {
    42  			return err
    43  		}
    44  		if err := resolveContainerClusterNodeConfig(r, liveState, config); err != nil {
    45  			return err
    46  		}
    47  		return nil
    48  	case "ContainerNodePool":
    49  		if err := resolveContainerNodePoolVersion(r, config); err != nil {
    50  			return err
    51  		}
    52  		if err := resolveContainerNodePoolInitialNodeCount(r, config); err != nil {
    53  			return err
    54  		}
    55  		if err := resolveContainerNodePoolNodeCount(r, config); err != nil {
    56  			return err
    57  		}
    58  		return nil
    59  	case "ComputeBackendService":
    60  		return resolveComputeBackendServiceBackend(r, config)
    61  	case "BigtableInstance":
    62  		return resolveBigtableInstanceNumNodes(r, config)
    63  	default:
    64  		return nil
    65  	}
    66  }
    67  
    68  func isGCPManagedField(kind, field string) bool {
    69  	// TODO(kcc-eng): This is a temporary workaround for the well-known cases of
    70  	//  autoscaling/auto-updating fields. When the full GCP-managed fields story
    71  	//  is in place, this will be made fully generic.
    72  	switch kind {
    73  	case "SQLInstance":
    74  		return field == "settings.disk_size"
    75  	case "ContainerCluster":
    76  		return field == "node_version"
    77  	case "ContainerNodePool":
    78  		switch field {
    79  		case "version", "node_count", "initial_node_count":
    80  			return true
    81  		default:
    82  			return false
    83  		}
    84  	}
    85  	return false
    86  }
    87  
    88  func resolveSQLInstanceDiskSize(r *Resource, config map[string]interface{}) error {
    89  	// Customers can opt-in to automatic disk resizes. Customers can set this
    90  	// manually, but GCP will also automatically update the field if the
    91  	// capacity reaches a certain threshold. For more information, see:
    92  	// https://cloud.google.com/sql/docs/mysql/instance-settings
    93  	autoresizeEnabled, found, err := unstructured.NestedBool(config, "settings", "diskAutoresize")
    94  	if err != nil {
    95  		return fmt.Errorf("error determining if disk autoresize is set: %w", err)
    96  	}
    97  	if !found || !autoresizeEnabled {
    98  		// Autoresize is disabled, so no special behavior required.
    99  		return nil
   100  	}
   101  	if err := removeFromConfigIfNotApplied(r, config, "settings", "diskSize"); err != nil {
   102  		return fmt.Errorf("error resolving disk size in config: %w", err)
   103  	}
   104  	return nil
   105  }
   106  
   107  func resolveContainerClusterNodeVersion(r *Resource, config map[string]interface{}) error {
   108  	// If the customer sets a release channel on their cluster, then GKE assumes ownership
   109  	// of the node version and will automatically revert any changes.
   110  	releaseChannel, found, err := unstructured.NestedMap(config, "releaseChannel")
   111  	if err != nil {
   112  		return fmt.Errorf("error determining if release channel is set: %w", err)
   113  	}
   114  	if !found || releaseChannel == nil {
   115  		// Release channel is not specified, so no special behavior required.
   116  		return nil
   117  	}
   118  	if err := removeFromConfigIfNotApplied(r, config, "nodeVersion"); err != nil {
   119  		return fmt.Errorf("error resolving node version in config: %w", err)
   120  	}
   121  	return nil
   122  }
   123  
   124  func resolveContainerNodePoolVersion(r *Resource, config map[string]interface{}) error {
   125  	autoUpgrade, found, err := unstructured.NestedBool(config, "management", "autoUpgrade")
   126  	if err != nil {
   127  		return fmt.Errorf("error determining if autoupgrade is set: %w", err)
   128  	}
   129  	if !found || !autoUpgrade {
   130  		// Autoupgrade is disabled, so no special behavior required.
   131  		return nil
   132  	}
   133  	field := "version"
   134  	if err := removeFromConfigIfNotApplied(r, config, field); err != nil {
   135  		return fmt.Errorf("error resolving field '%v' in config: %w", field, err)
   136  	}
   137  	return nil
   138  }
   139  
   140  // Remove `nodeConfig` from the desired config when `remove-default-node-pool`
   141  // directive is set to `true` and the liveState doesn't contain this field.
   142  //
   143  // When `remove-default-node-pool` directive is set to `true`, the default node
   144  // pool will be removed, and `spec.nodeConfig` field should be managed by the API.
   145  // However, because the service-generated value of `spec.nodeConfig` contains
   146  // lists, which are preserved by KCC even if the live state of the GCP resource
   147  // no longer has `nodeConfig` field, it triggers unexpected recreation of the
   148  // resource. So in this case, we need to manually clean up `nodeConfig` field.
   149  func resolveContainerClusterNodeConfig(r *Resource, liveState *terraform.InstanceState, config map[string]interface{}) error {
   150  	removeDefaultNodePoolDirective := "remove-default-node-pool"
   151  	nodeConfigFieldInTFState := "node_config"
   152  	nodeConfigFieldInKRMConfig := text.SnakeCaseToLowerCamelCase(nodeConfigFieldInTFState)
   153  
   154  	key := k8s.FormatAnnotation(removeDefaultNodePoolDirective)
   155  	val, ok := k8s.GetAnnotation(key, r)
   156  	if !ok || val != "true" {
   157  		return nil
   158  	}
   159  
   160  	liveStateMap := InstanceStateToMap(r.TFResource, liveState)
   161  	exists, err := topLevelObjectFieldExistsInStateMap(liveStateMap, nodeConfigFieldInTFState)
   162  	if err != nil {
   163  		return fmt.Errorf("error resolving field '%v' in 'ContainerCluster': %w", nodeConfigFieldInKRMConfig, err)
   164  	}
   165  	if exists {
   166  		return nil
   167  	}
   168  
   169  	if err := removeFromConfigIfNotApplied(r, config, nodeConfigFieldInKRMConfig); err != nil {
   170  		return fmt.Errorf("error removing field '%v' in config: %w", nodeConfigFieldInKRMConfig, err)
   171  	}
   172  	return nil
   173  }
   174  
   175  func topLevelObjectFieldExistsInStateMap(state map[string]interface{}, field string) (bool, error) {
   176  	value, ok := state[field]
   177  	if !ok {
   178  		return false, nil
   179  	}
   180  	listVal, ok := value.([]interface{})
   181  	if !ok {
   182  		return false, fmt.Errorf("field '%v' is not an object field", field)
   183  	}
   184  	// An object field should be considered non-existent if no sub-field is specified.
   185  	if len(listVal) == 0 {
   186  		return false, nil
   187  	}
   188  	// The response returned by terraform may insert a list of size 1 for nested fields.
   189  	return listVal[0] != nil, nil
   190  }
   191  
   192  func resolveContainerNodePoolInitialNodeCount(r *Resource, config map[string]interface{}) error {
   193  	// After create, `initialNodeCount` floats with the last value passed to the
   194  	// `setSize` custom verb. For KCC, we will push users to use `spec.nodeCount`
   195  	// instead, and treat initialNodeCount as GCP-managed.
   196  	if err := removeFromConfigIfNotApplied(r, config, "initialNodeCount"); err != nil {
   197  		return fmt.Errorf("error resolving initialNodeCount in config: %w", err)
   198  	}
   199  	return nil
   200  }
   201  
   202  func resolveContainerNodePoolNodeCount(r *Resource, config map[string]interface{}) error {
   203  	// The `spec.nodeCount` field should be assumed to be GCP-managed when autoscaling
   204  	// is enabled. This is determined by the presence of the "autoscaling" field.
   205  	if val := config["autoscaling"]; val == nil {
   206  		// Autoscaling is not enabled; so no special behavior required.
   207  		return nil
   208  	}
   209  	// Autoscaling is enabled. Treat spec.nodeCount as GCP-managed.
   210  	if err := removeFromConfigIfNotApplied(r, config, "nodeCount"); err != nil {
   211  		return fmt.Errorf("error resolving nodeCount in config: %w", err)
   212  	}
   213  	return nil
   214  }
   215  
   216  func resolveComputeBackendServiceBackend(r *Resource, config map[string]interface{}) error {
   217  	// If the customer omits backend definitions in their backendservice,
   218  	// assume backend field is owned by another process
   219  	if err := removeFromConfigIfNotApplied(r, config, "backend"); err != nil {
   220  		return fmt.Errorf("error resolving backend in config: %w", err)
   221  	}
   222  	return nil
   223  }
   224  
   225  func resolveBigtableInstanceNumNodes(r *Resource, config map[string]interface{}) error {
   226  	// If numNodes is not in a cluster's last applied configuration
   227  	// remove from config
   228  	applied, found, err := getLastAppliedValue(r, "cluster")
   229  	if err != nil {
   230  		return fmt.Errorf("error determining last applied clusters: %w", err)
   231  	}
   232  	if !found {
   233  		return nil
   234  	}
   235  	appliedClusters, ok := applied.([]interface{})
   236  	if !ok {
   237  		return fmt.Errorf("cannot decode last applied clusters")
   238  	}
   239  	for _, c := range appliedClusters {
   240  		c, ok := c.(map[string]interface{})
   241  		if !ok {
   242  			return fmt.Errorf("cannot decode cluster")
   243  		}
   244  		clusterId, found, err := unstructured.NestedString(c, "clusterId")
   245  		if err != nil {
   246  			return fmt.Errorf("error determining clusterId: %w", err)
   247  		} else if !found {
   248  			return fmt.Errorf("cannot determine clusterId")
   249  		}
   250  		_, found, err = unstructured.NestedFloat64(c, "numNodes")
   251  		if err != nil {
   252  			return fmt.Errorf("error determining numNodes: %w", err)
   253  		}
   254  		if !found {
   255  			// remove from output config
   256  			if err = removeNumNodesFromBigtableCluster(config, clusterId); err != nil {
   257  				return fmt.Errorf("error removing numNodes: %w", err)
   258  			}
   259  		}
   260  	}
   261  	return nil
   262  }
   263  
   264  // removeNumNodesFromBigtableCluster removes the numNodes field from the named
   265  // cluster specification if it exists
   266  func removeNumNodesFromBigtableCluster(config map[string]interface{}, cluster string) error {
   267  	// Fetch clusters from config
   268  	clusters, found, err := unstructured.NestedSlice(config, "cluster")
   269  	if err != nil {
   270  		return fmt.Errorf("error finding clusters in config: %w", err)
   271  	}
   272  	if !found {
   273  		return nil
   274  	}
   275  	for _, c := range clusters {
   276  		c, ok := c.(map[string]interface{})
   277  		if !ok {
   278  			return fmt.Errorf("cannot decode cluster")
   279  		}
   280  		id, found, _ := unstructured.NestedString(c, "clusterId")
   281  		if !found {
   282  			return fmt.Errorf("cannot determine cluster id")
   283  		}
   284  		if id == cluster {
   285  			unstructured.RemoveNestedField(c, "numNodes")
   286  		}
   287  	}
   288  	if err := unstructured.SetNestedSlice(config, clusters, "cluster"); err != nil {
   289  		return fmt.Errorf("error setting cluster list: %w", err)
   290  	}
   291  	return nil
   292  }
   293  
   294  func getLastAppliedValue(r *Resource, path ...string) (val interface{}, found bool, err error) {
   295  	// Note: Only values from kubectl's last applied configuration will be recognized. Values
   296  	// set manually with server-side apply or edited via POST/PUT/PATCH directly will not be
   297  	// recognized. kubectl's annotation is present on all `kubectl apply`-ed resources for
   298  	// GKE versions 1.14-1.16.
   299  	lastAppliedConfigRaw, ok := k8s.GetAnnotation(k8s.LastAppliedConfigurationAnnotation, r)
   300  	if !ok {
   301  		return nil, false, nil
   302  	}
   303  	lastAppliedConfig := make(map[string]interface{})
   304  	if err := json.Unmarshal([]byte(lastAppliedConfigRaw), &lastAppliedConfig); err != nil {
   305  		return nil, false, fmt.Errorf("error unmarshaling last applied configuration: %w", err)
   306  	}
   307  	specPath := append([]string{"spec"}, path...)
   308  	return unstructured.NestedFieldCopy(lastAppliedConfig, specPath...)
   309  }
   310  
   311  // removeFromConfigIfNotApplied removes the specified field from the config, unless it was
   312  // applied explicitly by the customer.
   313  func removeFromConfigIfNotApplied(r *Resource, config map[string]interface{}, path ...string) error {
   314  	// Note that this does not perform any mappings. It is assumed the path and config are
   315  	// in the KRM camelCase format.
   316  	_, found, err := getLastAppliedValue(r, path...)
   317  	if err != nil {
   318  		return fmt.Errorf("error finding last applied value for disk size: %w", err)
   319  	}
   320  	if !found {
   321  		// The value was not found in the last applied configuration. Delegate
   322  		// instead to GCP by removing the field from the config. The live state's
   323  		// value will be substituted during the diff calculation.
   324  		unstructured.RemoveNestedField(config, path...)
   325  	}
   326  	return nil
   327  }
   328  

View as plain text