From e0a798924032066cbb6dc456fef68b9cb7198da1 Mon Sep 17 00:00:00 2001 From: wenyingd Date: Fri, 9 Sep 2022 16:09:08 +0800 Subject: [PATCH] [ExternalNode] Implement support bundle collection status on Controller Signed-off-by: wenyingd --- cmd/antrea-controller/controller.go | 2 +- pkg/apiserver/apiserver.go | 1 + .../supportbundlecollection/subresources.go | 63 +++ .../subresources_test.go | 109 ++++ .../fake_supportbundlecollection_expansion.go | 25 + .../v1beta2/generated_expansion.go | 2 - .../supportbundlecollection_expansion.go | 29 + .../supportbundlecollection/controller.go | 299 ++++++++++- .../controller_test.go | 494 ++++++++++++++++++ 9 files changed, 1013 insertions(+), 11 deletions(-) create mode 100644 pkg/apiserver/registry/controlplane/supportbundlecollection/subresources.go create mode 100644 pkg/apiserver/registry/controlplane/supportbundlecollection/subresources_test.go create mode 100644 pkg/client/clientset/versioned/typed/controlplane/v1beta2/fake/fake_supportbundlecollection_expansion.go create mode 100644 pkg/client/clientset/versioned/typed/controlplane/v1beta2/supportbundlecollection_expansion.go diff --git a/cmd/antrea-controller/controller.go b/cmd/antrea-controller/controller.go index 76525364629..9114ea2d2fb 100644 --- a/cmd/antrea-controller/controller.go +++ b/cmd/antrea-controller/controller.go @@ -457,7 +457,7 @@ func createAPIServerConfig(kubeconfig string, secureServing.BindPort = bindPort secureServing.BindAddress = net.IPv4zero - // kubeconfig file is useful when antrea-controller isn't not running as a pod, like during development. + // kubeconfig file is useful when antrea-controller is not running as a pod, like during development. if len(kubeconfig) > 0 { authentication.RemoteKubeConfigFile = kubeconfig authorization.RemoteKubeConfigFile = kubeconfig diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1e29ec0a3b2..24f89c0021f 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -192,6 +192,7 @@ func installAPIGroup(s *APIServer, c completedConfig) error { cpv1beta2Storage["clustergroupmembers"] = clusterGroupMembershipStorage cpv1beta2Storage["egressgroups"] = egressGroupStorage cpv1beta2Storage["supportbundlecollections"] = bundleCollectionStorage + cpv1beta2Storage["supportbundlecollections/status"] = bundleCollectionStorage cpGroup.VersionedResourcesStorageMap["v1beta2"] = cpv1beta2Storage systemGroup := genericapiserver.NewDefaultAPIGroupInfo(system.GroupName, Scheme, metav1.ParameterCodec, Codecs) diff --git a/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources.go b/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources.go new file mode 100644 index 00000000000..5d35094c195 --- /dev/null +++ b/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources.go @@ -0,0 +1,63 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package supportbundlecollection + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + "antrea.io/antrea/pkg/apis/controlplane" +) + +// StatusREST implements the REST endpoint for getting NetworkPolicy's obj. +type StatusREST struct { + collector statusCollector +} + +// NewStatusREST returns a REST object that will work against API services. +func NewStatusREST(collector statusCollector) *StatusREST { + return &StatusREST{collector} +} + +// statusCollector is the interface required by the handler. +type statusCollector interface { + UpdateStatus(status *controlplane.SupportBundleCollectionStatus) error +} + +var _ rest.NamedCreater = &StatusREST{} + +func (s StatusREST) New() runtime.Object { + return &controlplane.SupportBundleCollectionStatus{} +} + +func (s StatusREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + status, ok := obj.(*controlplane.SupportBundleCollectionStatus) + if !ok { + return nil, errors.NewBadRequest(fmt.Sprintf("not a SupportBundleCollectionStatus object: %T", obj)) + } + if name != status.Name { + return nil, errors.NewBadRequest("name in URL does not match name in SupportBundleCollectionStatus object") + } + err := s.collector.UpdateStatus(status) + if err != nil { + return nil, err + } + return &metav1.Status{Status: metav1.StatusSuccess}, nil +} diff --git a/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources_test.go b/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources_test.go new file mode 100644 index 00000000000..56d7823230a --- /dev/null +++ b/pkg/apiserver/registry/controlplane/supportbundlecollection/subresources_test.go @@ -0,0 +1,109 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package supportbundlecollection + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "antrea.io/antrea/pkg/apis/controlplane" +) + +func TestCreate(t *testing.T) { + for _, tc := range []struct { + name string + obj runtime.Object + expError error + }{ + { + name: "invalid-status-type", + obj: runtime.Object(&controlplane.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-status-type", + }, + }), + expError: errors.NewBadRequest("not a SupportBundleCollectionStatus object"), + }, + { + name: "invalid-status-name", + obj: runtime.Object(&controlplane.SupportBundleCollectionStatus{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-name", + }, + }), + expError: errors.NewBadRequest("name in URL does not match name in SupportBundleCollectionStatus object"), + }, + { + name: "unable-update", + obj: runtime.Object(&controlplane.SupportBundleCollectionStatus{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unable-update", + }, + }), + expError: fmt.Errorf("no Nodes status is updated"), + }, + { + name: "valid-status-update", + obj: runtime.Object(&controlplane.SupportBundleCollectionStatus{ + ObjectMeta: metav1.ObjectMeta{ + Name: "valid-status-update", + }, + Nodes: []controlplane.SupportBundleCollectionNodeStatus{ + { + NodeName: "n1", + NodeType: "Node", + }, + }, + }), + expError: nil, + }, + } { + statusController := &fakeStatusCollector{ + bundleCollectionStatuses: make(map[string]*controlplane.SupportBundleCollectionStatus), + } + r := NewStatusREST(statusController) + rsp, err := r.Create(context.TODO(), tc.name, tc.obj, nil, &metav1.CreateOptions{}) + if tc.expError == nil { + assert.NoError(t, err) + assert.Equal(t, &metav1.Status{Status: metav1.StatusSuccess}, rsp) + expStatus := tc.obj.(*controlplane.SupportBundleCollectionStatus) + status := statusController.bundleCollectionStatuses[tc.name] + assert.Equal(t, expStatus, status) + } else { + assert.Error(t, err) + assert.True(t, strings.Contains(err.Error(), tc.expError.Error())) + } + } +} + +type fakeStatusCollector struct { + bundleCollectionStatuses map[string]*controlplane.SupportBundleCollectionStatus +} + +func (c *fakeStatusCollector) UpdateStatus(status *controlplane.SupportBundleCollectionStatus) error { + bundleCollectionName := status.Name + if len(status.Nodes) == 0 { + return fmt.Errorf("no Nodes status is updated") + } + c.bundleCollectionStatuses[bundleCollectionName] = status + return nil +} diff --git a/pkg/client/clientset/versioned/typed/controlplane/v1beta2/fake/fake_supportbundlecollection_expansion.go b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/fake/fake_supportbundlecollection_expansion.go new file mode 100644 index 00000000000..7c129324fac --- /dev/null +++ b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/fake/fake_supportbundlecollection_expansion.go @@ -0,0 +1,25 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fake + +import ( + "context" + + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" +) + +func (c *FakeSupportBundleCollections) UpdateStatus(ctx context.Context, name string, status *v1beta2.SupportBundleCollectionStatus) error { + return nil +} diff --git a/pkg/client/clientset/versioned/typed/controlplane/v1beta2/generated_expansion.go b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/generated_expansion.go index e0ca3b1ebde..8a05241d9d3 100644 --- a/pkg/client/clientset/versioned/typed/controlplane/v1beta2/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/generated_expansion.go @@ -27,5 +27,3 @@ type EgressGroupExpansion interface{} type GroupAssociationExpansion interface{} type NodeStatsSummaryExpansion interface{} - -type SupportBundleCollectionExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/controlplane/v1beta2/supportbundlecollection_expansion.go b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/supportbundlecollection_expansion.go new file mode 100644 index 00000000000..13b868159f8 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/controlplane/v1beta2/supportbundlecollection_expansion.go @@ -0,0 +1,29 @@ +// Copyright 2022 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1beta2 + +import ( + "context" + + "antrea.io/antrea/pkg/apis/controlplane/v1beta2" +) + +type SupportBundleCollectionExpansion interface { + UpdateStatus(ctx context.Context, name string, status *v1beta2.SupportBundleCollectionStatus) error +} + +func (c *supportBundleCollections) UpdateStatus(ctx context.Context, name string, status *v1beta2.SupportBundleCollectionStatus) error { + return c.client.Post().Resource("supportbundlecollections").Name(name).SubResource("status").Body(status).Do(ctx).Error() +} diff --git a/pkg/controller/supportbundlecollection/controller.go b/pkg/controller/supportbundlecollection/controller.go index 1ed823a53eb..e3d30271926 100644 --- a/pkg/controller/supportbundlecollection/controller.go +++ b/pkg/controller/supportbundlecollection/controller.go @@ -19,10 +19,14 @@ import ( "context" "fmt" "reflect" + "sort" + "strings" + "sync" "time" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" @@ -115,6 +119,12 @@ type Controller struct { // supportBundleCollectionAppliedToStore is the storage where the required Nodes or ExternalNodes of a // SupportBundleCollection are stored. supportBundleCollectionAppliedToStore cache.Indexer + + // statuses is a nested map that keeps the realization statuses reported by antrea-agents. + // The outer map's keys are the SupportBundleCollection names. The inner map's keys are the Node names. The inner + // map's values are statuses reported by each Node for a SupportBundleCollection. + statuses map[string]map[string]*controlplane.SupportBundleCollectionNodeStatus + statusesLock sync.RWMutex } func NewSupportBundleCollectionController( @@ -141,6 +151,7 @@ func NewSupportBundleCollectionController( processingNodesIndex: processingNodesIndexFunc, processingExternalNodesIndex: processingExternalNodesIndexFunc, }), + statuses: make(map[string]map[string]*controlplane.SupportBundleCollectionNodeStatus), } c.supportBundleCollectionInformer.Informer().AddEventHandlerWithResyncPeriod( cache.ResourceEventHandlerFuncs{ @@ -172,6 +183,32 @@ func (c *Controller) Run(stopCh <-chan struct{}) { <-stopCh } +// UpdateStatus is called when Agent reports status to the internal SupportBundleCollection resource. +func (c *Controller) UpdateStatus(status *controlplane.SupportBundleCollectionStatus) error { + key := status.Name + _, found, _ := c.supportBundleCollectionStore.Get(key) + if !found { + klog.InfoS("SupportBundleCollection has been deleted, skip updating its status", "supportBundleCollection", key) + return nil + } + func() { + c.statusesLock.Lock() + defer c.statusesLock.Unlock() + statusPerNode, exists := c.statuses[key] + if !exists { + statusPerNode = map[string]*controlplane.SupportBundleCollectionNodeStatus{} + c.statuses[key] = statusPerNode + } + for i := range status.Nodes { + nodeStatus := status.Nodes[i] + nodeStatusKey := getNodeKey(&nodeStatus) + statusPerNode[nodeStatusKey] = &nodeStatus + } + }() + c.queue.Add(key) + return nil +} + func (c *Controller) addSupportBundleCollection(obj interface{}) { bundleCollection := obj.(*v1alpha1.SupportBundleCollection) if isCollectionCompleted(bundleCollection) { @@ -277,6 +314,12 @@ func (c *Controller) syncSupportBundleCollection(key string) error { } return nil } + // Update internal SupportBundleCollection status. This is triggered when Agent reports status. + // The event that SupportBundleCollection CR status update will not trigger this logic. + if internalBundleCollectionObj, found, _ := c.supportBundleCollectionStore.Get(key); found { + return c.updateStatus(internalBundleCollectionObj.(*types.SupportBundleCollection)) + } + if err := c.createInternalSupportBundleCollection(bundle); err != nil { return err } @@ -343,11 +386,16 @@ func (c *Controller) createInternalSupportBundleCollection(bundle *v1alpha1.Supp klog.ErrorS(err, "Failed to get authentication defined in the SupportBundleCollection CR", "name", bundle.Name, "authentication", bundle.Spec.Authentication) return err } - c.addInternalSupportBundleCollection(bundle, nodeSpan, authentication, metav1.NewTime(expiredAt)) + internalBundleCollection := c.addInternalSupportBundleCollection(bundle, nodeSpan, authentication, metav1.NewTime(expiredAt)) // Process the support bundle collection when time is up, this will create a CollectionFailure condition if the // bundle collection is not completed in time because any Agent fails to upload the files and does not report // the failure. c.queue.AddAfter(bundle.Name, expiredAt.Sub(now)) + // The SupportBundleCollection will re-enqueue if it fails to update the status, and the worker will retry status update operation. + if err := c.updateStatus(internalBundleCollection); err != nil { + klog.ErrorS(err, "Failed to update SupportBundleCollection status after the internal resource is created", "name", bundle.Name) + return err + } klog.InfoS("Created internal SupportBundleCollection", "name", bundle.Name) return nil } @@ -453,6 +501,7 @@ func (c *Controller) deleteInternalSupportBundleCollection(key string) error { if obj, exists, _ := c.supportBundleCollectionAppliedToStore.GetByKey(key); exists { c.supportBundleCollectionAppliedToStore.Delete(obj) } + c.clearStatuses(key) klog.InfoS("Deleted internal SupportBundleCollection", "name", key) return nil } @@ -517,7 +566,7 @@ func (c *Controller) addInternalSupportBundleCollection( bundleCollection *v1alpha1.SupportBundleCollection, nodeSpan sets.String, authentication *controlplane.BundleServerAuthConfiguration, - expiredAt metav1.Time) { + expiredAt metav1.Time) *types.SupportBundleCollection { var processNodes bool if bundleCollection.Spec.Nodes == nil { processNodes = false @@ -550,6 +599,7 @@ func (c *Controller) addInternalSupportBundleCollection( Authentication: *authentication, } _ = c.supportBundleCollectionStore.Create(internalBundleCollection) + return internalBundleCollection } // processConflictedCollection adds a Started failure condition on the conflicted the SupportBundleCollection request, @@ -627,6 +677,175 @@ func (c *Controller) isCollectionAvailable(bundleCollection *v1alpha1.SupportBun return true } +func (c *Controller) updateStatus(internalBundleCollection *types.SupportBundleCollection) error { + updateStatusFunc := func(currentNodes, desiredNodes int, updatedConditions []v1alpha1.SupportBundleCollectionCondition) error { + status := &v1alpha1.SupportBundleCollectionStatus{ + SucceededNodes: int32(currentNodes), + DesiredNodes: int32(desiredNodes), + Conditions: updatedConditions, + } + klog.V(2).InfoS("Updating SupportBundleCollection status", "supportBundleCollection", internalBundleCollection.Name, "status", status) + return c.updateSupportBundleCollectionStatus(internalBundleCollection.Name, status) + } + + // It means the SupportBundleCollection hasn't been processed once. Set it to started failed to differentiate from + // SupportBundleCollection that spans 0 Node. + now := metav1.Now() + if internalBundleCollection.SpanMeta.NodeNames == nil { + return updateStatusFunc(0, 0, []v1alpha1.SupportBundleCollectionCondition{ + { + Type: v1alpha1.CollectionStarted, + Status: metav1.ConditionFalse, + LastTransitionTime: now, + }, + }) + } + + desiredNodes := len(internalBundleCollection.SpanMeta.NodeNames) + succeededNodes := 0 + // TODO: Use a map to store the failed Node/ExternalNode and its error, and report the failed list when the + // collection is completed. + failedNodes := make(map[string][]string) + failedNodesCount := 0 + statuses := c.getNodeStatuses(internalBundleCollection.Name) + for _, status := range statuses { + nodeKey := getNodeKey(status) + // The node is no longer in the span of this Support Bundle Collection, delete its status. + if !internalBundleCollection.NodeNames.Has(nodeKey) { + c.deleteNodeStatus(internalBundleCollection.Name, nodeKey) + continue + } + if status.Completed { + succeededNodes += 1 + } else { + failedNodesCount += 1 + failedReason := status.Error + if failedReason == "" { + failedReason = "unknown error" + } + _, exists := failedNodes[failedReason] + if !exists { + failedNodes[failedReason] = make([]string, 0) + } + failedNodes[failedReason] = append(failedNodes[failedReason], nodeKey) + } + } + + newConditions := []v1alpha1.SupportBundleCollectionCondition{ + // Mark the support bundle collection as started since the internal resource successfully created. + // It will not be added as a duplication if it already exists. + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.Now()}, + } + if succeededNodes > 0 { + newConditions = append(newConditions, + v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.BundleCollected, + Status: metav1.ConditionTrue, + LastTransitionTime: now, + }, + ) + } + if failedNodesCount > 0 { + failedNodeErrors := make([]string, 0, len(failedNodes)) + for k, v := range failedNodes { + sort.Strings(v) + failedNodeErrors = append(failedNodeErrors, fmt.Sprintf(`"%s":[%s]`, k, strings.Join(v, ", "))) + } + sort.Strings(failedNodeErrors) + failedConditionMessage := fmt.Sprintf("Failed Agent count: %d, %s", failedNodesCount, strings.Join(failedNodeErrors, ", ")) + newConditions = append(newConditions, + v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionFailure, + Status: metav1.ConditionTrue, + Reason: string(metav1.StatusReasonInternalError), + Message: failedConditionMessage, + LastTransitionTime: now, + }, + ) + } + if succeededNodes == desiredNodes { + newConditions = append(newConditions, + v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionFailure, + Status: metav1.ConditionFalse, + LastTransitionTime: now, + }, + ) + } + if succeededNodes+failedNodesCount == desiredNodes { + newConditions = append(newConditions, + v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionCompleted, + Status: metav1.ConditionTrue, + LastTransitionTime: now, + }, + ) + } + return updateStatusFunc(succeededNodes, desiredNodes, newConditions) +} + +func (c *Controller) getNodeStatuses(key string) []*controlplane.SupportBundleCollectionNodeStatus { + c.statusesLock.RLock() + defer c.statusesLock.RUnlock() + statusPerNode, exists := c.statuses[key] + if !exists { + return nil + } + statuses := make([]*controlplane.SupportBundleCollectionNodeStatus, 0, len(c.statuses[key])) + for _, status := range statusPerNode { + statuses = append(statuses, status) + } + return statuses +} + +func (c *Controller) deleteNodeStatus(key string, nodeName string) { + c.statusesLock.Lock() + defer c.statusesLock.Unlock() + statusPerNode, exists := c.statuses[key] + if !exists { + return + } + delete(statusPerNode, nodeName) +} + +func (c *Controller) clearStatuses(key string) { + c.statusesLock.Lock() + defer c.statusesLock.Unlock() + delete(c.statuses, key) +} + +func (c *Controller) updateSupportBundleCollectionStatus(name string, updatedStatus *v1alpha1.SupportBundleCollectionStatus) error { + bundleCollection, err := c.supportBundleCollectionLister.Get(name) + if err != nil { + klog.InfoS("Didn't find the original SupportBundleCollection, skip updating status", "supportBundleCollection", name) + return nil + } + toUpdate := bundleCollection.DeepCopy() + if err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + updatedConditions := appendConditions(toUpdate.Status.Conditions, updatedStatus.Conditions) + updatedStatus.Conditions = updatedConditions + // If the current status equals to the desired status, no need to update. + if supportBundleCollectionStatusEqual(toUpdate.Status, *updatedStatus) { + return nil + } + toUpdate.Status = *updatedStatus + klog.V(2).InfoS("Updating SupportBundleCollection", "supportBundleCollection", name, "status", klog.KObj(toUpdate)) + _, updateErr := c.crdClient.CrdV1alpha1().SupportBundleCollections().UpdateStatus(context.TODO(), toUpdate, metav1.UpdateOptions{}) + if updateErr != nil && k8serrors.IsConflict(updateErr) { + var getErr error + if toUpdate, getErr = c.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.TODO(), name, metav1.GetOptions{}); getErr != nil { + return getErr + } + } + // Return the error from UPDATE. + return updateErr + }); err != nil { + return err + } + klog.V(2).InfoS("Updated SupportBundleCollection", "supportBundleCollection", name) + return nil +} + // isCollectionCompleted check if CollectionCompleted condition with status ConditionTrue is added in the bundleCollection or not. func isCollectionCompleted(bundleCollection *v1alpha1.SupportBundleCollection) bool { for _, condition := range bundleCollection.Status.Conditions { @@ -667,13 +886,77 @@ func conditionExistsIgnoreLastTransitionTime(conditions []v1alpha1.SupportBundle return false } -func appendConditions(oldConditions, updatedConditions []v1alpha1.SupportBundleCollectionCondition) []v1alpha1.SupportBundleCollectionCondition { - newConditions := oldConditions - for _, c := range updatedConditions { - if conditionExistsIgnoreLastTransitionTime(newConditions, c) { +func appendConditions(oldConditions, newConditions []v1alpha1.SupportBundleCollectionCondition) []v1alpha1.SupportBundleCollectionCondition { + finalConditions := make([]v1alpha1.SupportBundleCollectionCondition, 0) + newConditionMap := make(map[v1alpha1.SupportBundleCollectionConditionType]v1alpha1.SupportBundleCollectionCondition) + addedConditions := sets.NewString() + for _, condition := range newConditions { + newConditionMap[condition.Type] = condition + } + for _, oldCondition := range oldConditions { + newCondition, exists := newConditionMap[oldCondition.Type] + if !exists { + finalConditions = append(finalConditions, oldCondition) continue } - newConditions = append(newConditions, c) + // Use the original Condition if the only change is about lastTransition time + if conditionEqualsIgnoreLastTransitionTime(newCondition, oldCondition) { + finalConditions = append(finalConditions, oldCondition) + } else { + // Use the latest Condition. + finalConditions = append(finalConditions, newCondition) + } + addedConditions.Insert(string(newCondition.Type)) + } + for key, newCondition := range newConditionMap { + if !addedConditions.Has(string(key)) { + finalConditions = append(finalConditions, newCondition) + } + } + return finalConditions +} + +func getNodeKey(status *controlplane.SupportBundleCollectionNodeStatus) string { + // TODO: use NodeNamespace/NodeName for ExternalNode after the Namespace is added in the connection key between + // antrea-agent and antrea-controller. + return status.NodeName +} + +// supportBundleCollectionStatusEqual compares two SupportBundleCollectionStatus objects. It disregards +// the LastTransitionTime field in the status Conditions. +func supportBundleCollectionStatusEqual(oldStatus, newStatus v1alpha1.SupportBundleCollectionStatus) bool { + semanticIgnoreLastTransitionTime := conversion.EqualitiesOrDie( + conditionSliceEqualsIgnoreLastTransitionTime, + ) + return semanticIgnoreLastTransitionTime.DeepEqual(oldStatus, newStatus) +} + +func conditionSliceEqualsIgnoreLastTransitionTime(as, bs []v1alpha1.SupportBundleCollectionCondition) bool { + sort.Slice(as, func(i, j int) bool { + a := as[i] + b := as[j] + if a.Type == b.Type { + return a.Status < b.Status + } + return a.Type < b.Type + }) + sort.Slice(bs, func(i, j int) bool { + a := bs[i] + b := bs[j] + if a.Type == b.Type { + return a.Status < b.Status + } + return a.Type < b.Type + }) + if len(as) != len(bs) { + return false } - return newConditions + for i := range as { + a := as[i] + b := bs[i] + if !conditionEqualsIgnoreLastTransitionTime(a, b) { + return false + } + } + return true } diff --git a/pkg/controller/supportbundlecollection/controller_test.go b/pkg/controller/supportbundlecollection/controller_test.go index a5a394c250d..aa586795ccf 100644 --- a/pkg/controller/supportbundlecollection/controller_test.go +++ b/pkg/controller/supportbundlecollection/controller_test.go @@ -1278,6 +1278,500 @@ func TestIsCollectionAvailable(t *testing.T) { } } +func TestSupportBundleCollectionStatusEqual(t *testing.T) { + for _, tc := range []struct { + oldStatus v1alpha1.SupportBundleCollectionStatus + newStatus v1alpha1.SupportBundleCollectionStatus + equal bool + }{ + { + oldStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + }, + }, + newStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 5, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + }, + }, + equal: false, + }, + { + oldStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + }, + }, + newStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue}, + }, + }, + equal: false, + }, + { + oldStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue}, + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + }, + }, + newStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue}, + }, + }, + equal: true, + }, { + oldStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(time.Now())}, + }, + }, + newStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(time.Now().Add(time.Minute))}, + }, + }, + equal: true, + }, { + oldStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(time.Now())}, + }, + }, + newStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 100, + SucceededNodes: 4, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionFalse, LastTransitionTime: metav1.NewTime(time.Now())}, + }, + }, + equal: false, + }, + } { + equals := supportBundleCollectionStatusEqual(tc.oldStatus, tc.newStatus) + assert.Equal(t, tc.equal, equals) + } +} + +func TestUpdateSupportBundleCollectionStatus(t *testing.T) { + now := time.Now() + for _, tc := range []struct { + existingCollection *v1alpha1.SupportBundleCollection + updateStatus *v1alpha1.SupportBundleCollectionStatus + expectedStatus v1alpha1.SupportBundleCollectionStatus + }{ + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + }, + }, + }, + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionFalse, LastTransitionTime: metav1.NewTime(now)}, + }, + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second))}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second))}, + }, + }, + }, + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + }, + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 1, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 1, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + }, + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 1, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + }, + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 5, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 5, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + }, + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 5, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 8, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + {Type: v1alpha1.CollectionFailure, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20)), Reason: string(metav1.StatusReasonInternalError), Message: "Agent error"}, + {Type: v1alpha1.CollectionCompleted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 8, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + {Type: v1alpha1.CollectionFailure, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20)), Reason: string(metav1.StatusReasonInternalError), Message: "Agent error"}, + {Type: v1alpha1.CollectionCompleted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + }, + }, + }, + { + existingCollection: &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{ + Name: "b1", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 8, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + }, + }, + }, + updateStatus: &v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 10, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + {Type: v1alpha1.CollectionFailure, Status: metav1.ConditionFalse, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + {Type: v1alpha1.CollectionCompleted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + }, + }, + expectedStatus: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: 10, + SucceededNodes: 10, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now)}, + {Type: v1alpha1.BundleCollected, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 10))}, + {Type: v1alpha1.CollectionFailure, Status: metav1.ConditionFalse, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + {Type: v1alpha1.CollectionCompleted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(now.Add(time.Second * 20))}, + }, + }, + }, + } { + testClient := newTestClient(nil, []runtime.Object{tc.existingCollection}) + controller := newController(testClient) + stopCh := make(chan struct{}) + testClient.start(stopCh) + testClient.waitForSync(stopCh) + collectionName := tc.existingCollection.Name + err := controller.updateSupportBundleCollectionStatus(collectionName, tc.updateStatus) + require.NoError(t, err) + updatedCollection, err := controller.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.TODO(), collectionName, metav1.GetOptions{}) + require.NoError(t, err) + assert.True(t, supportBundleCollectionStatusEqual(tc.expectedStatus, updatedCollection.Status)) + } +} + +func TestUpdateStatus(t *testing.T) { + var controller *Controller + getSpan := func(nodesCount int) []string { + nodes := make([]string, nodesCount) + for i := 0; i < nodesCount; i++ { + nodes[i] = fmt.Sprintf("n%d", i) + } + return nodes + } + prepareController := func(collectionName string, desiredNodes int) { + testClient := newTestClient(nil, []runtime.Object{ + &v1alpha1.SupportBundleCollection{ + ObjectMeta: metav1.ObjectMeta{Name: collectionName}, + Spec: v1alpha1.SupportBundleCollectionSpec{ + FileServer: v1alpha1.BundleFileServer{ + URL: "sftp://1.1.1.1/supportbundles/upload", + }, + ExpirationMinutes: 60, + SinceTime: "2h", + }, + Status: v1alpha1.SupportBundleCollectionStatus{ + DesiredNodes: int32(desiredNodes), + SucceededNodes: 0, + Conditions: []v1alpha1.SupportBundleCollectionCondition{ + {Type: v1alpha1.CollectionStarted, Status: metav1.ConditionTrue, LastTransitionTime: metav1.NewTime(time.Now())}, + }, + }, + }, + }) + controller = newController(testClient) + controller.supportBundleCollectionStore.Create(&types.SupportBundleCollection{ + Name: collectionName, + SpanMeta: types.SpanMeta{ + NodeNames: sets.NewString(getSpan(desiredNodes)...), + }, + }) + stopCh := make(chan struct{}) + testClient.start(stopCh) + testClient.waitForSync(stopCh) + } + + updateStatusFunc := func(collectionName string, nodeStatus controlplane.SupportBundleCollectionNodeStatus) { + status := &controlplane.SupportBundleCollectionStatus{ + ObjectMeta: metav1.ObjectMeta{ + Name: collectionName, + }, + Nodes: []controlplane.SupportBundleCollectionNodeStatus{ + nodeStatus, + }, + } + err := controller.UpdateStatus(status) + require.NoError(t, err) + } + + syncSupportBundleCollection := func() { + key, _ := controller.queue.Get() + controller.queue.Done(key) + err := controller.syncSupportBundleCollection(key.(string)) + assert.NoError(t, err) + } + + namespace := "ns1" + agentReportStatus := func(nodesCount, failedNodes int, collectionName string) { + for i := 0; i < nodesCount; i++ { + nodeName := fmt.Sprintf("n%d", i) + nodeType := controlplane.SupportBundleCollectionNodeTypeNode + if i%2 == 0 { + nodeType = controlplane.SupportBundleCollectionNodeTypeExternalNode + } + completed := true + if i < failedNodes { + completed = false + } + nodeStatus := controlplane.SupportBundleCollectionNodeStatus{ + NodeName: nodeName, + NodeType: nodeType, + Completed: completed, + } + if nodeType == controlplane.SupportBundleCollectionNodeTypeExternalNode { + nodeStatus.NodeNamespace = namespace + } + updateStatusFunc(collectionName, nodeStatus) + } + } + + updateFailure := func(collectionName, name string, errMessage string) { + nodeStatus := controlplane.SupportBundleCollectionNodeStatus{ + NodeName: name, + NodeType: controlplane.SupportBundleCollectionNodeTypeNode, + Completed: false, + Error: errMessage, + } + updateStatusFunc(collectionName, nodeStatus) + } + + checkCompletedStatus := func(bundleCollection *v1alpha1.SupportBundleCollection) { + collectionName := bundleCollection.Name + assert.True(t, conditionExistsIgnoreLastTransitionTime(bundleCollection.Status.Conditions, v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionCompleted, + Status: metav1.ConditionTrue, + })) + assert.Eventually(t, func() bool { + err := controller.syncSupportBundleCollection(collectionName) + assert.NoError(t, err) + _, exists, err := controller.supportBundleCollectionStore.Get(collectionName) + assert.NoError(t, err) + return !exists + }, time.Second, time.Millisecond*10) + + _, exists := controller.statuses[collectionName] + assert.False(t, exists) + } + + t.Run("all-agent-succeeded", func(t *testing.T) { + collectionName := "b1" + desiredNodes := 5 + prepareController(collectionName, desiredNodes) + agentReportStatus(desiredNodes, 0, collectionName) + syncSupportBundleCollection() + bundleCollection, err := controller.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.Background(), collectionName, metav1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, int32(desiredNodes), bundleCollection.Status.SucceededNodes) + checkCompletedStatus(bundleCollection) + }) + + t.Run("agent-failure", func(t *testing.T) { + collectionName := "b2" + desiredNodes := 6 + prepareController(collectionName, desiredNodes) + reportedNodes := 5 + agentReportStatus(reportedNodes, 2, collectionName) + syncSupportBundleCollection() + bundleCollection, err := controller.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.Background(), collectionName, metav1.GetOptions{}) + assert.NoError(t, err) + assert.Equal(t, int32(3), bundleCollection.Status.SucceededNodes) + failureStatus := v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionFailure, + Status: metav1.ConditionTrue, + Reason: string(metav1.StatusReasonInternalError), + Message: fmt.Sprintf(`Failed Agent count: 2, "unknown error":[n0, n1]`), + } + assert.True(t, conditionExistsIgnoreLastTransitionTime(bundleCollection.Status.Conditions, failureStatus)) + // Test merging failure message. + updateFailure(collectionName, "n5", "agent internal error") + syncSupportBundleCollection() + bundleCollection, err = controller.crdClient.CrdV1alpha1().SupportBundleCollections().Get(context.Background(), collectionName, metav1.GetOptions{}) + assert.NoError(t, err) + assert.True(t, conditionExistsIgnoreLastTransitionTime(bundleCollection.Status.Conditions, v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionFailure, + Status: metav1.ConditionTrue, + Reason: string(metav1.StatusReasonInternalError), + Message: fmt.Sprintf(`Failed Agent count: 3, "agent internal error":[n5], "unknown error":[n0, n1]`), + })) + assert.False(t, conditionExistsIgnoreLastTransitionTime(bundleCollection.Status.Conditions, failureStatus)) + assert.True(t, conditionExistsIgnoreLastTransitionTime(bundleCollection.Status.Conditions, v1alpha1.SupportBundleCollectionCondition{ + Type: v1alpha1.CollectionCompleted, + Status: metav1.ConditionTrue, + })) + checkCompletedStatus(bundleCollection) + }) + + t.Run("unkown-agent-report", func(t *testing.T) { + collectionName := "b3" + desiredNodes := 3 + prepareController(collectionName, desiredNodes) + reportedNodes := 1 + agentReportStatus(reportedNodes, 0, collectionName) + statusPerNode, _ := controller.statuses[collectionName] + assert.Equal(t, reportedNodes, len(statusPerNode)) + syncSupportBundleCollection() + assert.Equal(t, reportedNodes, len(statusPerNode)) + // Test status report from the Agent which is not in the SupportBundleCollection span + updateStatusFunc(collectionName, controlplane.SupportBundleCollectionNodeStatus{ + NodeName: "n10", + NodeType: controlplane.SupportBundleCollectionNodeTypeNode, + Completed: true, + }) + statusPerNode, _ = controller.statuses[collectionName] + assert.Equal(t, reportedNodes+1, len(statusPerNode)) + syncSupportBundleCollection() + statusPerNode, _ = controller.statuses[collectionName] + assert.Equal(t, reportedNodes, len(statusPerNode)) + }) + + t.Run("non-existing-collection", func(t *testing.T) { + // Test UpdateStatus with non-existing SupportBundleCollection + nonExistCollectionName := "no-existing-collection" + updateStatusFunc(nonExistCollectionName, controlplane.SupportBundleCollectionNodeStatus{ + NodeName: "n1", + NodeType: "Node", + Completed: true, + }) + _, exists := controller.statuses[nonExistCollectionName] + assert.False(t, exists) + }) +} + func newController(tc *testClient) *Controller { nodeInformer := tc.informerFactory.Core().V1().Nodes() externalNodeInformer := tc.crdInformerFactory.Crd().V1alpha1().ExternalNodes()