github.com/google/osv-scalibr@v0.4.1/extractor/standalone/containers/containerd/containerd_test.go (about)

     1  // Copyright 2025 Google LLC
     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  //go:build linux
    16  
    17  // Package containerd_test contains unit tests for containerd extractor.
    18  package containerd_test
    19  
    20  import (
    21  	"fmt"
    22  	"runtime"
    23  	"slices"
    24  	"testing"
    25  
    26  	containerd "github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/api/types/task"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  	"github.com/google/osv-scalibr/extractor"
    31  	"github.com/google/osv-scalibr/extractor/standalone"
    32  	plugin "github.com/google/osv-scalibr/extractor/standalone/containers/containerd"
    33  	md "github.com/google/osv-scalibr/extractor/standalone/containers/containerd/containerdmetadata"
    34  	"github.com/google/osv-scalibr/extractor/standalone/containers/containerd/fakeclient"
    35  	"github.com/google/osv-scalibr/inventory"
    36  )
    37  
    38  func TestExtract(t *testing.T) {
    39  	tests := []struct {
    40  		name         string
    41  		onGoos       []string
    42  		nssTaskIDs   map[string][]string
    43  		tsks         []*task.Process
    44  		ctrs         []containerd.Container
    45  		wantPackages []*extractor.Package
    46  		wantErr      error
    47  	}{
    48  		{
    49  			name:         "valid with no tasks",
    50  			onGoos:       []string{"linux"},
    51  			nssTaskIDs:   map[string][]string{"default": {}, "k8s.io": {}},
    52  			tsks:         []*task.Process{},
    53  			ctrs:         []containerd.Container{},
    54  			wantPackages: []*extractor.Package{},
    55  		},
    56  		{
    57  			name:       "valid with tasks and rootfs",
    58  			onGoos:     []string{"linux"},
    59  			nssTaskIDs: map[string][]string{"default": {"123456789"}, "k8s.io": {"567890123"}},
    60  			tsks:       []*task.Process{{ID: "123456789", ContainerID: "", Pid: 12345}, {ID: "567890123", ContainerID: "", Pid: 5678}},
    61  			ctrs:       []containerd.Container{fakeclient.NewFakeContainer("123456789", "image1", "digest1", "/run/containerd/io.containerd.runtime.v2.task/default/123456789/rootfs"), fakeclient.NewFakeContainer("567890123", "image2", "digest2", "/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890123/rootfs")},
    62  			wantPackages: []*extractor.Package{
    63  				{
    64  					Name:      "image1",
    65  					Version:   "digest1",
    66  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/default/123456789/rootfs"},
    67  					Metadata: &md.Metadata{
    68  						Namespace:   "default",
    69  						ImageName:   "image1",
    70  						ImageDigest: "digest1",
    71  						Runtime:     "fake_runc",
    72  						ID:          "123456789",
    73  						PID:         12345,
    74  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/default/123456789/rootfs",
    75  					},
    76  				},
    77  				{
    78  					Name:      "image2",
    79  					Version:   "digest2",
    80  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890123/rootfs"},
    81  					Metadata: &md.Metadata{
    82  						Namespace:   "k8s.io",
    83  						ImageName:   "image2",
    84  						ImageDigest: "digest2",
    85  						ID:          "567890123",
    86  						Runtime:     "fake_runc",
    87  						PID:         5678,
    88  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890123/rootfs",
    89  					},
    90  				},
    91  			},
    92  		},
    93  		{
    94  			name:       "valid with tasks and no rootfs",
    95  			onGoos:     []string{"linux"},
    96  			nssTaskIDs: map[string][]string{"default": {"123456789"}, "k8s.io": {"567890123"}},
    97  			tsks:       []*task.Process{{ID: "123456789", ContainerID: "", Pid: 12345}, {ID: "567890123", ContainerID: "", Pid: 5678}},
    98  			ctrs:       []containerd.Container{fakeclient.NewFakeContainer("123456789", "image1", "digest1", ""), fakeclient.NewFakeContainer("567890123", "image2", "digest2", "")},
    99  			wantPackages: []*extractor.Package{
   100  				{
   101  					Name:      "image1",
   102  					Version:   "digest1",
   103  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/default/123456789/rootfs"},
   104  					Metadata: &md.Metadata{
   105  						Namespace:   "default",
   106  						ImageName:   "image1",
   107  						ImageDigest: "digest1",
   108  						Runtime:     "fake_runc",
   109  						ID:          "123456789",
   110  						PID:         12345,
   111  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/default/123456789/rootfs",
   112  					},
   113  				},
   114  				{
   115  					Name:      "image2",
   116  					Version:   "digest2",
   117  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890123/rootfs"},
   118  					Metadata: &md.Metadata{
   119  						Namespace:   "k8s.io",
   120  						ImageName:   "image2",
   121  						ImageDigest: "digest2",
   122  						ID:          "567890123",
   123  						Runtime:     "fake_runc",
   124  						PID:         5678,
   125  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890123/rootfs",
   126  					},
   127  				},
   128  			},
   129  		},
   130  		{
   131  			name:       "valid with tasks and relative-path-only rootfs",
   132  			onGoos:     []string{"linux"},
   133  			nssTaskIDs: map[string][]string{"default": {"123456788"}, "k8s.io": {"567890122"}},
   134  			tsks:       []*task.Process{{ID: "123456788", ContainerID: "", Pid: 12346}, {ID: "567890122", ContainerID: "", Pid: 5677}},
   135  			ctrs:       []containerd.Container{fakeclient.NewFakeContainer("123456788", "image1", "digest1", "test/rootfs"), fakeclient.NewFakeContainer("567890122", "image2", "digest2", "test2/rootfs")},
   136  			wantPackages: []*extractor.Package{
   137  				{
   138  					Name:      "image1",
   139  					Version:   "digest1",
   140  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/default/123456788/test/rootfs"},
   141  					Metadata: &md.Metadata{
   142  						Namespace:   "default",
   143  						ImageName:   "image1",
   144  						ImageDigest: "digest1",
   145  						Runtime:     "fake_runc",
   146  						ID:          "123456788",
   147  						PID:         12346,
   148  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/default/123456788/test/rootfs",
   149  					},
   150  				},
   151  				{
   152  					Name:      "image2",
   153  					Version:   "digest2",
   154  					Locations: []string{"/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890122/test2/rootfs"},
   155  					Metadata: &md.Metadata{
   156  						Namespace:   "k8s.io",
   157  						ImageName:   "image2",
   158  						ImageDigest: "digest2",
   159  						ID:          "567890122",
   160  						Runtime:     "fake_runc",
   161  						PID:         5677,
   162  						RootFS:      "/run/containerd/io.containerd.runtime.v2.task/k8s.io/567890122/test2/rootfs",
   163  					},
   164  				},
   165  			},
   166  		},
   167  	}
   168  
   169  	for _, tt := range tests {
   170  		t.Run(tt.name, func(t *testing.T) {
   171  			if len(tt.onGoos) > 0 && !slices.Contains(tt.onGoos, runtime.GOOS) {
   172  				t.Skipf("Skipping test on %s", runtime.GOOS)
   173  			}
   174  
   175  			var input *standalone.ScanInput
   176  			cli, err := fakeclient.NewFakeCtrdClient(t.Context(), tt.nssTaskIDs, tt.tsks, tt.ctrs)
   177  			if err != nil {
   178  				t.Fatalf("NewFakeCtrdClient() error: %v", err)
   179  			}
   180  			e := plugin.NewWithClient(&cli, "test")
   181  			got, err := e.Extract(t.Context(), input)
   182  			if !cmp.Equal(err, tt.wantErr, cmpopts.EquateErrors()) {
   183  				t.Fatalf("Extract(%+v) error: got %v, want %v\n", tt.name, err, tt.wantErr)
   184  			}
   185  
   186  			ignoreOrder := cmpopts.SortSlices(func(a, b any) bool {
   187  				return fmt.Sprintf("%+v", a) < fmt.Sprintf("%+v", b)
   188  			})
   189  			wantInv := inventory.Inventory{Packages: tt.wantPackages}
   190  			if diff := cmp.Diff(wantInv, got, ignoreOrder); diff != "" {
   191  				t.Errorf("Extract(%s) (-want +got):\n%s", tt.name, diff)
   192  			}
   193  		})
   194  	}
   195  }