mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 05:40:42 +00:00 
			
		
		
		
	This patch cleans up pkg/util/mount/* and pkg/util/volume/* to always use filepath.Join instead of path.Join. filepath.Join is preferred because path.Join can have issues on Windows.
		
			
				
	
	
		
			986 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			986 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // +build linux
 | |
| 
 | |
| /*
 | |
| Copyright 2016 The Kubernetes Authors.
 | |
| 
 | |
| 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 util
 | |
| 
 | |
| import (
 | |
| 	"encoding/base64"
 | |
| 	"io/ioutil"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"reflect"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"k8s.io/apimachinery/pkg/util/sets"
 | |
| 	utiltesting "k8s.io/client-go/util/testing"
 | |
| )
 | |
| 
 | |
| func TestNewAtomicWriter(t *testing.T) {
 | |
| 	targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("unexpected error creating tmp dir: %v", err)
 | |
| 	}
 | |
| 	defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 	_, err = NewAtomicWriter(targetDir, "-test-")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("unexpected error creating writer for existing target dir: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	nonExistentDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("unexpected error creating tmp dir: %v", err)
 | |
| 	}
 | |
| 	err = os.Remove(nonExistentDir)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("unexpected error ensuring dir %v does not exist: %v", nonExistentDir, err)
 | |
| 	}
 | |
| 
 | |
| 	_, err = NewAtomicWriter(nonExistentDir, "-test-")
 | |
| 	if err == nil {
 | |
| 		t.Fatalf("unexpected success creating writer for nonexistent target dir: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidatePath(t *testing.T) {
 | |
| 	maxPath := strings.Repeat("a", maxPathLength+1)
 | |
| 	maxFile := strings.Repeat("a", maxFileNameLength+1)
 | |
| 
 | |
| 	cases := []struct {
 | |
| 		name  string
 | |
| 		path  string
 | |
| 		valid bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:  "valid 1",
 | |
| 			path:  "i/am/well/behaved.txt",
 | |
| 			valid: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "valid 2",
 | |
| 			path:  "keepyourheaddownandfollowtherules.txt",
 | |
| 			valid: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "max path length",
 | |
| 			path:  maxPath,
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "max file length",
 | |
| 			path:  maxFile,
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "absolute failure",
 | |
| 			path:  "/dev/null",
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "reserved path",
 | |
| 			path:  "..sneaky.txt",
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "contains doubledot 1",
 | |
| 			path:  "hello/there/../../../../../../etc/passwd",
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "contains doubledot 2",
 | |
| 			path:  "hello/../etc/somethingbad",
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:  "empty",
 | |
| 			path:  "",
 | |
| 			valid: false,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		err := validatePath(tc.path)
 | |
| 		if tc.valid && err != nil {
 | |
| 			t.Errorf("%v: unexpected failure: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if !tc.valid && err == nil {
 | |
| 			t.Errorf("%v: unexpected success", tc.name)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestPathsToRemove(t *testing.T) {
 | |
| 	cases := []struct {
 | |
| 		name     string
 | |
| 		payload1 map[string]FileProjection
 | |
| 		payload2 map[string]FileProjection
 | |
| 		expected sets.String
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "simple",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("bar.txt"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple 2",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":     {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zip/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zip/bar.txt", "zip"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirs 1",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":         {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zip/zap/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zip/zap/bar.txt", "zip", "zip/zap"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirs 2",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirs 3",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
 | |
| 				"zap/a/b/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4", "zap", "zap/a", "zap/a/b", "zap/a/b/c", "zap/a/b/c/bar.txt"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirs 4",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
 | |
| 				"zap/1/2/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
 | |
| 				"zap/1/2/magic.txt":   {Mode: 0644, Data: []byte("indigo")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt":           {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirs 5",
 | |
| 			payload1: map[string]FileProjection{
 | |
| 				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
 | |
| 				"zap/1/2/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
 | |
| 			},
 | |
| 			payload2: map[string]FileProjection{
 | |
| 				"foo.txt":           {Mode: 0644, Data: []byte("foo")},
 | |
| 				"zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
 | |
| 			},
 | |
| 			expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | |
| 		err = writer.Write(tc.payload1)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		dataDirPath := filepath.Join(targetDir, dataDirName)
 | |
| 		oldTsDir, err := os.Readlink(dataDirPath)
 | |
| 		if err != nil && os.IsNotExist(err) {
 | |
| 			t.Errorf("Data symlink does not exist: %v", dataDirPath)
 | |
| 			continue
 | |
| 		} else if err != nil {
 | |
| 			t.Errorf("Unable to read symlink %v: %v", dataDirPath, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		actual, err := writer.pathsToRemove(tc.payload2, filepath.Join(targetDir, oldTsDir))
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error determining paths to remove: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if e, a := tc.expected, actual; !e.Equal(a) {
 | |
| 			t.Errorf("%v: unexpected paths to remove:\nexpected: %v\n     got: %v", tc.name, e, a)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWriteOnce(t *testing.T) {
 | |
| 	// $1 if you can tell me what this binary is
 | |
| 	encodedMysteryBinary := `f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAAB
 | |
| AAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAfQAAAAAAAAB9AAAAAAAAAAAA
 | |
| IAAAAAAAsDyZDwU=`
 | |
| 
 | |
| 	mysteryBinaryBytes := make([]byte, base64.StdEncoding.DecodedLen(len(encodedMysteryBinary)))
 | |
| 	numBytes, err := base64.StdEncoding.Decode(mysteryBinaryBytes, []byte(encodedMysteryBinary))
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Unexpected error decoding binary payload: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	if numBytes != 125 {
 | |
| 		t.Fatalf("Unexpected decoded binary size: expected 125, got %v", numBytes)
 | |
| 	}
 | |
| 
 | |
| 	cases := []struct {
 | |
| 		name    string
 | |
| 		payload map[string]FileProjection
 | |
| 		success bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "invalid payload 1",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo":        {Mode: 0644, Data: []byte("foo")},
 | |
| 				"..bar":      {Mode: 0644, Data: []byte("bar")},
 | |
| 				"binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
 | |
| 			},
 | |
| 			success: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "invalid payload 2",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/../bar": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			success: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "basic 1",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "basic 2",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"binary.bin":  {Mode: 0644, Data: mysteryBinaryBytes},
 | |
| 				".binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "basic mode 1",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0777, Data: []byte("foo")},
 | |
| 				"bar": {Mode: 0400, Data: []byte("bar")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dotfiles",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo":           {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar":           {Mode: 0644, Data: []byte("bar")},
 | |
| 				".dotfile":      {Mode: 0644, Data: []byte("dotfile")},
 | |
| 				".dotfile.file": {Mode: 0644, Data: []byte("dotfile.file")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "dotfiles mode",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo":           {Mode: 0407, Data: []byte("foo")},
 | |
| 				"bar":           {Mode: 0440, Data: []byte("bar")},
 | |
| 				".dotfile":      {Mode: 0777, Data: []byte("dotfile")},
 | |
| 				".dotfile.file": {Mode: 0666, Data: []byte("dotfile.file")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirectories 1",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirectories mode 1",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0400, Data: []byte("foo/bar")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirectories 2",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo//bar.txt":      {Mode: 0644, Data: []byte("foo//bar")},
 | |
| 				"bar///bar/zab.txt": {Mode: 0644, Data: []byte("bar/../bar/zab.txt")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirectories 3",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 				"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 				"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
 | |
| 				"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "kitchen sink",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo.log":                           {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar.zap":                           {Mode: 0644, Data: []byte("bar")},
 | |
| 				".dotfile":                          {Mode: 0644, Data: []byte("dotfile")},
 | |
| 				"foo/bar.txt":                       {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 				"bar/zab.txt":                       {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 				"foo/blaz/bar.txt":                  {Mode: 0644, Data: []byte("foo/blaz/bar")},
 | |
| 				"bar/zib/zab.txt":                   {Mode: 0400, Data: []byte("bar/zib/zab.txt")},
 | |
| 				"1/2/3/4/5/6/7/8/9/10/.dotfile.lib": {Mode: 0777, Data: []byte("1-2-3-dotfile")},
 | |
| 			},
 | |
| 			success: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | |
| 		err = writer.Write(tc.payload)
 | |
| 		if err != nil && tc.success {
 | |
| 			t.Errorf("%v: unexpected error writing payload: %v", tc.name, err)
 | |
| 			continue
 | |
| 		} else if err == nil && !tc.success {
 | |
| 			t.Errorf("%v: unexpected success", tc.name)
 | |
| 			continue
 | |
| 		} else if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		checkVolumeContents(targetDir, tc.name, tc.payload, t)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdate(t *testing.T) {
 | |
| 	cases := []struct {
 | |
| 		name        string
 | |
| 		first       map[string]FileProjection
 | |
| 		next        map[string]FileProjection
 | |
| 		shouldWrite bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "update",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0644, Data: []byte("foo2")},
 | |
| 				"bar": {Mode: 0640, Data: []byte("bar2")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no update",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			shouldWrite: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "no update 2",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			shouldWrite: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add 1",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 				"blu/zip.txt": {Mode: 0644, Data: []byte("zip")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add 2",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt":             {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt":             {Mode: 0644, Data: []byte("bar")},
 | |
| 				"blu/two/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add 3",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt":         {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt":         {Mode: 0644, Data: []byte("bar")},
 | |
| 				"bar/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "delete 1",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "delete 2",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt":       {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "delete 3",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt":       {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/sip.txt":   {Mode: 0644, Data: []byte("sip")},
 | |
| 				"bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt":     {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "delete 4",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt":            {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/sip.txt":        {Mode: 0644, Data: []byte("sip")},
 | |
| 				"bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"foo/bar.txt":     {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "delete all",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt":            {Mode: 0644, Data: []byte("foo")},
 | |
| 				"bar/1/2/sip.txt":        {Mode: 0644, Data: []byte("sip")},
 | |
| 				"bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
 | |
| 			},
 | |
| 			next:        map[string]FileProjection{},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add and delete 1",
 | |
| 			first: map[string]FileProjection{
 | |
| 				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
 | |
| 			},
 | |
| 			next: map[string]FileProjection{
 | |
| 				"bar/baz.txt": {Mode: 0644, Data: []byte("baz")},
 | |
| 			},
 | |
| 			shouldWrite: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | |
| 
 | |
| 		err = writer.Write(tc.first)
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		checkVolumeContents(targetDir, tc.name, tc.first, t)
 | |
| 		if !tc.shouldWrite {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		err = writer.Write(tc.next)
 | |
| 		if err != nil {
 | |
| 			if tc.shouldWrite {
 | |
| 				t.Errorf("%v: unexpected error writing: %v", tc.name, err)
 | |
| 				continue
 | |
| 			}
 | |
| 		} else if !tc.shouldWrite {
 | |
| 			t.Errorf("%v: unexpected success", tc.name)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		checkVolumeContents(targetDir, tc.name, tc.next, t)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestMultipleUpdates(t *testing.T) {
 | |
| 	cases := []struct {
 | |
| 		name     string
 | |
| 		payloads []map[string]FileProjection
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "update 1",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 					"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo": {Mode: 0400, Data: []byte("foo2")},
 | |
| 					"bar": {Mode: 0400, Data: []byte("bar2")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo": {Mode: 0600, Data: []byte("foo3")},
 | |
| 					"bar": {Mode: 0600, Data: []byte("bar3")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "update 2",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 					"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt": {Mode: 0400, Data: []byte("bar/zab.txt2")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "clear sentinel",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo": {Mode: 0644, Data: []byte("foo")},
 | |
| 					"bar": {Mode: 0644, Data: []byte("bar")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo": {Mode: 0644, Data: []byte("foo2")},
 | |
| 					"bar": {Mode: 0644, Data: []byte("bar2")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo": {Mode: 0644, Data: []byte("foo3")},
 | |
| 					"bar": {Mode: 0644, Data: []byte("bar3")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo": {Mode: 0644, Data: []byte("foo4")},
 | |
| 					"bar": {Mode: 0644, Data: []byte("bar4")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "subdirectories 2",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
 | |
| 					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
 | |
| 					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
 | |
| 					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add 1",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo/bar.txt":            {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 					"bar//zab.txt":           {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 					"foo/blaz/bar.txt":       {Mode: 0644, Data: []byte("foo/blaz/bar")},
 | |
| 					"bar/zib////zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
 | |
| 					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
 | |
| 					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
 | |
| 					"add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "add 2",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
 | |
| 					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
 | |
| 					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
 | |
| 					"add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt":       {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt":       {Mode: 0644, Data: []byte("bar/zab.txt2")},
 | |
| 					"foo/blaz/bar.txt":  {Mode: 0644, Data: []byte("foo/blaz/bar2")},
 | |
| 					"bar/zib/zab.txt":   {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
 | |
| 					"add/new/keys.txt":  {Mode: 0644, Data: []byte("addNewKeys")},
 | |
| 					"add/new/keys2.txt": {Mode: 0644, Data: []byte("addNewKeys2")},
 | |
| 					"add/new/keys3.txt": {Mode: 0644, Data: []byte("addNewKeys3")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "remove 1",
 | |
| 			payloads: []map[string]FileProjection{
 | |
| 				{
 | |
| 					"foo/bar.txt":         {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 					"bar//zab.txt":        {Mode: 0644, Data: []byte("bar/zab.txt")},
 | |
| 					"foo/blaz/bar.txt":    {Mode: 0644, Data: []byte("foo/blaz/bar")},
 | |
| 					"zip/zap/zup/fop.txt": {Mode: 0644, Data: []byte("zip/zap/zup/fop.txt")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
 | |
| 					"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
 | |
| 				},
 | |
| 				{
 | |
| 					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | |
| 
 | |
| 		for _, payload := range tc.payloads {
 | |
| 			writer.Write(payload)
 | |
| 
 | |
| 			checkVolumeContents(targetDir, tc.name, payload, t)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func checkVolumeContents(targetDir, tcName string, payload map[string]FileProjection, t *testing.T) {
 | |
| 	dataDirPath := filepath.Join(targetDir, dataDirName)
 | |
| 	// use filepath.Walk to reconstruct the payload, then deep equal
 | |
| 	observedPayload := make(map[string]FileProjection)
 | |
| 	visitor := func(path string, info os.FileInfo, err error) error {
 | |
| 		if info.IsDir() {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		relativePath := strings.TrimPrefix(path, dataDirPath)
 | |
| 		relativePath = strings.TrimPrefix(relativePath, "/")
 | |
| 		if strings.HasPrefix(relativePath, "..") {
 | |
| 			return nil
 | |
| 		}
 | |
| 
 | |
| 		content, err := ioutil.ReadFile(path)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		fileInfo, err := os.Stat(path)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		mode := int32(fileInfo.Mode())
 | |
| 
 | |
| 		observedPayload[relativePath] = FileProjection{Data: content, Mode: mode}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	d, err := ioutil.ReadDir(targetDir)
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Unable to read dir %v: %v", targetDir, err)
 | |
| 		return
 | |
| 	}
 | |
| 	for _, info := range d {
 | |
| 		if strings.HasPrefix(info.Name(), "..") {
 | |
| 			continue
 | |
| 		}
 | |
| 		if info.Mode()&os.ModeSymlink != 0 {
 | |
| 			p := filepath.Join(targetDir, info.Name())
 | |
| 			actual, err := os.Readlink(p)
 | |
| 			if err != nil {
 | |
| 				t.Errorf("Unable to read symlink %v: %v", p, err)
 | |
| 				continue
 | |
| 			}
 | |
| 			if err := filepath.Walk(filepath.Join(targetDir, actual), visitor); err != nil {
 | |
| 				t.Errorf("%v: unexpected error walking directory: %v", tcName, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	cleanPathPayload := make(map[string]FileProjection, len(payload))
 | |
| 	for k, v := range payload {
 | |
| 		cleanPathPayload[filepath.Clean(k)] = v
 | |
| 	}
 | |
| 
 | |
| 	if !reflect.DeepEqual(cleanPathPayload, observedPayload) {
 | |
| 		t.Errorf("%v: payload and observed payload do not match.", tcName)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidatePayload(t *testing.T) {
 | |
| 	maxPath := strings.Repeat("a", maxPathLength+1)
 | |
| 
 | |
| 	cases := []struct {
 | |
| 		name     string
 | |
| 		payload  map[string]FileProjection
 | |
| 		expected sets.String
 | |
| 		valid    bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "valid payload",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo": {},
 | |
| 				"bar": {},
 | |
| 			},
 | |
| 			valid:    true,
 | |
| 			expected: sets.NewString("foo", "bar"),
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with path length > 4096 is invalid",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				maxPath: {},
 | |
| 			},
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with absolute path is invalid",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"/dev/null": {},
 | |
| 			},
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with reserved path is invalid",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"..sneaky.txt": {},
 | |
| 			},
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with doubledot path is invalid",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/../etc/password": {},
 | |
| 			},
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with empty path is invalid",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"": {},
 | |
| 			},
 | |
| 			valid: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name: "payload with unclean path should be cleaned",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo////bar": {},
 | |
| 			},
 | |
| 			valid:    true,
 | |
| 			expected: sets.NewString("foo/bar"),
 | |
| 		},
 | |
| 	}
 | |
| 	getPayloadPaths := func(payload map[string]FileProjection) sets.String {
 | |
| 		paths := sets.NewString()
 | |
| 		for path := range payload {
 | |
| 			paths.Insert(path)
 | |
| 		}
 | |
| 		return paths
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		real, err := validatePayload(tc.payload)
 | |
| 		if !tc.valid && err == nil {
 | |
| 			t.Errorf("%v: unexpected success", tc.name)
 | |
| 		}
 | |
| 
 | |
| 		if tc.valid {
 | |
| 			if err != nil {
 | |
| 				t.Errorf("%v: unexpected failure: %v", tc.name, err)
 | |
| 				continue
 | |
| 			}
 | |
| 
 | |
| 			realPaths := getPayloadPaths(real)
 | |
| 			if !realPaths.Equal(tc.expected) {
 | |
| 				t.Errorf("%v: unexpected payload paths: %v is not equal to %v", tc.name, realPaths, tc.expected)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestCreateUserVisibleFiles(t *testing.T) {
 | |
| 	cases := []struct {
 | |
| 		name     string
 | |
| 		payload  map[string]FileProjection
 | |
| 		expected map[string]string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "simple path",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo": {},
 | |
| 				"bar": {},
 | |
| 			},
 | |
| 			expected: map[string]string{
 | |
| 				"foo": "..data/foo",
 | |
| 				"bar": "..data/bar",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "simple nested path",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"foo/bar":     {},
 | |
| 				"foo/bar/txt": {},
 | |
| 				"bar/txt":     {},
 | |
| 			},
 | |
| 			expected: map[string]string{
 | |
| 				"foo": "..data/foo",
 | |
| 				"bar": "..data/bar",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "unclean nested path",
 | |
| 			payload: map[string]FileProjection{
 | |
| 				"./bar":     {},
 | |
| 				"foo///bar": {},
 | |
| 			},
 | |
| 			expected: map[string]string{
 | |
| 				"bar": "..data/bar",
 | |
| 				"foo": "..data/foo",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range cases {
 | |
| 		targetDir, err := utiltesting.MkTmpdir("atomic-write")
 | |
| 		if err != nil {
 | |
| 			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
 | |
| 			continue
 | |
| 		}
 | |
| 		defer os.RemoveAll(targetDir)
 | |
| 
 | |
| 		dataDirPath := filepath.Join(targetDir, dataDirName)
 | |
| 		err = os.MkdirAll(dataDirPath, 0755)
 | |
| 		if err != nil {
 | |
| 			t.Fatalf("%v: unexpected error creating data path: %v", tc.name, err)
 | |
| 		}
 | |
| 
 | |
| 		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
 | |
| 		payload, err := validatePayload(tc.payload)
 | |
| 		if err != nil {
 | |
| 			t.Fatalf("%v: unexpected error validating payload: %v", tc.name, err)
 | |
| 		}
 | |
| 		err = writer.createUserVisibleFiles(payload)
 | |
| 		if err != nil {
 | |
| 			t.Fatalf("%v: unexpected error creating visible files: %v", tc.name, err)
 | |
| 		}
 | |
| 
 | |
| 		for subpath, expectedDest := range tc.expected {
 | |
| 			visiblePath := filepath.Join(targetDir, subpath)
 | |
| 			destination, err := os.Readlink(visiblePath)
 | |
| 			if err != nil && os.IsNotExist(err) {
 | |
| 				t.Fatalf("%v: visible symlink does not exist: %v", tc.name, visiblePath)
 | |
| 			} else if err != nil {
 | |
| 				t.Fatalf("%v: unable to read symlink %v: %v", tc.name, dataDirPath, err)
 | |
| 			}
 | |
| 
 | |
| 			if expectedDest != destination {
 | |
| 				t.Fatalf("%v: symlink destination %q not same with expected data dir %q", tc.name, destination, expectedDest)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 |