github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/imagescan/collector/collector_test.go (about)

     1  package collector
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"path"
    12  	"runtime"
    13  	"runtime/pprof"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/aquasecurity/trivy/pkg/fanal/types"
    18  	"github.com/google/go-containerregistry/pkg/name"
    19  	v1 "github.com/google/go-containerregistry/pkg/v1"
    20  	"github.com/sirupsen/logrus"
    21  	"github.com/stretchr/testify/require"
    22  	"google.golang.org/grpc"
    23  	"google.golang.org/protobuf/encoding/protojson"
    24  
    25  	"github.com/castai/image-analyzer/image"
    26  	"github.com/castai/image-analyzer/image/hostfs"
    27  	castaipb "github.com/castai/kvisor/api/v1/runtime"
    28  	"github.com/castai/kvisor/cmd/imagescan/config"
    29  	mockblobcache "github.com/castai/kvisor/pkg/blobscache/mock"
    30  )
    31  
    32  func TestCollector(t *testing.T) {
    33  	t.Run("collect and sends metadata", func(t *testing.T) {
    34  		imgName := "notused"
    35  		imgID := "gke.gcr.io/phpmyadmin@sha256:b0d9c54760b35edd1854e5710c1a62a28ad2d2b070c801da3e30a3e59c19e7e3" //nolint:gosec
    36  
    37  		r := require.New(t)
    38  		ctx := context.Background()
    39  		log := logrus.New()
    40  		log.SetLevel(logrus.DebugLevel)
    41  
    42  		mockCache := mockblobcache.MockClient{}
    43  
    44  		cwd, _ := os.Getwd()
    45  		p := path.Join(cwd, "testdata/amd64-linux/io.containerd.content.v1.content")
    46  
    47  		ingestClient := &mockIngestClient{}
    48  
    49  		c := New(
    50  			log,
    51  			config.Config{
    52  				ImageID:           imgID,
    53  				ImageName:         imgName,
    54  				Timeout:           5 * time.Minute,
    55  				Mode:              config.ModeHostFS,
    56  				Runtime:           config.RuntimeContainerd,
    57  				ImageArchitecture: "amd64",
    58  				ImageOS:           "linux",
    59  				Parallel:          1,
    60  			},
    61  			ingestClient,
    62  			mockCache,
    63  			&hostfs.ContainerdHostFSConfig{
    64  				Platform: v1.Platform{
    65  					Architecture: "amd64",
    66  					OS:           "linux",
    67  				},
    68  				ContentDir: p,
    69  			},
    70  		)
    71  
    72  		r.NoError(c.Collect(ctx))
    73  
    74  		// Read expect metadata.
    75  		var expected castaipb.ImageMetadata
    76  		b, err := os.ReadFile("./testdata/expected_image_scan_meta1.json")
    77  		r.NoError(err)
    78  		r.NoError(protojson.Unmarshal(b, &expected))
    79  
    80  		receivedMetadataJson, err := protojson.Marshal(ingestClient.receivedMeta)
    81  		r.NoError(err)
    82  		var actual castaipb.ImageMetadata
    83  		r.NoError(protojson.Unmarshal(receivedMetadataJson, &actual))
    84  
    85  		var expectedPackages []types.BlobInfo
    86  		var actualPackages []types.BlobInfo
    87  		r.NoError(json.Unmarshal(expected.Packages, &expectedPackages))
    88  		r.NoError(json.Unmarshal(actual.Packages, &actualPackages))
    89  		expected.Packages = nil
    90  		actual.Packages = nil
    91  
    92  		r.Equal(&expected, &actual)
    93  		r.ElementsMatch(expectedPackages, actualPackages)
    94  	})
    95  }
    96  
    97  func TestCollectorLargeImageDocker(t *testing.T) {
    98  	// Skip this test by default. Uncomment to run locally.
    99  	if os.Getenv("LOCAL_IMAGE") == "" {
   100  		t.Skip()
   101  	}
   102  
   103  	// You will spend a lot of time on macOS to fetch image into temp file from daemon.
   104  	// Instead, export image once to local tar file.
   105  	// docker save ghcr.io/castai/egressd:am1 -o egressd.tar
   106  	imgName := "kvisor:local"
   107  	imgID := imgName
   108  
   109  	r := require.New(t)
   110  	ctx := context.Background()
   111  	log := logrus.New()
   112  	log.SetLevel(logrus.DebugLevel)
   113  	//debug.SetGCPercent(-1)
   114  	mockCache := mockblobcache.MockClient{}
   115  
   116  	var receivedMetaBytes []byte
   117  	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   118  		data, err := io.ReadAll(req.Body)
   119  		r.NoError(err)
   120  		receivedMetaBytes = data
   121  	}))
   122  	defer srv.Close()
   123  
   124  	ingestClient := &mockIngestClient{}
   125  
   126  	c := New(log, config.Config{
   127  		CastaiAPIGrpcAddr: srv.URL,
   128  		ImageID:           imgID,
   129  		ImageName:         imgName,
   130  		Timeout:           5 * time.Minute,
   131  		Mode:              config.ModeTarArchive,
   132  		Runtime:           config.RuntimeDocker,
   133  		Parallel:          1,
   134  		ImageLocalTarPath: "egressd.tar",
   135  	}, ingestClient, mockCache, nil)
   136  
   137  	go func() {
   138  		for {
   139  			printMemStats()
   140  			time.Sleep(500 * time.Millisecond)
   141  		}
   142  	}()
   143  
   144  	r.NoError(c.Collect(ctx))
   145  	writeMemProfile("heap.prof")
   146  	r.NoError(os.WriteFile("metadata.json", receivedMetaBytes, 0600))
   147  }
   148  
   149  func printMemStats() {
   150  	runtime.GC() // Get up-to-date statistics.
   151  	var stats runtime.MemStats
   152  	runtime.ReadMemStats(&stats)
   153  	fmt.Printf("allocs=%d MB, total_allocs=%d MB, sys=%d MB\n", stats.Alloc/1024/1024, stats.TotalAlloc/1024/1024, stats.Sys/1024/1024)
   154  }
   155  
   156  func writeMemProfile(name string) {
   157  	f, err := os.Create(name)
   158  	if err != nil {
   159  		logrus.Fatalf("could not create memory profile: %v", err)
   160  	}
   161  	defer f.Close() // error handling omitted for example
   162  	if err := pprof.WriteHeapProfile(f); err != nil {
   163  		logrus.Fatalf("could not write memory profile: %v", err)
   164  	}
   165  }
   166  
   167  func TestFindRegistryAuth(t *testing.T) {
   168  	registryAuth := image.RegistryAuth{Username: "u", Password: "p", Token: "t"}
   169  
   170  	tests := []struct {
   171  		name     string
   172  		cfg      image.DockerConfig
   173  		imageRef name.Reference
   174  
   175  		expectedFound bool
   176  		expectedKey   string
   177  		expectedAuth  image.RegistryAuth
   178  	}{
   179  		{
   180  			name: "find auth for image",
   181  			cfg: image.DockerConfig{
   182  				Auths: map[string]image.RegistryAuth{
   183  					"a":                               registryAuth,
   184  					"gitlab.com":                      registryAuth,
   185  					"us-east4-docker.pkg.dev":         registryAuth,
   186  					"us-east4-docker.pkg.dev/project": registryAuth,
   187  					"x":                               {},
   188  				},
   189  			},
   190  			imageRef:      name.MustParseReference("us-east4-docker.pkg.dev/project/repo/name:tag"),
   191  			expectedFound: true,
   192  			expectedKey:   "us-east4-docker.pkg.dev",
   193  			expectedAuth:  registryAuth,
   194  		},
   195  		{
   196  			name: "find auth scoped by repository",
   197  			cfg: image.DockerConfig{
   198  				Auths: map[string]image.RegistryAuth{
   199  					"a":                                    registryAuth,
   200  					"us-east4-docker.pkg.dev/project/repo": registryAuth,
   201  					"x":                                    {},
   202  				},
   203  			},
   204  			imageRef:      name.MustParseReference("us-east4-docker.pkg.dev/project/repo/name:tag"),
   205  			expectedFound: true,
   206  			expectedKey:   "us-east4-docker.pkg.dev/project/repo",
   207  			expectedAuth:  registryAuth,
   208  		},
   209  		{
   210  			name: "find auth for http or https prefixed auths",
   211  			cfg: image.DockerConfig{
   212  				Auths: map[string]image.RegistryAuth{
   213  					"a": registryAuth,
   214  					"https://us-east4-docker.pkg.dev/project/repo": registryAuth,
   215  					"x": {},
   216  				},
   217  			},
   218  			imageRef:      name.MustParseReference("us-east4-docker.pkg.dev/project/repo/name:tag"),
   219  			expectedFound: true,
   220  			expectedKey:   "https://us-east4-docker.pkg.dev/project/repo",
   221  			expectedAuth:  registryAuth,
   222  		},
   223  		{
   224  			name: "no auth for unmatched auths",
   225  			cfg: image.DockerConfig{
   226  				Auths: map[string]image.RegistryAuth{
   227  					"a": registryAuth,
   228  					"https://us-east4-docker.pkg.dev/project/repo": registryAuth,
   229  					"x": {},
   230  				},
   231  			},
   232  			imageRef:      name.MustParseReference("nginx:latest"),
   233  			expectedFound: false,
   234  			expectedKey:   "",
   235  			expectedAuth:  image.RegistryAuth{},
   236  		},
   237  	}
   238  
   239  	for _, test := range tests {
   240  		t.Run(test.name, func(t *testing.T) {
   241  			r := require.New(t)
   242  			actualKey, actualAuth, found := findRegistryAuth(test.cfg, test.imageRef)
   243  			r.Equal(test.expectedFound, found)
   244  			r.Equal(test.expectedKey, actualKey)
   245  			r.Equal(test.expectedAuth, actualAuth)
   246  		})
   247  	}
   248  }
   249  
   250  type mockIngestClient struct {
   251  	receivedMeta *castaipb.ImageMetadata
   252  }
   253  
   254  func (m *mockIngestClient) ImageMetadataIngest(ctx context.Context, in *castaipb.ImageMetadata, opts ...grpc.CallOption) (*castaipb.ImageMetadataIngestResponse, error) {
   255  	m.receivedMeta = in
   256  	return &castaipb.ImageMetadataIngestResponse{}, nil
   257  }