mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			487 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			487 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package bake
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"slices"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"github.com/docker/buildx/build"
 | 
						|
	"github.com/docker/buildx/controller/pb"
 | 
						|
	"github.com/docker/buildx/util/osutil"
 | 
						|
	"github.com/moby/buildkit/client/llb"
 | 
						|
	"github.com/moby/buildkit/util/entitlements"
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
)
 | 
						|
 | 
						|
func TestEvaluateToExistingPath(t *testing.T) {
 | 
						|
	tempDir, err := osutil.GetLongPathName(t.TempDir())
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	// Setup temporary directory structure for testing
 | 
						|
	existingFile := filepath.Join(tempDir, "existing_file")
 | 
						|
	require.NoError(t, os.WriteFile(existingFile, []byte("test"), 0644))
 | 
						|
 | 
						|
	existingDir := filepath.Join(tempDir, "existing_dir")
 | 
						|
	require.NoError(t, os.Mkdir(existingDir, 0755))
 | 
						|
 | 
						|
	symlinkToFile := filepath.Join(tempDir, "symlink_to_file")
 | 
						|
	require.NoError(t, os.Symlink(existingFile, symlinkToFile))
 | 
						|
 | 
						|
	symlinkToDir := filepath.Join(tempDir, "symlink_to_dir")
 | 
						|
	require.NoError(t, os.Symlink(existingDir, symlinkToDir))
 | 
						|
 | 
						|
	nonexistentPath := filepath.Join(tempDir, "nonexistent", "path", "file.txt")
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name      string
 | 
						|
		input     string
 | 
						|
		expected  string
 | 
						|
		expectErr bool
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:      "Existing file",
 | 
						|
			input:     existingFile,
 | 
						|
			expected:  existingFile,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Existing directory",
 | 
						|
			input:     existingDir,
 | 
						|
			expected:  existingDir,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Symlink to file",
 | 
						|
			input:     symlinkToFile,
 | 
						|
			expected:  existingFile,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Symlink to directory",
 | 
						|
			input:     symlinkToDir,
 | 
						|
			expected:  existingDir,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Non-existent path",
 | 
						|
			input:     nonexistentPath,
 | 
						|
			expected:  tempDir,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Non-existent intermediate path",
 | 
						|
			input:     filepath.Join(tempDir, "nonexistent", "file.txt"),
 | 
						|
			expected:  tempDir,
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:  "Root path",
 | 
						|
			input: "/",
 | 
						|
			expected: func() string {
 | 
						|
				root, _ := filepath.Abs("/")
 | 
						|
				return root
 | 
						|
			}(),
 | 
						|
			expectErr: false,
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			result, _, err := evaluateToExistingPath(tt.input)
 | 
						|
 | 
						|
			if tt.expectErr {
 | 
						|
				require.Error(t, err)
 | 
						|
			} else {
 | 
						|
				require.NoError(t, err)
 | 
						|
				require.Equal(t, tt.expected, result)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestDedupePaths(t *testing.T) {
 | 
						|
	wd := osutil.GetWd()
 | 
						|
	tcases := []struct {
 | 
						|
		in  map[string]struct{}
 | 
						|
		out map[string]struct{}
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			in: map[string]struct{}{
 | 
						|
				"/a/b/c": {},
 | 
						|
				"/a/b/d": {},
 | 
						|
				"/a/b/e": {},
 | 
						|
			},
 | 
						|
			out: map[string]struct{}{
 | 
						|
				"/a/b/c": {},
 | 
						|
				"/a/b/d": {},
 | 
						|
				"/a/b/e": {},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			in: map[string]struct{}{
 | 
						|
				"/a/b/c":      {},
 | 
						|
				"/a/b/c/d":    {},
 | 
						|
				"/a/b/c/d/e":  {},
 | 
						|
				"/a/b/../b/c": {},
 | 
						|
			},
 | 
						|
			out: map[string]struct{}{
 | 
						|
				"/a/b/c": {},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			in: map[string]struct{}{
 | 
						|
				filepath.Join(wd, "a/b/c"):    {},
 | 
						|
				filepath.Join(wd, "../aa"):    {},
 | 
						|
				filepath.Join(wd, "a/b"):      {},
 | 
						|
				filepath.Join(wd, "a/b/d"):    {},
 | 
						|
				filepath.Join(wd, "../aa/b"):  {},
 | 
						|
				filepath.Join(wd, "../../bb"): {},
 | 
						|
			},
 | 
						|
			out: map[string]struct{}{
 | 
						|
				"a/b":                         {},
 | 
						|
				"../aa":                       {},
 | 
						|
				filepath.Join(wd, "../../bb"): {},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for i, tc := range tcases {
 | 
						|
		t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
 | 
						|
			out, err := dedupPaths(tc.in)
 | 
						|
			if err != nil {
 | 
						|
				require.NoError(t, err)
 | 
						|
			}
 | 
						|
			// convert to relative paths as that is shown to user
 | 
						|
			arr := make([]string, 0, len(out))
 | 
						|
			for k := range out {
 | 
						|
				arr = append(arr, k)
 | 
						|
			}
 | 
						|
			require.NoError(t, err)
 | 
						|
			arr = toRelativePaths(arr, wd)
 | 
						|
			m := make(map[string]struct{})
 | 
						|
			for _, v := range arr {
 | 
						|
				m[filepath.ToSlash(v)] = struct{}{}
 | 
						|
			}
 | 
						|
			o := make(map[string]struct{}, len(tc.out))
 | 
						|
			for k := range tc.out {
 | 
						|
				o[filepath.ToSlash(k)] = struct{}{}
 | 
						|
			}
 | 
						|
			require.Equal(t, o, m)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestValidateEntitlements(t *testing.T) {
 | 
						|
	dir1 := t.TempDir()
 | 
						|
	dir2 := t.TempDir()
 | 
						|
 | 
						|
	// the paths returned by entitlements validation will have symlinks resolved
 | 
						|
	expDir1, err := filepath.EvalSymlinks(dir1)
 | 
						|
	require.NoError(t, err)
 | 
						|
	expDir2, err := filepath.EvalSymlinks(dir2)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	escapeLink := filepath.Join(dir1, "escape_link")
 | 
						|
	require.NoError(t, os.Symlink("../../aa", escapeLink))
 | 
						|
 | 
						|
	wd, err := os.Getwd()
 | 
						|
	require.NoError(t, err)
 | 
						|
	expWd, err := filepath.EvalSymlinks(wd)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	tcases := []struct {
 | 
						|
		name     string
 | 
						|
		conf     EntitlementConf
 | 
						|
		opt      build.Options
 | 
						|
		expected EntitlementConf
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "No entitlements",
 | 
						|
			opt: build.Options{
 | 
						|
				Inputs: build.Inputs{
 | 
						|
					ContextState: &llb.State{},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "NetworkHostMissing",
 | 
						|
			opt: build.Options{
 | 
						|
				Allow: []entitlements.Entitlement{
 | 
						|
					entitlements.EntitlementNetworkHost,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				NetworkHost: true,
 | 
						|
				FSRead:      []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "NetworkHostSet",
 | 
						|
			conf: EntitlementConf{
 | 
						|
				NetworkHost: true,
 | 
						|
			},
 | 
						|
			opt: build.Options{
 | 
						|
				Allow: []entitlements.Entitlement{
 | 
						|
					entitlements.EntitlementNetworkHost,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSRead: []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecurityAndNetworkHostMissing",
 | 
						|
			opt: build.Options{
 | 
						|
				Allow: []entitlements.Entitlement{
 | 
						|
					entitlements.EntitlementNetworkHost,
 | 
						|
					entitlements.EntitlementSecurityInsecure,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				NetworkHost:      true,
 | 
						|
				SecurityInsecure: true,
 | 
						|
				FSRead:           []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecurityMissingAndNetworkHostSet",
 | 
						|
			conf: EntitlementConf{
 | 
						|
				NetworkHost: true,
 | 
						|
			},
 | 
						|
			opt: build.Options{
 | 
						|
				Allow: []entitlements.Entitlement{
 | 
						|
					entitlements.EntitlementNetworkHost,
 | 
						|
					entitlements.EntitlementSecurityInsecure,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				SecurityInsecure: true,
 | 
						|
				FSRead:           []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SSHMissing",
 | 
						|
			opt: build.Options{
 | 
						|
				SSHSpecs: []*pb.SSH{
 | 
						|
					{
 | 
						|
						ID: "test",
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				SSH:    true,
 | 
						|
				FSRead: []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "ExportLocal",
 | 
						|
			opt: build.Options{
 | 
						|
				ExportsLocalPathsTemporary: []string{
 | 
						|
					dir1,
 | 
						|
					filepath.Join(dir1, "subdir"),
 | 
						|
					dir2,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSWrite: func() []string {
 | 
						|
					exp := []string{expDir1, expDir2}
 | 
						|
					slices.Sort(exp)
 | 
						|
					return exp
 | 
						|
				}(),
 | 
						|
				FSRead: []string{expWd},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecretFromSubFile",
 | 
						|
			opt: build.Options{
 | 
						|
				SecretSpecs: []*pb.Secret{
 | 
						|
					{
 | 
						|
						FilePath: filepath.Join(dir1, "subfile"),
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead: []string{wd, dir1},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecretFromEscapeLink",
 | 
						|
			opt: build.Options{
 | 
						|
				SecretSpecs: []*pb.Secret{
 | 
						|
					{
 | 
						|
						FilePath: escapeLink,
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead: []string{wd, dir1},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSRead: []string{filepath.Join(expDir1, "../..")},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecretFromEscapeLinkAllowRoot",
 | 
						|
			opt: build.Options{
 | 
						|
				SecretSpecs: []*pb.Secret{
 | 
						|
					{
 | 
						|
						FilePath: escapeLink,
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead: []string{"/"},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSRead: func() []string {
 | 
						|
					// on windows root (/) is only allowed if it is the same volume as wd
 | 
						|
					if filepath.VolumeName(wd) == filepath.VolumeName(escapeLink) {
 | 
						|
						return nil
 | 
						|
					}
 | 
						|
					// if not, then escapeLink is not allowed
 | 
						|
					exp, _, err := evaluateToExistingPath(escapeLink)
 | 
						|
					require.NoError(t, err)
 | 
						|
					exp, err = filepath.EvalSymlinks(exp)
 | 
						|
					require.NoError(t, err)
 | 
						|
					return []string{exp}
 | 
						|
				}(),
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "SecretFromEscapeLinkAllowAny",
 | 
						|
			opt: build.Options{
 | 
						|
				SecretSpecs: []*pb.Secret{
 | 
						|
					{
 | 
						|
						FilePath: escapeLink,
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead: []string{"*"},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "NonExistingAllowedPathSubpath",
 | 
						|
			opt: build.Options{
 | 
						|
				ExportsLocalPathsTemporary: []string{
 | 
						|
					dir1,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead:  []string{wd},
 | 
						|
				FSWrite: []string{filepath.Join(dir1, "not/exists")},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSWrite: []string{expDir1}, // dir1 is still needed as only subpath was allowed
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "NonExistingAllowedPathMatches",
 | 
						|
			opt: build.Options{
 | 
						|
				ExportsLocalPathsTemporary: []string{
 | 
						|
					filepath.Join(dir1, "not/exists"),
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead:  []string{wd},
 | 
						|
				FSWrite: []string{filepath.Join(dir1, "not/exists")},
 | 
						|
			},
 | 
						|
			expected: EntitlementConf{
 | 
						|
				FSWrite: []string{expDir1}, // dir1 is still needed as build also needs to write not/exists directory
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "NonExistingBuildPath",
 | 
						|
			opt: build.Options{
 | 
						|
				ExportsLocalPathsTemporary: []string{
 | 
						|
					filepath.Join(dir1, "not/exists"),
 | 
						|
				},
 | 
						|
			},
 | 
						|
			conf: EntitlementConf{
 | 
						|
				FSRead:  []string{wd},
 | 
						|
				FSWrite: []string{dir1},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tcases {
 | 
						|
		t.Run(tc.name, func(t *testing.T) {
 | 
						|
			expected, err := tc.conf.Validate(map[string]build.Options{"test": tc.opt})
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, tc.expected, expected)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestGroupSamePaths(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name      string
 | 
						|
		in1       []string
 | 
						|
		in2       []string
 | 
						|
		expected1 []string
 | 
						|
		expected2 []string
 | 
						|
		expectedC []string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:      "All common paths",
 | 
						|
			in1:       []string{"/path/a", "/path/b", "/path/c"},
 | 
						|
			in2:       []string{"/path/a", "/path/b", "/path/c"},
 | 
						|
			expected1: []string{},
 | 
						|
			expected2: []string{},
 | 
						|
			expectedC: []string{"/path/a", "/path/b", "/path/c"},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "No common paths",
 | 
						|
			in1:       []string{"/path/a", "/path/b"},
 | 
						|
			in2:       []string{"/path/c", "/path/d"},
 | 
						|
			expected1: []string{"/path/a", "/path/b"},
 | 
						|
			expected2: []string{"/path/c", "/path/d"},
 | 
						|
			expectedC: []string{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Some common paths",
 | 
						|
			in1:       []string{"/path/a", "/path/b", "/path/c"},
 | 
						|
			in2:       []string{"/path/b", "/path/c", "/path/d"},
 | 
						|
			expected1: []string{"/path/a"},
 | 
						|
			expected2: []string{"/path/d"},
 | 
						|
			expectedC: []string{"/path/b", "/path/c"},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Empty inputs",
 | 
						|
			in1:       []string{},
 | 
						|
			in2:       []string{},
 | 
						|
			expected1: []string{},
 | 
						|
			expected2: []string{},
 | 
						|
			expectedC: []string{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "One empty input",
 | 
						|
			in1:       []string{"/path/a", "/path/b"},
 | 
						|
			in2:       []string{},
 | 
						|
			expected1: []string{"/path/a", "/path/b"},
 | 
						|
			expected2: []string{},
 | 
						|
			expectedC: []string{},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Unsorted inputs with common paths",
 | 
						|
			in1:       []string{"/path/c", "/path/a", "/path/b"},
 | 
						|
			in2:       []string{"/path/b", "/path/c", "/path/a"},
 | 
						|
			expected1: []string{},
 | 
						|
			expected2: []string{},
 | 
						|
			expectedC: []string{"/path/a", "/path/b", "/path/c"},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			out1, out2, common := groupSamePaths(tt.in1, tt.in2)
 | 
						|
			require.Equal(t, tt.expected1, out1, "in1 should match expected1")
 | 
						|
			require.Equal(t, tt.expected2, out2, "in2 should match expected2")
 | 
						|
			require.Equal(t, tt.expectedC, common, "common should match expectedC")
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |