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 }