github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/label.go (about)

     1  package k8s
     2  
     3  import (
     4  	"reflect"
     5  
     6  	appsv1beta1 "k8s.io/api/apps/v1beta1"
     7  	appsv1beta2 "k8s.io/api/apps/v1beta2"
     8  	v1 "k8s.io/api/core/v1"
     9  	extv1beta1 "k8s.io/api/extensions/v1beta1"
    10  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    11  	"k8s.io/apimachinery/pkg/runtime"
    12  
    13  	"github.com/tilt-dev/tilt/pkg/model"
    14  )
    15  
    16  func InjectLabels(entity K8sEntity, labels []model.LabelPair) (K8sEntity, error) {
    17  	return injectLabels(entity, labels, false)
    18  }
    19  
    20  func OverwriteLabels(entity K8sEntity, labels []model.LabelPair) (K8sEntity, error) {
    21  	return injectLabels(entity, labels, true)
    22  }
    23  
    24  // labels: labels to be added to `dest`
    25  // overwrite: if true, merge `labels` into `dest`. if false, replace `dest` with `labels`
    26  // addNew: if true, add all `labels` to `dest`. if false, only add `labels` whose keys are already in `dest`
    27  func applyLabelsToMap(dest *map[string]string, labels []model.LabelPair, overwrite bool, addNew bool) {
    28  	orig := *dest
    29  	if overwrite {
    30  		*dest = nil
    31  	}
    32  	for _, label := range labels {
    33  		if *dest == nil {
    34  			*dest = make(map[string]string, 1)
    35  		}
    36  		preexisting := false
    37  		if orig != nil {
    38  			_, preexisting = orig[label.Key]
    39  		}
    40  
    41  		if addNew || preexisting {
    42  			(*dest)[label.Key] = label.Value
    43  		}
    44  	}
    45  }
    46  
    47  // injectLabels injects the given labels into the given k8sEntity
    48  // (if `overwrite`, replacing existing labels)
    49  func injectLabels(entity K8sEntity, labels []model.LabelPair, overwrite bool) (K8sEntity, error) {
    50  	entity = entity.DeepCopy()
    51  
    52  	// Don't modify persistent volume claims
    53  	// because they're supposed to be immutable.
    54  	pvc := reflect.TypeOf(v1.PersistentVolumeClaim{})
    55  	metas, err := extractObjectMetas(&entity, func(v reflect.Value) bool {
    56  		return v.Type() != pvc
    57  	})
    58  	if err != nil {
    59  		return K8sEntity{}, err
    60  	}
    61  
    62  	for _, meta := range metas {
    63  		applyLabelsToMap(&meta.Labels, labels, overwrite, true)
    64  	}
    65  
    66  	switch obj := entity.Obj.(type) {
    67  	case *appsv1beta1.Deployment,
    68  		*appsv1beta2.Deployment,
    69  		*extv1beta1.Deployment,
    70  		*appsv1beta2.ReplicaSet,
    71  		*extv1beta1.ReplicaSet,
    72  		*appsv1beta2.DaemonSet,
    73  		*extv1beta1.DaemonSet,
    74  		*appsv1beta1.StatefulSet,
    75  		*appsv1beta2.StatefulSet:
    76  		allowLabelChangesInOptionalSelector(obj)
    77  	}
    78  
    79  	selectors, err := extractSelectors(&entity, func(v reflect.Value) bool {
    80  		return v.Type() != pvc
    81  	})
    82  	if err != nil {
    83  		return K8sEntity{}, err
    84  	}
    85  	for _, selector := range selectors {
    86  		applyLabelsToMap(&selector.MatchLabels, labels, overwrite, false)
    87  	}
    88  
    89  	// ServiceSpecs have a map[string]string instead of a LabelSelector, so handle them specially
    90  	serviceSpecs, err := extractServiceSpecs(&entity)
    91  	if err != nil {
    92  		return K8sEntity{}, err
    93  	}
    94  	for _, s := range serviceSpecs {
    95  		applyLabelsToMap(&s.Selector, labels, overwrite, false)
    96  	}
    97  	return entity, nil
    98  }
    99  
   100  // In the v1beta1 API, if a Deployment didn't have a selector,
   101  // Kubernetes would automatically infer a selector based on the labels
   102  // in the pod.
   103  //
   104  // The problem is that Selectors are immutable. So this had the unintended
   105  // side-effect of making pod labels immutable. Since many tools attach
   106  // arbitrary labels to pods, v1beta1 Deployments broke lots of tools.
   107  //
   108  // The v1 Deployment fixed this problem by making Selector mandatory.
   109  // But for old versions of Deployment, we need to auto-infer the selector
   110  // before we add labels to the pod.
   111  func allowLabelChangesInOptionalSelector(obj runtime.Object) {
   112  	// First, check to make sure this has a Spec object with:
   113  	// 1) A Selector field of type metav1.LabelSelector
   114  	// 2) A Template field of type v1.PodTemplateSpec
   115  	objPtrV := reflect.ValueOf(obj)
   116  	if objPtrV.Kind() != reflect.Ptr {
   117  		return
   118  	}
   119  
   120  	objV := objPtrV.Elem()
   121  	if objV.Kind() != reflect.Struct {
   122  		return
   123  	}
   124  
   125  	specField := objV.FieldByName("Spec")
   126  	if specField.Kind() != reflect.Struct {
   127  		return
   128  	}
   129  
   130  	specType := specField.Type()
   131  	selectorFieldType, ok := specType.FieldByName("Selector")
   132  	if !ok || selectorFieldType.Type != reflect.TypeOf(&metav1.LabelSelector{}) {
   133  		return
   134  	}
   135  
   136  	podTemplateFieldType, ok := specType.FieldByName("Template")
   137  	if !ok || podTemplateFieldType.Type != reflect.TypeOf(v1.PodTemplateSpec{}) {
   138  		return
   139  	}
   140  
   141  	selectorField := specField.FieldByName("Selector")
   142  	var selector *metav1.LabelSelector
   143  	if selectorField.IsValid() {
   144  		selector, ok = selectorField.Interface().(*metav1.LabelSelector)
   145  		if !ok {
   146  			return
   147  		}
   148  	}
   149  
   150  	// If the selector is already filled in, we don't need to do anything.
   151  	if selector != nil &&
   152  		(len(selector.MatchLabels) > 0 || len(selector.MatchExpressions) > 0) {
   153  		return
   154  	}
   155  
   156  	podTemplateField := specField.FieldByName("Template")
   157  	podTemplate, ok := podTemplateField.Interface().(v1.PodTemplateSpec)
   158  	if !ok {
   159  		return
   160  	}
   161  
   162  	podSpecLabels := podTemplate.Labels
   163  	matchLabels := make(map[string]string, len(podSpecLabels))
   164  	for k, v := range podSpecLabels {
   165  		matchLabels[k] = v
   166  	}
   167  
   168  	if selector == nil {
   169  		selector = &metav1.LabelSelector{}
   170  	}
   171  	selector.MatchLabels = matchLabels
   172  	selectorField.Set(reflect.ValueOf(selector))
   173  }
   174  
   175  // SelectorMatchesLabels indicates whether the pod selector of the given entity
   176  // matches the given label(s). For an empty selector, returns `false`.
   177  // Currently only supports Services, but may be expanded to support other types that
   178  // match pods via selectors.
   179  func (e K8sEntity) SelectorMatchesLabels(labels map[string]string) bool {
   180  	svc, ok := e.Obj.(*v1.Service)
   181  	if !ok {
   182  		return false
   183  	}
   184  	selector := svc.Spec.Selector
   185  
   186  	if len(selector) == 0 {
   187  		return false
   188  	}
   189  
   190  	for k, selVal := range selector {
   191  		realVal, ok := labels[k]
   192  		if !ok || realVal != selVal {
   193  			return false
   194  		}
   195  	}
   196  	return true
   197  
   198  }
   199  
   200  // MatchesMetadataLabels indicates whether the given label(s) are a subset
   201  // of metadata labels for the given entity.
   202  func (e K8sEntity) MatchesMetadataLabels(labels map[string]string) (bool, error) {
   203  	// TODO(nick): This doesn't seem right.
   204  	// This should only look at the top-level obj meta, not all of them.
   205  	metas, err := extractObjectMetas(&e, NoFilter)
   206  	if err != nil {
   207  		return false, err
   208  	}
   209  
   210  	for _, meta := range metas {
   211  		for k, v := range labels {
   212  			realVal, ok := meta.Labels[k]
   213  			if !ok || realVal != v {
   214  				return false, nil
   215  			}
   216  		}
   217  	}
   218  
   219  	return true, nil
   220  
   221  }