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 [](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.