sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/tree/tree.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package tree 18 19 import ( 20 "fmt" 21 "sort" 22 "strconv" 23 "strings" 24 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 "k8s.io/apimachinery/pkg/types" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 32 "sigs.k8s.io/cluster-api/util" 33 ) 34 35 // ObjectTreeOptions defines the options for an ObjectTree. 36 type ObjectTreeOptions struct { 37 // ShowOtherConditions is a list of comma separated kind or kind/name for which we should add the ShowObjectConditionsAnnotation 38 // to signal to the presentation layer to show all the conditions for the objects. 39 ShowOtherConditions string 40 41 // ShowMachineSets instructs the discovery process to include machine sets in the ObjectTree. 42 ShowMachineSets bool 43 44 // ShowClusterResourceSets instructs the discovery process to include cluster resource sets in the ObjectTree. 45 ShowClusterResourceSets bool 46 47 // ShowTemplates instructs the discovery process to include infrastructure and bootstrap config templates in the ObjectTree. 48 ShowTemplates bool 49 50 // AddTemplateVirtualNode instructs the discovery process to group template under a virtual node. 51 AddTemplateVirtualNode bool 52 53 // Echo displays objects if the object's ready condition has the 54 // same Status, Severity and Reason of the parent's object ready condition (it is an echo) 55 Echo bool 56 57 // Grouping groups sibling object in case the ready conditions 58 // have the same Status, Severity and Reason 59 Grouping bool 60 } 61 62 // ObjectTree defines an object tree representing the status of a Cluster API cluster. 63 type ObjectTree struct { 64 root client.Object 65 options ObjectTreeOptions 66 items map[types.UID]client.Object 67 ownership map[types.UID]map[types.UID]bool 68 } 69 70 // NewObjectTree creates a new object tree with the given root and options. 71 func NewObjectTree(root client.Object, options ObjectTreeOptions) *ObjectTree { 72 // If it is requested to show all the conditions for the root, add 73 // the ShowObjectConditionsAnnotation to signal this to the presentation layer. 74 if isObjDebug(root, options.ShowOtherConditions) { 75 addAnnotation(root, ShowObjectConditionsAnnotation, "True") 76 } 77 78 return &ObjectTree{ 79 root: root, 80 options: options, 81 items: make(map[types.UID]client.Object), 82 ownership: make(map[types.UID]map[types.UID]bool), 83 } 84 } 85 86 // Add a object to the object tree. 87 func (od ObjectTree) Add(parent, obj client.Object, opts ...AddObjectOption) (added bool, visible bool) { 88 if parent == nil || obj == nil { 89 return false, false 90 } 91 addOpts := &addObjectOptions{} 92 addOpts.ApplyOptions(opts) 93 94 objReady := GetReadyCondition(obj) 95 parentReady := GetReadyCondition(parent) 96 97 // If it is requested to show all the conditions for the object, add 98 // the ShowObjectConditionsAnnotation to signal this to the presentation layer. 99 if isObjDebug(obj, od.options.ShowOtherConditions) { 100 addAnnotation(obj, ShowObjectConditionsAnnotation, "True") 101 } 102 103 // If the object should be hidden if the object's ready condition is true ot it has the 104 // same Status, Severity and Reason of the parent's object ready condition (it is an echo), 105 // return early. 106 if addOpts.NoEcho && !od.options.Echo { 107 if (objReady != nil && objReady.Status == corev1.ConditionTrue) || hasSameReadyStatusSeverityAndReason(parentReady, objReady) { 108 return false, false 109 } 110 } 111 112 // If it is requested to use a meta name for the object in the presentation layer, add 113 // the ObjectMetaNameAnnotation to signal this to the presentation layer. 114 if addOpts.MetaName != "" { 115 addAnnotation(obj, ObjectMetaNameAnnotation, addOpts.MetaName) 116 } 117 118 // Add the ObjectZOrderAnnotation to signal this to the presentation layer. 119 addAnnotation(obj, ObjectZOrderAnnotation, strconv.Itoa(addOpts.ZOrder)) 120 121 // If it is requested that this object and its sibling should be grouped in case the ready condition 122 // has the same Status, Severity and Reason, process all the sibling nodes. 123 if IsGroupingObject(parent) { 124 siblings := od.GetObjectsByParent(parent.GetUID()) 125 126 // The loop below will process the next node and decide if it belongs in a group. Since objects in the same group 127 // must have the same Kind, we sort by Kind so objects of the same Kind will be together in the list. 128 sort.Slice(siblings, func(i, j int) bool { 129 return siblings[i].GetObjectKind().GroupVersionKind().Kind < siblings[j].GetObjectKind().GroupVersionKind().Kind 130 }) 131 132 for i := range siblings { 133 s := siblings[i] 134 sReady := GetReadyCondition(s) 135 136 // If the object's ready condition has a different Status, Severity and Reason than the sibling object, 137 // move on (they should not be grouped). 138 if !hasSameReadyStatusSeverityAndReason(objReady, sReady) { 139 continue 140 } 141 142 // If the sibling node is already a group object 143 if IsGroupObject(s) { 144 // Check to see if the group object kind matches the object, i.e. group is MachineGroup and object is Machine. 145 // If so, upgrade it with the current object. 146 if s.GetObjectKind().GroupVersionKind().Kind == obj.GetObjectKind().GroupVersionKind().Kind+"Group" { 147 updateGroupNode(s, sReady, obj, objReady) 148 return true, false 149 } 150 } else if s.GetObjectKind().GroupVersionKind().Kind != obj.GetObjectKind().GroupVersionKind().Kind { 151 // If the sibling is not a group object, check if the sibling and the object are of the same kind. If not, move on. 152 continue 153 } 154 155 // Otherwise the object and the current sibling should be merged in a group. 156 157 // Create virtual object for the group and add it to the object tree. 158 groupNode := createGroupNode(s, sReady, obj, objReady) 159 // By default, grouping objects should be sorted last. 160 addAnnotation(groupNode, ObjectZOrderAnnotation, strconv.Itoa(GetZOrder(obj))) 161 162 od.addInner(parent, groupNode) 163 164 // Remove the current sibling (now merged in the group). 165 od.remove(parent, s) 166 return true, false 167 } 168 } 169 170 // If it is requested that the child of this node should be grouped in case the ready condition 171 // has the same Status, Severity and Reason, add the GroupingObjectAnnotation to signal 172 // this to the presentation layer. 173 if addOpts.GroupingObject && od.options.Grouping { 174 addAnnotation(obj, GroupingObjectAnnotation, "True") 175 } 176 177 // Add the object to the object tree. 178 od.addInner(parent, obj) 179 180 return true, true 181 } 182 183 func (od ObjectTree) remove(parent client.Object, s client.Object) { 184 for _, child := range od.GetObjectsByParent(s.GetUID()) { 185 od.remove(s, child) 186 } 187 delete(od.items, s.GetUID()) 188 delete(od.ownership[parent.GetUID()], s.GetUID()) 189 } 190 191 func (od ObjectTree) addInner(parent client.Object, obj client.Object) { 192 od.items[obj.GetUID()] = obj 193 if od.ownership[parent.GetUID()] == nil { 194 od.ownership[parent.GetUID()] = make(map[types.UID]bool) 195 } 196 od.ownership[parent.GetUID()][obj.GetUID()] = true 197 } 198 199 // GetRoot returns the root of the tree. 200 func (od ObjectTree) GetRoot() client.Object { return od.root } 201 202 // GetObject returns the object with the given uid. 203 func (od ObjectTree) GetObject(id types.UID) client.Object { return od.items[id] } 204 205 // IsObjectWithChild determines if an object has dependants. 206 func (od ObjectTree) IsObjectWithChild(id types.UID) bool { 207 return len(od.ownership[id]) > 0 208 } 209 210 // GetObjectsByParent returns all the dependant objects for the given uid. 211 func (od ObjectTree) GetObjectsByParent(id types.UID) []client.Object { 212 out := make([]client.Object, 0, len(od.ownership[id])) 213 for k := range od.ownership[id] { 214 out = append(out, od.GetObject(k)) 215 } 216 return out 217 } 218 219 func hasSameReadyStatusSeverityAndReason(a, b *clusterv1.Condition) bool { 220 if a == nil && b == nil { 221 return true 222 } 223 if (a == nil) != (b == nil) { 224 return false 225 } 226 227 return a.Status == b.Status && 228 a.Severity == b.Severity && 229 a.Reason == b.Reason 230 } 231 232 func createGroupNode(sibling client.Object, siblingReady *clusterv1.Condition, obj client.Object, objReady *clusterv1.Condition) *unstructured.Unstructured { 233 kind := fmt.Sprintf("%sGroup", obj.GetObjectKind().GroupVersionKind().Kind) 234 235 // Create a new group node and add the GroupObjectAnnotation to signal 236 // this to the presentation layer. 237 // NB. The group nodes gets a unique ID to avoid conflicts. 238 groupNode := VirtualObject(obj.GetNamespace(), kind, readyStatusSeverityAndReasonUID(obj)) 239 addAnnotation(groupNode, GroupObjectAnnotation, "True") 240 241 // Update the list of items included in the group and store it in the GroupItemsAnnotation. 242 items := []string{obj.GetName(), sibling.GetName()} 243 sort.Strings(items) 244 addAnnotation(groupNode, GroupItemsAnnotation, strings.Join(items, GroupItemsSeparator)) 245 246 // Update the group's ready condition. 247 if objReady != nil { 248 objReady.LastTransitionTime = minLastTransitionTime(objReady, siblingReady) 249 objReady.Message = "" 250 setReadyCondition(groupNode, objReady) 251 } 252 return groupNode 253 } 254 255 func readyStatusSeverityAndReasonUID(obj client.Object) string { 256 ready := GetReadyCondition(obj) 257 if ready == nil { 258 return fmt.Sprintf("zzz_%s", util.RandomString(6)) 259 } 260 return fmt.Sprintf("zz_%s_%s_%s_%s", ready.Status, ready.Severity, ready.Reason, util.RandomString(6)) 261 } 262 263 func minLastTransitionTime(a, b *clusterv1.Condition) metav1.Time { 264 if a == nil && b == nil { 265 return metav1.Time{} 266 } 267 if (a != nil) && (b == nil) { 268 return a.LastTransitionTime 269 } 270 if (a == nil) && (b != nil) { 271 return b.LastTransitionTime 272 } 273 if a.LastTransitionTime.Time.After(b.LastTransitionTime.Time) { 274 return b.LastTransitionTime 275 } 276 return a.LastTransitionTime 277 } 278 279 func updateGroupNode(groupObj client.Object, groupReady *clusterv1.Condition, obj client.Object, objReady *clusterv1.Condition) { 280 // Update the list of items included in the group and store it in the GroupItemsAnnotation. 281 items := strings.Split(GetGroupItems(groupObj), GroupItemsSeparator) 282 items = append(items, obj.GetName()) 283 sort.Strings(items) 284 addAnnotation(groupObj, GroupItemsAnnotation, strings.Join(items, GroupItemsSeparator)) 285 286 // Update the group's ready condition. 287 if groupReady != nil { 288 groupReady.LastTransitionTime = minLastTransitionTime(objReady, groupReady) 289 groupReady.Message = "" 290 setReadyCondition(groupObj, groupReady) 291 } 292 } 293 294 func isObjDebug(obj client.Object, debugFilter string) bool { 295 if debugFilter == "" { 296 return false 297 } 298 for _, filter := range strings.Split(debugFilter, ",") { 299 filter = strings.TrimSpace(filter) 300 if filter == "" { 301 continue 302 } 303 if strings.EqualFold(filter, "all") { 304 return true 305 } 306 kn := strings.Split(filter, "/") 307 if len(kn) == 2 { 308 if obj.GetObjectKind().GroupVersionKind().Kind == kn[0] && obj.GetName() == kn[1] { 309 return true 310 } 311 continue 312 } 313 if obj.GetObjectKind().GroupVersionKind().Kind == kn[0] { 314 return true 315 } 316 } 317 return false 318 }