github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/controllers/trace_controller_test.go (about)

     1  // Copyright 2021 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package controllers
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sync"
    21  
    22  	. "github.com/onsi/ginkgo"
    23  	. "github.com/onsi/gomega"
    24  	gomegatype "github.com/onsi/gomega/types"
    25  
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"sigs.k8s.io/controller-runtime/pkg/client"
    28  
    29  	gadgetv1alpha1 "github.com/inspektor-gadget/inspektor-gadget/pkg/apis/gadget/v1alpha1"
    30  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-collection/gadgets"
    31  )
    32  
    33  // FakeFactory is a fake implementation of the TraceFactory interface for
    34  // tests. It records the calls to its methods for assertions in the unit tests.
    35  type FakeFactory struct {
    36  	gadgets.BaseFactory
    37  	mu    sync.Mutex
    38  	calls map[string]struct{}
    39  }
    40  
    41  func NewFakeFactory() gadgets.TraceFactory {
    42  	return &FakeFactory{
    43  		BaseFactory: gadgets.BaseFactory{DeleteTrace: deleteTrace},
    44  		calls:       make(map[string]struct{}),
    45  	}
    46  }
    47  
    48  func deleteTrace(name string, trace interface{}) {
    49  	f := trace.(*FakeFactory)
    50  	f.mu.Lock()
    51  	f.calls["delete/"+name] = struct{}{}
    52  	f.mu.Unlock()
    53  }
    54  
    55  func (f *FakeFactory) OutputModesSupported() map[gadgetv1alpha1.TraceOutputMode]struct{} {
    56  	return map[gadgetv1alpha1.TraceOutputMode]struct{}{
    57  		gadgetv1alpha1.TraceOutputModeStatus: {},
    58  	}
    59  }
    60  
    61  func (f *FakeFactory) Operations() map[gadgetv1alpha1.Operation]gadgets.TraceOperation {
    62  	n := func() interface{} {
    63  		return f
    64  	}
    65  	return map[gadgetv1alpha1.Operation]gadgets.TraceOperation{
    66  		"magic": {
    67  			Doc: "Collect a snapshot of the list of sockets",
    68  			Operation: func(name string, trace *gadgetv1alpha1.Trace) {
    69  				f.LookupOrCreate(name, n).(*FakeFactory).Magic(trace)
    70  			},
    71  		},
    72  	}
    73  }
    74  
    75  func (f *FakeFactory) Magic(trace *gadgetv1alpha1.Trace) {
    76  	f.mu.Lock()
    77  	key := fmt.Sprintf("operation/%s/%s/%s/",
    78  		trace.ObjectMeta.Namespace,
    79  		trace.ObjectMeta.Name,
    80  		"magic",
    81  	)
    82  	f.calls[key] = struct{}{}
    83  	f.mu.Unlock()
    84  
    85  	trace.Status.OperationError = "FakeError"
    86  	trace.Status.OperationWarning = "FakeWarning"
    87  	trace.Status.State = gadgetv1alpha1.TraceStateCompleted
    88  	trace.Status.Output = "FakeOutput"
    89  }
    90  
    91  // methodHasBeenCalled is a helper function to check if a method has been
    92  // called on the gadget
    93  func (f *FakeFactory) methodHasBeenCalled(key string) bool {
    94  	f.mu.Lock()
    95  	defer f.mu.Unlock()
    96  	_, ok := f.calls[key]
    97  	delete(f.calls, key)
    98  	return ok
    99  }
   100  
   101  // OperationMethodHasBeenCalled returns a Gomega assertion checking if the
   102  // method Operation() has been called
   103  func OperationMethodHasBeenCalled(factory gadgets.TraceFactory, name, operation string) func() bool {
   104  	fakeGadget := factory.(*FakeFactory)
   105  	key := fmt.Sprintf("operation/%s/%s/",
   106  		name,
   107  		operation,
   108  	)
   109  	return func() bool {
   110  		return fakeGadget.methodHasBeenCalled(key)
   111  	}
   112  }
   113  
   114  // DeleteMethodHasBeenCalled returns a Gomega assertion checking if the method
   115  // Delete() has been called
   116  func DeleteMethodHasBeenCalled(factory gadgets.TraceFactory, name string) func() bool {
   117  	fakeGadget := factory.(*FakeFactory)
   118  	key := "delete/" + name
   119  	return func() bool {
   120  		return fakeGadget.methodHasBeenCalled(key)
   121  	}
   122  }
   123  
   124  // UpdatedTrace returns a function that fetches the Trace object in a way that
   125  // can be used in Gomega's 'Eventually' or 'Consistently' methods.
   126  func UpdatedTrace(ctx context.Context, key client.ObjectKey) func() *gadgetv1alpha1.Trace {
   127  	trace := &gadgetv1alpha1.Trace{}
   128  
   129  	return func() *gadgetv1alpha1.Trace {
   130  		err := k8sClient.Get(ctx, key, trace)
   131  		if err != nil {
   132  			return nil
   133  		} else {
   134  			return trace
   135  		}
   136  	}
   137  }
   138  
   139  // HaveState returns a GomegaMatcher that checks if the Trace.Status.State has
   140  // the expected value
   141  func HaveState(expectedState gadgetv1alpha1.TraceState) gomegatype.GomegaMatcher {
   142  	return WithTransform(func(trace *gadgetv1alpha1.Trace) gadgetv1alpha1.TraceState {
   143  		if trace == nil {
   144  			return "<trace is nil>"
   145  		}
   146  		return trace.Status.State
   147  	}, Equal(expectedState))
   148  }
   149  
   150  // HaveOperationError returns a GomegaMatcher that checks if the
   151  // Trace.Status.OperationError has the expected value
   152  func HaveOperationError(expectedOperationError string) gomegatype.GomegaMatcher {
   153  	return WithTransform(func(trace *gadgetv1alpha1.Trace) string {
   154  		if trace == nil {
   155  			return "<trace is nil>"
   156  		}
   157  		return trace.Status.OperationError
   158  	}, Equal(expectedOperationError))
   159  }
   160  
   161  // HaveOperationWarning returns a GomegaMatcher that checks if the
   162  // Trace.Status.OperationWarning has the expected value
   163  func HaveOperationWarning(expectedOperationWarning string) gomegatype.GomegaMatcher {
   164  	return WithTransform(func(trace *gadgetv1alpha1.Trace) string {
   165  		if trace == nil {
   166  			return "<trace is nil>"
   167  		}
   168  		return trace.Status.OperationWarning
   169  	}, Equal(expectedOperationWarning))
   170  }
   171  
   172  // HaveOutput returns a GomegaMatcher that checks if the Trace.Status.Output
   173  // has the expected value
   174  func HaveOutput(expectedOutput string) gomegatype.GomegaMatcher {
   175  	return WithTransform(func(trace *gadgetv1alpha1.Trace) string {
   176  		if trace == nil {
   177  			return "<trace is nil>"
   178  		}
   179  		return trace.Status.Output
   180  	}, Equal(expectedOutput))
   181  }
   182  
   183  // HaveAnnotation returns a GomegaMatcher that checks if the Trace
   184  // has an annotation with the expected value
   185  func HaveAnnotation(annotation, expectedOperation string) gomegatype.GomegaMatcher {
   186  	return WithTransform(func(trace *gadgetv1alpha1.Trace) string {
   187  		if trace == nil {
   188  			return "<trace is nil>"
   189  		}
   190  		annotations := trace.GetAnnotations()
   191  		if annotations == nil {
   192  			return ""
   193  		}
   194  		op := annotations[annotation]
   195  		return op
   196  	}, Equal(expectedOperation))
   197  }
   198  
   199  // Tests
   200  
   201  var _ = Context("Controller with a fake gadget", func() {
   202  	ctx := context.TODO()
   203  	traceFactories := make(map[string]gadgets.TraceFactory)
   204  	fakeFactory := NewFakeFactory()
   205  	traceFactories["fakegadget"] = fakeFactory
   206  
   207  	ns := SetupTest(ctx, traceFactories)
   208  
   209  	Describe("when no existing resources exist", func() {
   210  		It("should create a new Trace resource", func() {
   211  			traceObjectKey := client.ObjectKey{
   212  				Name:      "mytrace",
   213  				Namespace: ns.Name,
   214  			}
   215  
   216  			myTrace := &gadgetv1alpha1.Trace{
   217  				ObjectMeta: metav1.ObjectMeta{
   218  					Name:      traceObjectKey.Name,
   219  					Namespace: traceObjectKey.Namespace,
   220  					Annotations: map[string]string{
   221  						GadgetOperation:  "magic",
   222  						"hiking.walking": "mountains",
   223  					},
   224  				},
   225  				Spec: gadgetv1alpha1.TraceSpec{
   226  					Node:       "fake-node",
   227  					Gadget:     "fakegadget",
   228  					RunMode:    gadgetv1alpha1.RunModeManual,
   229  					OutputMode: gadgetv1alpha1.TraceOutputModeStatus,
   230  				},
   231  			}
   232  
   233  			Consistently(UpdatedTrace(ctx, traceObjectKey)).Should(BeNil())
   234  
   235  			err := k8sClient.Create(ctx, myTrace)
   236  			Expect(err).NotTo(HaveOccurred(), "failed to create test Trace resource")
   237  
   238  			Eventually(OperationMethodHasBeenCalled(fakeFactory, traceObjectKey.String(), "magic")).Should(BeTrue())
   239  
   240  			Eventually(UpdatedTrace(ctx, traceObjectKey)).Should(SatisfyAll(
   241  				HaveState(gadgetv1alpha1.TraceStateCompleted),
   242  				HaveOperationError("FakeError"),
   243  				HaveOperationWarning("FakeWarning"),
   244  				HaveOutput("FakeOutput"),
   245  				HaveAnnotation(GadgetOperation, ""),
   246  				HaveAnnotation("hiking.walking", "mountains"),
   247  			))
   248  
   249  			Consistently(OperationMethodHasBeenCalled(fakeFactory, traceObjectKey.String(), "magic")).Should(BeFalse())
   250  
   251  			err = k8sClient.Delete(ctx, myTrace)
   252  			Expect(err).NotTo(HaveOccurred(), "failed to delete test Trace resource")
   253  
   254  			Eventually(DeleteMethodHasBeenCalled(fakeFactory, traceObjectKey.String())).Should(BeTrue())
   255  			Consistently(DeleteMethodHasBeenCalled(fakeFactory, traceObjectKey.String())).Should(BeFalse())
   256  		})
   257  	})
   258  })