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  }