github.com/inspektor-gadget/inspektor-gadget@v0.28.1/docs/devel/hello-world-gadget.md (about)

     1  ---
     2  title: Hello world gadget
     3  weight: 110
     4  description: >
     5    Hello world gadget
     6  ---
     7  
     8  > [!WARNING]
     9  > This feature is experimental. To activate the commands, you must set the `IG_EXPERIMENTAL` environment variable to `true`.
    10  >
    11  > ```bash
    12  > $ export IG_EXPERIMENTAL=true
    13  > ```
    14  
    15  This is a short getting started guide to write your first gadget. This guide will get you familiar
    16  with the key concepts by implementing a simplified version of the "trace open" (opensnoop) tool.
    17  
    18  ## Starting from a template
    19  
    20  If you want to create a new repository for your gadget, you can use the [gadget-template
    21  repository](https://github.com/inspektor-gadget/gadget-template). This is a
    22  [GitHub tempate repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
    23  
    24  You can also look for examples in gadgets published on Artifact Hub:
    25  
    26  [![Artifact Hub: Gadgets](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/gadgets)](https://artifacthub.io/packages/search?repo=gadgets)
    27  
    28  ## Starting from scratch
    29  
    30  If you already have a git repository for your project and want to add a gadget
    31  to it, you can start from scratch. The rest of this guide assumes you will
    32  start from scratch.
    33  
    34  The first step is to create an empty folder where the source code of the gadget will be stored:
    35  
    36  ```bash
    37  $ mkdir mygadget
    38  ```
    39  
    40  ## Implementing the eBPF program
    41  
    42  The eBPF code contains the source code for the programs that are injected in the kernel to collect
    43  information. Let's create a file called `program.bpf.c` and put the following contents in there.
    44  
    45  The first thing we need is to include some header files.
    46  
    47  ```c
    48  // Kernel types definitions
    49  // Check https://blog.aquasec.com/vmlinux.h-ebpf-programs for more details
    50  #include <vmlinux.h>
    51  
    52  // eBPF helpers signatures
    53  // Check https://man7.org/linux/man-pages/man7/bpf-helpers.7.html to learn
    54  // more about different available helpers
    55  #include <bpf/bpf_helpers.h>
    56  
    57  // Inspektor Gadget buffer
    58  #include <gadget/buffer.h>
    59  
    60  // Inspektor Gadget macros
    61  #include <gadget/macros.h>
    62  ```
    63  
    64  Then, we have to specify a structure with all the information our gadget will provide. Let's only
    65  put the pid for the time being.
    66  
    67  ```c
    68  struct event {
    69  	__u32 pid;
    70  };
    71  ```
    72  
    73  Then, create a buffer eBPF map to send events to user space:
    74  
    75  ```c
    76  // events is the name of the buffer map and 1024 * 256 is its size.
    77  GADGET_TRACER_MAP(events, 1024 * 256);
    78  ```
    79  
    80  This macro will automatically create a ring buffer if the kernel supports it.
    81  Otherwise, a perf array will be created.
    82  
    83  Optionally, you can employ the `GADGET_TRACER` macro to define a tracer with the
    84  following parameters:
    85  
    86  - Tracer's Name: `open`
    87  - Buffer Map Name: `events`
    88  - Event Structure Name: `event`
    89  
    90  This information enables Inspektor Gadget to generate the metadata file automatically.
    91  Refer to the [metadata file](#creating-a-metadata-file) section for detailed instructions.
    92  
    93  ```c
    94  // [Optional] Define a tracer
    95  GADGET_TRACER(open, events, event);
    96  ```
    97  
    98  After that, we need to define a program that is attached to a hook that provides the information we
    99  need, in this case we'll attach to a tracepoint that is called each time the openat() syscall
   100  is executed.
   101  
   102  This program collects the information to fill the event (only pid for now), and then calls
   103  `gadget_submit_buf()` helper to send the event to user space.
   104  
   105  ```c
   106  SEC("tracepoint/syscalls/sys_enter_openat")
   107  int enter_openat(struct syscall_trace_enter *ctx)
   108  {
   109  	struct event *event;
   110  
   111  	event = gadget_reserve_buf(&events, sizeof(*event));
   112  	if (!event)
   113  		return 0;
   114  
   115  	event->pid = bpf_get_current_pid_tgid() >> 32;
   116  
   117  	gadget_submit_buf(ctx, &events, event, sizeof(*event));
   118  
   119  	return 0;
   120  }
   121  ```
   122  
   123  Finally, it's needed to define the license of the eBPF code.
   124  
   125  ```c
   126  char LICENSE[] SEC("license") = "GPL";
   127  ```
   128  
   129  The full file should look like:
   130  
   131  ```c
   132  // program.bpf.c
   133  
   134  // Kernel types definitions
   135  // Check https://blog.aquasec.com/vmlinux.h-ebpf-programs for more details
   136  #include <vmlinux.h>
   137  
   138  // eBPF helpers signatures
   139  // Check https://man7.org/linux/man-pages/man7/bpf-helpers.7.html to learn
   140  // more about different available helpers
   141  #include <bpf/bpf_helpers.h>
   142  
   143  // Inspektor Gadget buffer
   144  #include <gadget/buffer.h>
   145  
   146  // Inspektor Gadget macros
   147  #include <gadget/macros.h>
   148  
   149  struct event {
   150  	__u32 pid;
   151  };
   152  
   153  // events is the name of the buffer map and 1024 * 256 is its size.
   154  GADGET_TRACER_MAP(events, 1024 * 256);
   155  
   156  // [Optional] Define a tracer
   157  GADGET_TRACER(open, events, event);
   158  
   159  SEC("tracepoint/syscalls/sys_enter_openat")
   160  int enter_openat(struct syscall_trace_enter *ctx)
   161  {
   162  	struct event *event;
   163  
   164  	event = gadget_reserve_buf(&events, sizeof(*event));
   165  	if (!event)
   166  		return 0;
   167  
   168  	event->pid = bpf_get_current_pid_tgid() >> 32;
   169  
   170  	gadget_submit_buf(ctx, &events, event, sizeof(*event));
   171  
   172  	return 0;
   173  }
   174  
   175  char LICENSE[] SEC("license") = "GPL";
   176  ```
   177  
   178  ## Building the gadget for the first time
   179  
   180  We can now compile our gadget. You don't need to have any dependency on the machine, the `image
   181  build` by default uses docker to run a container with all dependencies to compile the code.
   182  
   183  ```bash
   184  $ cd mygadget
   185  $ sudo -E ig image build -t mygadget:latest .
   186  INFO[0000] Experimental features enabled
   187  Successfully built ghcr.io/inspektor-gadget/gadget/mygadget:latest@sha256:dd3f5c357983bb863ef86942e36f4c851933eec4b32ba65ee375acb1c514f628
   188  ```
   189  
   190  Take into account that it is possible to customize the build process by defining a `build.yaml` file.
   191  Check the [Customizing your build](../../docs/core-concepts/images.md#customizing-your-build)
   192  section for more details.
   193  
   194  ## (Optional) Pushing the gadget image to a container registry
   195  
   196  You could push the gadget to a remote container registry. If you're using the same machine for
   197  building and running the gadget, this step can be skipped.
   198  
   199  ```bash
   200  $ sudo -E ig image tag mygadget:latest ghcr.io/my-org/mygadget:latest
   201  INFO[0000] Experimental features enabled
   202  Successfully tagged with ghcr.io/my-org/mygadget:latest@sha256:dd3f5c357983bb863ef86942e36f4c851933eec4b32ba65ee375acb1c514f628
   203  
   204  $ sudo -E ig image push ghcr.io/my-org/mygadget:latest
   205  INFO[0000] Experimental features enabled
   206  Pushing ghcr.io/my-org/mygadget:latest...
   207  Successfully pushed ghcr.io/my-org/mygadget:latest@sha256:dd3f5c357983bb863ef86942e36f4c851933eec4b32ba65ee375acb1c514f628
   208  ```
   209  
   210  ## Running the gadget
   211  
   212  We're now all set to run our gadget for the first time.
   213  
   214  ```bash
   215  $ sudo -E ig run mygadget:latest
   216  INFO[0000] Experimental features enabled
   217  PID
   218  1113
   219  1113
   220  1113
   221  1113
   222  1113
   223  1113
   224  1113
   225  1113
   226  1113
   227  1113
   228  1113
   229  1113
   230  1113
   231  1113
   232  1113
   233  1219
   234  220121
   235  ```
   236  
   237  Great, our program already shows the PID! Can we improve it further?
   238  
   239  ## Adding more information to the gadget
   240  
   241  Let's add some more information to our event, like command and file name.
   242  
   243  Add the fields in the event structure.
   244  
   245  ```c
   246  #define NAME_MAX 255
   247  
   248  struct event {
   249  	__u32 pid;
   250  	__u8 comm[TASK_COMM_LEN];
   251  	__u8 filename[NAME_MAX];
   252  };
   253  ```
   254  
   255  Now create the logic to fill those fields in the `enter_openat` program. Insert them after you have
   256  reserved space for your event structure and before you submit the buffer.
   257  
   258  ```c
   259  	bpf_get_current_comm(event->comm, sizeof(event->comm));
   260  	bpf_probe_read_user_str(event->filename, sizeof(event->filename), (const char *)ctx->args[1]);
   261  ```
   262  
   263  Build and run the gadget again. Now it provides more information.
   264  
   265  ```bash
   266  $ sudo -E ig image build -t mygadget:latest .
   267  ....
   268  $ sudo -E ig run mygadget:latest
   269  INFO[0000] Experimental features enabled
   270  PID                      COMM                     FILENAME
   271  11305                    Chrome_ChildIOT          /dev/shm/.org.chromium.…
   272  11305                    ThreadPoolForeg          /home/mvb/.config/Slack…
   273  11305                    Chrome_ChildIOT          /dev/shm/.org.chromium.…
   274  11305                    ThreadPoolForeg          /home/mvb/.config/Slack…
   275  11305                    Chrome_ChildIOT          /dev/shm/.org.chromium.…
   276  1349                     containerd               /var/lib/containerd/io.…
   277  1349                     containerd               /var/lib/containerd/io.…
   278  1349                     containerd               /var/lib/containerd/io.…
   279  ```
   280  
   281  ## Creating a metadata file
   282  
   283  The above formatting is not totally great, the pid column is taking a lot of space while the
   284  filename is being trimmed. The metadata file contains extra information about the gadget, among
   285  other things, it can be used to specify the format to be used.
   286  
   287  An initial version of the metadata file can be created by passing `--update-metadata` to the build command:
   288  
   289  > [!NOTE]
   290  > The `tracers` and `structs` sections will only be generated if the eBPF program defined a tracer
   291  > using the `GADGET_TRACER` macro.
   292  
   293  ```bash
   294  $ sudo -E ig image build . -t mygadget --update-metadata
   295  ```
   296  
   297  It'll create a `gadget.yaml` file:
   298  
   299  ```yaml
   300  name: 'TODO: Fill the gadget name'
   301  description: 'TODO: Fill the gadget description'
   302  homepageURL: 'TODO: Fill the gadget homepage URL'
   303  documentationURL: 'TODO: Fill the gadget documentation URL'
   304  sourceURL: 'TODO: Fill the gadget source code URL'
   305  tracers:
   306    open:
   307      mapName: events
   308      structName: event
   309  structs:
   310    event:
   311      fields:
   312      - name: pid
   313        description: 'TODO: Fill field description'
   314        attributes:
   315          width: 16
   316          alignment: left
   317          ellipsis: end
   318      - name: comm
   319        description: 'TODO: Fill field description'
   320        attributes:
   321          width: 16
   322          alignment: left
   323          ellipsis: end
   324      - name: filename
   325        description: 'TODO: Fill field description'
   326        attributes:
   327          width: 16
   328          alignment: left
   329          ellipsis: end
   330  ```
   331  
   332  Let's edit the file to customize the output. We define some templates for well-known fields like
   333  pid, comm, etc.
   334  
   335  ```yaml
   336  name: mygadget
   337  description: Example gadget
   338  homepageURL: http://mygadget.com
   339  documentationURL: https://mygadget.com/docs
   340  sourceURL: https://github.com/my-org/mygadget/
   341  tracers:
   342    open:
   343      mapName: events
   344      structName: event
   345  structs:
   346    event:
   347      fields:
   348      - name: pid
   349        description: PID of the process opening a file
   350        attributes:
   351          template: pid
   352      - name: comm
   353        description: Name of the process opening a file
   354        attributes:
   355          template: comm
   356      - name: filename
   357        description: Path of the file being opened
   358        attributes:
   359          width: 64
   360          alignment: left
   361          ellipsis: end
   362  ```
   363  
   364  Now we can build and run the gadget again
   365  
   366  ```bash
   367  $ sudo -E ig image build . -t mygadget
   368  ...
   369  
   370  $ sudo -E ig run mygadget:latest
   371  INFO[0000] Experimental features enabled
   372  PID             COMM            FILENAME
   373  224707          git             .git/objects/cd/4968fd25e0b4d597f93993a29a9821c1a263d6
   374  224707          git             .git/objects/57/d7fb78a6f22dbfcf66d3175d06ce49d0e0dff5
   375  224707          git             .git/objects/03/5159622b915b7f55f64b6c0a30536531d08c5f
   376  19463           CompositorTileW /dev/shm/.org.chromium.Chromium.5pbSUV
   377  19463           CompositorTileW /dev/shm/.org.chromium.Chromium.96jgiV
   378  19463           CompositorTileW /dev/shm/.org.chromium.Chromium.3tqlBS
   379  224708          Sandbox Forked  /proc/self/uid_map
   380  224708          Sandbox Forked  /proc/self/setgroups
   381  224708          Sandbox Forked  /proc/self/gid_map
   382  3830            firefox-bin     /proc/224708/oom_score_adj
   383  224708          Sandbox Forked
   384  224710          Chroot Helper
   385  224708          firefox-bin     /usr/lib/firefox/tls/x86_64/x86_64/libmozsandbox.so
   386  224708          firefox-bin     /usr/lib/firefox/tls/x86_64/libmozsandbox.so
   387  224708          firefox-bin     /usr/lib/firefox/tls/x86_64/libmozsandbox.so
   388  224708          firefox-bin     /usr/lib/firefox/tls/libmozsandbox.so
   389  224708          firefox-bin     /usr/lib/firefox/x86_64/x86_64/libmozsandbox.so
   390  224708          firefox-bin     /usr/lib/firefox/x86_64/libmozsandbox.so
   391  224708          firefox-bin     /usr/lib/firefox/x86_64/libmozsandbox.so
   392  224708          firefox-bin     /usr/lib/firefox/libmozsandbox.so
   393  ```
   394  
   395  Now the output is much better.
   396  
   397  ### Filtering and container enrichement
   398  
   399  The gadget we created provides information about all events happening on the host, however it (a)
   400  doesn't provide any information about the container generating the event nor (b) allows it to filter
   401  events by a given container.
   402  
   403  Inspektor Gadget provides the logic to filter and enrich events with container information.
   404  The first step is to include these two addional header files:
   405  
   406  ```c
   407  // Inspektor Gadget filtering
   408  #include <gadget/mntns_filter.h>
   409  
   410  // Inspektor Gadget types
   411  #include <gadget/types.h>
   412  ```
   413  
   414  - `gadget/mntns_filter.h`: Defines an eBPF map and some helper functions used to filter events by containers
   415  - `gadget/types.h`: Inspektor Gadget specific types, like `gadget_mntns_id`.
   416  
   417  Add the following field to the event structure.
   418  
   419  ```c
   420  struct event {
   421  	...
   422  	gadget_mntns_id mntns_id;
   423  	...
   424  }
   425  ```
   426  
   427  And then, on the program, set this field. `gadget_get_mntns_id` is a helper function provided by
   428  Inspektor Gadget to get the current mount namespace.
   429  
   430  ```c
   431  	struct event *event;
   432  	u64 mntns_id;
   433  
   434  	mntns_id = gadget_get_mntns_id();
   435  ```
   436  
   437  Finally, we need to discard the events we're not interested in:
   438  
   439  ```c
   440  	if (gadget_should_discard_mntns_id(mntns_id)) {
   441  		return 0;
   442  	}
   443  
   444  	event = gadget_reserve_buf(&events, sizeof(*event));
   445  	if (!event)
   446  		return 0;
   447  
   448  	event->mntns_id = mntns_id;
   449  	...
   450  ```
   451  
   452  The `gadget_should_discard_mntns_id` function is provided to determine if a given event should be
   453  traced or not. This function should be called as early as possible in the program to avoid unnecessary work.
   454  
   455  After adding the `gadget_mntns_id` field to the event structure, compiling and running again,
   456  Inspektor Gadget will automatically add the container name column to the output:
   457  
   458  ```bash
   459  $ sudo -E ig run mygadget:latest
   460  INFO[0000] Experimental features enabled
   461  RUNTIME.CONTAINERNAME        PID             COMM            FILENAME                        MNTNS_ID
   462  ```
   463  
   464  However, the output is empty. It's because now the gadget is filtering only events generated by containers.
   465  Create a container and run some commands there:
   466  
   467  ```bash
   468  $ docker run --rm -ti --name=mycontainer busybox cat /dev/null
   469  ```
   470  
   471  Only events generated in containers are now printed, and they include the name of the container
   472  generating them.
   473  
   474  ```bash
   475  RUNTIME.CONTAINERNAME        PID             COMM            FILENAME                        MNTNS_ID
   476  mycontainer                  225805          cat             /lib/tls/libm.so.6              4026532256
   477  mycontainer                  225805          cat             /lib/x86_64/x86_64/libm.so.6    4026532256
   478  mycontainer                  225805          cat             /lib/x86_64/libm.so.6           4026532256
   479  mycontainer                  225805          cat             /lib/x86_64/libm.so.6           4026532256
   480  mycontainer                  225805          cat             /lib/libm.so.6                  4026532256
   481  mycontainer                  225805          cat             /lib/libresolv.so.2             4026532256
   482  mycontainer                  225805          cat             /lib/libc.so.6                  4026532256
   483  mycontainer                  225805          cat             /dev/null                       4026532256
   484  ```
   485  
   486  Additionally, after adding the `gadget_mntns_id` field to the event structure, Inspektor Gadget will
   487  automatically add the flag `--containername`/`-c` to the gadget. This flag allows filtering
   488  events by container name.
   489  
   490  The following command doesn't show any event as there is no container with the specified name:
   491  
   492  ```bash
   493  $ sudo -E ig run mygadget:latest -c non_existing_container
   494  INFO[0000] Experimental features enabled
   495  RUNTIME.CONTAINERNAME        PID             COMM            FILENAME                        MNTNS_ID
   496  ```
   497  
   498  ## Updating the gadget
   499  
   500  Some times we have to update our gadgets, like adding more fields to the generated event for instance.
   501  Let's add the following fields to the event struct:
   502  
   503  ```c
   504  struct event {
   505  	...
   506  	__u32 uid;
   507  	__u32 gid;
   508  	...
   509  }
   510  ```
   511  
   512  and add the logic in the eBPF program to fill them:
   513  
   514  ```c
   515  	__u64 uid_gid = bpf_get_current_uid_gid();
   516  	event->uid = (__u32)uid_gid;
   517  	event->gid = (__u32)(uid_gid >> 32);
   518  ```
   519  
   520  Let's build the gadget with the `--update-metadata` file, so our new fields are automatically added
   521  to the metadata file. Notice the -v option is used to get debugging messages.
   522  
   523  ```bash
   524  $ sudo -E ig image build . -t mygadget --update-metadata -v
   525  INFO[0000] Experimental features enabled
   526  ...
   527  DEBU[0001] Metadata file found, updating it
   528  DEBU[0001] Tracer "open" already defined, skipping
   529  DEBU[0001] Field "pid" already exists, skipping
   530  DEBU[0001] Field "comm" already exists, skipping
   531  DEBU[0001] Adding field "uid"
   532  DEBU[0001] Adding field "gid"
   533  DEBU[0001] Field "filename" already exists, skipping
   534  DEBU[0001] Adding field "mntns_id"
   535  ...
   536  ```
   537  
   538  The uid, gid and mntns_id (added in the [previous
   539  step](#filtering-and-container-enrichement) fields were added to the metadata
   540  file:
   541  
   542  ```yaml
   543      - name: uid
   544        description: 'TODO: Fill field description'
   545        attributes:
   546          width: 16
   547          alignment: left
   548          ellipsis: end
   549      - name: gid
   550        description: 'TODO: Fill field description'
   551        attributes:
   552          width: 16
   553          alignment: left
   554          ellipsis: end
   555      - name: mntns_id
   556        description: 'TODO: Fill field description'
   557        attributes:
   558          width: 20
   559          alignment: left
   560          ellipsis: end
   561  ```
   562  
   563  Edit them, build and run the gadget again:
   564  
   565  ```yaml
   566      - name: uid
   567        description: User ID opening the file
   568        attributes:
   569          template: uid
   570      - name: gid
   571        description: Group ID opening the file
   572        attributes:
   573          template: uid
   574      - name: mntns_id
   575        description: Mount namespace inode id
   576        attributes:
   577          template: ns
   578  ```
   579  
   580  ```bash
   581  $ sudo -E ig image build . -t mygadget --update-metadata -v
   582  ...
   583  
   584  $ sudo -E ig run mygadget:latest
   585  INFO[0000] Experimental features enabled
   586  RUNTIME.CONTAINERNAME  PID          COMM         FILENAME                                        UID         GID
   587  ```
   588  
   589  Now, the UID and GID columns have the expected format. Notice also that the MNTNS_ID column is
   590  not showed because the template `ns` hides it by default.
   591  
   592  ## Adding tests for the gadget
   593  
   594  In order to ensure the gadget works as expected, you can create a corresponding test file.
   595  To do so, you need to call it `mygadget_test.go` and import some packages, like:
   596  
   597  ```golang
   598  package main
   599  
   600  import (
   601    "testing"
   602  
   603    "github.com/stretchr/testify/require"
   604  
   605    // helper functions for creating and running commands in a container.
   606    "github.com/inspektor-gadget/inspektor-gadget/pkg/testing/containers"
   607  
   608    igtesting "github.com/inspektor-gadget/inspektor-gadget/pkg/testing"
   609  
   610    // wrapper function for ig binary
   611    igrunner "github.com/inspektor-gadget/inspektor-gadget/pkg/testing/ig"
   612  
   613    // helper functions for parsing and comparing output.
   614    "github.com/inspektor-gadget/inspektor-gadget/pkg/testing/match"
   615  
   616    // Event struct for fields enriched by ig.
   617    eventtypes "github.com/inspektor-gadget/inspektor-gadget/pkg/types"
   618  )
   619  ```
   620  
   621  Then, we create a structure with all the information the gadget provides.
   622  
   623  ```golang
   624  type mygadgetEvent struct {
   625    eventtypes.Event
   626  
   627    MountNsID uint64 `json:"mountnsid"`
   628    Pid       uint32 `json:"pid"`
   629    Uid       uint32 `json:"uid"`
   630    Gid       uint32 `json:"gid"`
   631    Comm      string `json:"comm"`
   632    Filename  string `json:"filename"`
   633  }
   634  ```
   635  
   636  Later we create a test function called `TestMyGadget()`. 
   637  In this, we first create a container manager (can be either `docker` or `containerd`). After that, we create a command to run the gadget with various options. 
   638  Finally, these commands are used as arguments in `RunTestSteps()`:
   639  
   640  ```golang
   641  func TestMyGadget(t *testing.T) {
   642    cn := "test-mygadget"
   643  
   644    // returns a container manager which implements an interface with methods for creating new container
   645    // and running commands within that container.
   646    containerFactory, err := containers.NewContainerFactory("docker")
   647    require.NoError(t, err, "new container factory")
   648  
   649    mygadgetCmd := igrunner.New(
   650      // gadget repository and tag can be added with the following environment variables:
   651      // - $GADGET_REPOSITORY
   652      // - $GADGET_TAG
   653      "mygadget",
   654      igrunner.WithFlags("--runtimes=docker", "--timeout=5"),
   655      igrunner.WithValidateOutput(
   656        func(t *testing.T, output string) {
   657          expectedEntry := &mygadgetEvent{
   658            Event: eventtypes.Event{
   659              CommonData: eventtypes.CommonData{
   660                Runtime: eventtypes.BasicRuntimeMetadata{
   661                  RuntimeName:   eventtypes.String2RuntimeName("docker"),
   662                  ContainerName: cn,
   663                },
   664              },
   665            },
   666            Comm:     "cat",
   667            Filename: "/dev/null",
   668            Uid:      1000,
   669            Gid:      1111,
   670          }
   671  
   672          // used to "normalize" the output, sets random value fields to a default value
   673          // so that it only includes non-default values for the fields we can verify.
   674          normalize := func(e *mygadgetEvent) {
   675            e.MountNsID = 0
   676            e.Pid = 0
   677  
   678            e.Runtime.ContainerID = ""
   679            e.Runtime.ContainerImageName = ""
   680            e.Runtime.ContainerImageDigest = ""
   681          }
   682  
   683          // parses the output and matches it to expectedEntry.
   684          match.ExpectEntriesToMatch(t, output, normalize, expectedEntry)
   685        },
   686      ),
   687    )
   688  
   689    testSteps := []igtesting.TestStep{
   690      // WithStartAndStop used to start the container command, then, wait for other commands to run
   691      // and stop later and verify the output. 
   692      containerFactory.NewContainer(cn, "while true; do setuidgid 1000:1111 cat /dev/null; sleep 0.1; done", containers.WithStartAndStop()),
   693      mygadgetCmd,
   694    }
   695  
   696    igtesting.RunTestSteps(testSteps, t)
   697  }
   698  ```
   699  
   700  (Optional) If running the test for a gadget whose image resides in a remote container registry, you can define environment variables for the gadget repository and tag.
   701  
   702  ```bash
   703  $ export GADGET_REPOSITORY=ghcr.io/my-org GADGET_TAG=latest
   704  ```
   705  
   706  We are all set now to run the test.
   707  
   708  ```bash
   709  $ go test -exec 'sudo -E' -v ./mygadget_test.go
   710  ```
   711  
   712  ### Closing
   713  
   714  Congratulations! You've implemented your first gadget. Check out our documentation to get more
   715  information.