Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Fix multiline Ready condition in clusterctl describe for v1beta2 #11781

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 70 additions & 8 deletions cmd/clusterctl/cmd/describe_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,10 @@ func addObjectRowV1Beta2(prefix string, tbl *tablewriter.Table, objectTree *tree
rowDescriptor.age,
msg0})

multilinePrefix := getRootMultiLineObjectPrefix(obj, objectTree, prefix)
for _, m := range msg[1:] {
tbl.Append([]string{
getMultilinePrefix(gray.Sprint(prefix)),
gray.Sprint(multilinePrefix),
"",
"",
"",
Expand All @@ -324,7 +325,14 @@ func addObjectRowV1Beta2(prefix string, tbl *tablewriter.Table, objectTree *tree

// Add a row for each object's children, taking care of updating the tree view prefix.
childrenObj := objectTree.GetObjectsByParent(obj.GetUID())
childrenObj = orderChildrenObjects(childrenObj)

for i, child := range childrenObj {
addObjectRowV1Beta2(getChildPrefix(prefix, i, len(childrenObj)), tbl, objectTree, child)
}
}

func orderChildrenObjects(childrenObj []ctrlclient.Object) []ctrlclient.Object {
// printBefore returns true if children[i] should be printed before children[j]. Objects are sorted by z-order and
// row name such that objects with higher z-order are printed first, and objects with the same z-order are
// printed in alphabetical order.
Expand All @@ -336,10 +344,7 @@ func addObjectRowV1Beta2(prefix string, tbl *tablewriter.Table, objectTree *tree
return tree.GetZOrder(childrenObj[i]) > tree.GetZOrder(childrenObj[j])
}
sort.Slice(childrenObj, printBefore)

for i, child := range childrenObj {
addObjectRowV1Beta2(getChildPrefix(prefix, i, len(childrenObj)), tbl, objectTree, child)
}
return childrenObj
}

// addObjectRow add a row for a given object, and recursively for all the object's children.
Expand Down Expand Up @@ -452,7 +457,7 @@ func addOtherConditionsV1Beta2(prefix string, tbl *tablewriter.Table, objectTree

for _, m := range msg[1:] {
tbl.Append([]string{
gray.Sprint(getMultilinePrefix(childPrefix)),
gray.Sprint(getMultilineConditionPrefix(childPrefix)),
"",
"",
"",
Expand Down Expand Up @@ -510,8 +515,8 @@ func getChildPrefix(currentPrefix string, childIndex, childCount int) string {
return nextPrefix + lastElemPrefix
}

// getMultilinePrefix return the tree view prefix for a multiline condition.
func getMultilinePrefix(currentPrefix string) string {
// getMultilineConditionPrefix return the tree view prefix for a multiline condition.
func getMultilineConditionPrefix(currentPrefix string) string {
// All ├─ should be replaced by |, so all the existing hierarchic dependencies are carried on
if strings.HasSuffix(currentPrefix, firstElemPrefix) {
return strings.TrimSuffix(currentPrefix, firstElemPrefix) + pipe
Expand All @@ -524,6 +529,63 @@ func getMultilinePrefix(currentPrefix string) string {
return "?"
}

// getRootMultiLineObjectPrefix return the tree view prefix for an object with a message that span on more than one line.
func getRootMultiLineObjectPrefix(obj ctrlclient.Object, objectTree *tree.ObjectTree, prefix string) string {
// If it is the last object we can return early with an empty string.
orderedObjects := getOrderedTreeObjects(objectTree)
if orderedObjects[len(orderedObjects)-1].GetUID() == obj.GetUID() {
return ""
}

// Get the multiline prefix for the objects condition.
multilinePrefix := getMultilineConditionPrefix(prefix)

// If it is the top-level root object, set the multiline prefix to a pipe.
if prefix == "" {
multilinePrefix = pipe
}

// If the object is not the root object and has children, we need to add a pipe to the multiline prefix.
childrenObj := objectTree.GetObjectsByParent(obj.GetUID())
if obj.GetUID() != objectTree.GetRoot().GetUID() && len(childrenObj) > 0 {
// If the multiline prefix is empty, indent the multiline prefix first
if multilinePrefix == "" {
multilinePrefix = indent
}
multilinePrefix += pipe
}

// If the multiline prefix is empty, we have to ensure that even multiline conditions on this root object are indented.
if strings.TrimSpace(multilinePrefix) == "" {
filler := strings.Repeat(" ", 10)
childrenPipe := indent
if objectTree.IsObjectWithChild(obj.GetUID()) {
childrenPipe = pipe
}
// We just want to indent the multiline message for the Ready condition.
// All ├─ should be replaced by |, so all the existing hierarchic dependencies are carried on
multilinePrefix = strings.ReplaceAll(prefix+childrenPipe+filler, firstElemPrefix, pipe)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little bit confused by having a filler at the end of the prefix given that what comes next goes in a different column (and so it automatically goes in the right position, no need to indent)
Same below for the additional spaces

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy from the code for the conditions

childPrefix := getChildPrefix(prefix+childrenPipe+filler, i, len(conditions))
c, status, age, reason, message := v1Beta2ConditionInfo(condition, positivePolarity)

This is necessary as the objects condition might have a multiline message and should be handled exactly as conditions via --show-conditions

Copy link
Member Author

@tobiasgiese tobiasgiese Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to reference addOtherConditionsV1Beta2() and getChildPrefix() in a comment

// All └─ should be replaced by " " because we are under the last element of the tree (nothing to carry on)
multilinePrefix = strings.ReplaceAll(multilinePrefix, lastElemPrefix, strings.Repeat(" ", len([]rune(lastElemPrefix))))
}

return multilinePrefix
}

// getOrderedTreeObjects returns the objects in the tree in the order they should be printed.
func getOrderedTreeObjects(objectTree *tree.ObjectTree) []ctrlclient.Object {
rootObjs := objectTree.GetObjectsByParent(objectTree.GetRoot().GetUID())
rootObjs = orderChildrenObjects(rootObjs)
objs := []ctrlclient.Object{objectTree.GetRoot()}
for _, obj := range rootObjs {
childrenObjs := objectTree.GetObjectsByParent(obj.GetUID())
childrenObjs = orderChildrenObjects(childrenObjs)
objs = append(objs, obj)
objs = append(objs, childrenObjs...)
}
return objs
}

// getRowName returns the object name in the tree view according to following rules:
// - group objects are represented as #of objects kind, e.g. 3 Machines...
// - other virtual objects are represented using the object name, e.g. Workers, or meta name if provided.
Expand Down
174 changes: 174 additions & 0 deletions cmd/clusterctl/cmd/describe_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,154 @@ func Test_TreePrefix(t *testing.T) {
}
}

func Test_V1Beta2TreePrefix(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be

Suggested change
func Test_V1Beta2TreePrefix(t *testing.T) {
func Test_addObjectRowV1Beta2(t *testing.T) {

(this is what we are testing)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same again as for the above comments, I copied it from the v1beta1 test

tests := []struct {
name string
objectTree *tree.ObjectTree
expectPrefix []string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be

Suggested change
expectPrefix []string
expectOutput []string

or expectedTable given that it is more than the simple prefix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just copied from the v1beta1 test above :) can change it, but I should change it in both cases then

}{
{
name: "Conditions should get the right prefix with multiline message",
objectTree: func() *tree.ObjectTree {
root := fakeObject("root",
withV1Beta2Condition(metav1.Condition{
Type: "Available",
Status: metav1.ConditionFalse,
Reason: "NotAvailable",
Message: "1\n2",
}),
)
obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{
V1Beta2: true,
})

o1 := fakeObject("child1",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)

o2 := fakeObject("child2",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
obectjTree.Add(root, o1)
obectjTree.Add(root, o2)
return obectjTree
}(),
expectPrefix: []string{
"Object/root",
"│ 2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little bit confused by seeing only the second line of the message (2) and not the first line (1).
Also, why are we not seeing condition status, reason, age?
Is this correct?

Copy link
Member Author

@tobiasgiese tobiasgiese Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is just like the other/v1beta1 tests, we just check the prefix. We cannot ensure the age, thus we should only verify the prefix here.

"├─Object/child1",
"│ 2",
"└─Object/child2",
Comment on lines +330 to +334
Copy link
Member Author

@tobiasgiese tobiasgiese Feb 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the fix (where ? is no prefix)

Object/root              Available: False  NotAvailable  292y  1  
?                                                              2  
├─Object/child1          Available: False  NotAvailable  292y  1  
│                                                              2  
└─Object/child2          Available: False  NotAvailable  292y  1  
                                                               2  

},
},
{
name: "Conditions should get the right prefix with multiline message and a child",
objectTree: func() *tree.ObjectTree {
root := fakeObject("root",
withV1Beta2Condition(metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: "Available",
}),
)
obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{
V1Beta2: true,
})

o1 := fakeObject("child1",
withV1Beta2Condition(trueV1Beta2Condition()),
)

o2 := fakeObject("child2",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
o2_1 := fakeObject("child2.1")
obectjTree.Add(root, o1)
obectjTree.Add(root, o2)
obectjTree.Add(o2, o2_1)
return obectjTree
}(),
expectPrefix: []string{
"Object/root",
"├─Object/child1",
"└─Object/child2",
" │ 2",
" └─Object/child2.1",
Comment on lines +365 to +369
Copy link
Member Author

@tobiasgiese tobiasgiese Feb 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the fix

Object/root                  Available: True   Available     292y     
├─Object/child1              Available: True   Available     292y     
└─Object/child2              Available: False  NotAvailable  292y  1  
                                                                   2  
  └─Object/child2.1

},
},
{
name: "Conditions should get the right prefix with childs",
objectTree: func() *tree.ObjectTree {
root := fakeObject("root",
withV1Beta2Condition(metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: "Available",
}),
)
obectjTree := tree.NewObjectTree(root, tree.ObjectTreeOptions{
V1Beta2: true,
})

o1 := fakeObject("child1",
withV1Beta2Condition(trueV1Beta2Condition()),
)

o2 := fakeObject("child2",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
o2_1 := fakeObject("child2.1",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
o3 := fakeObject("child3",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
o3_1 := fakeObject("child3.1",
withV1Beta2Condition(falseV1Beta2Condition("Available", "1\n2")),
)
obectjTree.Add(root, o1)
obectjTree.Add(root, o2)
obectjTree.Add(o2, o2_1)
obectjTree.Add(root, o3)
obectjTree.Add(o3, o3_1)
return obectjTree
}(),
expectPrefix: []string{
"Object/root",
"├─Object/child1",
"├─Object/child2",
"│ │ 2",
"│ └─Object/child2.1",
"│ 2",
"└─Object/child3",
" │ 2",
" └─Object/child3.1",
" 2",
Comment on lines +410 to +419
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without the fix

Object/root                  Available: True   Available     292y     
├─Object/child1              Available: True   Available     292y     
├─Object/child2              Available: False  NotAvailable  292y  1  
│                                                                  2  
│ └─Object/child2.1          Available: False  NotAvailable  292y  1  
│                                                                  2  
└─Object/child3              Available: False  NotAvailable  292y  1  
                                                                   2  
  └─Object/child3.1          Available: False  NotAvailable  292y  1  
                                                                   2 

},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
var output bytes.Buffer

// Creates the output table
tbl := tablewriter.NewWriter(&output)

formatTableTreeV1Beta2(tbl)

// Add row for the root object, the cluster, and recursively for all the nodes representing the cluster status.
addObjectRowV1Beta2("", tbl, tt.objectTree, tt.objectTree.GetRoot())
tbl.Render()

// remove empty lines from the output. We need this because v1beta2 adds lines at the beginning and end.
outputString := strings.TrimSpace(output.String())

g.Expect(outputString).Should(MatchTable(tt.expectPrefix))
})
}
}

type objectOption func(object ctrlclient.Object)

func fakeObject(name string, options ...objectOption) ctrlclient.Object {
Expand Down Expand Up @@ -331,6 +479,32 @@ func withCondition(c *clusterv1.Condition) func(ctrlclient.Object) {
}
}

func withV1Beta2Condition(c metav1.Condition) func(ctrlclient.Object) {
return func(m ctrlclient.Object) {
cluster := m.(*clusterv1.Cluster)
conds := cluster.GetV1Beta2Conditions()
conds = append(conds, c)
cluster.SetV1Beta2Conditions(conds)
}
}

func trueV1Beta2Condition() metav1.Condition {
return metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: "Available",
}
}

func falseV1Beta2Condition(t, m string) metav1.Condition {
return metav1.Condition{
Type: t,
Status: metav1.ConditionFalse,
Reason: "Not" + t,
Message: m,
}
}

func withDeletionTimestamp(object ctrlclient.Object) {
now := metav1.Now()
object.SetDeletionTimestamp(&now)
Expand Down