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  }