github.com/ishita82/trivy-gitaction@v0.0.0-20240206054925-e937cc05f8e3/integration/client_server_test.go (about)

     1  //go:build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	dockercontainer "github.com/docker/docker/api/types/container"
    15  	"github.com/docker/go-connections/nat"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  	testcontainers "github.com/testcontainers/testcontainers-go"
    19  
    20  	"github.com/aquasecurity/trivy/pkg/report"
    21  	"github.com/aquasecurity/trivy/pkg/uuid"
    22  )
    23  
    24  type csArgs struct {
    25  	Command           string
    26  	RemoteAddrOption  string
    27  	Format            string
    28  	TemplatePath      string
    29  	IgnoreUnfixed     bool
    30  	Severity          []string
    31  	IgnoreIDs         []string
    32  	Input             string
    33  	ClientToken       string
    34  	ClientTokenHeader string
    35  	ListAllPackages   bool
    36  	Target            string
    37  	secretConfig      string
    38  }
    39  
    40  func TestClientServer(t *testing.T) {
    41  	tests := []struct {
    42  		name    string
    43  		args    csArgs
    44  		golden  string
    45  		wantErr string
    46  	}{
    47  		{
    48  			name: "alpine 3.9",
    49  			args: csArgs{
    50  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    51  			},
    52  			golden: "testdata/alpine-39.json.golden",
    53  		},
    54  		{
    55  			name: "alpine 3.9 with high and critical severity",
    56  			args: csArgs{
    57  				IgnoreUnfixed: true,
    58  				Severity: []string{
    59  					"HIGH",
    60  					"CRITICAL",
    61  				},
    62  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    63  			},
    64  			golden: "testdata/alpine-39-high-critical.json.golden",
    65  		},
    66  		{
    67  			name: "alpine 3.9 with .trivyignore",
    68  			args: csArgs{
    69  				IgnoreUnfixed: false,
    70  				IgnoreIDs: []string{
    71  					"CVE-2019-1549",
    72  					"CVE-2019-14697",
    73  				},
    74  				Input: "testdata/fixtures/images/alpine-39.tar.gz",
    75  			},
    76  			golden: "testdata/alpine-39-ignore-cveids.json.golden",
    77  		},
    78  		{
    79  			name: "alpine 3.10",
    80  			args: csArgs{
    81  				Input: "testdata/fixtures/images/alpine-310.tar.gz",
    82  			},
    83  			golden: "testdata/alpine-310.json.golden",
    84  		},
    85  		{
    86  			name: "alpine distroless",
    87  			args: csArgs{
    88  				Input: "testdata/fixtures/images/alpine-distroless.tar.gz",
    89  			},
    90  			golden: "testdata/alpine-distroless.json.golden",
    91  		},
    92  		{
    93  			name: "debian buster/10",
    94  			args: csArgs{
    95  				Input: "testdata/fixtures/images/debian-buster.tar.gz",
    96  			},
    97  			golden: "testdata/debian-buster.json.golden",
    98  		},
    99  		{
   100  			name: "debian buster/10 with --ignore-unfixed option",
   101  			args: csArgs{
   102  				IgnoreUnfixed: true,
   103  				Input:         "testdata/fixtures/images/debian-buster.tar.gz",
   104  			},
   105  			golden: "testdata/debian-buster-ignore-unfixed.json.golden",
   106  		},
   107  		{
   108  			name: "debian stretch/9",
   109  			args: csArgs{
   110  				Input: "testdata/fixtures/images/debian-stretch.tar.gz",
   111  			},
   112  			golden: "testdata/debian-stretch.json.golden",
   113  		},
   114  		{
   115  			name: "ubuntu 18.04",
   116  			args: csArgs{
   117  				Input: "testdata/fixtures/images/ubuntu-1804.tar.gz",
   118  			},
   119  			golden: "testdata/ubuntu-1804.json.golden",
   120  		},
   121  		{
   122  			name: "centos 7",
   123  			args: csArgs{
   124  				Input: "testdata/fixtures/images/centos-7.tar.gz",
   125  			},
   126  			golden: "testdata/centos-7.json.golden",
   127  		},
   128  		{
   129  			name: "centos 7 with --ignore-unfixed option",
   130  			args: csArgs{
   131  				IgnoreUnfixed: true,
   132  				Input:         "testdata/fixtures/images/centos-7.tar.gz",
   133  			},
   134  			golden: "testdata/centos-7-ignore-unfixed.json.golden",
   135  		},
   136  		{
   137  			name: "centos 7 with medium severity",
   138  			args: csArgs{
   139  				IgnoreUnfixed: true,
   140  				Severity:      []string{"MEDIUM"},
   141  				Input:         "testdata/fixtures/images/centos-7.tar.gz",
   142  			},
   143  			golden: "testdata/centos-7-medium.json.golden",
   144  		},
   145  		{
   146  			name: "centos 6",
   147  			args: csArgs{
   148  				Input: "testdata/fixtures/images/centos-6.tar.gz",
   149  			},
   150  			golden: "testdata/centos-6.json.golden",
   151  		},
   152  		{
   153  			name: "ubi 7",
   154  			args: csArgs{
   155  				Input: "testdata/fixtures/images/ubi-7.tar.gz",
   156  			},
   157  			golden: "testdata/ubi-7.json.golden",
   158  		},
   159  		{
   160  			name: "almalinux 8",
   161  			args: csArgs{
   162  				Input: "testdata/fixtures/images/almalinux-8.tar.gz",
   163  			},
   164  			golden: "testdata/almalinux-8.json.golden",
   165  		},
   166  		{
   167  			name: "rocky linux 8",
   168  			args: csArgs{
   169  				Input: "testdata/fixtures/images/rockylinux-8.tar.gz",
   170  			},
   171  			golden: "testdata/rockylinux-8.json.golden",
   172  		},
   173  		{
   174  			name: "distroless base",
   175  			args: csArgs{
   176  				Input: "testdata/fixtures/images/distroless-base.tar.gz",
   177  			},
   178  			golden: "testdata/distroless-base.json.golden",
   179  		},
   180  		{
   181  			name: "distroless python27",
   182  			args: csArgs{
   183  				Input: "testdata/fixtures/images/distroless-python27.tar.gz",
   184  			},
   185  			golden: "testdata/distroless-python27.json.golden",
   186  		},
   187  		{
   188  			name: "amazon 1",
   189  			args: csArgs{
   190  				Input: "testdata/fixtures/images/amazon-1.tar.gz",
   191  			},
   192  			golden: "testdata/amazon-1.json.golden",
   193  		},
   194  		{
   195  			name: "amazon 2",
   196  			args: csArgs{
   197  				Input: "testdata/fixtures/images/amazon-2.tar.gz",
   198  			},
   199  			golden: "testdata/amazon-2.json.golden",
   200  		},
   201  		{
   202  			name: "oracle 8",
   203  			args: csArgs{
   204  				Input: "testdata/fixtures/images/oraclelinux-8.tar.gz",
   205  			},
   206  			golden: "testdata/oraclelinux-8.json.golden",
   207  		},
   208  		{
   209  			name: "opensuse leap 15.1",
   210  			args: csArgs{
   211  				Input: "testdata/fixtures/images/opensuse-leap-151.tar.gz",
   212  			},
   213  			golden: "testdata/opensuse-leap-151.json.golden",
   214  		},
   215  		{
   216  			name: "photon 3.0",
   217  			args: csArgs{
   218  				Input: "testdata/fixtures/images/photon-30.tar.gz",
   219  			},
   220  			golden: "testdata/photon-30.json.golden",
   221  		},
   222  		{
   223  			name: "CBL-Mariner 1.0",
   224  			args: csArgs{
   225  				Input: "testdata/fixtures/images/mariner-1.0.tar.gz",
   226  			},
   227  			golden: "testdata/mariner-1.0.json.golden",
   228  		},
   229  		{
   230  			name: "busybox with Cargo.lock",
   231  			args: csArgs{
   232  				Input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz",
   233  			},
   234  			golden: "testdata/busybox-with-lockfile.json.golden",
   235  		},
   236  		{
   237  			name: "scan pox.xml with repo command in client/server mode",
   238  			args: csArgs{
   239  				Command:          "repo",
   240  				RemoteAddrOption: "--server",
   241  				Target:           "testdata/fixtures/repo/pom/",
   242  			},
   243  			golden: "testdata/pom.json.golden",
   244  		},
   245  		{
   246  			name: "scan sample.pem with repo command in client/server mode",
   247  			args: csArgs{
   248  				Command:          "repo",
   249  				RemoteAddrOption: "--server",
   250  				secretConfig:     "testdata/fixtures/repo/secrets/trivy-secret.yaml",
   251  				Target:           "testdata/fixtures/repo/secrets/",
   252  			},
   253  			golden: "testdata/secrets.json.golden",
   254  		},
   255  		{
   256  			name: "scan remote repository with repo command in client/server mode",
   257  			args: csArgs{
   258  				Command:          "repo",
   259  				RemoteAddrOption: "--server",
   260  				Target:           "https://github.com/knqyf263/trivy-ci-test",
   261  			},
   262  			golden: "testdata/test-repo.json.golden",
   263  		},
   264  	}
   265  
   266  	addr, cacheDir := setup(t, setupOptions{})
   267  
   268  	for _, c := range tests {
   269  		t.Run(c.name, func(t *testing.T) {
   270  			osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden)
   271  
   272  			if c.args.secretConfig != "" {
   273  				osArgs = append(osArgs, "--secret-config", c.args.secretConfig)
   274  			}
   275  
   276  			//
   277  			err := execute(osArgs)
   278  			require.NoError(t, err)
   279  
   280  			compareReports(t, c.golden, outputFile, nil)
   281  		})
   282  	}
   283  }
   284  
   285  func TestClientServerWithFormat(t *testing.T) {
   286  	tests := []struct {
   287  		name   string
   288  		args   csArgs
   289  		golden string
   290  	}{
   291  		{
   292  			name: "alpine 3.10 with gitlab template",
   293  			args: csArgs{
   294  				Format:       "template",
   295  				TemplatePath: "@../contrib/gitlab.tpl",
   296  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   297  			},
   298  			golden: "testdata/alpine-310.gitlab.golden",
   299  		},
   300  		{
   301  			name: "alpine 3.10 with gitlab-codequality template",
   302  			args: csArgs{
   303  				Format:       "template",
   304  				TemplatePath: "@../contrib/gitlab-codequality.tpl",
   305  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   306  			},
   307  			golden: "testdata/alpine-310.gitlab-codequality.golden",
   308  		},
   309  		{
   310  			name: "alpine 3.10 with sarif format",
   311  			args: csArgs{
   312  				Format: "sarif",
   313  				Input:  "testdata/fixtures/images/alpine-310.tar.gz",
   314  			},
   315  			golden: "testdata/alpine-310.sarif.golden",
   316  		},
   317  		{
   318  			name: "alpine 3.10 with ASFF template",
   319  			args: csArgs{
   320  				Format:       "template",
   321  				TemplatePath: "@../contrib/asff.tpl",
   322  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   323  			},
   324  			golden: "testdata/alpine-310.asff.golden",
   325  		},
   326  		{
   327  			name: "scan secrets with ASFF template",
   328  			args: csArgs{
   329  				Command:          "repo",
   330  				RemoteAddrOption: "--server",
   331  				Format:           "template",
   332  				TemplatePath:     "@../contrib/asff.tpl",
   333  				Target:           "testdata/fixtures/repo/secrets/",
   334  			},
   335  			golden: "testdata/secrets.asff.golden",
   336  		},
   337  		{
   338  			name: "alpine 3.10 with html template",
   339  			args: csArgs{
   340  				Format:       "template",
   341  				TemplatePath: "@../contrib/html.tpl",
   342  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   343  			},
   344  			golden: "testdata/alpine-310.html.golden",
   345  		},
   346  		{
   347  			name: "alpine 3.10 with junit template",
   348  			args: csArgs{
   349  				Format:       "template",
   350  				TemplatePath: "@../contrib/junit.tpl",
   351  				Input:        "testdata/fixtures/images/alpine-310.tar.gz",
   352  			},
   353  			golden: "testdata/alpine-310.junit.golden",
   354  		},
   355  		{
   356  			name: "alpine 3.10 with github dependency snapshots format",
   357  			args: csArgs{
   358  				Format: "github",
   359  				Input:  "testdata/fixtures/images/alpine-310.tar.gz",
   360  			},
   361  			golden: "testdata/alpine-310.gsbom.golden",
   362  		},
   363  	}
   364  
   365  	fakeTime := time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)
   366  	report.CustomTemplateFuncMap = map[string]interface{}{
   367  		"now": func() time.Time {
   368  			return fakeTime
   369  		},
   370  		"date": func(format string, t time.Time) string {
   371  			return t.Format(format)
   372  		},
   373  	}
   374  
   375  	// For GitHub Dependency Snapshots
   376  	t.Setenv("GITHUB_REF", "/ref/feature-1")
   377  	t.Setenv("GITHUB_SHA", "39da54a1ff04120a31df8cbc94ce9ede251d21a3")
   378  	t.Setenv("GITHUB_JOB", "integration")
   379  	t.Setenv("GITHUB_RUN_ID", "1910764383")
   380  	t.Setenv("GITHUB_WORKFLOW", "workflow-name")
   381  
   382  	t.Cleanup(func() {
   383  		report.CustomTemplateFuncMap = map[string]interface{}{}
   384  	})
   385  
   386  	addr, cacheDir := setup(t, setupOptions{})
   387  
   388  	for _, tt := range tests {
   389  		t.Run(tt.name, func(t *testing.T) {
   390  			t.Setenv("AWS_REGION", "test-region")
   391  			t.Setenv("AWS_ACCOUNT_ID", "123456789012")
   392  			osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden)
   393  
   394  			// Run Trivy client
   395  			err := execute(osArgs)
   396  			require.NoError(t, err)
   397  
   398  			want, err := os.ReadFile(tt.golden)
   399  			require.NoError(t, err)
   400  
   401  			got, err := os.ReadFile(outputFile)
   402  			require.NoError(t, err)
   403  
   404  			assert.EqualValues(t, string(want), string(got))
   405  		})
   406  	}
   407  }
   408  
   409  func TestClientServerWithCycloneDX(t *testing.T) {
   410  	tests := []struct {
   411  		name   string
   412  		args   csArgs
   413  		golden string
   414  	}{
   415  		{
   416  			name: "fluentd with RubyGems with CycloneDX format",
   417  			args: csArgs{
   418  				Format: "cyclonedx",
   419  				Input:  "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz",
   420  			},
   421  			golden: "testdata/fluentd-multiple-lockfiles.cdx.json.golden",
   422  		},
   423  	}
   424  
   425  	addr, cacheDir := setup(t, setupOptions{})
   426  	for _, tt := range tests {
   427  		t.Run(tt.name, func(t *testing.T) {
   428  			uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d")
   429  
   430  			osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden)
   431  
   432  			// Run Trivy client
   433  			err := execute(osArgs)
   434  			require.NoError(t, err)
   435  
   436  			compareCycloneDX(t, tt.golden, outputFile)
   437  		})
   438  	}
   439  }
   440  
   441  func TestClientServerWithToken(t *testing.T) {
   442  	cases := []struct {
   443  		name    string
   444  		args    csArgs
   445  		golden  string
   446  		wantErr string
   447  	}{
   448  		{
   449  			name: "alpine 3.9 with token",
   450  			args: csArgs{
   451  				Input:             "testdata/fixtures/images/alpine-39.tar.gz",
   452  				ClientToken:       "token",
   453  				ClientTokenHeader: "Trivy-Token",
   454  			},
   455  			golden: "testdata/alpine-39.json.golden",
   456  		},
   457  		{
   458  			name: "invalid token",
   459  			args: csArgs{
   460  				Input:             "testdata/fixtures/images/distroless-base.tar.gz",
   461  				ClientToken:       "invalidtoken",
   462  				ClientTokenHeader: "Trivy-Token",
   463  			},
   464  			wantErr: "twirp error unauthenticated: invalid token",
   465  		},
   466  		{
   467  			name: "invalid token header",
   468  			args: csArgs{
   469  				Input:             "testdata/fixtures/images/distroless-base.tar.gz",
   470  				ClientToken:       "token",
   471  				ClientTokenHeader: "Unknown-Header",
   472  			},
   473  			wantErr: "twirp error unauthenticated: invalid token",
   474  		},
   475  	}
   476  
   477  	serverToken := "token"
   478  	serverTokenHeader := "Trivy-Token"
   479  	addr, cacheDir := setup(t, setupOptions{
   480  		token:       serverToken,
   481  		tokenHeader: serverTokenHeader,
   482  	})
   483  
   484  	for _, c := range cases {
   485  		t.Run(c.name, func(t *testing.T) {
   486  			osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden)
   487  
   488  			// Run Trivy client
   489  			err := execute(osArgs)
   490  			if c.wantErr != "" {
   491  				require.Error(t, err, c.name)
   492  				assert.Contains(t, err.Error(), c.wantErr, c.name)
   493  				return
   494  			}
   495  
   496  			require.NoError(t, err, c.name)
   497  			compareReports(t, c.golden, outputFile, nil)
   498  		})
   499  	}
   500  }
   501  
   502  func TestClientServerWithRedis(t *testing.T) {
   503  	// Set up a Redis container
   504  	ctx := context.Background()
   505  	// This test includes 2 checks
   506  	// redisC container will terminate after first check
   507  	redisC, addr := setupRedis(t, ctx)
   508  
   509  	// Set up Trivy server
   510  	addr, cacheDir := setup(t, setupOptions{cacheBackend: addr})
   511  	t.Cleanup(func() { os.RemoveAll(cacheDir) })
   512  
   513  	// Test parameters
   514  	testArgs := csArgs{
   515  		Input: "testdata/fixtures/images/alpine-39.tar.gz",
   516  	}
   517  	golden := "testdata/alpine-39.json.golden"
   518  
   519  	t.Run("alpine 3.9", func(t *testing.T) {
   520  		osArgs, outputFile := setupClient(t, testArgs, addr, cacheDir, golden)
   521  
   522  		// Run Trivy client
   523  		err := execute(osArgs)
   524  		require.NoError(t, err)
   525  
   526  		compareReports(t, golden, outputFile, nil)
   527  	})
   528  
   529  	// Terminate the Redis container
   530  	require.NoError(t, redisC.Terminate(ctx))
   531  
   532  	t.Run("sad path", func(t *testing.T) {
   533  		osArgs, _ := setupClient(t, testArgs, addr, cacheDir, golden)
   534  
   535  		// Run Trivy client
   536  		err := execute(osArgs)
   537  		require.Error(t, err)
   538  		assert.Contains(t, err.Error(), "unable to store cache")
   539  	})
   540  }
   541  
   542  type setupOptions struct {
   543  	token        string
   544  	tokenHeader  string
   545  	cacheBackend string
   546  }
   547  
   548  func setup(t *testing.T, options setupOptions) (string, string) {
   549  	t.Helper()
   550  
   551  	// Set up testing DB
   552  	cacheDir := initDB(t)
   553  
   554  	// Set a temp dir so that modules will not be loaded
   555  	t.Setenv("XDG_DATA_HOME", cacheDir)
   556  
   557  	port, err := getFreePort()
   558  	assert.NoError(t, err)
   559  	addr := fmt.Sprintf("localhost:%d", port)
   560  
   561  	go func() {
   562  		osArgs := setupServer(addr, options.token, options.tokenHeader, cacheDir, options.cacheBackend)
   563  
   564  		// Run Trivy server
   565  		require.NoError(t, execute(osArgs))
   566  	}()
   567  
   568  	ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
   569  	err = waitPort(ctx, addr)
   570  	assert.NoError(t, err)
   571  
   572  	return addr, cacheDir
   573  }
   574  
   575  func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string {
   576  	osArgs := []string{
   577  		"--cache-dir",
   578  		cacheDir,
   579  		"server",
   580  		"--skip-update",
   581  		"--listen",
   582  		addr,
   583  	}
   584  	if token != "" {
   585  		osArgs = append(osArgs, []string{
   586  			"--token",
   587  			token,
   588  			"--token-header",
   589  			tokenHeader,
   590  		}...)
   591  	}
   592  	if cacheBackend != "" {
   593  		osArgs = append(osArgs, "--cache-backend", cacheBackend)
   594  	}
   595  	return osArgs
   596  }
   597  
   598  func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden string) ([]string, string) {
   599  	if c.Command == "" {
   600  		c.Command = "image"
   601  	}
   602  	if c.RemoteAddrOption == "" {
   603  		c.RemoteAddrOption = "--server"
   604  	}
   605  	t.Helper()
   606  	osArgs := []string{
   607  		"--cache-dir",
   608  		cacheDir,
   609  		c.Command,
   610  		c.RemoteAddrOption,
   611  		"http://" + addr,
   612  	}
   613  
   614  	if c.Format != "" {
   615  		osArgs = append(osArgs, "--format", c.Format)
   616  		if c.TemplatePath != "" {
   617  			osArgs = append(osArgs, "--template", c.TemplatePath)
   618  		}
   619  	} else {
   620  		osArgs = append(osArgs, "--format", "json")
   621  	}
   622  
   623  	if c.IgnoreUnfixed {
   624  		osArgs = append(osArgs, "--ignore-unfixed")
   625  	}
   626  	if len(c.Severity) != 0 {
   627  		osArgs = append(osArgs,
   628  			"--severity", strings.Join(c.Severity, ","),
   629  		)
   630  	}
   631  
   632  	if len(c.IgnoreIDs) != 0 {
   633  		trivyIgnore := filepath.Join(t.TempDir(), ".trivyignore")
   634  		err := os.WriteFile(trivyIgnore, []byte(strings.Join(c.IgnoreIDs, "\n")), 0444)
   635  		require.NoError(t, err, "failed to write .trivyignore")
   636  		osArgs = append(osArgs, "--ignorefile", trivyIgnore)
   637  	}
   638  	if c.ClientToken != "" {
   639  		osArgs = append(osArgs, "--token", c.ClientToken, "--token-header", c.ClientTokenHeader)
   640  	}
   641  	if c.Input != "" {
   642  		osArgs = append(osArgs, "--input", c.Input)
   643  	}
   644  
   645  	// Set up the output file
   646  	outputFile := filepath.Join(t.TempDir(), "output.json")
   647  	if *update {
   648  		outputFile = golden
   649  	}
   650  
   651  	osArgs = append(osArgs, "--output", outputFile)
   652  
   653  	if c.Target != "" {
   654  		osArgs = append(osArgs, c.Target)
   655  	}
   656  
   657  	return osArgs, outputFile
   658  }
   659  
   660  func setupRedis(t *testing.T, ctx context.Context) (testcontainers.Container, string) {
   661  	t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
   662  	t.Helper()
   663  	imageName := "redis:5.0"
   664  	port := "6379/tcp"
   665  	req := testcontainers.ContainerRequest{
   666  		Name:         "redis",
   667  		Image:        imageName,
   668  		ExposedPorts: []string{port},
   669  		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
   670  			hostConfig.AutoRemove = true
   671  		},
   672  	}
   673  
   674  	redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
   675  		ContainerRequest: req,
   676  		Started:          true,
   677  	})
   678  	require.NoError(t, err)
   679  
   680  	ip, err := redis.Host(ctx)
   681  	require.NoError(t, err)
   682  
   683  	p, err := redis.MappedPort(ctx, nat.Port(port))
   684  	require.NoError(t, err)
   685  
   686  	addr := fmt.Sprintf("redis://%s:%s", ip, p.Port())
   687  	return redis, addr
   688  }