get.porter.sh/porter@v1.3.0/tests/smoke/airgap_test.go (about)

     1  //go:build smoke
     2  
     3  package smoke
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"get.porter.sh/porter/pkg/yaml"
    13  	"get.porter.sh/porter/tests/testdata"
    14  	"get.porter.sh/porter/tests/tester"
    15  	"github.com/cnabio/cnab-go/bundle/loader"
    16  	"github.com/cnabio/cnab-go/packager"
    17  	"github.com/cnabio/cnab-to-oci/relocation"
    18  	"github.com/stretchr/testify/require"
    19  	"github.com/uwu-tools/magex/shx"
    20  )
    21  
    22  // Validate that we can move a bundle into an aigraped environment
    23  // and that it works without referencing the old environment/images.
    24  // This also validates a lot of our insecure/unsecured registry configurations.
    25  func TestAirgappedEnvironment(t *testing.T) {
    26  	testcases := []struct {
    27  		name     string
    28  		useTLS   bool
    29  		useAlias bool
    30  		insecure bool
    31  	}{
    32  		// Validate we "just work" with an unsecured registry on localhost, just like docker does, without specifying --insecure-registry
    33  		{name: "plain http, no alias", useTLS: false, useAlias: false, insecure: false},
    34  		// Validate we can connect to plain http when we can't detect that it's loopback/localhost as long as --insecure-registry is specified
    35  		// We do not support docker's extra automagic where it resolves the host and treats it like localhost. You have to specify --insecure-registry with a custom hostname
    36  		{name: "plain http, use alias", useTLS: false, useAlias: true, insecure: true},
    37  		// Validate that --insecure-registry works with self-signed certificates
    38  		{name: "untrusted tls, no alias", useTLS: false, useAlias: true, insecure: true},
    39  	}
    40  	for _, tc := range testcases {
    41  		tc := tc
    42  		t.Run(tc.name, func(t *testing.T) {
    43  			insecureFlag := fmt.Sprintf("--insecure-registry=%t", tc.insecure)
    44  
    45  			test, err := tester.NewTest(t)
    46  			defer test.Close()
    47  			require.NoError(t, err, "test setup failed")
    48  			test.Chdir(test.TestDir)
    49  
    50  			// Start a temporary insecure test registry
    51  			reg1 := test.StartTestRegistry(tester.TestRegistryOptions{UseTLS: tc.useTLS, UseAlias: tc.useAlias})
    52  			t.Cleanup(reg1.Close)
    53  
    54  			// Publish referenced image to the insecure registry
    55  			// This helps test that we can publish a bundle that references images from multiple registries
    56  			// and that we properly apply --insecure-registry to those registries (and not just the registry to which we are pushing)
    57  			referencedImg := "carolynvs/whalesayd@sha256:8b92b7269f59e3ed824e811a1ff1ee64f0d44c0218efefada57a4bebc2d7ef6f"
    58  			localRegRepo := fmt.Sprintf("%s/whalesayd", reg1)
    59  			localRegRef := localRegRepo + ":latest"
    60  			require.NoError(t, shx.RunV("docker", "pull", referencedImg))
    61  			require.NoError(t, shx.RunV("docker", "tag", referencedImg, localRegRef))
    62  			output, err := shx.OutputV("docker", "push", localRegRef)
    63  			require.NoError(t, err)
    64  			digest := getDigestFromDockerOutput(test.T, output)
    65  			localRefWithDigest := fmt.Sprintf("%s@%s", localRegRepo, digest)
    66  
    67  			// Start a second insecure test registry
    68  			reg2 := test.StartTestRegistry(tester.TestRegistryOptions{UseTLS: tc.useTLS, UseAlias: tc.useAlias})
    69  			t.Cleanup(reg2.Close)
    70  
    71  			// Edit the bundle so that it's referencing the image on the temporary registry
    72  			// make sure the referenced image is not in local image cache
    73  			shx.RunV("docker", "rmi", localRegRef)
    74  			err = shx.Copy(filepath.Join(test.RepoRoot, fmt.Sprintf("tests/testdata/%s/*", testdata.MyBuns)), test.TestDir)
    75  			require.NoError(t, err, "failed to copy test bundle")
    76  			test.EditYaml(filepath.Join(test.TestDir, "porter.yaml"), func(yq *yaml.Editor) error {
    77  				// Remove the bundle's dependencies to simplify installation
    78  				if err := yq.DeleteNode("dependencies"); err != nil {
    79  					return err
    80  				}
    81  
    82  				// Reference our copy of the whalesayd image
    83  				return yq.SetValue("images.whalesayd.repository", fmt.Sprintf("%s/whalesayd", reg1))
    84  			})
    85  
    86  			// Build the test bundle separate from publish so we best validate that we aren't pulling referenced images during build anymore
    87  			test.RequirePorter("build", insecureFlag)
    88  
    89  			// Validate that the referenced bundle is not in the local docker cache and that build did not pull it
    90  			err = shx.RunE("docker", "image", "inspect", localRefWithDigest)
    91  			if err == nil {
    92  				t.Fatalf("expected %s to not be in the local docker cache after building the bundle. Build should no longer pull referenced images.", localRefWithDigest)
    93  			}
    94  
    95  			// Publish a test bundle that references the image from the temp registry, and push to another insecure registry
    96  			_, output = test.RequirePorter("publish", "--registry", reg2.String(), insecureFlag, "--verbosity=debug")
    97  
    98  			// Validate that we aren't getting a rebuild since we are building immediately before publish
    99  			// I am using --verbosity=debug above so that we can see the reason why a rebuild was triggered
   100  			// which helps with troubleshooting if/when a regression occurs.
   101  			require.NotContains(t, output, "Building bundle", "expected a rebuild to not happen because we built before publishing")
   102  
   103  			// Stop the original registry, this ensures that we are relying 100% on the copy of the bundle in the second registry
   104  			reg1.Close()
   105  
   106  			//
   107  			// Try out the two ways to move a bundle between registries:
   108  			// 1. Copy the bundle from one registry to the other directly
   109  			//
   110  			origRef := fmt.Sprintf("%s/%s:%s", reg2, testdata.MyBuns, "v0.1.2")
   111  			newRef := fmt.Sprintf("%s/%s-second:%s", reg2, testdata.MyBuns, "v0.2.0")
   112  			test.RequirePorter("copy", "--source", origRef, "--destination", newRef, insecureFlag)
   113  
   114  			//
   115  			// 2. Use archive + publish to copy the bundle from one registry to the other
   116  			//
   117  			archiveFilePath := filepath.Join(test.TestDir, "archive-test.tgz")
   118  			test.RequirePorter("archive", archiveFilePath, "--reference", origRef, insecureFlag)
   119  			relocMap := getRelocationMap(test, archiveFilePath)
   120  			require.Equal(test.T, fmt.Sprintf("%s/mybuns@sha256:499f71eec2e3bd78f26c268bbf5b2a65f73b96216fac4a89b86b5ebf115527b6", reg2), relocMap[localRefWithDigest], "expected the relocation entry for the image to be the new published location")
   121  
   122  			// Publish from the archived bundle to a new repository on the second registry
   123  			// Specify --force since we are overwriting the tag pushed to during the last copy
   124  			test.RequirePorter("publish", "--archive", archiveFilePath, "-r", newRef, insecureFlag, "--force")
   125  			archiveFilePath2 := filepath.Join(test.TestDir, "archive-test2.tgz")
   126  
   127  			// Archive from the new location on the second registry
   128  			test.RequirePorter("archive", archiveFilePath2, "--reference", newRef, insecureFlag)
   129  			relocMap2 := getRelocationMap(test, archiveFilePath2)
   130  			require.Equal(test.T, fmt.Sprintf("%s/mybuns-second@sha256:499f71eec2e3bd78f26c268bbf5b2a65f73b96216fac4a89b86b5ebf115527b6", reg2), relocMap2[localRefWithDigest], "expected the relocation entry for the image to be the new published location")
   131  
   132  			// Validate that we can pull the bundle from the new location
   133  			test.RequirePorter("explain", newRef)
   134  
   135  			// Validate that we can install from the new location
   136  			test.ApplyTestBundlePrerequisites()
   137  			test.RequirePorter("install", "-r", newRef, insecureFlag, "-c=mybuns", "-p=mybuns")
   138  		})
   139  	}
   140  }
   141  
   142  func getRelocationMap(test tester.Tester, archiveFilePath string) relocation.ImageRelocationMap {
   143  	l := loader.NewLoader()
   144  	imp := packager.NewImporter(archiveFilePath, test.TestDir, l)
   145  	err := imp.Import()
   146  	require.NoError(test.T, err, "opening archive failed")
   147  
   148  	_, err = test.TestContext.FileSystem.Stat(filepath.Join(test.TestDir, strings.TrimSuffix(filepath.Base(archiveFilePath), ".tgz"), "bundle.json"))
   149  	require.NoError(test.T, err)
   150  	relocMapBytes, err := test.TestContext.FileSystem.ReadFile(filepath.Join(test.TestDir, strings.TrimSuffix(filepath.Base(archiveFilePath), ".tgz"), "relocation-mapping.json"))
   151  	require.NoError(test.T, err)
   152  
   153  	// make sure the relocation map contains the expected image
   154  	relocMap := relocation.ImageRelocationMap{}
   155  	require.NoError(test.T, json.Unmarshal(relocMapBytes, &relocMap))
   156  	return relocMap
   157  }
   158  
   159  func getDigestFromDockerOutput(t *testing.T, output string) string {
   160  	_, after, found := strings.Cut(output, "digest: ")
   161  	require.True(t, found)
   162  	results := strings.Split(after, " ")
   163  	require.Greater(t, len(results), 1)
   164  	require.Contains(t, results[0], "sha256")
   165  
   166  	return results[0]
   167  }