github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/store/k8sconv/resource.go (about)

     1  package k8sconv
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/pkg/errors"
     7  	v1 "k8s.io/api/core/v1"
     8  	"k8s.io/apimachinery/pkg/types"
     9  
    10  	"github.com/tilt-dev/tilt/internal/k8s"
    11  	"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
    12  )
    13  
    14  // A KubernetesResource exposes a high-level status that summarizes
    15  // the Pods we care about in a KubernetesDiscovery.
    16  //
    17  // If we have a KubernetesApply, KubernetesResource will use that
    18  // to narrow down the list of pods to only the pods we care about
    19  // for the current Apply.
    20  //
    21  // KubernetesResource is intended to be a non-stateful object (i.e., it is
    22  // immutable and its status can be inferred from the state of child
    23  // objects.)
    24  //
    25  // Long-term, this may become an explicit API server object, but
    26  // for now it's intended to provide an API-server compatible
    27  // layer around KubernetesDiscovery + KubernetesApply.
    28  type KubernetesResource struct {
    29  	Discovery   *v1alpha1.KubernetesDiscovery
    30  	ApplyStatus *v1alpha1.KubernetesApplyStatus
    31  
    32  	// A set of properties we use to determine which pods in Discovery
    33  	// belong to the current Apply.
    34  	ApplyFilter *KubernetesApplyFilter
    35  
    36  	// A set of pods that belong to the current Discovery
    37  	// and the current ApplyStatus (if available).
    38  	//
    39  	// Excludes pods that are being deleted
    40  	// or which belong to a previous apply.
    41  	FilteredPods []v1alpha1.Pod
    42  }
    43  
    44  func NewKubernetesResource(discovery *v1alpha1.KubernetesDiscovery, status *v1alpha1.KubernetesApplyStatus) (*KubernetesResource, error) {
    45  	var filter *KubernetesApplyFilter
    46  	if status != nil {
    47  		var err error
    48  		filter, err = NewKubernetesApplyFilter(status.ResultYAML)
    49  		if err != nil {
    50  			return nil, err
    51  		}
    52  	}
    53  
    54  	return NewKubernetesResourceWithFilter(discovery, status, filter), nil
    55  }
    56  
    57  // NewKubernetesResourceWithFilter is just NewKubernetesResource but with a specified KubernetesApplyFilter
    58  func NewKubernetesResourceWithFilter(
    59  	discovery *v1alpha1.KubernetesDiscovery,
    60  	status *v1alpha1.KubernetesApplyStatus,
    61  	filter *KubernetesApplyFilter) *KubernetesResource {
    62  
    63  	var filteredPods []v1alpha1.Pod
    64  	if discovery != nil {
    65  		filteredPods = FilterPods(filter, discovery.Status.Pods)
    66  	}
    67  
    68  	return &KubernetesResource{
    69  		Discovery:    discovery,
    70  		ApplyStatus:  status,
    71  		ApplyFilter:  filter,
    72  		FilteredPods: filteredPods,
    73  	}
    74  }
    75  
    76  // Filter to determine whether a pod or resource belongs to the current
    77  // KubernetesApply. Used to filter out pods from previous applies
    78  // when looking at a KubernetesDiscovery object.
    79  //
    80  // Considered immutable once created.
    81  type KubernetesApplyFilter struct {
    82  	// DeployedRefs are references to the objects that we deployed to a Kubernetes cluster.
    83  	DeployedRefs []v1.ObjectReference
    84  
    85  	// Hashes of the pod template specs that we deployed to a Kubernetes cluster.
    86  	PodTemplateSpecHashes []k8s.PodTemplateSpecHash
    87  }
    88  
    89  func NewKubernetesApplyFilter(yaml string) (*KubernetesApplyFilter, error) {
    90  	deployed, err := k8s.ParseYAMLFromString(yaml)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	deployed = k8s.SortedEntities(deployed)
    95  
    96  	podTemplateSpecHashes := []k8s.PodTemplateSpecHash{}
    97  	for _, entity := range deployed {
    98  		if entity.UID() == "" {
    99  			return nil, fmt.Errorf("Resource missing uid: %s", entity.Name())
   100  		}
   101  		hs, err := k8s.ReadPodTemplateSpecHashes(entity)
   102  		if err != nil {
   103  			return nil, errors.Wrap(err, "reading pod template spec hashes")
   104  		}
   105  		podTemplateSpecHashes = append(podTemplateSpecHashes, hs...)
   106  	}
   107  	return &KubernetesApplyFilter{
   108  		DeployedRefs:          k8s.ToRefList(deployed),
   109  		PodTemplateSpecHashes: podTemplateSpecHashes,
   110  	}, nil
   111  }
   112  
   113  func ContainsHash(filter *KubernetesApplyFilter, hash k8s.PodTemplateSpecHash) bool {
   114  	if filter == nil {
   115  		return false
   116  	}
   117  
   118  	for _, h := range filter.PodTemplateSpecHashes {
   119  		if h == hash {
   120  			return true
   121  		}
   122  	}
   123  	return false
   124  }
   125  
   126  func ContainsUID(filter *KubernetesApplyFilter, uid types.UID) bool {
   127  	if filter == nil {
   128  		return false
   129  	}
   130  
   131  	for _, ref := range filter.DeployedRefs {
   132  		if ref.UID == uid {
   133  			return true
   134  		}
   135  	}
   136  	return false
   137  }
   138  
   139  // Checks to see if the given pod is allowed by the current filter.
   140  func HasOKPodTemplateSpecHash(pod *v1alpha1.Pod, filter *KubernetesApplyFilter) bool {
   141  	// if it doesn't have a label, just let it through - maybe it's from a CRD w/ no pod template spec
   142  	hash := k8s.PodTemplateSpecHash(pod.PodTemplateSpecHash)
   143  	if hash == "" {
   144  		return true
   145  	}
   146  
   147  	return ContainsHash(filter, hash)
   148  }
   149  
   150  // Filter out any pods that are being deleted.
   151  // Filter pods from old replica sets.
   152  // Only keep pods that belong in the current filter.
   153  func FilterPods(filter *KubernetesApplyFilter, pods []v1alpha1.Pod) []v1alpha1.Pod {
   154  	result := []v1alpha1.Pod{}
   155  
   156  	// We want to make sure that if one Deployment
   157  	// creates 2 ReplicaSets, we prune pods from the older ReplicaSet.
   158  	newestOwnerByAncestorUID := make(map[string]*v1alpha1.PodOwner)
   159  	for _, pod := range pods {
   160  		if pod.AncestorUID == "" || !hasValidOwner(pod) {
   161  			continue
   162  		}
   163  
   164  		owner := pod.Owner
   165  		existing := newestOwnerByAncestorUID[pod.AncestorUID]
   166  		if existing == nil || owner.CreationTimestamp.After(existing.CreationTimestamp.Time) {
   167  			newestOwnerByAncestorUID[pod.AncestorUID] = owner
   168  		}
   169  	}
   170  
   171  	for _, pod := range pods {
   172  		// Ignore pods from an old replicaset.
   173  		newestOwner := newestOwnerByAncestorUID[pod.AncestorUID]
   174  		if hasValidOwner(pod) && newestOwner != nil && pod.Owner.Name != newestOwner.Name {
   175  			continue
   176  		}
   177  
   178  		// Ignore pods that have a stale pod template hash
   179  		if filter != nil && !HasOKPodTemplateSpecHash(&pod, filter) {
   180  			continue
   181  		}
   182  
   183  		// Ignore pods that were tracked by UID but
   184  		// aren't owned by a current Apply.
   185  		if filter != nil && pod.AncestorUID != "" && !ContainsUID(filter, types.UID(pod.AncestorUID)) {
   186  			continue
   187  		}
   188  
   189  		result = append(result, pod)
   190  	}
   191  
   192  	return result
   193  }
   194  
   195  func hasValidOwner(pod v1alpha1.Pod) bool {
   196  	return pod.Owner != nil && pod.Owner.Name != "" && !pod.Owner.CreationTimestamp.IsZero()
   197  }
   198  
   199  func MostRecentPod(pod []v1alpha1.Pod) v1alpha1.Pod {
   200  	bestPod := v1alpha1.Pod{}
   201  	found := false
   202  
   203  	for _, v := range pod {
   204  		if !found || PodCompare(v, bestPod) {
   205  			bestPod = v
   206  			found = true
   207  		}
   208  	}
   209  
   210  	return bestPod
   211  }
   212  
   213  // PodCompare is a stable sort order for pods.
   214  func PodCompare(p1 v1alpha1.Pod, p2 v1alpha1.Pod) bool {
   215  	if p1.CreatedAt.After(p2.CreatedAt.Time) {
   216  		return true
   217  	} else if p2.CreatedAt.After(p1.CreatedAt.Time) {
   218  		return false
   219  	}
   220  	return p1.Name > p2.Name
   221  }