github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/entity.go (about) 1 package k8s 2 3 import ( 4 "fmt" 5 "net/url" 6 "sort" 7 "strings" 8 9 "k8s.io/apimachinery/pkg/api/meta" 10 "k8s.io/apimachinery/pkg/runtime/schema" 11 "k8s.io/apimachinery/pkg/types" 12 "k8s.io/client-go/kubernetes/scheme" 13 14 "github.com/tilt-dev/tilt/internal/kustomize" 15 16 "github.com/pkg/errors" 17 v1 "k8s.io/api/core/v1" 18 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 "k8s.io/apimachinery/pkg/runtime" 20 21 "github.com/tilt-dev/tilt/internal/container" 22 ) 23 24 type K8sEntity struct { 25 Obj runtime.Object 26 } 27 28 func NewK8sEntity(obj runtime.Object) K8sEntity { 29 return K8sEntity{Obj: obj} 30 } 31 32 type entityList []K8sEntity 33 34 func (l entityList) Len() int { return len(l) } 35 func (l entityList) Less(i, j int) bool { 36 // Sort entities by the priority of their Kind 37 indexI := kustomize.TypeOrders[l[i].GVK().Kind] 38 indexJ := kustomize.TypeOrders[l[j].GVK().Kind] 39 if indexI != indexJ { 40 return indexI < indexJ 41 } 42 return i < j 43 } 44 func (l entityList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 45 46 func SortedEntities(entities []K8sEntity) []K8sEntity { 47 entList := entityList(CopyEntities(entities)) 48 sort.Stable(entList) 49 return []K8sEntity(entList) 50 } 51 52 func ReverseSortedEntities(entities []K8sEntity) []K8sEntity { 53 entList := entityList(CopyEntities(entities)) 54 sort.Sort(sort.Reverse(entList)) 55 return entList 56 } 57 58 func (e K8sEntity) Meta() metav1.Object { 59 m, err := meta.Accessor(e.Obj) 60 if err != nil { 61 return &metav1.ObjectMeta{} 62 } 63 return m 64 } 65 66 func (e K8sEntity) ToObjectReference() v1.ObjectReference { 67 meta := e.Meta() 68 apiVersion, kind := e.GVK().ToAPIVersionAndKind() 69 return v1.ObjectReference{ 70 Kind: kind, 71 APIVersion: apiVersion, 72 Name: meta.GetName(), 73 Namespace: meta.GetNamespace(), 74 UID: meta.GetUID(), 75 } 76 } 77 78 func (e K8sEntity) WithNamespace(ns string) K8sEntity { 79 newE := e.DeepCopy() 80 newE.Meta().SetNamespace(ns) 81 return newE 82 } 83 84 func (e K8sEntity) GVK() schema.GroupVersionKind { 85 gvk := e.Obj.GetObjectKind().GroupVersionKind() 86 if gvk.Empty() { 87 // On typed go objects, the GVK is usually empty by convention, so we grab it from the Scheme 88 // See https://github.com/kubernetes/kubernetes/pull/59264#issuecomment-362575608 89 // for discussion on why the API behaves this way. 90 gvks, _, _ := scheme.Scheme.ObjectKinds(e.Obj) 91 if len(gvks) > 0 { 92 return gvks[0] 93 } 94 } 95 return gvk 96 } 97 98 // Clean up internal bookkeeping fields. See 99 // https://github.com/kubernetes/kubernetes/issues/90066 100 func (e K8sEntity) Clean() { 101 e.Meta().SetManagedFields(nil) 102 103 annotations := e.Meta().GetAnnotations() 104 if len(annotations) != 0 { 105 delete(annotations, "kubectl.kubernetes.io/last-applied-configuration") 106 } 107 } 108 109 func (e K8sEntity) SetUID(uid string) { 110 e.Meta().SetUID(types.UID(uid)) 111 } 112 113 func (e K8sEntity) Name() string { 114 return e.Meta().GetName() 115 } 116 117 func (e K8sEntity) Namespace() Namespace { 118 n := e.Meta().GetNamespace() 119 if n == "" { 120 return DefaultNamespace 121 } 122 return Namespace(n) 123 } 124 125 func (e K8sEntity) NamespaceOrDefault(defaultVal string) string { 126 n := e.Meta().GetNamespace() 127 if n == "" { 128 return defaultVal 129 } 130 return n 131 } 132 133 func (e K8sEntity) UID() types.UID { 134 return e.Meta().GetUID() 135 } 136 137 func (e K8sEntity) Annotations() map[string]string { 138 return e.Meta().GetAnnotations() 139 } 140 141 func (e K8sEntity) Labels() map[string]string { 142 return e.Meta().GetLabels() 143 } 144 145 // Most entities can be updated once running, but a few cannot. 146 func (e K8sEntity) ImmutableOnceCreated() bool { 147 return e.GVK().Kind == "Job" || e.GVK().Kind == "Pod" 148 } 149 150 func (e K8sEntity) DeepCopy() K8sEntity { 151 return NewK8sEntity(e.Obj.DeepCopyObject()) 152 } 153 154 func CopyEntities(entities []K8sEntity) []K8sEntity { 155 res := make([]K8sEntity, len(entities)) 156 for i, e := range entities { 157 res[i] = e.DeepCopy() 158 } 159 return res 160 } 161 162 type LoadBalancerSpec struct { 163 Name string 164 Namespace Namespace 165 Ports []int32 166 } 167 168 type LoadBalancer struct { 169 Spec LoadBalancerSpec 170 URL *url.URL 171 } 172 173 func ToLoadBalancerSpecs(entities []K8sEntity) []LoadBalancerSpec { 174 result := make([]LoadBalancerSpec, 0) 175 for _, e := range entities { 176 lb, ok := ToLoadBalancerSpec(e) 177 if ok { 178 result = append(result, lb) 179 } 180 } 181 return result 182 } 183 184 // Try to convert the current entity to a LoadBalancerSpec service 185 func ToLoadBalancerSpec(entity K8sEntity) (LoadBalancerSpec, bool) { 186 service, ok := entity.Obj.(*v1.Service) 187 if !ok { 188 return LoadBalancerSpec{}, false 189 } 190 191 meta := service.ObjectMeta 192 name := meta.Name 193 spec := service.Spec 194 if spec.Type != v1.ServiceTypeLoadBalancer { 195 return LoadBalancerSpec{}, false 196 } 197 198 result := LoadBalancerSpec{ 199 Name: name, 200 Namespace: Namespace(meta.Namespace), 201 } 202 for _, portSpec := range spec.Ports { 203 if portSpec.Port != 0 { 204 result.Ports = append(result.Ports, portSpec.Port) 205 } 206 } 207 208 if len(result.Ports) == 0 { 209 return LoadBalancerSpec{}, false 210 } 211 212 return result, true 213 } 214 215 // Filter returns two slices of entities: those passing the given test, and the remainder of the input. 216 func Filter(entities []K8sEntity, test func(e K8sEntity) (bool, error)) (passing, rest []K8sEntity, err error) { 217 for _, e := range entities { 218 pass, err := test(e) 219 if err != nil { 220 return nil, nil, err 221 } 222 if pass { 223 passing = append(passing, e) 224 } else { 225 rest = append(rest, e) 226 } 227 } 228 return passing, rest, nil 229 } 230 231 func FilterByImage(entities []K8sEntity, img container.RefSelector, locators []ImageLocator, inEnvVars bool) (passing, rest []K8sEntity, err error) { 232 return Filter(entities, func(e K8sEntity) (bool, error) { return e.HasImage(img, locators, inEnvVars) }) 233 } 234 235 func FilterBySelectorMatchesLabels(entities []K8sEntity, labels map[string]string) (passing, rest []K8sEntity, err error) { 236 return Filter(entities, func(e K8sEntity) (bool, error) { return e.SelectorMatchesLabels(labels), nil }) 237 } 238 239 func FilterByMetadataLabels(entities []K8sEntity, labels map[string]string) (passing, rest []K8sEntity, err error) { 240 return Filter(entities, func(e K8sEntity) (bool, error) { return e.MatchesMetadataLabels(labels) }) 241 } 242 243 func FilterByHasPodTemplateSpec(entities []K8sEntity) (passing, rest []K8sEntity, err error) { 244 return Filter(entities, func(e K8sEntity) (bool, error) { 245 templateSpecs, err := ExtractPodTemplateSpec(&e) 246 if err != nil { 247 return false, err 248 } 249 return len(templateSpecs) > 0, nil 250 }) 251 } 252 253 func FilterByMatchesPodTemplateSpec(withPodSpec K8sEntity, entities []K8sEntity) (passing, rest []K8sEntity, err error) { 254 podTemplates, err := ExtractPodTemplateSpec(withPodSpec) 255 if err != nil { 256 return nil, nil, errors.Wrap(err, "extracting pod template spec") 257 } 258 259 if len(podTemplates) == 0 { 260 return nil, entities, nil 261 } 262 263 var allMatches []K8sEntity 264 remaining := append([]K8sEntity{}, entities...) 265 for _, template := range podTemplates { 266 match, rest, err := FilterBySelectorMatchesLabels(remaining, template.Labels) 267 if err != nil { 268 return nil, nil, errors.Wrap(err, "filtering entities by label") 269 } 270 allMatches = append(allMatches, match...) 271 remaining = rest 272 } 273 return allMatches, remaining, nil 274 } 275 276 func (e K8sEntity) HasName(name string) bool { 277 return e.Name() == name 278 } 279 280 func (e K8sEntity) HasNamespace(ns string) bool { 281 realNs := e.Namespace() 282 if ns == "" { 283 return realNs == DefaultNamespace 284 } 285 return realNs.String() == ns 286 } 287 288 func (e K8sEntity) HasKind(kind string) bool { 289 // TODO(maia): support kind aliases (e.g. "po" for "pod") 290 return strings.EqualFold(e.GVK().Kind, kind) 291 } 292 293 func NewNamespaceEntity(name string) K8sEntity { 294 yaml := fmt.Sprintf(`apiVersion: v1 295 kind: Namespace 296 metadata: 297 name: %s 298 `, name) 299 entities, err := ParseYAMLFromString(yaml) 300 301 // Something is wrong with our format string; this is definitely on us 302 if err != nil { 303 panic(fmt.Sprintf("unexpected error making new namespace: %v", err)) 304 } else if len(entities) != 1 { 305 // Something is wrong with our format string; this is definitely on us 306 panic(fmt.Sprintf( 307 "unexpected error making new namespace: got %d entities, expected exactly one", len(entities))) 308 } 309 return entities[0] 310 }