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 })