...

Source file src/k8s.io/kubernetes/pkg/volume/util/atomic_writer_test.go

Documentation: k8s.io/kubernetes/pkg/volume/util

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2016 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package util
    21  
    22  import (
    23  	"encoding/base64"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"reflect"
    28  	"strings"
    29  	"testing"
    30  
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	utiltesting "k8s.io/client-go/util/testing"
    33  )
    34  
    35  func TestNewAtomicWriter(t *testing.T) {
    36  	targetDir, err := utiltesting.MkTmpdir("atomic-write")
    37  	if err != nil {
    38  		t.Fatalf("unexpected error creating tmp dir: %v", err)
    39  	}
    40  	defer os.RemoveAll(targetDir)
    41  
    42  	_, err = NewAtomicWriter(targetDir, "-test-")
    43  	if err != nil {
    44  		t.Fatalf("unexpected error creating writer for existing target dir: %v", err)
    45  	}
    46  
    47  	nonExistentDir, err := utiltesting.MkTmpdir("atomic-write")
    48  	if err != nil {
    49  		t.Fatalf("unexpected error creating tmp dir: %v", err)
    50  	}
    51  	err = os.Remove(nonExistentDir)
    52  	if err != nil {
    53  		t.Fatalf("unexpected error ensuring dir %v does not exist: %v", nonExistentDir, err)
    54  	}
    55  
    56  	_, err = NewAtomicWriter(nonExistentDir, "-test-")
    57  	if err == nil {
    58  		t.Fatalf("unexpected success creating writer for nonexistent target dir: %v", err)
    59  	}
    60  }
    61  
    62  func TestValidatePath(t *testing.T) {
    63  	maxPath := strings.Repeat("a", maxPathLength+1)
    64  	maxFile := strings.Repeat("a", maxFileNameLength+1)
    65  
    66  	cases := []struct {
    67  		name  string
    68  		path  string
    69  		valid bool
    70  	}{
    71  		{
    72  			name:  "valid 1",
    73  			path:  "i/am/well/behaved.txt",
    74  			valid: true,
    75  		},
    76  		{
    77  			name:  "valid 2",
    78  			path:  "keepyourheaddownandfollowtherules.txt",
    79  			valid: true,
    80  		},
    81  		{
    82  			name:  "max path length",
    83  			path:  maxPath,
    84  			valid: false,
    85  		},
    86  		{
    87  			name:  "max file length",
    88  			path:  maxFile,
    89  			valid: false,
    90  		},
    91  		{
    92  			name:  "absolute failure",
    93  			path:  "/dev/null",
    94  			valid: false,
    95  		},
    96  		{
    97  			name:  "reserved path",
    98  			path:  "..sneaky.txt",
    99  			valid: false,
   100  		},
   101  		{
   102  			name:  "contains doubledot 1",
   103  			path:  "hello/there/../../../../../../etc/passwd",
   104  			valid: false,
   105  		},
   106  		{
   107  			name:  "contains doubledot 2",
   108  			path:  "hello/../etc/somethingbad",
   109  			valid: false,
   110  		},
   111  		{
   112  			name:  "empty",
   113  			path:  "",
   114  			valid: false,
   115  		},
   116  	}
   117  
   118  	for _, tc := range cases {
   119  		err := validatePath(tc.path)
   120  		if tc.valid && err != nil {
   121  			t.Errorf("%v: unexpected failure: %v", tc.name, err)
   122  			continue
   123  		}
   124  
   125  		if !tc.valid && err == nil {
   126  			t.Errorf("%v: unexpected success", tc.name)
   127  		}
   128  	}
   129  }
   130  
   131  func TestPathsToRemove(t *testing.T) {
   132  	cases := []struct {
   133  		name     string
   134  		payload1 map[string]FileProjection
   135  		payload2 map[string]FileProjection
   136  		expected sets.String
   137  	}{
   138  		{
   139  			name: "simple",
   140  			payload1: map[string]FileProjection{
   141  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   142  				"bar.txt": {Mode: 0644, Data: []byte("bar")},
   143  			},
   144  			payload2: map[string]FileProjection{
   145  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   146  			},
   147  			expected: sets.NewString("bar.txt"),
   148  		},
   149  		{
   150  			name: "simple 2",
   151  			payload1: map[string]FileProjection{
   152  				"foo.txt":     {Mode: 0644, Data: []byte("foo")},
   153  				"zip/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
   154  			},
   155  			payload2: map[string]FileProjection{
   156  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   157  			},
   158  			expected: sets.NewString("zip/bar.txt", "zip"),
   159  		},
   160  		{
   161  			name: "subdirs 1",
   162  			payload1: map[string]FileProjection{
   163  				"foo.txt":         {Mode: 0644, Data: []byte("foo")},
   164  				"zip/zap/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
   165  			},
   166  			payload2: map[string]FileProjection{
   167  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   168  			},
   169  			expected: sets.NewString("zip/zap/bar.txt", "zip", "zip/zap"),
   170  		},
   171  		{
   172  			name: "subdirs 2",
   173  			payload1: map[string]FileProjection{
   174  				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
   175  				"zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
   176  			},
   177  			payload2: map[string]FileProjection{
   178  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   179  			},
   180  			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"),
   181  		},
   182  		{
   183  			name: "subdirs 3",
   184  			payload1: map[string]FileProjection{
   185  				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
   186  				"zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")},
   187  				"zap/a/b/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
   188  			},
   189  			payload2: map[string]FileProjection{
   190  				"foo.txt": {Mode: 0644, Data: []byte("foo")},
   191  			},
   192  			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"),
   193  		},
   194  		{
   195  			name: "subdirs 4",
   196  			payload1: map[string]FileProjection{
   197  				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
   198  				"zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
   199  				"zap/1/2/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
   200  				"zap/1/2/magic.txt":   {Mode: 0644, Data: []byte("indigo")},
   201  			},
   202  			payload2: map[string]FileProjection{
   203  				"foo.txt":           {Mode: 0644, Data: []byte("foo")},
   204  				"zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
   205  			},
   206  			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"),
   207  		},
   208  		{
   209  			name: "subdirs 5",
   210  			payload1: map[string]FileProjection{
   211  				"foo.txt":             {Mode: 0644, Data: []byte("foo")},
   212  				"zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")},
   213  				"zap/1/2/c/bar.txt":   {Mode: 0644, Data: []byte("zap/bar")},
   214  			},
   215  			payload2: map[string]FileProjection{
   216  				"foo.txt":           {Mode: 0644, Data: []byte("foo")},
   217  				"zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")},
   218  			},
   219  			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"),
   220  		},
   221  	}
   222  
   223  	for _, tc := range cases {
   224  		targetDir, err := utiltesting.MkTmpdir("atomic-write")
   225  		if err != nil {
   226  			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
   227  			continue
   228  		}
   229  		defer os.RemoveAll(targetDir)
   230  
   231  		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
   232  		err = writer.Write(tc.payload1, nil)
   233  		if err != nil {
   234  			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
   235  			continue
   236  		}
   237  
   238  		dataDirPath := filepath.Join(targetDir, dataDirName)
   239  		oldTsDir, err := os.Readlink(dataDirPath)
   240  		if err != nil && os.IsNotExist(err) {
   241  			t.Errorf("Data symlink does not exist: %v", dataDirPath)
   242  			continue
   243  		} else if err != nil {
   244  			t.Errorf("Unable to read symlink %v: %v", dataDirPath, err)
   245  			continue
   246  		}
   247  
   248  		actual, err := writer.pathsToRemove(tc.payload2, filepath.Join(targetDir, oldTsDir))
   249  		if err != nil {
   250  			t.Errorf("%v: unexpected error determining paths to remove: %v", tc.name, err)
   251  			continue
   252  		}
   253  
   254  		if e, a := tc.expected, actual; !e.Equal(a) {
   255  			t.Errorf("%v: unexpected paths to remove:\nexpected: %v\n     got: %v", tc.name, e, a)
   256  		}
   257  	}
   258  }
   259  
   260  func TestWriteOnce(t *testing.T) {
   261  	// $1 if you can tell me what this binary is
   262  	encodedMysteryBinary := `f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAAB
   263  AAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAfQAAAAAAAAB9AAAAAAAAAAAA
   264  IAAAAAAAsDyZDwU=`
   265  
   266  	mysteryBinaryBytes := make([]byte, base64.StdEncoding.DecodedLen(len(encodedMysteryBinary)))
   267  	numBytes, err := base64.StdEncoding.Decode(mysteryBinaryBytes, []byte(encodedMysteryBinary))
   268  	if err != nil {
   269  		t.Fatalf("Unexpected error decoding binary payload: %v", err)
   270  	}
   271  
   272  	if numBytes != 125 {
   273  		t.Fatalf("Unexpected decoded binary size: expected 125, got %v", numBytes)
   274  	}
   275  
   276  	cases := []struct {
   277  		name    string
   278  		payload map[string]FileProjection
   279  		success bool
   280  	}{
   281  		{
   282  			name: "invalid payload 1",
   283  			payload: map[string]FileProjection{
   284  				"foo":        {Mode: 0644, Data: []byte("foo")},
   285  				"..bar":      {Mode: 0644, Data: []byte("bar")},
   286  				"binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
   287  			},
   288  			success: false,
   289  		},
   290  		{
   291  			name: "invalid payload 2",
   292  			payload: map[string]FileProjection{
   293  				"foo/../bar": {Mode: 0644, Data: []byte("foo")},
   294  			},
   295  			success: false,
   296  		},
   297  		{
   298  			name: "basic 1",
   299  			payload: map[string]FileProjection{
   300  				"foo": {Mode: 0644, Data: []byte("foo")},
   301  				"bar": {Mode: 0644, Data: []byte("bar")},
   302  			},
   303  			success: true,
   304  		},
   305  		{
   306  			name: "basic 2",
   307  			payload: map[string]FileProjection{
   308  				"binary.bin":  {Mode: 0644, Data: mysteryBinaryBytes},
   309  				".binary.bin": {Mode: 0644, Data: mysteryBinaryBytes},
   310  			},
   311  			success: true,
   312  		},
   313  		{
   314  			name: "basic mode 1",
   315  			payload: map[string]FileProjection{
   316  				"foo": {Mode: 0777, Data: []byte("foo")},
   317  				"bar": {Mode: 0400, Data: []byte("bar")},
   318  			},
   319  			success: true,
   320  		},
   321  		{
   322  			name: "dotfiles",
   323  			payload: map[string]FileProjection{
   324  				"foo":           {Mode: 0644, Data: []byte("foo")},
   325  				"bar":           {Mode: 0644, Data: []byte("bar")},
   326  				".dotfile":      {Mode: 0644, Data: []byte("dotfile")},
   327  				".dotfile.file": {Mode: 0644, Data: []byte("dotfile.file")},
   328  			},
   329  			success: true,
   330  		},
   331  		{
   332  			name: "dotfiles mode",
   333  			payload: map[string]FileProjection{
   334  				"foo":           {Mode: 0407, Data: []byte("foo")},
   335  				"bar":           {Mode: 0440, Data: []byte("bar")},
   336  				".dotfile":      {Mode: 0777, Data: []byte("dotfile")},
   337  				".dotfile.file": {Mode: 0666, Data: []byte("dotfile.file")},
   338  			},
   339  			success: true,
   340  		},
   341  		{
   342  			name: "subdirectories 1",
   343  			payload: map[string]FileProjection{
   344  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
   345  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
   346  			},
   347  			success: true,
   348  		},
   349  		{
   350  			name: "subdirectories mode 1",
   351  			payload: map[string]FileProjection{
   352  				"foo/bar.txt": {Mode: 0400, Data: []byte("foo/bar")},
   353  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
   354  			},
   355  			success: true,
   356  		},
   357  		{
   358  			name: "subdirectories 2",
   359  			payload: map[string]FileProjection{
   360  				"foo//bar.txt":      {Mode: 0644, Data: []byte("foo//bar")},
   361  				"bar///bar/zab.txt": {Mode: 0644, Data: []byte("bar/../bar/zab.txt")},
   362  			},
   363  			success: true,
   364  		},
   365  		{
   366  			name: "subdirectories 3",
   367  			payload: map[string]FileProjection{
   368  				"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar")},
   369  				"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt")},
   370  				"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
   371  				"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
   372  			},
   373  			success: true,
   374  		},
   375  		{
   376  			name: "kitchen sink",
   377  			payload: map[string]FileProjection{
   378  				"foo.log":                           {Mode: 0644, Data: []byte("foo")},
   379  				"bar.zap":                           {Mode: 0644, Data: []byte("bar")},
   380  				".dotfile":                          {Mode: 0644, Data: []byte("dotfile")},
   381  				"foo/bar.txt":                       {Mode: 0644, Data: []byte("foo/bar")},
   382  				"bar/zab.txt":                       {Mode: 0644, Data: []byte("bar/zab.txt")},
   383  				"foo/blaz/bar.txt":                  {Mode: 0644, Data: []byte("foo/blaz/bar")},
   384  				"bar/zib/zab.txt":                   {Mode: 0400, Data: []byte("bar/zib/zab.txt")},
   385  				"1/2/3/4/5/6/7/8/9/10/.dotfile.lib": {Mode: 0777, Data: []byte("1-2-3-dotfile")},
   386  			},
   387  			success: true,
   388  		},
   389  	}
   390  
   391  	for _, tc := range cases {
   392  		targetDir, err := utiltesting.MkTmpdir("atomic-write")
   393  		if err != nil {
   394  			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
   395  			continue
   396  		}
   397  		defer os.RemoveAll(targetDir)
   398  
   399  		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
   400  		err = writer.Write(tc.payload, nil)
   401  		if err != nil && tc.success {
   402  			t.Errorf("%v: unexpected error writing payload: %v", tc.name, err)
   403  			continue
   404  		} else if err == nil && !tc.success {
   405  			t.Errorf("%v: unexpected success", tc.name)
   406  			continue
   407  		} else if err != nil {
   408  			continue
   409  		}
   410  
   411  		checkVolumeContents(targetDir, tc.name, tc.payload, t)
   412  	}
   413  }
   414  
   415  func TestUpdate(t *testing.T) {
   416  	cases := []struct {
   417  		name        string
   418  		first       map[string]FileProjection
   419  		next        map[string]FileProjection
   420  		shouldWrite bool
   421  	}{
   422  		{
   423  			name: "update",
   424  			first: map[string]FileProjection{
   425  				"foo": {Mode: 0644, Data: []byte("foo")},
   426  				"bar": {Mode: 0644, Data: []byte("bar")},
   427  			},
   428  			next: map[string]FileProjection{
   429  				"foo": {Mode: 0644, Data: []byte("foo2")},
   430  				"bar": {Mode: 0640, Data: []byte("bar2")},
   431  			},
   432  			shouldWrite: true,
   433  		},
   434  		{
   435  			name: "no update",
   436  			first: map[string]FileProjection{
   437  				"foo": {Mode: 0644, Data: []byte("foo")},
   438  				"bar": {Mode: 0644, Data: []byte("bar")},
   439  			},
   440  			next: map[string]FileProjection{
   441  				"foo": {Mode: 0644, Data: []byte("foo")},
   442  				"bar": {Mode: 0644, Data: []byte("bar")},
   443  			},
   444  			shouldWrite: false,
   445  		},
   446  		{
   447  			name: "no update 2",
   448  			first: map[string]FileProjection{
   449  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   450  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   451  			},
   452  			next: map[string]FileProjection{
   453  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   454  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   455  			},
   456  			shouldWrite: false,
   457  		},
   458  		{
   459  			name: "add 1",
   460  			first: map[string]FileProjection{
   461  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   462  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   463  			},
   464  			next: map[string]FileProjection{
   465  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   466  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   467  				"blu/zip.txt": {Mode: 0644, Data: []byte("zip")},
   468  			},
   469  			shouldWrite: true,
   470  		},
   471  		{
   472  			name: "add 2",
   473  			first: map[string]FileProjection{
   474  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   475  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   476  			},
   477  			next: map[string]FileProjection{
   478  				"foo/bar.txt":             {Mode: 0644, Data: []byte("foo")},
   479  				"bar/zab.txt":             {Mode: 0644, Data: []byte("bar")},
   480  				"blu/two/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
   481  			},
   482  			shouldWrite: true,
   483  		},
   484  		{
   485  			name: "add 3",
   486  			first: map[string]FileProjection{
   487  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   488  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   489  			},
   490  			next: map[string]FileProjection{
   491  				"foo/bar.txt":         {Mode: 0644, Data: []byte("foo")},
   492  				"bar/zab.txt":         {Mode: 0644, Data: []byte("bar")},
   493  				"bar/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")},
   494  			},
   495  			shouldWrite: true,
   496  		},
   497  		{
   498  			name: "delete 1",
   499  			first: map[string]FileProjection{
   500  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   501  				"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   502  			},
   503  			next: map[string]FileProjection{
   504  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   505  			},
   506  			shouldWrite: true,
   507  		},
   508  		{
   509  			name: "delete 2",
   510  			first: map[string]FileProjection{
   511  				"foo/bar.txt":       {Mode: 0644, Data: []byte("foo")},
   512  				"bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
   513  			},
   514  			next: map[string]FileProjection{
   515  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   516  			},
   517  			shouldWrite: true,
   518  		},
   519  		{
   520  			name: "delete 3",
   521  			first: map[string]FileProjection{
   522  				"foo/bar.txt":       {Mode: 0644, Data: []byte("foo")},
   523  				"bar/1/2/sip.txt":   {Mode: 0644, Data: []byte("sip")},
   524  				"bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")},
   525  			},
   526  			next: map[string]FileProjection{
   527  				"foo/bar.txt":     {Mode: 0644, Data: []byte("foo")},
   528  				"bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
   529  			},
   530  			shouldWrite: true,
   531  		},
   532  		{
   533  			name: "delete 4",
   534  			first: map[string]FileProjection{
   535  				"foo/bar.txt":            {Mode: 0644, Data: []byte("foo")},
   536  				"bar/1/2/sip.txt":        {Mode: 0644, Data: []byte("sip")},
   537  				"bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
   538  			},
   539  			next: map[string]FileProjection{
   540  				"foo/bar.txt":     {Mode: 0644, Data: []byte("foo")},
   541  				"bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")},
   542  			},
   543  			shouldWrite: true,
   544  		},
   545  		{
   546  			name: "delete all",
   547  			first: map[string]FileProjection{
   548  				"foo/bar.txt":            {Mode: 0644, Data: []byte("foo")},
   549  				"bar/1/2/sip.txt":        {Mode: 0644, Data: []byte("sip")},
   550  				"bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")},
   551  			},
   552  			next:        map[string]FileProjection{},
   553  			shouldWrite: true,
   554  		},
   555  		{
   556  			name: "add and delete 1",
   557  			first: map[string]FileProjection{
   558  				"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   559  			},
   560  			next: map[string]FileProjection{
   561  				"bar/baz.txt": {Mode: 0644, Data: []byte("baz")},
   562  			},
   563  			shouldWrite: true,
   564  		},
   565  	}
   566  
   567  	for _, tc := range cases {
   568  		targetDir, err := utiltesting.MkTmpdir("atomic-write")
   569  		if err != nil {
   570  			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
   571  			continue
   572  		}
   573  		defer os.RemoveAll(targetDir)
   574  
   575  		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
   576  
   577  		err = writer.Write(tc.first, nil)
   578  		if err != nil {
   579  			t.Errorf("%v: unexpected error writing: %v", tc.name, err)
   580  			continue
   581  		}
   582  
   583  		checkVolumeContents(targetDir, tc.name, tc.first, t)
   584  		if !tc.shouldWrite {
   585  			continue
   586  		}
   587  
   588  		err = writer.Write(tc.next, nil)
   589  		if err != nil {
   590  			if tc.shouldWrite {
   591  				t.Errorf("%v: unexpected error writing: %v", tc.name, err)
   592  				continue
   593  			}
   594  		} else if !tc.shouldWrite {
   595  			t.Errorf("%v: unexpected success", tc.name)
   596  			continue
   597  		}
   598  
   599  		checkVolumeContents(targetDir, tc.name, tc.next, t)
   600  	}
   601  }
   602  
   603  func TestMultipleUpdates(t *testing.T) {
   604  	cases := []struct {
   605  		name     string
   606  		payloads []map[string]FileProjection
   607  	}{
   608  		{
   609  			name: "update 1",
   610  			payloads: []map[string]FileProjection{
   611  				{
   612  					"foo": {Mode: 0644, Data: []byte("foo")},
   613  					"bar": {Mode: 0644, Data: []byte("bar")},
   614  				},
   615  				{
   616  					"foo": {Mode: 0400, Data: []byte("foo2")},
   617  					"bar": {Mode: 0400, Data: []byte("bar2")},
   618  				},
   619  				{
   620  					"foo": {Mode: 0600, Data: []byte("foo3")},
   621  					"bar": {Mode: 0600, Data: []byte("bar3")},
   622  				},
   623  			},
   624  		},
   625  		{
   626  			name: "update 2",
   627  			payloads: []map[string]FileProjection{
   628  				{
   629  					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
   630  					"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")},
   631  				},
   632  				{
   633  					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
   634  					"bar/zab.txt": {Mode: 0400, Data: []byte("bar/zab.txt2")},
   635  				},
   636  			},
   637  		},
   638  		{
   639  			name: "clear sentinel",
   640  			payloads: []map[string]FileProjection{
   641  				{
   642  					"foo": {Mode: 0644, Data: []byte("foo")},
   643  					"bar": {Mode: 0644, Data: []byte("bar")},
   644  				},
   645  				{
   646  					"foo": {Mode: 0644, Data: []byte("foo2")},
   647  					"bar": {Mode: 0644, Data: []byte("bar2")},
   648  				},
   649  				{
   650  					"foo": {Mode: 0644, Data: []byte("foo3")},
   651  					"bar": {Mode: 0644, Data: []byte("bar3")},
   652  				},
   653  				{
   654  					"foo": {Mode: 0644, Data: []byte("foo4")},
   655  					"bar": {Mode: 0644, Data: []byte("bar4")},
   656  				},
   657  			},
   658  		},
   659  		{
   660  			name: "subdirectories 2",
   661  			payloads: []map[string]FileProjection{
   662  				{
   663  					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar")},
   664  					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt")},
   665  					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")},
   666  					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
   667  				},
   668  				{
   669  					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
   670  					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
   671  					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
   672  					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
   673  				},
   674  			},
   675  		},
   676  		{
   677  			name: "add 1",
   678  			payloads: []map[string]FileProjection{
   679  				{
   680  					"foo/bar.txt":            {Mode: 0644, Data: []byte("foo/bar")},
   681  					"bar//zab.txt":           {Mode: 0644, Data: []byte("bar/zab.txt")},
   682  					"foo/blaz/bar.txt":       {Mode: 0644, Data: []byte("foo/blaz/bar")},
   683  					"bar/zib////zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")},
   684  				},
   685  				{
   686  					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
   687  					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
   688  					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
   689  					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
   690  					"add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
   691  				},
   692  			},
   693  		},
   694  		{
   695  			name: "add 2",
   696  			payloads: []map[string]FileProjection{
   697  				{
   698  					"foo/bar.txt":      {Mode: 0644, Data: []byte("foo/bar2")},
   699  					"bar/zab.txt":      {Mode: 0644, Data: []byte("bar/zab.txt2")},
   700  					"foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")},
   701  					"bar/zib/zab.txt":  {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
   702  					"add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")},
   703  				},
   704  				{
   705  					"foo/bar.txt":       {Mode: 0644, Data: []byte("foo/bar2")},
   706  					"bar/zab.txt":       {Mode: 0644, Data: []byte("bar/zab.txt2")},
   707  					"foo/blaz/bar.txt":  {Mode: 0644, Data: []byte("foo/blaz/bar2")},
   708  					"bar/zib/zab.txt":   {Mode: 0644, Data: []byte("bar/zib/zab.txt2")},
   709  					"add/new/keys.txt":  {Mode: 0644, Data: []byte("addNewKeys")},
   710  					"add/new/keys2.txt": {Mode: 0644, Data: []byte("addNewKeys2")},
   711  					"add/new/keys3.txt": {Mode: 0644, Data: []byte("addNewKeys3")},
   712  				},
   713  			},
   714  		},
   715  		{
   716  			name: "remove 1",
   717  			payloads: []map[string]FileProjection{
   718  				{
   719  					"foo/bar.txt":         {Mode: 0644, Data: []byte("foo/bar")},
   720  					"bar//zab.txt":        {Mode: 0644, Data: []byte("bar/zab.txt")},
   721  					"foo/blaz/bar.txt":    {Mode: 0644, Data: []byte("foo/blaz/bar")},
   722  					"zip/zap/zup/fop.txt": {Mode: 0644, Data: []byte("zip/zap/zup/fop.txt")},
   723  				},
   724  				{
   725  					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")},
   726  					"bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")},
   727  				},
   728  				{
   729  					"foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")},
   730  				},
   731  			},
   732  		},
   733  	}
   734  
   735  	for _, tc := range cases {
   736  		targetDir, err := utiltesting.MkTmpdir("atomic-write")
   737  		if err != nil {
   738  			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
   739  			continue
   740  		}
   741  		defer os.RemoveAll(targetDir)
   742  
   743  		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
   744  
   745  		for _, payload := range tc.payloads {
   746  			writer.Write(payload, nil)
   747  
   748  			checkVolumeContents(targetDir, tc.name, payload, t)
   749  		}
   750  	}
   751  }
   752  
   753  func checkVolumeContents(targetDir, tcName string, payload map[string]FileProjection, t *testing.T) {
   754  	dataDirPath := filepath.Join(targetDir, dataDirName)
   755  	// use filepath.Walk to reconstruct the payload, then deep equal
   756  	observedPayload := make(map[string]FileProjection)
   757  	visitor := func(path string, info os.FileInfo, _ error) error {
   758  		if info.IsDir() {
   759  			return nil
   760  		}
   761  
   762  		relativePath := strings.TrimPrefix(path, dataDirPath)
   763  		relativePath = strings.TrimPrefix(relativePath, "/")
   764  		if strings.HasPrefix(relativePath, "..") {
   765  			return nil
   766  		}
   767  
   768  		content, err := os.ReadFile(path)
   769  		if err != nil {
   770  			return err
   771  		}
   772  		fileInfo, err := os.Stat(path)
   773  		if err != nil {
   774  			return err
   775  		}
   776  		mode := int32(fileInfo.Mode())
   777  
   778  		observedPayload[relativePath] = FileProjection{Data: content, Mode: mode}
   779  
   780  		return nil
   781  	}
   782  
   783  	d, err := os.ReadDir(targetDir)
   784  	if err != nil {
   785  		t.Errorf("Unable to read dir %v: %v", targetDir, err)
   786  		return
   787  	}
   788  	for _, info := range d {
   789  		if strings.HasPrefix(info.Name(), "..") {
   790  			continue
   791  		}
   792  		if info.Type()&os.ModeSymlink != 0 {
   793  			p := filepath.Join(targetDir, info.Name())
   794  			actual, err := os.Readlink(p)
   795  			if err != nil {
   796  				t.Errorf("Unable to read symlink %v: %v", p, err)
   797  				continue
   798  			}
   799  			if err := filepath.Walk(filepath.Join(targetDir, actual), visitor); err != nil {
   800  				t.Errorf("%v: unexpected error walking directory: %v", tcName, err)
   801  			}
   802  		}
   803  	}
   804  
   805  	cleanPathPayload := make(map[string]FileProjection, len(payload))
   806  	for k, v := range payload {
   807  		cleanPathPayload[filepath.Clean(k)] = v
   808  	}
   809  
   810  	if !reflect.DeepEqual(cleanPathPayload, observedPayload) {
   811  		t.Errorf("%v: payload and observed payload do not match.", tcName)
   812  	}
   813  }
   814  
   815  func TestValidatePayload(t *testing.T) {
   816  	maxPath := strings.Repeat("a", maxPathLength+1)
   817  
   818  	cases := []struct {
   819  		name     string
   820  		payload  map[string]FileProjection
   821  		expected sets.String
   822  		valid    bool
   823  	}{
   824  		{
   825  			name: "valid payload",
   826  			payload: map[string]FileProjection{
   827  				"foo": {},
   828  				"bar": {},
   829  			},
   830  			valid:    true,
   831  			expected: sets.NewString("foo", "bar"),
   832  		},
   833  		{
   834  			name: "payload with path length > 4096 is invalid",
   835  			payload: map[string]FileProjection{
   836  				maxPath: {},
   837  			},
   838  			valid: false,
   839  		},
   840  		{
   841  			name: "payload with absolute path is invalid",
   842  			payload: map[string]FileProjection{
   843  				"/dev/null": {},
   844  			},
   845  			valid: false,
   846  		},
   847  		{
   848  			name: "payload with reserved path is invalid",
   849  			payload: map[string]FileProjection{
   850  				"..sneaky.txt": {},
   851  			},
   852  			valid: false,
   853  		},
   854  		{
   855  			name: "payload with doubledot path is invalid",
   856  			payload: map[string]FileProjection{
   857  				"foo/../etc/password": {},
   858  			},
   859  			valid: false,
   860  		},
   861  		{
   862  			name: "payload with empty path is invalid",
   863  			payload: map[string]FileProjection{
   864  				"": {},
   865  			},
   866  			valid: false,
   867  		},
   868  		{
   869  			name: "payload with unclean path should be cleaned",
   870  			payload: map[string]FileProjection{
   871  				"foo////bar": {},
   872  			},
   873  			valid:    true,
   874  			expected: sets.NewString("foo/bar"),
   875  		},
   876  	}
   877  	getPayloadPaths := func(payload map[string]FileProjection) sets.String {
   878  		paths := sets.NewString()
   879  		for path := range payload {
   880  			paths.Insert(path)
   881  		}
   882  		return paths
   883  	}
   884  
   885  	for _, tc := range cases {
   886  		real, err := validatePayload(tc.payload)
   887  		if !tc.valid && err == nil {
   888  			t.Errorf("%v: unexpected success", tc.name)
   889  		}
   890  
   891  		if tc.valid {
   892  			if err != nil {
   893  				t.Errorf("%v: unexpected failure: %v", tc.name, err)
   894  				continue
   895  			}
   896  
   897  			realPaths := getPayloadPaths(real)
   898  			if !realPaths.Equal(tc.expected) {
   899  				t.Errorf("%v: unexpected payload paths: %v is not equal to %v", tc.name, realPaths, tc.expected)
   900  			}
   901  		}
   902  
   903  	}
   904  }
   905  
   906  func TestCreateUserVisibleFiles(t *testing.T) {
   907  	cases := []struct {
   908  		name     string
   909  		payload  map[string]FileProjection
   910  		expected map[string]string
   911  	}{
   912  		{
   913  			name: "simple path",
   914  			payload: map[string]FileProjection{
   915  				"foo": {},
   916  				"bar": {},
   917  			},
   918  			expected: map[string]string{
   919  				"foo": "..data/foo",
   920  				"bar": "..data/bar",
   921  			},
   922  		},
   923  		{
   924  			name: "simple nested path",
   925  			payload: map[string]FileProjection{
   926  				"foo/bar":     {},
   927  				"foo/bar/txt": {},
   928  				"bar/txt":     {},
   929  			},
   930  			expected: map[string]string{
   931  				"foo": "..data/foo",
   932  				"bar": "..data/bar",
   933  			},
   934  		},
   935  		{
   936  			name: "unclean nested path",
   937  			payload: map[string]FileProjection{
   938  				"./bar":     {},
   939  				"foo///bar": {},
   940  			},
   941  			expected: map[string]string{
   942  				"bar": "..data/bar",
   943  				"foo": "..data/foo",
   944  			},
   945  		},
   946  	}
   947  
   948  	for _, tc := range cases {
   949  		targetDir, err := utiltesting.MkTmpdir("atomic-write")
   950  		if err != nil {
   951  			t.Errorf("%v: unexpected error creating tmp dir: %v", tc.name, err)
   952  			continue
   953  		}
   954  		defer os.RemoveAll(targetDir)
   955  
   956  		dataDirPath := filepath.Join(targetDir, dataDirName)
   957  		err = os.MkdirAll(dataDirPath, 0755)
   958  		if err != nil {
   959  			t.Fatalf("%v: unexpected error creating data path: %v", tc.name, err)
   960  		}
   961  
   962  		writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
   963  		payload, err := validatePayload(tc.payload)
   964  		if err != nil {
   965  			t.Fatalf("%v: unexpected error validating payload: %v", tc.name, err)
   966  		}
   967  		err = writer.createUserVisibleFiles(payload)
   968  		if err != nil {
   969  			t.Fatalf("%v: unexpected error creating visible files: %v", tc.name, err)
   970  		}
   971  
   972  		for subpath, expectedDest := range tc.expected {
   973  			visiblePath := filepath.Join(targetDir, subpath)
   974  			destination, err := os.Readlink(visiblePath)
   975  			if err != nil && os.IsNotExist(err) {
   976  				t.Fatalf("%v: visible symlink does not exist: %v", tc.name, visiblePath)
   977  			} else if err != nil {
   978  				t.Fatalf("%v: unable to read symlink %v: %v", tc.name, dataDirPath, err)
   979  			}
   980  
   981  			if expectedDest != destination {
   982  				t.Fatalf("%v: symlink destination %q not same with expected data dir %q", tc.name, destination, expectedDest)
   983  			}
   984  		}
   985  	}
   986  }
   987  
   988  func TestSetPerms(t *testing.T) {
   989  	targetDir, err := utiltesting.MkTmpdir("atomic-write")
   990  	if err != nil {
   991  		t.Fatalf("unexpected error creating tmp dir: %v", err)
   992  	}
   993  	defer os.RemoveAll(targetDir)
   994  
   995  	// Test that setPerms() is called once and with valid timestamp directory.
   996  	payload1 := map[string]FileProjection{
   997  		"foo/bar.txt": {Mode: 0644, Data: []byte("foo")},
   998  		"bar/zab.txt": {Mode: 0644, Data: []byte("bar")},
   999  	}
  1000  
  1001  	var setPermsCalled int
  1002  	writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
  1003  	err = writer.Write(payload1, func(subPath string) error {
  1004  		fileInfo, err := os.Stat(filepath.Join(targetDir, subPath))
  1005  		if err != nil {
  1006  			t.Fatalf("unexpected error getting file info: %v", err)
  1007  		}
  1008  		// Ensure that given timestamp directory really exists.
  1009  		if !fileInfo.IsDir() {
  1010  			t.Fatalf("subPath is not a directory: %v", subPath)
  1011  		}
  1012  		setPermsCalled++
  1013  		return nil
  1014  	})
  1015  	if err != nil {
  1016  		t.Fatalf("unexpected error writing: %v", err)
  1017  	}
  1018  	if setPermsCalled != 1 {
  1019  		t.Fatalf("unexpected number of calls to setPerms: %v", setPermsCalled)
  1020  	}
  1021  
  1022  	// Test that errors from setPerms() are propagated.
  1023  	payload2 := map[string]FileProjection{
  1024  		"foo/bar.txt": {Mode: 0644, Data: []byte("foo2")},
  1025  		"bar/zab.txt": {Mode: 0644, Data: []byte("bar2")},
  1026  	}
  1027  
  1028  	err = writer.Write(payload2, func(_ string) error {
  1029  		return fmt.Errorf("error in setPerms")
  1030  	})
  1031  	if err == nil {
  1032  		t.Fatalf("expected error while writing but got nil")
  1033  	}
  1034  	if !strings.Contains(err.Error(), "error in setPerms") {
  1035  		t.Fatalf("unexpected error while writing: %v", err)
  1036  	}
  1037  }
  1038  
  1039  func TestWriteAgainAfterUnexpectedExit(t *testing.T) {
  1040  	testCases := []struct {
  1041  		name       string
  1042  		payload    map[string]FileProjection
  1043  		simulateFn func(targetDir string, payload map[string]FileProjection) error
  1044  	}{
  1045  		{
  1046  			name: "process killed before creating user visible files",
  1047  			payload: map[string]FileProjection{
  1048  				"foo": {Mode: 0644, Data: []byte("foo")},
  1049  				"bar": {Mode: 0644, Data: []byte("bar")},
  1050  			},
  1051  			simulateFn: func(targetDir string, payload map[string]FileProjection) error {
  1052  				for filename := range payload {
  1053  					path := filepath.Join(targetDir, filename)
  1054  					if err := os.RemoveAll(path); err != nil {
  1055  						return err
  1056  					}
  1057  				}
  1058  				return nil
  1059  			},
  1060  		},
  1061  	}
  1062  
  1063  	for _, tc := range testCases {
  1064  		tc := tc
  1065  		t.Run(tc.name, func(t *testing.T) {
  1066  			targetDir, err := utiltesting.MkTmpdir("atomic-write")
  1067  			if err != nil {
  1068  				t.Fatalf("unexpected error creating tmp dir: %v", err)
  1069  			}
  1070  			defer func() {
  1071  				err := os.RemoveAll(targetDir)
  1072  				if err != nil {
  1073  					t.Errorf("%v: unexpected error removing tmp dir: %v", tc.name, err)
  1074  				}
  1075  			}()
  1076  
  1077  			writer := &AtomicWriter{targetDir: targetDir, logContext: "-test-"}
  1078  			err = writer.Write(tc.payload, nil)
  1079  			if err != nil {
  1080  				t.Fatalf("unexpected error writing payload: %v", err)
  1081  			}
  1082  
  1083  			err = tc.simulateFn(targetDir, tc.payload)
  1084  			if err != nil {
  1085  				t.Fatalf("failed to simulate the unexpected exit: %v", err)
  1086  			}
  1087  
  1088  			err = writer.Write(tc.payload, nil)
  1089  			if err != nil {
  1090  				t.Fatalf("unexpected error writing payload again: %v", err)
  1091  			}
  1092  			checkVolumeContents(targetDir, tc.name, tc.payload, t)
  1093  		})
  1094  	}
  1095  }
  1096  

View as plain text