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 }