github.com/grahambrereton-form3/tilt@v0.10.18/internal/k8s/entity.go (about) 1 package k8s 2 3 import ( 4 "fmt" 5 "net/url" 6 "reflect" 7 "sort" 8 "strings" 9 "testing" 10 11 "k8s.io/apimachinery/pkg/runtime/schema" 12 "k8s.io/apimachinery/pkg/types" 13 "k8s.io/client-go/kubernetes/scheme" 14 15 "github.com/windmilleng/tilt/internal/kustomize" 16 17 "github.com/pkg/errors" 18 v1 "k8s.io/api/core/v1" 19 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 20 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 21 "k8s.io/apimachinery/pkg/runtime" 22 23 "github.com/windmilleng/tilt/internal/container" 24 ) 25 26 type K8sEntity struct { 27 Obj runtime.Object 28 } 29 30 func NewK8sEntity(obj runtime.Object) K8sEntity { 31 return K8sEntity{Obj: obj} 32 } 33 34 type k8sMeta interface { 35 GetName() string 36 GetNamespace() string 37 GetUID() types.UID 38 GetLabels() map[string]string 39 GetOwnerReferences() []metav1.OwnerReference 40 SetNamespace(ns string) 41 } 42 43 type emptyMeta struct{} 44 45 func (emptyMeta) GetName() string { return "" } 46 func (emptyMeta) GetNamespace() string { return "" } 47 func (emptyMeta) GetUID() types.UID { return "" } 48 func (emptyMeta) GetLabels() map[string]string { return make(map[string]string) } 49 func (emptyMeta) GetOwnerReferences() []metav1.OwnerReference { return nil } 50 func (emptyMeta) SetNamespace(ns string) {} 51 52 var _ k8sMeta = emptyMeta{} 53 var _ k8sMeta = &metav1.ObjectMeta{} 54 55 type entityList []K8sEntity 56 57 func (l entityList) Len() int { return len(l) } 58 func (l entityList) Less(i, j int) bool { 59 // Sort entities by the priority of their Kind 60 indexI := kustomize.TypeOrders[l[i].GVK().Kind] 61 indexJ := kustomize.TypeOrders[l[j].GVK().Kind] 62 if indexI != indexJ { 63 return indexI < indexJ 64 } 65 return i < j 66 } 67 func (l entityList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 68 69 func SortedEntities(entities []K8sEntity) []K8sEntity { 70 entList := entityList(CopyEntities(entities)) 71 sort.Stable(entList) 72 return []K8sEntity(entList) 73 } 74 75 func (e K8sEntity) ToObjectReference() v1.ObjectReference { 76 meta := e.meta() 77 apiVersion, kind := e.GVK().ToAPIVersionAndKind() 78 return v1.ObjectReference{ 79 Kind: kind, 80 APIVersion: apiVersion, 81 Name: meta.GetName(), 82 Namespace: meta.GetNamespace(), 83 UID: meta.GetUID(), 84 } 85 } 86 87 func (e K8sEntity) WithNamespace(ns string) K8sEntity { 88 newE := e.DeepCopy() 89 meta := newE.meta() 90 meta.SetNamespace(ns) 91 return newE 92 } 93 94 func (e K8sEntity) GVK() schema.GroupVersionKind { 95 gvk := e.Obj.GetObjectKind().GroupVersionKind() 96 if gvk.Empty() { 97 // On typed go objects, the GVK is usually empty by convention, so we grab it from the Scheme 98 // See https://github.com/kubernetes/kubernetes/pull/59264#issuecomment-362575608 99 // for discussion on why the API behaves this way. 100 gvks, _, _ := scheme.Scheme.ObjectKinds(e.Obj) 101 if len(gvks) > 0 { 102 return gvks[0] 103 } 104 } 105 return gvk 106 } 107 108 func (e K8sEntity) meta() k8sMeta { 109 if unstruct := e.maybeUnstructuredMeta(); unstruct != nil { 110 return unstruct 111 } 112 113 if structured, _ := e.maybeStructuredMeta(); structured != nil { 114 return structured 115 } 116 117 return emptyMeta{} 118 } 119 120 func (e K8sEntity) maybeUnstructuredMeta() *unstructured.Unstructured { 121 unstruct, isUnstructured := e.Obj.(*unstructured.Unstructured) 122 if isUnstructured { 123 return unstruct 124 } 125 return nil 126 } 127 128 func (e K8sEntity) maybeStructuredMeta() (meta *metav1.ObjectMeta, fieldIndex int) { 129 objVal := reflect.ValueOf(e.Obj) 130 if objVal.Kind() == reflect.Ptr { 131 if objVal.IsNil() { 132 return nil, -1 133 } 134 objVal = objVal.Elem() 135 } 136 137 if objVal.Kind() != reflect.Struct { 138 return nil, -1 139 } 140 141 // Find a field with type ObjectMeta 142 omType := reflect.TypeOf(metav1.ObjectMeta{}) 143 for i := 0; i < objVal.NumField(); i++ { 144 fieldVal := objVal.Field(i) 145 if omType != fieldVal.Type() { 146 continue 147 } 148 149 if !fieldVal.CanAddr() { 150 continue 151 } 152 153 metadata, ok := fieldVal.Addr().Interface().(*metav1.ObjectMeta) 154 if !ok { 155 continue 156 } 157 158 return metadata, i 159 } 160 return nil, -1 161 } 162 163 func SetUID(e *K8sEntity, UID string) error { 164 unstruct := e.maybeUnstructuredMeta() 165 if unstruct != nil { 166 return fmt.Errorf("SetUIDForTesting not yet implemented for unstructured metadata") 167 } 168 169 structured, i := e.maybeStructuredMeta() 170 if structured == nil { 171 return fmt.Errorf("Cannot set UID -- entity has neither unstructured nor structured metadata. k8s entity: %+v", e) 172 } 173 174 structured.SetUID(types.UID(UID)) 175 objVal := reflect.ValueOf(e.Obj) 176 if objVal.Kind() == reflect.Ptr { 177 if objVal.IsNil() { 178 return fmt.Errorf("Cannot set UID -- e.Obj is a pointer. k8s entity: %+v", e) 179 } 180 objVal = objVal.Elem() 181 } 182 183 fieldVal := objVal.Field(i) 184 metaVal := reflect.ValueOf(*structured) 185 fieldVal.Set(metaVal) 186 return nil 187 } 188 189 func SetUIDForTest(t *testing.T, e *K8sEntity, UID string) { 190 err := SetUID(e, UID) 191 if err != nil { 192 t.Fatal(err) 193 } 194 } 195 196 func (e K8sEntity) Name() string { 197 return e.meta().GetName() 198 } 199 200 func (e K8sEntity) Namespace() Namespace { 201 n := e.meta().GetNamespace() 202 if n == "" { 203 return DefaultNamespace 204 } 205 return Namespace(n) 206 } 207 208 func (e K8sEntity) UID() types.UID { 209 return e.meta().GetUID() 210 } 211 212 func (e K8sEntity) Labels() map[string]string { 213 return e.meta().GetLabels() 214 } 215 216 // Most entities can be updated once running, but a few cannot. 217 func (e K8sEntity) ImmutableOnceCreated() bool { 218 return e.GVK().Kind == "Job" || e.GVK().Kind == "Pod" 219 } 220 221 func (e K8sEntity) DeepCopy() K8sEntity { 222 return NewK8sEntity(e.Obj.DeepCopyObject()) 223 } 224 225 func CopyEntities(entities []K8sEntity) []K8sEntity { 226 res := make([]K8sEntity, len(entities)) 227 for i, e := range entities { 228 res[i] = e.DeepCopy() 229 } 230 return res 231 } 232 233 // MutableAndImmutableEntities returns two lists of k8s entities: mutable ones (that can simply be 234 // `kubectl apply`'d), and immutable ones (such as jobs and pods, which will need to be `--force`'d). 235 // (We assume input entities are already sorted in a safe order to apply -- see kustomize/ordering.go.) 236 func MutableAndImmutableEntities(entities entityList) (mutable, immutable []K8sEntity) { 237 for _, e := range entities { 238 if e.ImmutableOnceCreated() { 239 immutable = append(immutable, e) 240 continue 241 } 242 mutable = append(mutable, e) 243 } 244 245 return mutable, immutable 246 } 247 248 func ImmutableEntities(entities []K8sEntity) []K8sEntity { 249 result := make([]K8sEntity, 0) 250 for _, e := range entities { 251 if e.ImmutableOnceCreated() { 252 result = append(result, e) 253 } 254 } 255 return result 256 } 257 258 func MutableEntities(entities []K8sEntity) []K8sEntity { 259 result := make([]K8sEntity, 0) 260 for _, e := range entities { 261 if !e.ImmutableOnceCreated() { 262 result = append(result, e) 263 } 264 } 265 return result 266 } 267 268 type LoadBalancerSpec struct { 269 Name string 270 Namespace Namespace 271 Ports []int32 272 } 273 274 type LoadBalancer struct { 275 Spec LoadBalancerSpec 276 URL *url.URL 277 } 278 279 func ToLoadBalancerSpecs(entities []K8sEntity) []LoadBalancerSpec { 280 result := make([]LoadBalancerSpec, 0) 281 for _, e := range entities { 282 lb, ok := ToLoadBalancerSpec(e) 283 if ok { 284 result = append(result, lb) 285 } 286 } 287 return result 288 } 289 290 // Try to convert the current entity to a LoadBalancerSpec service 291 func ToLoadBalancerSpec(entity K8sEntity) (LoadBalancerSpec, bool) { 292 service, ok := entity.Obj.(*v1.Service) 293 if !ok { 294 return LoadBalancerSpec{}, false 295 } 296 297 meta := service.ObjectMeta 298 name := meta.Name 299 spec := service.Spec 300 if spec.Type != v1.ServiceTypeLoadBalancer { 301 return LoadBalancerSpec{}, false 302 } 303 304 result := LoadBalancerSpec{ 305 Name: name, 306 Namespace: Namespace(meta.Namespace), 307 } 308 for _, portSpec := range spec.Ports { 309 if portSpec.Port != 0 { 310 result.Ports = append(result.Ports, portSpec.Port) 311 } 312 } 313 314 if len(result.Ports) == 0 { 315 return LoadBalancerSpec{}, false 316 } 317 318 return result, true 319 } 320 321 // Filter returns two slices of entities: those passing the given test, and the remainder of the input. 322 func Filter(entities []K8sEntity, test func(e K8sEntity) (bool, error)) (passing, rest []K8sEntity, err error) { 323 for _, e := range entities { 324 pass, err := test(e) 325 if err != nil { 326 return nil, nil, err 327 } 328 if pass { 329 passing = append(passing, e) 330 } else { 331 rest = append(rest, e) 332 } 333 } 334 return passing, rest, nil 335 } 336 337 func FilterByImage(entities []K8sEntity, img container.RefSelector, imageJSONPaths func(K8sEntity) []JSONPath, inEnvVars bool) (passing, rest []K8sEntity, err error) { 338 return Filter(entities, func(e K8sEntity) (bool, error) { return e.HasImage(img, imageJSONPaths(e), inEnvVars) }) 339 } 340 341 func FilterBySelectorMatchesLabels(entities []K8sEntity, labels map[string]string) (passing, rest []K8sEntity, err error) { 342 return Filter(entities, func(e K8sEntity) (bool, error) { return e.SelectorMatchesLabels(labels), nil }) 343 } 344 345 func FilterByMetadataLabels(entities []K8sEntity, labels map[string]string) (passing, rest []K8sEntity, err error) { 346 return Filter(entities, func(e K8sEntity) (bool, error) { return e.MatchesMetadataLabels(labels) }) 347 } 348 349 func FilterByHasPodTemplateSpec(entities []K8sEntity) (passing, rest []K8sEntity, err error) { 350 return Filter(entities, func(e K8sEntity) (bool, error) { 351 templateSpecs, err := ExtractPodTemplateSpec(&e) 352 if err != nil { 353 return false, err 354 } 355 return len(templateSpecs) > 0, nil 356 }) 357 } 358 359 func FilterByMatchesPodTemplateSpec(withPodSpec K8sEntity, entities []K8sEntity) (passing, rest []K8sEntity, err error) { 360 podTemplates, err := ExtractPodTemplateSpec(withPodSpec) 361 if err != nil { 362 return nil, nil, errors.Wrap(err, "extracting pod template spec") 363 } 364 365 if len(podTemplates) == 0 { 366 return nil, entities, nil 367 } 368 369 var allMatches []K8sEntity 370 remaining := append([]K8sEntity{}, entities...) 371 for _, template := range podTemplates { 372 match, rest, err := FilterBySelectorMatchesLabels(remaining, template.Labels) 373 if err != nil { 374 return nil, nil, errors.Wrap(err, "filtering entities by label") 375 } 376 allMatches = append(allMatches, match...) 377 remaining = rest 378 } 379 return allMatches, remaining, nil 380 } 381 382 func (e K8sEntity) HasName(name string) bool { 383 return e.Name() == name 384 } 385 386 func (e K8sEntity) HasNamespace(ns string) bool { 387 realNs := e.Namespace() 388 if ns == "" { 389 return realNs == DefaultNamespace 390 } 391 return realNs.String() == ns 392 } 393 394 func (e K8sEntity) HasKind(kind string) bool { 395 // TODO(maia): support kind aliases (e.g. "po" for "pod") 396 return strings.ToLower(e.GVK().Kind) == strings.ToLower(kind) 397 } 398 399 func NewNamespaceEntity(name string) K8sEntity { 400 yaml := fmt.Sprintf(`apiVersion: v1 401 kind: Namespace 402 metadata: 403 name: %s 404 `, name) 405 entities, err := ParseYAMLFromString(yaml) 406 407 // Something is wrong with our format string; this is definitely on us 408 if err != nil { 409 panic(fmt.Sprintf("unexpected error making new namespace: %v", err)) 410 } else if len(entities) != 1 { 411 // Something is wrong with our format string; this is definitely on us 412 panic(fmt.Sprintf( 413 "unexpected error making new namespace: got %d entities, expected exactly one", len(entities))) 414 } 415 return entities[0] 416 }