508 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			508 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2018 Google LLC
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package executor
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/GoogleContainerTools/kaniko/pkg/config"
 | |
| 	"github.com/GoogleContainerTools/kaniko/pkg/util"
 | |
| 	"github.com/GoogleContainerTools/kaniko/testutil"
 | |
| 	"github.com/google/go-containerregistry/pkg/authn"
 | |
| 	"github.com/google/go-containerregistry/pkg/name"
 | |
| 	"github.com/google/go-containerregistry/pkg/v1/layout"
 | |
| 	"github.com/google/go-containerregistry/pkg/v1/random"
 | |
| 	"github.com/google/go-containerregistry/pkg/v1/validate"
 | |
| 	"github.com/spf13/afero"
 | |
| )
 | |
| 
 | |
| func mustTag(t *testing.T, s string) name.Tag {
 | |
| 	tag, err := name.NewTag(s, name.StrictValidation)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("NewTag: %v", err)
 | |
| 	}
 | |
| 	return tag
 | |
| }
 | |
| 
 | |
| func TestWriteImageOutputs(t *testing.T) {
 | |
| 	img, err := random.Image(1024, 3)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("random.Image: %v", err)
 | |
| 	}
 | |
| 	d, err := img.Digest()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Digest: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	for _, c := range []struct {
 | |
| 		desc, env string
 | |
| 		tags      []name.Tag
 | |
| 		want      string
 | |
| 	}{{
 | |
| 		desc: "env unset, no output",
 | |
| 		env:  "",
 | |
| 	}, {
 | |
| 		desc: "env set, one tag",
 | |
| 		env:  "/foo",
 | |
| 		tags: []name.Tag{mustTag(t, "gcr.io/foo/bar:latest")},
 | |
| 		want: fmt.Sprintf(`{"name":"gcr.io/foo/bar:latest","digest":%q}
 | |
| `, d),
 | |
| 	}, {
 | |
| 		desc: "env set, two tags",
 | |
| 		env:  "/foo",
 | |
| 		tags: []name.Tag{
 | |
| 			mustTag(t, "gcr.io/foo/bar:latest"),
 | |
| 			mustTag(t, "gcr.io/baz/qux:latest"),
 | |
| 		},
 | |
| 		want: fmt.Sprintf(`{"name":"gcr.io/foo/bar:latest","digest":%q}
 | |
| {"name":"gcr.io/baz/qux:latest","digest":%q}
 | |
| `, d, d),
 | |
| 	}} {
 | |
| 		t.Run(c.desc, func(t *testing.T) {
 | |
| 			newOsFs = afero.NewMemMapFs()
 | |
| 			if c.want == "" {
 | |
| 				newOsFs = afero.NewReadOnlyFs(newOsFs) // No files should be written.
 | |
| 			}
 | |
| 
 | |
| 			os.Setenv("BUILDER_OUTPUT", c.env)
 | |
| 			if err := writeImageOutputs(img, c.tags); err != nil {
 | |
| 				t.Fatalf("writeImageOutputs: %v", err)
 | |
| 			}
 | |
| 
 | |
| 			if c.want == "" {
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			b, err := afero.ReadFile(newOsFs, filepath.Join(c.env, "images"))
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("ReadFile: %v", err)
 | |
| 			}
 | |
| 
 | |
| 			if got := string(b); got != c.want {
 | |
| 				t.Fatalf(" got: %s\nwant: %s", got, c.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHeaderAdded(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		upstream string
 | |
| 		expected string
 | |
| 	}{{
 | |
| 		name:     "upstream env variable set",
 | |
| 		upstream: "skaffold-v0.25.45",
 | |
| 		expected: "kaniko/unset,skaffold-v0.25.45",
 | |
| 	}, {
 | |
| 		name:     "upstream env variable not set",
 | |
| 		expected: "kaniko/unset",
 | |
| 	},
 | |
| 	}
 | |
| 	for _, test := range tests {
 | |
| 
 | |
| 		t.Run(test.name, func(t *testing.T) {
 | |
| 			rt := &withUserAgent{t: &mockRoundTripper{}}
 | |
| 			if test.upstream != "" {
 | |
| 				os.Setenv("UPSTREAM_CLIENT_TYPE", test.upstream)
 | |
| 				defer func() { os.Unsetenv("UPSTREAM_CLIENT_TYPE") }()
 | |
| 			}
 | |
| 			req, err := http.NewRequest("GET", "dummy", nil) //nolint:noctx
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("culd not create a req due to %s", err)
 | |
| 			}
 | |
| 			resp, err := rt.RoundTrip(req)
 | |
| 			testutil.CheckError(t, false, err)
 | |
| 			defer resp.Body.Close()
 | |
| 			body, err := io.ReadAll(resp.Body)
 | |
| 			testutil.CheckErrorAndDeepEqual(t, false, err, test.expected, string(body))
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| type mockRoundTripper struct {
 | |
| }
 | |
| 
 | |
| func (m *mockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
 | |
| 	ua := r.UserAgent()
 | |
| 	return &http.Response{Body: io.NopCloser(bytes.NewBufferString(ua))}, nil
 | |
| }
 | |
| 
 | |
| func TestOCILayoutPath(t *testing.T) {
 | |
| 	tmpDir := t.TempDir()
 | |
| 
 | |
| 	image, err := random.Image(1024, 4)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not create image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	digest, err := image.Digest()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get image digest: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	want, err := image.Manifest()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get image manifest: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	opts := config.KanikoOptions{
 | |
| 		NoPush:        true,
 | |
| 		OCILayoutPath: tmpDir,
 | |
| 	}
 | |
| 
 | |
| 	if err := DoPush(image, &opts); err != nil {
 | |
| 		t.Fatalf("could not push image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	layoutIndex, err := layout.ImageIndexFromPath(tmpDir)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get index from layout: %s", err)
 | |
| 	}
 | |
| 	testutil.CheckError(t, false, validate.Index(layoutIndex))
 | |
| 
 | |
| 	layoutImage, err := layoutIndex.Image(digest)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get image from layout: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	got, err := layoutImage.Manifest()
 | |
| 	testutil.CheckErrorAndDeepEqual(t, false, err, want, got)
 | |
| }
 | |
| 
 | |
| func TestImageNameDigestFile(t *testing.T) {
 | |
| 	image, err := random.Image(1024, 4)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not create image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	digest, err := image.Digest()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get image digest: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	opts := config.KanikoOptions{
 | |
| 		NoPush:              true,
 | |
| 		Destinations:        []string{"gcr.io/foo/bar:latest", "bob/image"},
 | |
| 		ImageNameDigestFile: "tmpFile",
 | |
| 	}
 | |
| 
 | |
| 	defer os.Remove("tmpFile")
 | |
| 
 | |
| 	if err := DoPush(image, &opts); err != nil {
 | |
| 		t.Fatalf("could not push image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	want := []byte("gcr.io/foo/bar@" + digest.String() + "\nindex.docker.io/bob/image@" + digest.String() + "\n")
 | |
| 
 | |
| 	got, err := os.ReadFile("tmpFile")
 | |
| 
 | |
| 	testutil.CheckErrorAndDeepEqual(t, false, err, want, got)
 | |
| 
 | |
| }
 | |
| 
 | |
| func TestDoPushWithOpts(t *testing.T) {
 | |
| 	tarPath := "image.tar"
 | |
| 
 | |
| 	for _, tc := range []struct {
 | |
| 		name        string
 | |
| 		opts        config.KanikoOptions
 | |
| 		expectedErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "no push with tarPath without destinations",
 | |
| 			opts: config.KanikoOptions{
 | |
| 				NoPush:  true,
 | |
| 				TarPath: tarPath,
 | |
| 			},
 | |
| 			expectedErr: false,
 | |
| 		}, {
 | |
| 			name: "no push with tarPath with destinations",
 | |
| 			opts: config.KanikoOptions{
 | |
| 				NoPush:       true,
 | |
| 				TarPath:      tarPath,
 | |
| 				Destinations: []string{"image"},
 | |
| 			},
 | |
| 			expectedErr: false,
 | |
| 		}, {
 | |
| 			name: "no push with tarPath with destinations empty",
 | |
| 			opts: config.KanikoOptions{
 | |
| 				NoPush:       true,
 | |
| 				TarPath:      tarPath,
 | |
| 				Destinations: []string{},
 | |
| 			},
 | |
| 			expectedErr: false,
 | |
| 		}, {
 | |
| 			name: "tarPath with destinations empty",
 | |
| 			opts: config.KanikoOptions{
 | |
| 				NoPush:       false,
 | |
| 				TarPath:      tarPath,
 | |
| 				Destinations: []string{},
 | |
| 			},
 | |
| 			expectedErr: true,
 | |
| 		}} {
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			image, err := random.Image(1024, 4)
 | |
| 			if err != nil {
 | |
| 				t.Fatalf("could not create image: %s", err)
 | |
| 			}
 | |
| 			defer os.Remove("image.tar")
 | |
| 
 | |
| 			err = DoPush(image, &tc.opts)
 | |
| 			if err != nil {
 | |
| 				if !tc.expectedErr {
 | |
| 					t.Errorf("unexpected error with opts: could not push image: %s", err)
 | |
| 				}
 | |
| 			} else {
 | |
| 				if tc.expectedErr {
 | |
| 					t.Error("expected error with opts not found")
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestImageNameTagDigestFile(t *testing.T) {
 | |
| 	image, err := random.Image(1024, 4)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not create image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	digest, err := image.Digest()
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("could not get image digest: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	opts := config.KanikoOptions{
 | |
| 		NoPush:                 true,
 | |
| 		Destinations:           []string{"gcr.io/foo/bar:123", "bob/image"},
 | |
| 		ImageNameTagDigestFile: "tmpFile",
 | |
| 	}
 | |
| 
 | |
| 	defer os.Remove("tmpFile")
 | |
| 
 | |
| 	if err := DoPush(image, &opts); err != nil {
 | |
| 		t.Fatalf("could not push image: %s", err)
 | |
| 	}
 | |
| 
 | |
| 	want := []byte("gcr.io/foo/bar:123@" + digest.String() + "\nindex.docker.io/bob/image:latest@" + digest.String() + "\n")
 | |
| 
 | |
| 	got, err := os.ReadFile("tmpFile")
 | |
| 
 | |
| 	testutil.CheckErrorAndDeepEqual(t, false, err, want, got)
 | |
| }
 | |
| 
 | |
| var checkPushPermsCallCount = 0
 | |
| 
 | |
| func resetCalledCount() {
 | |
| 	checkPushPermsCallCount = 0
 | |
| }
 | |
| 
 | |
| func fakeCheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error {
 | |
| 	checkPushPermsCallCount++
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func TestCheckPushPermissions(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		description                     string
 | |
| 		cacheRepo                       string
 | |
| 		checkPushPermsExpectedCallCount int
 | |
| 		destinations                    []string
 | |
| 		existingConfig                  bool
 | |
| 		noPush                          bool
 | |
| 		noPushCache                     bool
 | |
| 	}{
 | |
| 		{description: "a gcr image without config", destinations: []string{"gcr.io/test-image"}, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "a gcr image with config", destinations: []string{"gcr.io/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "a pkg.dev image without config", destinations: []string{"us-docker.pkg.dev/test-image"}, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "a pkg.dev image with config", destinations: []string{"us-docker.pkg.dev/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "localhost registry without config", destinations: []string{"localhost:5000/test-image"}, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "localhost registry with config", destinations: []string{"localhost:5000/test-image"}, existingConfig: true, checkPushPermsExpectedCallCount: 1},
 | |
| 		{description: "any other registry", destinations: []string{"notgcr.io/test-image"}, checkPushPermsExpectedCallCount: 1},
 | |
| 		{
 | |
| 			description: "multiple destinations pushed to different registry",
 | |
| 			destinations: []string{
 | |
| 				"us-central1-docker.pkg.dev/prj/test-image",
 | |
| 				"us-west-docker.pkg.dev/prj/test-image",
 | |
| 			},
 | |
| 			checkPushPermsExpectedCallCount: 2,
 | |
| 		},
 | |
| 		{
 | |
| 			description: "same image names with different tags",
 | |
| 			destinations: []string{
 | |
| 				"us-central1-docker.pkg.dev/prj/test-image:tag1",
 | |
| 				"us-central1-docker.pkg.dev/prj/test-image:tag2",
 | |
| 			},
 | |
| 			checkPushPermsExpectedCallCount: 1,
 | |
| 		},
 | |
| 		{
 | |
| 			description: "same destination image multiple times",
 | |
| 			destinations: []string{
 | |
| 				"us-central1-docker.pkg.dev/prj/test-image",
 | |
| 				"us-central1-docker.pkg.dev/prj/test-image",
 | |
| 			},
 | |
| 			checkPushPermsExpectedCallCount: 1,
 | |
| 		},
 | |
| 		{
 | |
| 			description:                     "no push and no push cache",
 | |
| 			destinations:                    []string{"us-central1-docker.pkg.dev/prj/test-image"},
 | |
| 			checkPushPermsExpectedCallCount: 0,
 | |
| 			noPush:                          true,
 | |
| 			noPushCache:                     true,
 | |
| 		},
 | |
| 		{
 | |
| 			description:                     "no push and push cache",
 | |
| 			destinations:                    []string{"us-central1-docker.pkg.dev/prj/test-image"},
 | |
| 			cacheRepo:                       "us-central1-docker.pkg.dev/prj/cache-image",
 | |
| 			checkPushPermsExpectedCallCount: 1,
 | |
| 			noPush:                          true,
 | |
| 		},
 | |
| 		{
 | |
| 			description:                     "no push and cache repo is OCI image layout",
 | |
| 			destinations:                    []string{"us-central1-docker.pkg.dev/prj/test-image"},
 | |
| 			cacheRepo:                       "oci:/some-layout-path",
 | |
| 			checkPushPermsExpectedCallCount: 0,
 | |
| 			noPush:                          true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	checkRemotePushPermission = fakeCheckPushPermission
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.description, func(t *testing.T) {
 | |
| 			resetCalledCount()
 | |
| 			newOsFs = afero.NewMemMapFs()
 | |
| 			opts := config.KanikoOptions{
 | |
| 				CacheRepo:    test.cacheRepo,
 | |
| 				Destinations: test.destinations,
 | |
| 				NoPush:       test.noPush,
 | |
| 				NoPushCache:  test.noPushCache,
 | |
| 			}
 | |
| 			if test.existingConfig {
 | |
| 				afero.WriteFile(newOsFs, util.DockerConfLocation(), []byte(""), os.FileMode(0644))
 | |
| 				defer newOsFs.Remove(util.DockerConfLocation())
 | |
| 			}
 | |
| 			CheckPushPermissions(&opts)
 | |
| 			if checkPushPermsCallCount != test.checkPushPermsExpectedCallCount {
 | |
| 				t.Errorf("expected check push permissions call count to be %d but it was %d", test.checkPushPermsExpectedCallCount, checkPushPermsCallCount)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestSkipPushPermission(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		description                     string
 | |
| 		cacheRepo                       string
 | |
| 		checkPushPermsExpectedCallCount int
 | |
| 		destinations                    []string
 | |
| 		existingConfig                  bool
 | |
| 		noPush                          bool
 | |
| 		noPushCache                     bool
 | |
| 		skipPushPermission              bool
 | |
| 	}{
 | |
| 		{description: "skip push permission enabled", destinations: []string{"test.io/skip"}, checkPushPermsExpectedCallCount: 0, skipPushPermission: true},
 | |
| 		{description: "skip push permission disabled", destinations: []string{"test.io/push"}, checkPushPermsExpectedCallCount: 1, skipPushPermission: false},
 | |
| 	}
 | |
| 
 | |
| 	checkRemotePushPermission = fakeCheckPushPermission
 | |
| 	for _, test := range tests {
 | |
| 		t.Run(test.description, func(t *testing.T) {
 | |
| 			resetCalledCount()
 | |
| 			newOsFs = afero.NewMemMapFs()
 | |
| 			opts := config.KanikoOptions{
 | |
| 				CacheRepo:               test.cacheRepo,
 | |
| 				Destinations:            test.destinations,
 | |
| 				NoPush:                  test.noPush,
 | |
| 				NoPushCache:             test.noPushCache,
 | |
| 				SkipPushPermissionCheck: test.skipPushPermission,
 | |
| 			}
 | |
| 			if test.existingConfig {
 | |
| 				afero.WriteFile(newOsFs, util.DockerConfLocation(), []byte(""), os.FileMode(0644))
 | |
| 				defer newOsFs.Remove(util.DockerConfLocation())
 | |
| 			}
 | |
| 			CheckPushPermissions(&opts)
 | |
| 			if checkPushPermsCallCount != test.checkPushPermsExpectedCallCount {
 | |
| 				t.Errorf("expected check push permissions call count to be %d but it was %d", test.checkPushPermsExpectedCallCount, checkPushPermsCallCount)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestHelperProcess(t *testing.T) {
 | |
| 	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
 | |
| 		return
 | |
| 	}
 | |
| 	fmt.Fprintf(os.Stdout, "fake result")
 | |
| 	os.Exit(0)
 | |
| }
 | |
| 
 | |
| func TestWriteDigestFile(t *testing.T) {
 | |
| 	tmpDir := t.TempDir()
 | |
| 
 | |
| 	t.Run("parent directory does not exist", func(t *testing.T) {
 | |
| 		err := writeDigestFile(tmpDir+"/test/df", []byte("test"))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("expected file to be written successfully, but got error: %v", err)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("parent directory exists", func(t *testing.T) {
 | |
| 		err := writeDigestFile(tmpDir+"/df", []byte("test"))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("expected file to be written successfully, but got error: %v", err)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("https PUT OK", func(t *testing.T) {
 | |
| 		var uploadedContent []byte
 | |
| 
 | |
| 		// Start a test server that checks the PUT request.
 | |
| 		server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | |
| 			if r.Method != http.MethodPut {
 | |
| 				w.WriteHeader(http.StatusMethodNotAllowed)
 | |
| 				return
 | |
| 			}
 | |
| 			uploadedContent, _ = io.ReadAll(r.Body)
 | |
| 			w.WriteHeader(http.StatusNoContent)
 | |
| 		}))
 | |
| 		defer server.Close()
 | |
| 
 | |
| 		// Temporarily replace the default client with the test server client to avoid TLS verification errors.
 | |
| 		oldClient := http.DefaultClient
 | |
| 		defer func() { http.DefaultClient = oldClient }()
 | |
| 		http.DefaultClient = server.Client()
 | |
| 
 | |
| 		err := writeDigestFile(server.URL+"/df?sig=1234", []byte("test"))
 | |
| 		if err != nil {
 | |
| 			t.Fatalf("expected file to be written successfully, but got error: %v", err)
 | |
| 		}
 | |
| 		if string(uploadedContent) != "test" {
 | |
| 			t.Errorf("expected uploaded content to be 'test', but got '%s'", uploadedContent)
 | |
| 		}
 | |
| 	})
 | |
| }
 |