Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
K
k3s
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Jacklull
k3s
Commits
10b6074a
Commit
10b6074a
authored
Feb 10, 2016
by
k8s-merge-robot
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #20397 from pmorie/atomic-projections
Auto commit by PR queue bot
parents
9520bb5d
b750ea67
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1309 additions
and
0 deletions
+1309
-0
atomic_writer.go
pkg/volume/util/atomic_writer.go
+457
-0
atomic_writer_test.go
pkg/volume/util/atomic_writer_test.go
+852
-0
No files found.
pkg/volume/util/atomic_writer.go
0 → 100644
View file @
10b6074a
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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
(
"bytes"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/util/sets"
)
const
(
maxFileNameLength
=
255
maxPathLength
=
4096
)
// AtomicWriter handles atomically projecting content for a set of files into
// a target directory. AtomicWriter maintains a sentinel file named
// "..sentinel" in the target directory which is updated after new data is
// projected into the target directory, allowing consumers of the data to
// listen for updates by monitoring the sentinel file with inotify or
// fanotify.
//
// Note:
//
// 1. AtomicWriter reserves the set of pathnames starting with `..`.
// 2. AtomicWriter offers no concurrency guarantees and must be synchronized
// by the caller.
//
// The visible files in this volume are symlinks to files in the writer's data
// directory. Actual files are stored in a hidden timestamped directory which
// is symlinked to by the data directory. The timestamped directory and
// data directory symlink are created in the writer's target dir. This scheme
// allows the files to be atomically updated by changing the target of the
// data directory symlink.
type
AtomicWriter
struct
{
targetDir
string
logContext
string
}
// NewAtomicWriter creates a new AtomicWriter configured to write to the given
// target directory, or returns an error if the target directory does not exist.
func
NewAtomicWriter
(
targetDir
,
logContext
string
)
(
*
AtomicWriter
,
error
)
{
_
,
err
:=
os
.
Stat
(
targetDir
)
if
os
.
IsNotExist
(
err
)
{
return
nil
,
err
}
return
&
AtomicWriter
{
targetDir
:
targetDir
,
logContext
:
logContext
},
nil
}
const
(
sentinelFileName
=
"..sentinel"
dataDirName
=
"..data"
newDataDirName
=
"..data_tmp"
)
// Write does an atomic projection of the given payload into the writer's target
// directory. Input paths must not begin with '..'.
//
// The Write algorithm is:
//
// 1. The payload is validated; if the payload is invalid, the function returns
// 2. The user-visible portion of the volume is walked to determine whether any
// portion of the payload was deleted and is still present on disk.
// If the payload is already present on disk and there are no deleted files,
// the function returns
// 3. A check is made to determine whether data present in the payload has changed
// 4. A new timestamped dir is created
// 5. The payload is written to the new timestamped directory
// 6. Symlinks and directory for new user-visible files are created (if needed).
//
// For example consider the files:
// <target-dir>/podName
// <target-dir>/user/labels
// <target-dir>/k8s/annotations
//
// The user visible files are symbolic links into the internal data directory:
// <target-dir>/podName -> ..data/podName
// <target-dir>/usr/labels -> ../..data/usr/labels
// <target-dir>/k8s/annotations -> ../..data/k8s/annotations
//
// Relative links are created into the data directory for files in subdirectories.
//
// The data directory itself is a link to a timestamped directory with
// the real data:
// <target-dir>/..data -> ..2016_02_01_15_04_05.12345678/
// 7. The current timestamped directory is detected by reading the data directory
// symlink
// 8. A symlink to the new timestamped directory ..data_tmp is created that will
// become the new data directory
// 9. The new data directory symlink is renamed to the data directory; rename is atomic
// 10. The sentinel file modification and access times are updated (file is created if it does not
// already exist)
// 11. Old paths are removed from the user-visible portion of the target directory
// 12. The previous timestamped directory is removed, if it exists
func
(
w
*
AtomicWriter
)
Write
(
payload
map
[
string
][]
byte
)
error
{
// (1)
cleanPayload
,
err
:=
validatePayload
(
payload
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: invalid payload: %v"
,
w
.
logContext
,
err
)
return
err
}
// (2)
pathsToRemove
,
err
:=
w
.
pathsToRemove
(
cleanPayload
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: error determining user-visible files to remove: %v"
,
w
.
logContext
,
err
)
return
err
}
// (3)
if
should
,
err
:=
w
.
shouldWritePayload
(
cleanPayload
);
err
!=
nil
{
glog
.
Errorf
(
"%s: error determining whether payload should be written to disk: %v"
,
w
.
logContext
,
err
)
return
err
}
else
if
!
should
&&
len
(
pathsToRemove
)
==
0
{
glog
.
V
(
5
)
.
Infof
(
"%s: no update required for target directory %v"
,
w
.
logContext
,
w
.
targetDir
)
return
nil
}
// (4)
tsDir
,
err
:=
w
.
newTimestampDir
()
if
err
!=
nil
{
glog
.
V
(
5
)
.
Infof
(
"%s: error creating new ts data directory: %v"
,
w
.
logContext
,
err
)
return
err
}
// (5)
if
err
=
w
.
writePayloadToDir
(
cleanPayload
,
tsDir
);
err
!=
nil
{
glog
.
Errorf
(
"%s: error writing payload to ts data directory %s: %v"
,
w
.
logContext
,
tsDir
,
err
)
return
err
}
// (6)
if
err
=
w
.
createUserVisibleFiles
(
cleanPayload
);
err
!=
nil
{
glog
.
Errorf
(
"%s: error creating visible symlinks in %s: %v"
,
w
.
logContext
,
w
.
targetDir
,
err
)
return
err
}
// (7)
_
,
tsDirName
:=
filepath
.
Split
(
tsDir
)
dataDirPath
:=
path
.
Join
(
w
.
targetDir
,
dataDirName
)
oldTsDir
,
err
:=
os
.
Readlink
(
dataDirPath
)
if
err
!=
nil
&&
!
os
.
IsNotExist
(
err
)
{
glog
.
Errorf
(
"%s: error reading link for data directory: %v"
,
w
.
logContext
,
err
)
return
err
}
// (8)
newDataDirPath
:=
path
.
Join
(
w
.
targetDir
,
newDataDirName
)
if
err
=
os
.
Symlink
(
tsDirName
,
newDataDirPath
);
err
!=
nil
{
os
.
RemoveAll
(
tsDir
)
glog
.
Errorf
(
"%s: error creating symbolic link for atomic update: %v"
,
w
.
logContext
,
err
)
return
err
}
// (9)
if
err
=
os
.
Rename
(
newDataDirPath
,
dataDirPath
);
err
!=
nil
{
os
.
Remove
(
newDataDirPath
)
os
.
RemoveAll
(
tsDir
)
glog
.
Errorf
(
"%s: error renaming symbolic link for data directory %s: %v"
,
w
.
logContext
,
newDataDirPath
,
err
)
return
err
}
// (10)
if
err
=
w
.
touchSentinelFile
();
err
!=
nil
{
glog
.
Errorf
(
"%s: error touching sentinel file: %v"
,
w
.
logContext
,
err
)
return
err
}
// (11)
if
err
=
w
.
removeUserVisiblePaths
(
pathsToRemove
);
err
!=
nil
{
glog
.
Errorf
(
"%s: error removing old visible symlinks: %v"
,
w
.
logContext
,
err
)
return
err
}
// (12)
if
len
(
oldTsDir
)
>
0
{
if
err
=
os
.
RemoveAll
(
path
.
Join
(
w
.
targetDir
,
oldTsDir
));
err
!=
nil
{
glog
.
Errorf
(
"%s: error removing old data directory %s: %v"
,
w
.
logContext
,
oldTsDir
,
err
)
return
err
}
}
return
nil
}
// validatePayload returns an error if any path in the payload returns a copy of the payload with the paths cleaned.
func
validatePayload
(
payload
map
[
string
][]
byte
)
(
map
[
string
][]
byte
,
error
)
{
cleanPayload
:=
make
(
map
[
string
][]
byte
)
for
k
,
content
:=
range
payload
{
if
err
:=
validatePath
(
k
);
err
!=
nil
{
return
nil
,
err
}
cleanPayload
[
path
.
Clean
(
k
)]
=
content
}
return
cleanPayload
,
nil
}
// validatePath validates a single path, returning an error if the path is
// invalid. paths may not:
//
// 1. be absolute
// 2. contain '..' as an element
// 3. start with '..'
// 4. contain filenames larger than 255 characters
// 5. be longer than 4096 characters
func
validatePath
(
targetPath
string
)
error
{
// TODO: somehow unify this with the similar api validation,
// validateVolumeSourcePath; the error semantics are just different enough
// from this that it was time-prohibitive trying to find the right
// refactoring to re-use.
if
targetPath
==
""
{
return
fmt
.
Errorf
(
"invalid path: must not be empty: %q"
,
targetPath
)
}
if
path
.
IsAbs
(
targetPath
)
{
return
fmt
.
Errorf
(
"invalid path: must be relative path: %s"
,
targetPath
)
}
if
len
(
targetPath
)
>
maxPathLength
{
return
fmt
.
Errorf
(
"invalid path: must be less than %d characters"
,
maxPathLength
)
}
items
:=
strings
.
Split
(
targetPath
,
string
(
os
.
PathSeparator
))
for
_
,
item
:=
range
items
{
if
item
==
".."
{
return
fmt
.
Errorf
(
"invalid path: must not contain '..': %s"
,
targetPath
)
}
if
len
(
item
)
>
maxFileNameLength
{
return
fmt
.
Errorf
(
"invalid path: filenames must be less than %d characters"
,
maxFileNameLength
)
}
}
if
strings
.
HasPrefix
(
items
[
0
],
".."
)
&&
len
(
items
[
0
])
>
2
{
return
fmt
.
Errorf
(
"invalid path: must not start with '..': %s"
,
targetPath
)
}
return
nil
}
// shouldWritePayload returns whether the payload should be written to disk.
func
(
w
*
AtomicWriter
)
shouldWritePayload
(
payload
map
[
string
][]
byte
)
(
bool
,
error
)
{
for
userVisiblePath
,
content
:=
range
payload
{
shouldWrite
,
err
:=
w
.
shouldWriteFile
(
path
.
Join
(
w
.
targetDir
,
userVisiblePath
),
content
)
if
err
!=
nil
{
return
false
,
err
}
if
shouldWrite
{
return
true
,
nil
}
}
return
false
,
nil
}
// shouldWriteFile returns whether a new version of a file should be written to disk.
func
(
w
*
AtomicWriter
)
shouldWriteFile
(
path
string
,
content
[]
byte
)
(
bool
,
error
)
{
_
,
err
:=
os
.
Lstat
(
path
)
if
os
.
IsNotExist
(
err
)
{
return
true
,
nil
}
contentOnFs
,
err
:=
ioutil
.
ReadFile
(
path
)
if
err
!=
nil
{
return
false
,
err
}
return
(
bytes
.
Compare
(
content
,
contentOnFs
)
!=
0
),
nil
}
// pathsToRemove walks the user-visible portion of the target directory and
// determines which paths should be removed (if any) after the payload is
// written to the target directory.
func
(
w
*
AtomicWriter
)
pathsToRemove
(
payload
map
[
string
][]
byte
)
(
sets
.
String
,
error
)
{
paths
:=
sets
.
NewString
()
visitor
:=
func
(
path
string
,
info
os
.
FileInfo
,
err
error
)
error
{
if
path
==
w
.
targetDir
{
return
nil
}
relativePath
:=
strings
.
TrimPrefix
(
path
,
w
.
targetDir
)
relativePath
=
strings
.
TrimPrefix
(
relativePath
,
"/"
)
if
strings
.
HasPrefix
(
relativePath
,
".."
)
{
return
nil
}
paths
.
Insert
(
relativePath
)
return
nil
}
err
:=
filepath
.
Walk
(
w
.
targetDir
,
visitor
)
if
os
.
IsNotExist
(
err
)
{
return
nil
,
nil
}
else
if
err
!=
nil
{
return
nil
,
err
}
glog
.
V
(
5
)
.
Infof
(
"%s: current paths: %+v"
,
w
.
targetDir
,
paths
.
List
())
newPaths
:=
sets
.
NewString
()
for
file
:=
range
payload
{
// add all subpaths for the payload to the set of new paths
// to avoid attempting to remove non-empty dirs
for
subPath
:=
file
;
subPath
!=
""
;
{
newPaths
.
Insert
(
subPath
)
subPath
,
_
=
filepath
.
Split
(
subPath
)
subPath
=
strings
.
TrimSuffix
(
subPath
,
"/"
)
}
}
glog
.
V
(
5
)
.
Infof
(
"%s: new paths: %+v"
,
w
.
targetDir
,
newPaths
.
List
())
result
:=
paths
.
Difference
(
newPaths
)
glog
.
V
(
5
)
.
Infof
(
"%s: paths to remove: %+v"
,
w
.
targetDir
,
result
)
return
result
,
nil
}
// newTimestampDir creates a new timestamp directory
func
(
w
*
AtomicWriter
)
newTimestampDir
()
(
string
,
error
)
{
tsDir
,
err
:=
ioutil
.
TempDir
(
w
.
targetDir
,
fmt
.
Sprintf
(
"..%s."
,
time
.
Now
()
.
Format
(
"1981_02_01_15_04_05"
)))
if
err
!=
nil
{
glog
.
Errorf
(
"%s: unable to create new temp directory: %v"
,
w
.
logContext
,
err
)
return
""
,
err
}
return
tsDir
,
nil
}
// writePayloadToDir writes the given payload to the given directory. The
// directory must exist.
func
(
w
*
AtomicWriter
)
writePayloadToDir
(
payload
map
[
string
][]
byte
,
dir
string
)
error
{
for
userVisiblePath
,
content
:=
range
payload
{
fullPath
:=
path
.
Join
(
dir
,
userVisiblePath
)
baseDir
,
_
:=
filepath
.
Split
(
fullPath
)
err
:=
os
.
MkdirAll
(
baseDir
,
os
.
ModePerm
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: unable to create directory %s: %v"
,
w
.
logContext
,
baseDir
,
err
)
return
err
}
err
=
ioutil
.
WriteFile
(
fullPath
,
content
,
0644
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: unable to write file %s: %v"
,
w
.
logContext
,
fullPath
,
err
)
return
err
}
}
return
nil
}
// createUserVisibleFiles creates the relative symlinks for all the
// files configured in the payload. If the directory in a file path does not
// exist, it is created.
//
// Viz:
// For files: "bar", "foo/bar", "baz/bar", "foo/baz/blah"
// the following symlinks and subdirectories are created:
// bar -> ..data/bar
// foo/bar -> ../..data/foo/bar
// baz/bar -> ../..data/baz/bar
// foo/baz/blah -> ../../..data/foo/baz/blah
func
(
w
*
AtomicWriter
)
createUserVisibleFiles
(
payload
map
[
string
][]
byte
)
error
{
for
userVisiblePath
:=
range
payload
{
dir
,
_
:=
filepath
.
Split
(
userVisiblePath
)
subDirs
:=
0
if
len
(
dir
)
>
0
{
// If dir is not empty, the projection path contains at least one
// subdirectory (example: userVisiblePath := "foo/bar").
// Since filepath.Split leaves a trailing path separator, in this
// example, dir = "foo/". In order to calculate the number of
// subdirectories, we must subtract 1 from the number returned by split.
subDirs
=
len
(
strings
.
Split
(
dir
,
"/"
))
-
1
err
:=
os
.
MkdirAll
(
path
.
Join
(
w
.
targetDir
,
dir
),
os
.
ModePerm
)
if
err
!=
nil
{
return
err
}
}
_
,
err
:=
os
.
Readlink
(
path
.
Join
(
w
.
targetDir
,
userVisiblePath
))
if
err
!=
nil
&&
os
.
IsNotExist
(
err
)
{
// The link into the data directory for this path doesn't exist; create it,
// respecting the number of subdirectories necessary to link
// correctly back into the data directory.
visibleFile
:=
path
.
Join
(
w
.
targetDir
,
userVisiblePath
)
dataDirFile
:=
path
.
Join
(
strings
.
Repeat
(
"../"
,
subDirs
),
dataDirName
,
userVisiblePath
)
err
=
os
.
Symlink
(
dataDirFile
,
visibleFile
)
if
err
!=
nil
{
return
err
}
}
}
return
nil
}
// removeUserVisiblePaths removes the set of paths from the user-visible
// portion of the writer's target directory.
func
(
w
*
AtomicWriter
)
removeUserVisiblePaths
(
paths
sets
.
String
)
error
{
orderedPaths
:=
paths
.
List
()
for
ii
:=
len
(
orderedPaths
)
-
1
;
ii
>=
0
;
ii
--
{
if
err
:=
os
.
Remove
(
path
.
Join
(
w
.
targetDir
,
orderedPaths
[
ii
]));
err
!=
nil
{
glog
.
Errorf
(
"%s: error pruning old user-visible path %s: %v"
,
w
.
logContext
,
orderedPaths
[
ii
],
err
)
return
err
}
}
return
nil
}
// touchSentinelFile touches the sentinel file or creates it if it doesn't exist.
func
(
w
*
AtomicWriter
)
touchSentinelFile
()
error
{
sentinelFilePath
:=
path
.
Join
(
w
.
targetDir
,
sentinelFileName
)
_
,
err
:=
os
.
Stat
(
sentinelFilePath
)
if
err
!=
nil
&&
os
.
IsNotExist
(
err
)
{
file
,
err
:=
os
.
Create
(
sentinelFilePath
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: unexpected error creating sentinel file %s: %v"
,
w
.
logContext
,
sentinelFilePath
,
err
)
return
err
}
file
.
Close
()
}
else
if
err
!=
nil
{
return
err
}
ts
:=
time
.
Now
()
err
=
os
.
Chtimes
(
sentinelFilePath
,
ts
,
ts
)
if
err
!=
nil
{
glog
.
Errorf
(
"%s: error updating sentinel file mod time: %v"
,
w
.
logContext
,
err
)
return
err
}
return
nil
}
pkg/volume/util/atomic_writer_test.go
0 → 100644
View file @
10b6074a
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
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"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"k8s.io/kubernetes/pkg/util/sets"
utiltesting
"k8s.io/kubernetes/pkg/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
)
}
_
,
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
][]
byte
payload2
map
[
string
][]
byte
expected
sets
.
String
}{
{
name
:
"simple"
,
payload1
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"bar.txt"
:
[]
byte
(
"bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
},
expected
:
sets
.
NewString
(
"bar.txt"
),
},
{
name
:
"simple 2"
,
payload1
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zip/bar.txt"
:
[]
byte
(
"zip/bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
},
expected
:
sets
.
NewString
(
"zip/bar.txt"
,
"zip"
),
},
{
name
:
"subdirs 1"
,
payload1
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zip/zap/bar.txt"
:
[]
byte
(
"zip/bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
},
expected
:
sets
.
NewString
(
"zip/zap/bar.txt"
,
"zip"
,
"zip/zap"
),
},
{
name
:
"subdirs 2"
,
payload1
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zip/1/2/3/4/bar.txt"
:
[]
byte
(
"zip/bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
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
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zip/1/2/3/4/bar.txt"
:
[]
byte
(
"zip/bar"
),
"zap/a/b/c/bar.txt"
:
[]
byte
(
"zap/bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
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
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zap/1/2/3/4/bar.txt"
:
[]
byte
(
"zip/bar"
),
"zap/1/2/c/bar.txt"
:
[]
byte
(
"zap/bar"
),
"zap/1/2/magic.txt"
:
[]
byte
(
"indigo"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zap/1/2/magic.txt"
:
[]
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
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zap/1/2/3/4/bar.txt"
:
[]
byte
(
"zip/bar"
),
"zap/1/2/c/bar.txt"
:
[]
byte
(
"zap/bar"
),
},
payload2
:
map
[
string
][]
byte
{
"foo.txt"
:
[]
byte
(
"foo"
),
"zap/1/2/magic.txt"
:
[]
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
}
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
}
actual
,
err
:=
writer
.
pathsToRemove
(
tc
.
payload2
)
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:
\n
expected: %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
][]
byte
success
bool
}{
{
name
:
"invalid payload 1"
,
payload
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"..bar"
:
[]
byte
(
"bar"
),
"binary.bin"
:
mysteryBinaryBytes
,
},
success
:
false
,
},
{
name
:
"invalid payload 2"
,
payload
:
map
[
string
][]
byte
{
"foo/../bar"
:
[]
byte
(
"foo"
),
},
success
:
false
,
},
{
name
:
"basic 1"
,
payload
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
success
:
true
,
},
{
name
:
"basic 2"
,
payload
:
map
[
string
][]
byte
{
"binary.bin"
:
mysteryBinaryBytes
,
".binary.bin"
:
mysteryBinaryBytes
,
},
success
:
true
,
},
{
name
:
"dotfiles"
,
payload
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
".dotfile"
:
[]
byte
(
"dotfile"
),
".dotfile.file"
:
[]
byte
(
"dotfile.file"
),
},
success
:
true
,
},
{
name
:
"subdirectories 1"
,
payload
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
},
success
:
true
,
},
{
name
:
"subdirectories 2"
,
payload
:
map
[
string
][]
byte
{
"foo//bar.txt"
:
[]
byte
(
"foo//bar"
),
"bar///bar/zab.txt"
:
[]
byte
(
"bar/../bar/zab.txt"
),
},
success
:
true
,
},
{
name
:
"subdirectories 3"
,
payload
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt"
),
},
success
:
true
,
},
{
name
:
"kitchen sink"
,
payload
:
map
[
string
][]
byte
{
"foo.log"
:
[]
byte
(
"foo"
),
"bar.zap"
:
[]
byte
(
"bar"
),
".dotfile"
:
[]
byte
(
"dotfile"
),
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt"
),
"1/2/3/4/5/6/7/8/9/10/.dotfile.lib"
:
[]
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
}
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
)
checkSentinelFile
(
targetDir
,
t
)
}
}
func
TestUpdate
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
name
string
first
map
[
string
][]
byte
next
map
[
string
][]
byte
shouldWrite
bool
}{
{
name
:
"update"
,
first
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo2"
),
"bar"
:
[]
byte
(
"bar2"
),
},
shouldWrite
:
true
,
},
{
name
:
"no update"
,
first
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
shouldWrite
:
false
,
},
{
name
:
"no update 2"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
shouldWrite
:
false
,
},
{
name
:
"add 1"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
"blu/zip.txt"
:
[]
byte
(
"zip"
),
},
shouldWrite
:
true
,
},
{
name
:
"add 2"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
"blu/two/2/3/4/5/zip.txt"
:
[]
byte
(
"zip"
),
},
shouldWrite
:
true
,
},
{
name
:
"add 3"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
"bar/2/3/4/5/zip.txt"
:
[]
byte
(
"zip"
),
},
shouldWrite
:
true
,
},
{
name
:
"delete 1"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
},
shouldWrite
:
true
,
},
{
name
:
"delete 2"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/3/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
},
shouldWrite
:
true
,
},
{
name
:
"delete 3"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/sip.txt"
:
[]
byte
(
"sip"
),
"bar/1/2/3/zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/sip.txt"
:
[]
byte
(
"sip"
),
},
shouldWrite
:
true
,
},
{
name
:
"delete 4"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/sip.txt"
:
[]
byte
(
"sip"
),
"bar/1/2/3/4/5/6zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/sip.txt"
:
[]
byte
(
"sip"
),
},
shouldWrite
:
true
,
},
{
name
:
"delete all"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
"bar/1/2/sip.txt"
:
[]
byte
(
"sip"
),
"bar/1/2/3/4/5/6zab.txt"
:
[]
byte
(
"bar"
),
},
next
:
map
[
string
][]
byte
{},
shouldWrite
:
true
,
},
{
name
:
"add and delete 1"
,
first
:
map
[
string
][]
byte
{
"foo/bar.txt"
:
[]
byte
(
"foo"
),
},
next
:
map
[
string
][]
byte
{
"bar/baz.txt"
:
[]
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
}
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
}
oldTs
:=
checkSentinelFile
(
targetDir
,
t
)
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
)
ts
:=
checkSentinelFile
(
targetDir
,
t
)
if
!
ts
.
After
(
oldTs
)
{
t
.
Errorf
(
"Unexpected timestamp on sentinel file; expected %v to be after %v"
,
ts
,
oldTs
)
}
}
}
func
TestMultipleUpdates
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
name
string
payloads
[]
map
[
string
][]
byte
clearSentinel
bool
}{
{
name
:
"update 1"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
{
"foo"
:
[]
byte
(
"foo2"
),
"bar"
:
[]
byte
(
"bar2"
),
},
{
"foo"
:
[]
byte
(
"foo3"
),
"bar"
:
[]
byte
(
"bar3"
),
},
},
},
{
name
:
"update 2"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
},
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
},
},
},
{
name
:
"clear sentinel"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo"
:
[]
byte
(
"foo"
),
"bar"
:
[]
byte
(
"bar"
),
},
{
"foo"
:
[]
byte
(
"foo2"
),
"bar"
:
[]
byte
(
"bar2"
),
},
{
"foo"
:
[]
byte
(
"foo3"
),
"bar"
:
[]
byte
(
"bar3"
),
},
{
"foo"
:
[]
byte
(
"foo4"
),
"bar"
:
[]
byte
(
"bar4"
),
},
},
clearSentinel
:
true
,
},
{
name
:
"subdirectories 2"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt"
),
},
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar2"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt2"
),
},
},
},
{
name
:
"add 1"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar//zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar"
),
"bar/zib////zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt"
),
},
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar2"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt2"
),
"add/new/keys.txt"
:
[]
byte
(
"addNewKeys"
),
},
},
},
{
name
:
"add 2"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar2"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt2"
),
"add/new/keys.txt"
:
[]
byte
(
"addNewKeys"
),
},
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar2"
),
"bar/zib/zab.txt"
:
[]
byte
(
"bar/zib/zab.txt2"
),
"add/new/keys.txt"
:
[]
byte
(
"addNewKeys"
),
"add/new/keys2.txt"
:
[]
byte
(
"addNewKeys2"
),
"add/new/keys3.txt"
:
[]
byte
(
"addNewKeys3"
),
},
},
},
{
name
:
"remove 1"
,
payloads
:
[]
map
[
string
][]
byte
{
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar"
),
"bar//zab.txt"
:
[]
byte
(
"bar/zab.txt"
),
"foo/blaz/bar.txt"
:
[]
byte
(
"foo/blaz/bar"
),
"zip/zap/zup/fop.txt"
:
[]
byte
(
"zip/zap/zup/fop.txt"
),
},
{
"foo/bar.txt"
:
[]
byte
(
"foo/bar2"
),
"bar/zab.txt"
:
[]
byte
(
"bar/zab.txt2"
),
},
{
"foo/bar.txt"
:
[]
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
}
var
oldTs
*
time
.
Time
=
nil
writer
:=
&
AtomicWriter
{
targetDir
:
targetDir
,
logContext
:
"-test-"
}
for
ii
,
payload
:=
range
tc
.
payloads
{
writer
.
Write
(
payload
)
checkVolumeContents
(
targetDir
,
tc
.
name
,
payload
,
t
)
ts
:=
checkSentinelFile
(
targetDir
,
t
)
if
oldTs
!=
nil
&&
!
ts
.
After
(
*
oldTs
)
{
t
.
Errorf
(
"%v[%v] unexpected timestamp on sentinel file; expected %v to be after %v"
,
tc
.
name
,
ii
,
ts
,
oldTs
)
}
oldTs
=
&
ts
if
tc
.
clearSentinel
{
clearSentinelFile
(
targetDir
,
t
)
}
}
}
}
func
TestSentinelFileModTimeIncreasing
(
t
*
testing
.
T
)
{
cases
:=
[]
struct
{
name
string
iterations
int
clearSentinelFile
bool
}{
{
name
:
"5 iters"
,
iterations
:
5
,
},
{
name
:
"50 iters"
,
iterations
:
50
,
},
{
name
:
"1000 iters"
,
iterations
:
1000
,
},
{
name
:
"1000 clear sentinel"
,
iterations
:
1000
,
clearSentinelFile
:
true
,
},
{
name
:
"10000 clear sentinel"
,
iterations
:
10000
,
clearSentinelFile
:
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
}
var
oldTs
*
time
.
Time
=
nil
writer
:=
&
AtomicWriter
{
targetDir
:
targetDir
,
logContext
:
"-test-"
}
for
i
:=
0
;
i
<
tc
.
iterations
;
i
++
{
err
=
writer
.
touchSentinelFile
()
if
err
!=
nil
{
t
.
Errorf
(
"%v: unexpected error touching sentinel file: %v"
,
tc
.
name
,
err
)
continue
}
ts
:=
checkSentinelFile
(
targetDir
,
t
)
if
oldTs
!=
nil
&&
!
ts
.
After
(
*
oldTs
)
{
t
.
Errorf
(
"%v: unexpected timestamp on sentinel file; expected %v to be after %v"
,
tc
.
name
,
ts
,
oldTs
)
continue
}
oldTs
=
&
ts
if
tc
.
clearSentinelFile
{
clearSentinelFile
(
targetDir
,
t
)
}
}
}
}
func
checkVolumeContents
(
targetDir
,
tcName
string
,
payload
map
[
string
][]
byte
,
t
*
testing
.
T
)
{
// use filepath.Walk to reconstruct the payload, then deep equal
observedPayload
:=
map
[
string
][]
byte
{}
visitor
:=
func
(
path
string
,
info
os
.
FileInfo
,
err
error
)
error
{
if
info
.
Mode
()
.
IsRegular
()
||
info
.
IsDir
()
{
return
nil
}
relativePath
:=
strings
.
TrimPrefix
(
path
,
targetDir
)
relativePath
=
strings
.
TrimPrefix
(
relativePath
,
"/"
)
if
strings
.
HasPrefix
(
relativePath
,
".."
)
{
return
nil
}
content
,
err
:=
ioutil
.
ReadFile
(
path
)
if
err
!=
nil
{
return
err
}
observedPayload
[
relativePath
]
=
content
return
nil
}
err
:=
filepath
.
Walk
(
targetDir
,
visitor
)
if
err
!=
nil
{
t
.
Errorf
(
"%v: unexpected error walking directory: %v"
,
tcName
,
err
)
}
cleanPathPayload
:=
make
(
map
[
string
][]
byte
,
len
(
payload
))
for
k
,
v
:=
range
payload
{
cleanPathPayload
[
path
.
Clean
(
k
)]
=
v
}
if
!
reflect
.
DeepEqual
(
cleanPathPayload
,
observedPayload
)
{
t
.
Errorf
(
"%v: payload and observed payload do not match."
,
tcName
)
}
}
func
checkSentinelFile
(
targetDir
string
,
t
*
testing
.
T
)
time
.
Time
{
sentinelFilePath
:=
filepath
.
Join
(
targetDir
,
sentinelFileName
)
info
,
err
:=
os
.
Stat
(
sentinelFilePath
)
if
err
!=
nil
{
t
.
Errorf
(
"Couldn't stat sentinel file for dir %v: %v"
,
targetDir
,
err
)
return
time
.
Now
()
}
return
info
.
ModTime
()
}
func
clearSentinelFile
(
targetDir
string
,
t
*
testing
.
T
)
{
sentinelFilePath
:=
filepath
.
Join
(
targetDir
,
sentinelFileName
)
_
,
err
:=
os
.
Stat
(
sentinelFilePath
)
if
err
!=
nil
{
t
.
Errorf
(
"Couldn't stat sentinel file for dir %v: %v"
,
targetDir
,
err
)
}
err
=
os
.
Remove
(
sentinelFilePath
)
if
err
!=
nil
{
t
.
Errorf
(
"Error removing sentinel file: %v"
,
err
)
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment