github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/label/labels.go (about)

     1  /*
     2  Copyright 2019 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package label
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"time"
    24  
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	patch "k8s.io/apimachinery/pkg/util/strategicpatch"
    29  	"k8s.io/client-go/discovery"
    30  	"k8s.io/client-go/dynamic"
    31  
    32  	deploy "github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/types"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/deploy/util"
    34  	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    35  	kubectx "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    37  )
    38  
    39  // retry 3 times to give the object time to propagate to the API server
    40  const (
    41  	tries     = 3
    42  	sleeptime = 300 * time.Millisecond
    43  )
    44  
    45  // Apply applies all provided labels to the created Kubernetes resources
    46  func Apply(ctx context.Context, labels map[string]string, results []deploy.Artifact, kubeContext string) error {
    47  	if len(labels) == 0 {
    48  		return nil
    49  	}
    50  
    51  	// use the kubectl client to update all k8s objects with a skaffold watermark
    52  	dynClient, err := kubernetesclient.DynamicClient(kubeContext)
    53  	if err != nil {
    54  		return fmt.Errorf("error getting Kubernetes dynamic client: %w", err)
    55  	}
    56  
    57  	client, err := kubernetesclient.Client(kubeContext)
    58  	if err != nil {
    59  		return fmt.Errorf("error getting Kubernetes client: %w", err)
    60  	}
    61  
    62  	for _, res := range results {
    63  		err = nil
    64  		for i := 0; i < tries; i++ {
    65  			if err = updateRuntimeObject(ctx, dynClient, client.Discovery(), labels, res, kubeContext); err == nil {
    66  				break
    67  			}
    68  			time.Sleep(sleeptime)
    69  		}
    70  		if err != nil {
    71  			log.Entry(ctx).Warnf("error adding label to runtime object: %s", err.Error())
    72  		}
    73  	}
    74  
    75  	return nil
    76  }
    77  
    78  func addLabels(labels map[string]string, accessor metav1.Object) {
    79  	kv := make(map[string]string)
    80  
    81  	copyMap(kv, labels)
    82  	copyMap(kv, accessor.GetLabels())
    83  
    84  	accessor.SetLabels(kv)
    85  }
    86  
    87  func updateRuntimeObject(ctx context.Context, client dynamic.Interface, disco discovery.DiscoveryInterface, labels map[string]string, res deploy.Artifact, kubeContext string) error {
    88  	originalJSON, _ := json.Marshal(res.Obj)
    89  	modifiedObj := res.Obj.DeepCopyObject()
    90  	accessor, err := meta.Accessor(modifiedObj)
    91  	if err != nil {
    92  		return fmt.Errorf("getting metadata accessor: %w", err)
    93  	}
    94  	name := accessor.GetName()
    95  
    96  	addLabels(labels, accessor)
    97  
    98  	modifiedJSON, _ := json.Marshal(modifiedObj)
    99  	p, _ := patch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, modifiedObj)
   100  
   101  	namespaced, gvr, err := util.GroupVersionResource(disco, modifiedObj.GetObjectKind().GroupVersionKind())
   102  	if err != nil {
   103  		return fmt.Errorf("getting group version resource from obj: %w", err)
   104  	}
   105  
   106  	if namespaced {
   107  		var namespace string
   108  		if accessor.GetNamespace() != "" {
   109  			namespace = accessor.GetNamespace()
   110  		} else {
   111  			namespace = res.Namespace
   112  		}
   113  
   114  		ns, err := resolveNamespace(namespace, kubeContext)
   115  		if err != nil {
   116  			return fmt.Errorf("resolving namespace: %w", err)
   117  		}
   118  
   119  		log.Entry(ctx).Debug("Patching", name, "in namespace", ns)
   120  		if _, err := client.Resource(gvr).Namespace(ns).Patch(ctx, name, types.StrategicMergePatchType, p, metav1.PatchOptions{}); err != nil {
   121  			return fmt.Errorf("patching resource %s/%q: %w", ns, name, err)
   122  		}
   123  	} else {
   124  		log.Entry(ctx).Debug("Patching", name)
   125  		if _, err := client.Resource(gvr).Patch(ctx, name, types.StrategicMergePatchType, p, metav1.PatchOptions{}); err != nil {
   126  			return fmt.Errorf("patching resource %q: %w", name, err)
   127  		}
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func resolveNamespace(ns, kubeContext string) (string, error) {
   134  	if ns != "" {
   135  		return ns, nil
   136  	}
   137  	cfg, err := kubectx.CurrentConfig()
   138  	if err != nil {
   139  		return "", fmt.Errorf("getting kubeconfig: %w", err)
   140  	}
   141  
   142  	current, present := cfg.Contexts[kubeContext]
   143  	if present && current.Namespace != "" {
   144  		return current.Namespace, nil
   145  	}
   146  	return "default", nil
   147  }
   148  
   149  func copyMap(dest, from map[string]string) {
   150  	for k, v := range from {
   151  		dest[k] = v
   152  	}
   153  }