github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/gadget-collection/gadgets/traceloop/gadget.go (about)

     1  // Copyright 2019-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 traceloop
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"strings"
    22  	"sync"
    23  
    24  	log "github.com/sirupsen/logrus"
    25  	"sigs.k8s.io/controller-runtime/pkg/client"
    26  
    27  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-collection/gadgets"
    28  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/traceloop/types"
    29  
    30  	tracelooptracer "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/traceloop/tracer"
    31  
    32  	gadgetv1alpha1 "github.com/inspektor-gadget/inspektor-gadget/pkg/apis/gadget/v1alpha1"
    33  	containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection"
    34  )
    35  
    36  type containerSlim struct {
    37  	mntnsid  uint64
    38  	detached bool
    39  }
    40  
    41  type Trace struct {
    42  	client  client.Client
    43  	helpers gadgets.GadgetHelpers
    44  
    45  	started bool
    46  
    47  	containerIDs map[string]*containerSlim
    48  
    49  	trace *gadgetv1alpha1.Trace
    50  }
    51  
    52  type TraceFactory struct {
    53  	gadgets.BaseFactory
    54  }
    55  
    56  type traceSingleton struct {
    57  	sync.Mutex
    58  
    59  	tracer *tracelooptracer.Tracer
    60  	users  int
    61  }
    62  
    63  var traceUnique traceSingleton
    64  
    65  func NewFactory() gadgets.TraceFactory {
    66  	return &TraceFactory{
    67  		BaseFactory: gadgets.BaseFactory{DeleteTrace: deleteTrace},
    68  	}
    69  }
    70  
    71  func (f *TraceFactory) Description() string {
    72  	return `The traceloop gadget traces system calls in a similar way to strace but with
    73  some differences:
    74  
    75  * traceloop uses eBPF instead of ptrace
    76  * traceloop's tracing granularity is the container instead of a process
    77  * traceloop's traces are recorded in a fast, in-memory, overwritable ring
    78    buffer like a flight recorder. The tracing could be permanently enabled and
    79    inspected in case of crash.
    80  `
    81  }
    82  
    83  func (f *TraceFactory) OutputModesSupported() map[gadgetv1alpha1.TraceOutputMode]struct{} {
    84  	return map[gadgetv1alpha1.TraceOutputMode]struct{}{
    85  		gadgetv1alpha1.TraceOutputModeStatus: {},
    86  		gadgetv1alpha1.TraceOutputModeStream: {},
    87  	}
    88  }
    89  
    90  func deleteTrace(name string, t interface{}) {
    91  	trace := t.(*Trace)
    92  	if trace.started {
    93  		traceUnique.Lock()
    94  		defer traceUnique.Unlock()
    95  
    96  		traceUnique.users--
    97  		if traceUnique.users == 0 {
    98  			trace.helpers.Unsubscribe(genPubSubKey(name))
    99  
   100  			traceUnique.tracer.Stop()
   101  
   102  			traceUnique.tracer = nil
   103  		}
   104  	}
   105  }
   106  
   107  func (f *TraceFactory) Operations() map[gadgetv1alpha1.Operation]gadgets.TraceOperation {
   108  	n := func() interface{} {
   109  		return &Trace{
   110  			client:  f.Client,
   111  			helpers: f.Helpers,
   112  		}
   113  	}
   114  
   115  	return map[gadgetv1alpha1.Operation]gadgets.TraceOperation{
   116  		gadgetv1alpha1.OperationStart: {
   117  			Doc: "Start traceloop",
   118  			Operation: func(name string, trace *gadgetv1alpha1.Trace) {
   119  				f.LookupOrCreate(name, n).(*Trace).Start(trace)
   120  			},
   121  		},
   122  		gadgetv1alpha1.OperationCollect: {
   123  			Doc: "Collect traceloop",
   124  			Operation: func(name string, trace *gadgetv1alpha1.Trace) {
   125  				// To overcome Status.Output character limit, we decided to create a
   126  				// Stream Trace CRD each time we do the collect operation.
   127  				// Nonetheless, this Trace CRD will use a previously created Trace.
   128  				// To do so, we use the Parameters["name"] which will contain the name
   129  				// of the long lived Trace CRD, thus we will be able to get the Trace
   130  				// and so all the mntNsIDs associated to it.
   131  				t, err := f.Lookup(trace.Spec.Parameters["name"])
   132  				if err != nil {
   133  					trace.Status.OperationError = fmt.Sprintf("no global trace with name %q: %s", name, err)
   134  
   135  					return
   136  				}
   137  				t.(*Trace).Collect(trace)
   138  			},
   139  		},
   140  		gadgetv1alpha1.OperationDelete: {
   141  			Doc: "Delete a perf ring buffer owned by traceloop",
   142  			Operation: func(name string, trace *gadgetv1alpha1.Trace) {
   143  				// To overcome Status.Output character limit, we decided to create a
   144  				// Stream Trace CRD each time we do the collect operation.
   145  				// Nonetheless, this Trace CRD will use a previously created Trace.
   146  				// To do so, we use the Parameters["name"] which will contain the name
   147  				// of the long lived Trace CRD, thus we will be able to get the Trace
   148  				// and so all the mntNsIDs associated to it.
   149  				t, err := f.Lookup(trace.Spec.Parameters["name"])
   150  				if err != nil {
   151  					trace.Status.OperationError = fmt.Sprintf("no global trace with name %q: %s", name, err)
   152  
   153  					return
   154  				}
   155  				t.(*Trace).Delete(trace)
   156  			},
   157  		},
   158  		gadgetv1alpha1.OperationStop: {
   159  			Doc: "Stop traceloop",
   160  			Operation: func(name string, trace *gadgetv1alpha1.Trace) {
   161  				f.LookupOrCreate(name, n).(*Trace).Stop(trace)
   162  			},
   163  		},
   164  	}
   165  }
   166  
   167  type pubSubKey string
   168  
   169  func genPubSubKey(name string) pubSubKey {
   170  	return pubSubKey(fmt.Sprintf("gadget/traceloop/%s", name))
   171  }
   172  
   173  func (t *Trace) Start(trace *gadgetv1alpha1.Trace) {
   174  	if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus {
   175  		trace.Status.OperationError = fmt.Sprintf("\"start\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode)
   176  
   177  		return
   178  	}
   179  
   180  	if t.started {
   181  		trace.Status.State = gadgetv1alpha1.TraceStateStarted
   182  		return
   183  	}
   184  
   185  	// Having this backlink is mandatory for delete operation.
   186  	t.trace = trace
   187  
   188  	// The output will contain an array of types.TraceloopInfo.
   189  	// So, to avoid problems, we initialize it to be a JSON array.
   190  	trace.Status.Output = "[]"
   191  	t.containerIDs = make(map[string]*containerSlim, 0)
   192  
   193  	genKey := func(container *containercollection.Container) string {
   194  		return container.K8s.Namespace + "/" + container.K8s.PodName
   195  	}
   196  
   197  	attachContainerFunc := func(container *containercollection.Container) error {
   198  		containerID := container.Runtime.ContainerID
   199  		mntNsID := container.Mntns
   200  		key := genKey(container)
   201  
   202  		traceUnique.Lock()
   203  		err := traceUnique.tracer.Attach(containerID, mntNsID)
   204  		traceUnique.Unlock()
   205  		if err != nil {
   206  			log.Errorf("failed to attach tracer: %s", err)
   207  
   208  			return err
   209  		}
   210  
   211  		t.containerIDs[containerID] = &containerSlim{
   212  			mntnsid: mntNsID,
   213  		}
   214  		log.Debugf("tracer attached for %q (%d)", key, mntNsID)
   215  
   216  		var infos []types.TraceloopInfo
   217  		err = json.Unmarshal([]byte(trace.Status.Output), &infos)
   218  		if err != nil {
   219  			log.Errorf("failed to unmarshal output: %s", err)
   220  
   221  			return err
   222  		}
   223  
   224  		infos = append(infos, types.TraceloopInfo{
   225  			Namespace:     container.K8s.Namespace,
   226  			Podname:       container.K8s.PodName,
   227  			Containername: container.K8s.ContainerName,
   228  			ContainerID:   containerID,
   229  		})
   230  
   231  		output, err := json.Marshal(infos)
   232  		if err != nil {
   233  			log.Errorf("failed to marshal infos: %s", err)
   234  
   235  			return err
   236  		}
   237  
   238  		traceBeforePatch := trace.DeepCopy()
   239  		trace.Status.Output = string(output)
   240  		patch := client.MergeFrom(traceBeforePatch)
   241  
   242  		// The surrounding function can be called from any context.
   243  		// So, we need to manually patch the trace CRD to have our modifications be
   244  		// taken into account.
   245  		err = t.client.Status().Patch(context.TODO(), trace, patch)
   246  		if err != nil {
   247  			log.Errorf("Failed to patch trace %q output: %s", trace.Name, err)
   248  
   249  			return err
   250  		}
   251  
   252  		return nil
   253  	}
   254  
   255  	detachContainerFunc := func(container *containercollection.Container) {
   256  		mntNsID := container.Mntns
   257  		key := genKey(container)
   258  
   259  		traceUnique.Lock()
   260  		err := traceUnique.tracer.Detach(mntNsID)
   261  		traceUnique.Unlock()
   262  		if err != nil {
   263  			log.Errorf("failed to detach tracer: %s", err)
   264  
   265  			return
   266  		}
   267  
   268  		_, ok := t.containerIDs[container.Runtime.ContainerID]
   269  		if ok {
   270  			t.containerIDs[container.Runtime.ContainerID].detached = true
   271  		} else {
   272  			log.Errorf("trace does not know about container with ID %q", container.Runtime.ContainerID)
   273  
   274  			return
   275  		}
   276  
   277  		log.Debugf("tracer detached for %q (%d)", key, mntNsID)
   278  	}
   279  
   280  	containerEventCallback := func(event containercollection.PubSubEvent) {
   281  		switch event.Type {
   282  		case containercollection.EventTypeAddContainer:
   283  			attachContainerFunc(event.Container)
   284  		case containercollection.EventTypeRemoveContainer:
   285  			detachContainerFunc(event.Container)
   286  		}
   287  	}
   288  
   289  	traceUnique.Lock()
   290  	if traceUnique.tracer == nil {
   291  		var syscallFilters string
   292  		var err error
   293  
   294  		filters, ok := trace.Spec.Parameters["syscall-filters"]
   295  		if ok {
   296  			syscallFilters = filters
   297  		}
   298  
   299  		traceUnique.tracer, err = tracelooptracer.NewTracer(t.helpers, strings.Split(syscallFilters, ","))
   300  		if err != nil {
   301  			traceUnique.Unlock()
   302  
   303  			trace.Status.OperationError = fmt.Sprintf("Failed to start traceloop tracer: %s", err)
   304  
   305  			return
   306  		}
   307  	}
   308  	traceUnique.users++
   309  	traceUnique.Unlock()
   310  
   311  	existingContainers := t.helpers.Subscribe(
   312  		genPubSubKey(trace.ObjectMeta.Namespace+"/"+trace.ObjectMeta.Name),
   313  		*gadgets.ContainerSelectorFromContainerFilter(trace.Spec.Filter),
   314  		containerEventCallback,
   315  	)
   316  
   317  	for _, c := range existingContainers {
   318  		err := attachContainerFunc(c)
   319  		if err != nil {
   320  			log.Warnf("couldn't attach BPF program: %s", err)
   321  
   322  			continue
   323  		}
   324  	}
   325  	t.started = true
   326  
   327  	trace.Status.State = gadgetv1alpha1.TraceStateStarted
   328  }
   329  
   330  func (t *Trace) Collect(trace *gadgetv1alpha1.Trace) {
   331  	if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStream {
   332  		trace.Status.OperationError = fmt.Sprintf("\"collect\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStream, trace.Spec.OutputMode)
   333  
   334  		return
   335  	}
   336  
   337  	traceUnique.Lock()
   338  	if traceUnique.tracer == nil {
   339  		traceUnique.Unlock()
   340  
   341  		trace.Status.OperationError = "Traceloop tracer is nil"
   342  
   343  		return
   344  	}
   345  	traceUnique.Unlock()
   346  
   347  	containerID := trace.Spec.Parameters["containerID"]
   348  	_, ok := t.containerIDs[containerID]
   349  	if !ok {
   350  		ids := make([]string, len(t.containerIDs))
   351  		i := 0
   352  		for id := range t.containerIDs {
   353  			ids[i] = id
   354  			i++
   355  		}
   356  
   357  		trace.Status.OperationError = fmt.Sprintf("%q is not a valid ID for this trace, valid IDs are: %v", containerID, strings.Join(ids, ","))
   358  
   359  		return
   360  	}
   361  
   362  	traceUnique.Lock()
   363  	events, err := traceUnique.tracer.Read(containerID)
   364  	traceUnique.Unlock()
   365  	if err != nil {
   366  		trace.Status.OperationError = fmt.Sprintf("Failed to read perf buffer: %s", err)
   367  
   368  		return
   369  	}
   370  
   371  	traceName := gadgets.TraceName(trace.ObjectMeta.Namespace, trace.ObjectMeta.Name)
   372  	r, err := json.Marshal(events)
   373  	if err != nil {
   374  		log.Warnf("Gadget %s: error marshaling event: %s", trace.Spec.Gadget, err)
   375  		return
   376  	}
   377  	// HACK Traceloop is really particular as it cannot use Status output because
   378  	// the size is limited.
   379  	// Also, if we send each event as a line, we will only be able to get the last
   380  	// 100 (or 250) events from the CLI due to this code:
   381  	// https://github.com/inspektor-gadget/inspektor-gadget/blob/9c7b6a126d82b54262ffdc5709d7c92480002830/pkg/gadgettracermanager/stream/stream.go#L24
   382  	// https://github.com/inspektor-gadget/inspektor-gadget/blob/9c7b6a126d82b54262ffdc5709d7c92480002830/pkg/gadgettracermanager/stream/stream.go#L95-L100
   383  	// To overcome this limitation, we just send all events as one big line.
   384  	// Then, the CLI receives this big line, parses it and prints each event.
   385  	// A proper solution would be to develop a specific "output" (neither status
   386  	// nor stream) for traceloop, this is let as TODO.
   387  	t.helpers.PublishEvent(traceName, string(r))
   388  
   389  	trace.Status.State = gadgetv1alpha1.TraceStateCompleted
   390  }
   391  
   392  func (t *Trace) Delete(trace *gadgetv1alpha1.Trace) {
   393  	if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus {
   394  		trace.Status.OperationError = fmt.Sprintf("\"delete\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode)
   395  
   396  		return
   397  	}
   398  
   399  	containerID := trace.Spec.Parameters["containerID"]
   400  	container, ok := t.containerIDs[containerID]
   401  	if !ok {
   402  		ids := make([]string, len(t.containerIDs))
   403  		i := 0
   404  		for id := range t.containerIDs {
   405  			ids[i] = id
   406  			i++
   407  		}
   408  
   409  		trace.Status.OperationError = fmt.Sprintf("%q is not a valid ID for this trace, valid IDs are: %v", containerID, strings.Join(ids, ","))
   410  
   411  		return
   412  	}
   413  
   414  	traceUnique.Lock()
   415  	if traceUnique.tracer == nil {
   416  		traceUnique.Unlock()
   417  
   418  		trace.Status.OperationError = "Traceloop tracer is nil"
   419  
   420  		return
   421  	}
   422  
   423  	// First, we need to detach the perf buffer.
   424  	// We do not check the returned error because if the container was deleted it
   425  	// was already detached.
   426  	if !container.detached {
   427  		_ = traceUnique.tracer.Detach(container.mntnsid)
   428  	}
   429  
   430  	// Then we can remove it.
   431  	err := traceUnique.tracer.Delete(containerID)
   432  	traceUnique.Unlock()
   433  	if err != nil {
   434  		trace.Status.OperationError = fmt.Sprintf("Failed to delete perf buffer: %s", err)
   435  
   436  		return
   437  	}
   438  
   439  	// We can now remove containerID from the map.
   440  	delete(t.containerIDs, containerID)
   441  
   442  	// Finally, we need to update the trace output.
   443  	var infos []types.TraceloopInfo
   444  	err = json.Unmarshal([]byte(t.trace.Status.Output), &infos)
   445  	if err != nil {
   446  		trace.Status.OperationError = fmt.Sprintf("failed to unmarshal output: %s", err)
   447  
   448  		return
   449  	}
   450  
   451  	newInfos := make([]types.TraceloopInfo, len(infos)-1)
   452  	i := 0
   453  	for _, info := range infos {
   454  		// We copy all the current information except the one corresponding to the
   455  		// container we removed.
   456  		if info.ContainerID == containerID {
   457  			continue
   458  		}
   459  
   460  		newInfos[i] = info
   461  		i++
   462  	}
   463  
   464  	output, err := json.Marshal(newInfos)
   465  	if err != nil {
   466  		trace.Status.OperationError = fmt.Sprintf("failed to marshal new infos: %s", err)
   467  
   468  		return
   469  	}
   470  
   471  	traceBeforePatch := t.trace.DeepCopy()
   472  	t.trace.Status.Output = string(output)
   473  	patch := client.MergeFrom(traceBeforePatch)
   474  
   475  	// We also need to manually patch the trace CRD to have our modifications be
   476  	// taken into account.
   477  	err = t.client.Status().Patch(context.TODO(), t.trace, patch)
   478  	if err != nil {
   479  		log.Errorf("failed to patch trace %q output: %s", trace.Name, err)
   480  
   481  		return
   482  	}
   483  
   484  	trace.Status.State = gadgetv1alpha1.TraceStateCompleted
   485  }
   486  
   487  func (t *Trace) Stop(trace *gadgetv1alpha1.Trace) {
   488  	if trace.Spec.OutputMode != gadgetv1alpha1.TraceOutputModeStatus {
   489  		trace.Status.OperationError = fmt.Sprintf("\"stop\" operation can only be used with %q trace while %q was given", gadgetv1alpha1.TraceOutputModeStatus, trace.Spec.OutputMode)
   490  
   491  		return
   492  	}
   493  
   494  	if !t.started {
   495  		trace.Status.OperationError = "Not started"
   496  		return
   497  	}
   498  
   499  	t.helpers.Unsubscribe(genPubSubKey(trace.ObjectMeta.Namespace + "/" + trace.ObjectMeta.Name))
   500  
   501  	traceUnique.Lock()
   502  	traceUnique.users--
   503  	if traceUnique.users == 0 {
   504  		traceUnique.tracer.Stop()
   505  
   506  		traceUnique.tracer = nil
   507  	}
   508  	traceUnique.Unlock()
   509  
   510  	t.started = false
   511  
   512  	trace.Status.State = gadgetv1alpha1.TraceStateStopped
   513  }