k3s/pkg/volume/util/atomic_writer_test.go

855 lines
20 KiB
Go
Raw Normal View History

2016-02-12 03:10:51 +00:00
// +build linux
/*
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:\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][]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)
}
}