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  }