diff --git a/pkg/apis/system/v1beta1/types.go b/pkg/apis/system/v1beta1/types.go index 5109669bbd9..988f9c86af2 100644 --- a/pkg/apis/system/v1beta1/types.go +++ b/pkg/apis/system/v1beta1/types.go @@ -39,3 +39,25 @@ type SupportBundle struct { Size uint32 `json:"size,omitempty"` Filepath string `json:"-"` } + +type PacketStatus string + +const ( + SamplingPacketStatusNone PacketStatus = "None" + SamplingPacketStatusCollecting PacketStatus = "Collecting" + SamplingPacketStatusCollected PacketStatus = "Collected" +) + +// +genclient +// +genclient:nonNamespaced +// +genclient:onlyVerbs=get,create,delete +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +type CapturedPacket struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Status PacketStatus `json:"status,omitempty"` + Size uint32 `json:"size,omitempty"` + Filepath string `json:"-"` +} diff --git a/pkg/apiserver/registry/system/samplingpacket/rest.go b/pkg/apiserver/registry/system/samplingpacket/rest.go new file mode 100644 index 00000000000..7c057119ef5 --- /dev/null +++ b/pkg/apiserver/registry/system/samplingpacket/rest.go @@ -0,0 +1,346 @@ +// Copyright 2020 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 samplingpacket + +import ( + "context" + "fmt" + "io" + "sync" + "time" + + "github.com/spf13/afero" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/registry/rest" + "k8s.io/klog/v2" + clockutils "k8s.io/utils/clock" + "k8s.io/utils/exec" + + systemv1beta1 "antrea.io/antrea/pkg/apis/system/v1beta1" + "antrea.io/antrea/pkg/support" + "antrea.io/antrea/pkg/util/compress" +) + +const ( + bundleExpireDuration = time.Hour + modeController = "controller" + modeAgent = "agent" +) + +var ( + // Declared as variables for testing. + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + newAgentDumper = support.NewAgentDumper + + clock clockutils.Clock = clockutils.RealClock{} +) + +// NewControllerStorage creates a sampling storage for working on antrea controller. +func NewControllerStorage() Storage { + bundle := &supportBundleREST{ + mode: modeController, + cache: &systemv1beta1.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{Name: modeController}, + Status: systemv1beta1.SupportBundleStatusNone, + }, + } + return Storage{ + Mode: modeController, + SupportBundle: bundle, + Download: &downloadREST{supportBundle: bundle}, + } +} + +// NewAgentStorage creates a sampling packet storage for working on antrea agent. +func NewAgentStorage() Storage { + bundle := &samplingPacketsREST{ + mode: modeAgent, + cache: &systemv1beta1.CapturedPacket{ + ObjectMeta: metav1.ObjectMeta{Name: modeAgent}, + }, + } + return Storage{ + Mode: modeAgent, + SupportBundle: bundle, + Download: &downloadREST{supportBundle: bundle}, + } +} + +// Storage contains REST resources for sampling packet, including status query and download. +type Storage struct { + SamplingPacket *samplingPacketsREST + Download *downloadREST + Mode string +} + +var ( + _ rest.Scoper = &samplingPacketsREST{} + _ rest.Getter = &samplingPacketsREST{} + _ rest.Creater = &samplingPacketsREST{} + _ rest.GracefulDeleter = &samplingPacketsREST{} +) + +// supportBundleREST implements REST interfaces for bundle status querying. +type samplingPacketsREST struct { + mode string + statusLocker sync.RWMutex + cancelFunc context.CancelFunc + cache *systemv1beta1.CapturedPacket +} + +// Create triggers a bundle generation. It only allows resource creation when +// the name matches the mode. It returns metav1.Status if there is any error, +// otherwise it returns the SupportBundle. +func (r *samplingPacketsREST) Create(ctx context.Context, obj runtime.Object, _ rest.ValidateObjectFunc, _ *metav1.CreateOptions) (runtime.Object, error) { + requestBundle := obj.(*systemv1beta1.CapturedPacket) + if requestBundle.Name != r.mode { + return nil, errors.NewForbidden(systemv1beta1.ControllerInfoVersionResource.GroupResource(), requestBundle.Name, fmt.Errorf("only resource name \"%s\" is allowed", r.mode)) + } + r.statusLocker.Lock() + defer r.statusLocker.Unlock() + + if r.cancelFunc != nil { + r.cancelFunc() + } + ctx, cancelFunc := context.WithCancel(context.Background()) + r.cache = &systemv1beta1.CapturedPacket{ + ObjectMeta: metav1.ObjectMeta{Name: r.mode}, + } + r.cancelFunc = cancelFunc + go func() { + var err error + var b *systemv1beta1.SupportBundle + if r.mode == modeAgent { + b, err = r.collectAgent(ctx, since) + } else if r.mode == modeController { + b, err = r.collectController(ctx, since) + } + func() { + r.statusLocker.Lock() + defer r.statusLocker.Unlock() + if err != nil { + klog.Errorf("Error when collecting supportBundle: %v", err) + r.cache.Status = systemv1beta1.SamplingPacketStatusNone + return + } + select { + case <-ctx.Done(): + default: + r.cache = b + } + }() + + if err == nil { + r.clean(ctx, b.Filepath, bundleExpireDuration) + } + }() + + return r.cache, nil +} + +func (r *samplingPacketsREST) New() runtime.Object { + return &systemv1beta1.SupportBundle{} +} + +func (r *samplingPacketsREST) Destroy() { +} + +// Get returns current status of the bundle. It only allows querying the resource +// whose name is equal to the mode. +func (r *samplingPacketsREST) Get(_ context.Context, name string, _ *metav1.GetOptions) (runtime.Object, error) { + r.statusLocker.RLock() + defer r.statusLocker.RUnlock() + if r.cache.Name != name { + return nil, errors.NewNotFound(systemv1beta1.Resource("supportBundle"), name) + } + return r.cache, nil +} + +// Delete can remove the current finished bundle or cancel a running bundle +// collecting. It only allows querying the resource whose name is equal to the mode. +func (r *samplingPacketsREST) Delete(_ context.Context, name string, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) { + if name != r.mode { + return nil, false, errors.NewNotFound(systemv1beta1.Resource("supportBundle"), name) + } + r.statusLocker.Lock() + defer r.statusLocker.Unlock() + if r.cancelFunc != nil { + r.cancelFunc() + } + r.cache = &systemv1beta1.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{Name: r.mode}, + Status: systemv1beta1.SupportBundleStatusNone, + } + return nil, true, nil +} + +func (r *samplingPacketsREST) NamespaceScoped() bool { + return false +} + +func (r *samplingPacketsREST) collect(ctx context.Context, dumpers ...func(string) error) (*systemv1beta1.SupportBundle, error) { + basedir, err := afero.TempDir(defaultFS, "", "bundle_tmp_") + if err != nil { + return nil, fmt.Errorf("error when creating tempdir: %w", err) + } + defer defaultFS.RemoveAll(basedir) + for _, dumper := range dumpers { + if err := dumper(basedir); err != nil { + return nil, err + } + } + outputFile, err := afero.TempFile(defaultFS, "", "bundle_*.tar.gz") + if err != nil { + return nil, fmt.Errorf("error when creating output tarfile: %w", err) + } + defer outputFile.Close() + hashSum, err := compress.PackDir(defaultFS, basedir, outputFile) + if err != nil { + return nil, fmt.Errorf("error when packaging supportBundle: %w", err) + } + + select { + case <-ctx.Done(): + _ = defaultFS.Remove(outputFile.Name()) + return nil, fmt.Errorf("collecting is canceled") + default: + } + stat, err := outputFile.Stat() + var fileSize int64 + if err == nil { + fileSize = stat.Size() + } + creationTime := metav1.Now() + deletionTime := metav1.NewTime(creationTime.Add(bundleExpireDuration)) + return &systemv1beta1.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.mode, + CreationTimestamp: creationTime, + DeletionTimestamp: &deletionTime, + }, + Status: systemv1beta1.SupportBundleStatusCollected, + Sum: fmt.Sprintf("%x", hashSum), + Size: uint32(fileSize), + Filepath: outputFile.Name(), + }, nil +} + +func (r *samplingPacketsREST) collectAgent(ctx context.Context, name string) (*systemv1beta1.SupportBundle, error) { + dumper := newAgentDumper(defaultFS, defaultExecutor, r.ovsCtlClient, r.aq, r.npq, since, r.v4Enabled, r.v6Enabled) + return r.collect( + ctx, + dumper.DumpLog, + dumper.DumpHostNetworkInfo, + dumper.DumpFlows, + dumper.DumpNetworkPolicyResources, + dumper.DumpAgentInfo, + dumper.DumpHeapPprof, + dumper.DumpOVSPorts, + dumper.DumpMemberlist, + ) +} + +func (r *samplingPacketsREST) collectController(ctx context.Context, since string) (*systemv1beta1.SupportBundle, error) { + dumper := support.NewControllerDumper(defaultFS, defaultExecutor, since) + return r.collect( + ctx, + dumper.DumpLog, + dumper.DumpNetworkPolicyResources, + dumper.DumpControllerInfo, + dumper.DumpHeapPprof, + ) +} + +func (r *samplingPacketsREST) clean(ctx context.Context, bundlePath string, duration time.Duration) { + select { + case <-ctx.Done(): + case <-clock.After(duration): + func() { + r.statusLocker.Lock() + defer r.statusLocker.Unlock() + select { // check the context again in case of cancellation when acquiring the lock. + case <-ctx.Done(): + default: + if r.cache.Status == systemv1beta1.SupportBundleStatusCollected { + r.cache = &systemv1beta1.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{Name: r.mode}, + Status: systemv1beta1.SupportBundleStatusNone, + } + } + } + }() + } + defaultFS.Remove(bundlePath) +} + +var ( + _ rest.Storage = new(downloadREST) + _ rest.Getter = new(downloadREST) + _ rest.StorageMetadata = new(downloadREST) +) + +// downloadREST implements the REST for downloading the bundle. +type downloadREST struct { + supportBundle *samplingPacketsREST +} + +func (d *downloadREST) New() runtime.Object { + return &systemv1beta1.SupportBundle{} +} + +func (d *downloadREST) Destroy() { +} + +func (d *downloadREST) Get(_ context.Context, _ string, _ *metav1.GetOptions) (runtime.Object, error) { + return &bundleStream{d.supportBundle.cache}, nil +} + +func (d *downloadREST) ProducesMIMETypes(_ string) []string { + return []string{"application/tar+gz"} +} + +func (d *downloadREST) ProducesObject(_ string) interface{} { + return "" +} + +var ( + _ rest.ResourceStreamer = new(bundleStream) + _ runtime.Object = new(bundleStream) +) + +type bundleStream struct { + cache *systemv1beta1.SupportBundle +} + +func (b *bundleStream) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (b *bundleStream) DeepCopyObject() runtime.Object { + panic("bundleStream does not have DeepCopyObject") +} + +func (b *bundleStream) InputStream(_ context.Context, _, _ string) (stream io.ReadCloser, flush bool, mimeType string, err error) { + // f will be closed by invoker, no need to close in this function. + f, err := defaultFS.Open(b.cache.Filepath) + if err != nil { + return nil, false, "", err + } + return f, true, "application/tar+gz", nil +} diff --git a/pkg/apiserver/registry/system/samplingpacket/rest_test.go b/pkg/apiserver/registry/system/samplingpacket/rest_test.go new file mode 100644 index 00000000000..118f12ba0ef --- /dev/null +++ b/pkg/apiserver/registry/system/samplingpacket/rest_test.go @@ -0,0 +1,347 @@ +// Copyright 2020 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 samplingpacket + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clockutils "k8s.io/utils/clock" + clocktesting "k8s.io/utils/clock/testing" + "k8s.io/utils/exec" + exectesting "k8s.io/utils/exec/testing" + + agentquerier "antrea.io/antrea/pkg/agent/querier" + agentqueriertest "antrea.io/antrea/pkg/agent/querier/testing" + system "antrea.io/antrea/pkg/apis/system/v1beta1" + "antrea.io/antrea/pkg/ovs/ovsctl" + ovsctltest "antrea.io/antrea/pkg/ovs/ovsctl/testing" + "antrea.io/antrea/pkg/querier" + queriertest "antrea.io/antrea/pkg/querier/testing" + "antrea.io/antrea/pkg/support" +) + +type testExec struct { + exectesting.FakeExec +} + +func (te *testExec) Command(cmd string, args ...string) exec.Cmd { + fakeCmd := new(exectesting.FakeCmd) + fakeCmd.CombinedOutputScript = append(fakeCmd.CombinedOutputScript, func() ([]byte, []byte, error) { + return []byte(fmt.Sprintf("%s %s", cmd, strings.Join(args, " "))), nil, nil + }) + return fakeCmd +} + +func TestREST(t *testing.T) { + r := &supportBundleREST{} + assert.Equal(t, &system.SupportBundle{}, r.New()) + assert.False(t, r.NamespaceScoped()) +} + +func TestClean(t *testing.T) { + defaultFS = afero.NewMemMapFs() + defaultExecutor = new(testExec) + defer func() { + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + }() + + for name, tc := range map[string]struct { + needCancel bool + duration time.Duration + }{ + "CleanByCancellation": { + needCancel: true, + duration: 1 * time.Hour, + }, + "CleanByTimeout": { + duration: 1 * time.Second, + }, + } { + t.Run(name, func(t *testing.T) { + fakeClock := clocktesting.NewFakeClock(time.Now()) + clock = fakeClock + defer func() { + clock = clockutils.RealClock{} + }() + f, err := defaultFS.Create("test.tar.gz") + require.NoError(t, err) + defer defaultFS.Remove(f.Name()) + require.NoError(t, f.Close()) + storage := NewControllerStorage() + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + require.False(t, fakeClock.HasWaiters()) + go storage.SupportBundle.clean(ctx, f.Name(), tc.duration) + // Wait for the clean function to have called clock.After: + // until it does, we cannot advance the fake clock. + require.Eventually(t, func() bool { + return fakeClock.HasWaiters() + }, 1*time.Second, 10*time.Millisecond) + if tc.needCancel { + cancelFunc() + } else { + fakeClock.Step(tc.duration) + } + assert.EventuallyWithT(t, func(c *assert.CollectT) { + exist, err := afero.Exists(defaultFS, f.Name()) + require.NoError(t, err) + assert.False(c, exist) + }, 1*time.Second, 10*time.Millisecond, "Supportbundle file was not deleted") + assert.Equal(t, system.SupportBundleStatusNone, storage.SupportBundle.cache.Status) + }) + } +} + +func TestCollect(t *testing.T) { + defaultFS = afero.NewMemMapFs() + defaultExecutor = new(testExec) + defer func() { + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + }() + + storage := NewControllerStorage() + dumper1Executed := false + dumper1 := func(string) error { + dumper1Executed = true + return nil + } + dumper2Executed := false + dumper2 := func(string) error { + dumper2Executed = true + return nil + } + collectedBundle, err := storage.SupportBundle.collect(context.TODO(), dumper1, dumper2) + require.NoError(t, err) + require.NotEmpty(t, collectedBundle.Filepath) + defer defaultFS.Remove(collectedBundle.Filepath) + assert.Equal(t, system.SupportBundleStatusCollected, collectedBundle.Status) + assert.NotEmpty(t, collectedBundle.Sum) + assert.Greater(t, collectedBundle.Size, uint32(0)) + assert.True(t, dumper1Executed) + assert.True(t, dumper2Executed) + exist, err := afero.Exists(defaultFS, collectedBundle.Filepath) + require.NoError(t, err) + require.True(t, exist) +} + +func TestControllerStorage(t *testing.T) { + defaultFS = afero.NewMemMapFs() + defaultExecutor = new(testExec) + defer func() { + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + }() + + storage := NewControllerStorage() + _, err := storage.SupportBundle.Create(context.TODO(), &system.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{ + Name: modeController, + }, + Status: system.SupportBundleStatusNone, + Since: "-1h", + }, nil, nil) + require.NoError(t, err) + + var collectedBundle *system.SupportBundle + assert.Eventually(t, func() bool { + object, err := storage.SupportBundle.Get(context.TODO(), modeController, nil) + require.NoError(t, err) + collectedBundle = object.(*system.SupportBundle) + return collectedBundle.Status == system.SupportBundleStatusCollected + }, time.Second*2, time.Millisecond*100) + filePath := collectedBundle.Filepath + require.NotEmpty(t, filePath) + defer defaultFS.Remove(filePath) + assert.NotEmpty(t, collectedBundle.Sum) + assert.Greater(t, collectedBundle.Size, uint32(0)) + exist, err := afero.Exists(defaultFS, filePath) + require.NoError(t, err) + require.True(t, exist) + + _, err = storage.SupportBundle.Get(context.TODO(), modeAgent, nil) + assert.Equal(t, errors.NewNotFound(system.Resource("supportBundle"), modeAgent), err) + + _, deleted, err := storage.SupportBundle.Delete(context.TODO(), modeAgent, nil, nil) + assert.Equal(t, errors.NewNotFound(system.Resource("supportBundle"), modeAgent), err) + assert.False(t, deleted) + + _, deleted, err = storage.SupportBundle.Delete(context.TODO(), modeController, nil, nil) + assert.NoError(t, err) + assert.True(t, deleted) + object, err := storage.SupportBundle.Get(context.TODO(), modeController, nil) + assert.NoError(t, err) + assert.Equal(t, &system.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{Name: modeController}, + Status: system.SupportBundleStatusNone, + }, object) + assert.Eventuallyf(t, func() bool { + exist, err := afero.Exists(defaultFS, filePath) + require.NoError(t, err) + return !exist + }, time.Second*2, time.Millisecond*100, "Supportbundle file %s was not deleted after deleting the Supportbundle object", filePath) +} + +type fakeAgentDumper struct { + returnErr error +} + +func (f *fakeAgentDumper) DumpFlows(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpHostNetworkInfo(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpLog(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpAgentInfo(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpNetworkPolicyResources(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpHeapPprof(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpOVSPorts(basedir string) error { + return f.returnErr +} + +func (f *fakeAgentDumper) DumpMemberlist(basedir string) error { + return f.returnErr +} + +func TestAgentStorage(t *testing.T) { + defaultFS = afero.NewMemMapFs() + defaultExecutor = new(testExec) + newAgentDumper = func(fs afero.Fs, executor exec.Interface, ovsCtlClient ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, since string, v4Enabled, v6Enabled bool) support.AgentDumper { + return &fakeAgentDumper{} + } + defer func() { + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + newAgentDumper = support.NewAgentDumper + }() + + ctx := context.Background() + ctrl := gomock.NewController(t) + fakeOVSCtl := ovsctltest.NewMockOVSCtlClient(ctrl) + fakeAgentQuerier := agentqueriertest.NewMockAgentQuerier(ctrl) + fakeNetworkPolicyQuerier := queriertest.NewMockAgentNetworkPolicyInfoQuerier(ctrl) + storage := NewAgentStorage(fakeOVSCtl, fakeAgentQuerier, fakeNetworkPolicyQuerier, true, true) + _, err := storage.SupportBundle.Create(ctx, &system.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{ + Name: modeAgent, + }, + Status: system.SupportBundleStatusNone, + Since: "-1h", + }, nil, nil) + require.NoError(t, err) + + var collectedBundle *system.SupportBundle + assert.Eventually(t, func() bool { + object, err := storage.SupportBundle.Get(ctx, modeAgent, nil) + require.NoError(t, err) + collectedBundle = object.(*system.SupportBundle) + return collectedBundle.Status == system.SupportBundleStatusCollected + }, time.Second*2, time.Millisecond*100) + require.NotEmpty(t, collectedBundle.Filepath) + filePath := collectedBundle.Filepath + defer defaultFS.Remove(filePath) + assert.NotEmpty(t, collectedBundle.Sum) + assert.Greater(t, collectedBundle.Size, uint32(0)) + exist, err := afero.Exists(defaultFS, filePath) + require.NoError(t, err) + require.True(t, exist) + + _, deleted, err := storage.SupportBundle.Delete(ctx, modeAgent, nil, nil) + assert.NoError(t, err) + assert.True(t, deleted) + object, err := storage.SupportBundle.Get(ctx, modeAgent, nil) + assert.NoError(t, err) + assert.Equal(t, &system.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{Name: modeAgent}, + Status: system.SupportBundleStatusNone, + }, object) + assert.Eventuallyf(t, func() bool { + exist, err := afero.Exists(defaultFS, filePath) + require.NoError(t, err) + return !exist + }, time.Second*2, time.Millisecond*100, "Supportbundle file %s was not deleted after deleting the Supportbundle object", filePath) +} + +func TestAgentStorageFailure(t *testing.T) { + defaultFS = afero.NewMemMapFs() + defaultExecutor = new(testExec) + newAgentDumper = func(fs afero.Fs, executor exec.Interface, ovsCtlClient ovsctl.OVSCtlClient, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, since string, v4Enabled, v6Enabled bool) support.AgentDumper { + return &fakeAgentDumper{returnErr: fmt.Errorf("iptables not found")} + } + defer func() { + defaultFS = afero.NewOsFs() + defaultExecutor = exec.New() + newAgentDumper = support.NewAgentDumper + }() + + ctx := context.Background() + ctrl := gomock.NewController(t) + fakeOVSCtl := ovsctltest.NewMockOVSCtlClient(ctrl) + fakeAgentQuerier := agentqueriertest.NewMockAgentQuerier(ctrl) + fakeNetworkPolicyQuerier := queriertest.NewMockAgentNetworkPolicyInfoQuerier(ctrl) + + storage := NewAgentStorage(fakeOVSCtl, fakeAgentQuerier, fakeNetworkPolicyQuerier, true, true) + _, err := storage.SupportBundle.Create(ctx, &system.SupportBundle{ + ObjectMeta: metav1.ObjectMeta{ + Name: modeAgent, + }, + Status: system.SupportBundleStatusNone, + Since: "-1h", + }, nil, nil) + require.NoError(t, err) + + var collectedBundle *system.SupportBundle + assert.Eventually(t, func() bool { + object, err := storage.SupportBundle.Get(ctx, modeAgent, nil) + require.NoError(t, err) + collectedBundle = object.(*system.SupportBundle) + return collectedBundle.Status == system.SupportBundleStatusNone + }, time.Second*2, time.Millisecond*100) + assert.Empty(t, collectedBundle.Filepath) + assert.Empty(t, collectedBundle.Sum) + assert.Empty(t, collectedBundle.Size) + + _, err = storage.SupportBundle.Get(ctx, modeController, nil) + assert.Equal(t, errors.NewNotFound(system.Resource("supportBundle"), modeController), err) + _, deleted, err := storage.SupportBundle.Delete(ctx, modeController, nil, nil) + assert.Equal(t, errors.NewNotFound(system.Resource("supportBundle"), modeController), err) + assert.False(t, deleted) +}