Added snapshot package and tests
This commit is contained in:
		
							parent
							
								
									72a5f1c916
								
							
						
					
					
						commit
						43bad54292
					
				|  | @ -19,6 +19,7 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/constants" | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/constants" | ||||||
| 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/dockerfile" | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/dockerfile" | ||||||
|  | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/snapshot" | ||||||
| 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"github.com/spf13/cobra" | 	"github.com/spf13/cobra" | ||||||
|  | @ -68,5 +69,13 @@ func execute() error { | ||||||
| 
 | 
 | ||||||
| 	// Unpack file system to root
 | 	// Unpack file system to root
 | ||||||
| 	logrus.Infof("Unpacking filesystem of %s...", baseImage) | 	logrus.Infof("Unpacking filesystem of %s...", baseImage) | ||||||
| 	return util.ExtractFileSystemFromImage(baseImage) | 	if err := util.ExtractFileSystemFromImage(baseImage); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	l := snapshot.NewLayeredMap(util.Hasher()) | ||||||
|  | 	snapshotter := snapshot.NewSnapshotter(l, constants.RootDir) | ||||||
|  | 
 | ||||||
|  | 	// Take initial snapshot
 | ||||||
|  | 	return snapshotter.Init() | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -23,5 +23,8 @@ const ( | ||||||
| 	// RootDir is the path to the root directory
 | 	// RootDir is the path to the root directory
 | ||||||
| 	RootDir = "/" | 	RootDir = "/" | ||||||
| 
 | 
 | ||||||
|  | 	// WorkspaceDir is the path to the workspace directory
 | ||||||
|  | 	WorkspaceDir = "/workspace" | ||||||
|  | 
 | ||||||
| 	WhitelistPath = "/proc/self/mountinfo" | 	WhitelistPath = "/proc/self/mountinfo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,53 @@ | ||||||
|  | /* | ||||||
|  | Copyright 2018 Google LLC | ||||||
|  | 
 | ||||||
|  | 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 snapshot | ||||||
|  | 
 | ||||||
|  | type LayeredMap struct { | ||||||
|  | 	layers []map[string]string | ||||||
|  | 	hasher func(string) string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func NewLayeredMap(h func(string) string) *LayeredMap { | ||||||
|  | 	l := LayeredMap{ | ||||||
|  | 		hasher: h, | ||||||
|  | 	} | ||||||
|  | 	l.layers = []map[string]string{} | ||||||
|  | 	return &l | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (l *LayeredMap) Snapshot() { | ||||||
|  | 	l.layers = append(l.layers, map[string]string{}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (l *LayeredMap) Get(s string) (string, bool) { | ||||||
|  | 	for i := len(l.layers) - 1; i >= 0; i-- { | ||||||
|  | 		if v, ok := l.layers[i][s]; ok { | ||||||
|  | 			return v, ok | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return "", false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (l *LayeredMap) MaybeAdd(s string) bool { | ||||||
|  | 	oldV, ok := l.Get(s) | ||||||
|  | 	newV := l.hasher(s) | ||||||
|  | 	if ok && newV == oldV { | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	l.layers[len(l.layers)-1][s] = newV | ||||||
|  | 	return true | ||||||
|  | } | ||||||
|  | @ -0,0 +1,98 @@ | ||||||
|  | /* | ||||||
|  | Copyright 2018 Google LLC | ||||||
|  | 
 | ||||||
|  | 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 snapshot | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"archive/tar" | ||||||
|  | 	"bytes" | ||||||
|  | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" | ||||||
|  | 	"github.com/sirupsen/logrus" | ||||||
|  | 
 | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Snapshotter holds the root directory from which to take snapshots, and a list of snapshots taken
 | ||||||
|  | type Snapshotter struct { | ||||||
|  | 	l         *LayeredMap | ||||||
|  | 	directory string | ||||||
|  | 	snapshots []string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // NewSnapshotter creates a new snapshotter rooted at d
 | ||||||
|  | func NewSnapshotter(l *LayeredMap, d string) *Snapshotter { | ||||||
|  | 	return &Snapshotter{l: l, directory: d, snapshots: []string{}} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Init initializes a new snapshotter
 | ||||||
|  | func (s *Snapshotter) Init() error { | ||||||
|  | 	if _, err := s.snapShotFS(ioutil.Discard); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TakeSnapshot takes a snapshot of the filesystem, avoiding directories in the whitelist
 | ||||||
|  | // It stores changed files in a tar, and returns the contents of this tar at the end
 | ||||||
|  | func (s *Snapshotter) TakeSnapshot() ([]byte, error) { | ||||||
|  | 
 | ||||||
|  | 	buf := bytes.NewBuffer([]byte{}) | ||||||
|  | 	added, err := s.snapShotFS(buf) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	if !added { | ||||||
|  | 		logrus.Infof("No files were changed in this command, this layer will not be appended.") | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// Add buffer contents until buffer is empty
 | ||||||
|  | 	var contents []byte | ||||||
|  | 	for { | ||||||
|  | 		next := buf.Next(buf.Len()) | ||||||
|  | 		if len(next) == 0 { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		contents = append(contents, next...) | ||||||
|  | 	} | ||||||
|  | 	return contents, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (s *Snapshotter) snapShotFS(f io.Writer) (bool, error) { | ||||||
|  | 	s.l.Snapshot() | ||||||
|  | 	added := false | ||||||
|  | 	w := tar.NewWriter(f) | ||||||
|  | 	defer w.Close() | ||||||
|  | 
 | ||||||
|  | 	err := filepath.Walk(s.directory, func(path string, info os.FileInfo, err error) error { | ||||||
|  | 		if util.PathInWhitelist(path, s.directory) { | ||||||
|  | 			return nil | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Only add to the tar if we add it to the layeredmap.
 | ||||||
|  | 		if s.l.MaybeAdd(path) { | ||||||
|  | 			added = true | ||||||
|  | 			return util.AddToTar(path, info, w) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  | 	return added, err | ||||||
|  | } | ||||||
|  | @ -0,0 +1,149 @@ | ||||||
|  | /* | ||||||
|  | Copyright 2018 Google LLC | ||||||
|  | 
 | ||||||
|  | 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 snapshot | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"archive/tar" | ||||||
|  | 	"bytes" | ||||||
|  | 	"github.com/GoogleCloudPlatform/k8s-container-builder/pkg/util" | ||||||
|  | 	"github.com/GoogleCloudPlatform/k8s-container-builder/testutil" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestSnapshotFileChange(t *testing.T) { | ||||||
|  | 
 | ||||||
|  | 	testDir, snapshotter, err := setUpTestDir() | ||||||
|  | 	defer os.RemoveAll(testDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	// Make some changes to the filesystem
 | ||||||
|  | 	newFiles := map[string]string{ | ||||||
|  | 		"foo":           "newbaz1", | ||||||
|  | 		"workspace/bat": "bat", | ||||||
|  | 	} | ||||||
|  | 	if err := testutil.SetupFiles(testDir, newFiles); err != nil { | ||||||
|  | 		t.Fatalf("Error setting up fs: %s", err) | ||||||
|  | 	} | ||||||
|  | 	// Take another snapshot
 | ||||||
|  | 	contents, err := snapshotter.TakeSnapshot() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error taking snapshot of fs: %s", err) | ||||||
|  | 	} | ||||||
|  | 	// Check contents of the snapshot, make sure contents is equivalent to snapshotFiles
 | ||||||
|  | 	reader := bytes.NewReader(contents) | ||||||
|  | 	tr := tar.NewReader(reader) | ||||||
|  | 	fooPath := filepath.Join(testDir, "foo") | ||||||
|  | 	snapshotFiles := map[string]string{ | ||||||
|  | 		fooPath: "newbaz1", | ||||||
|  | 	} | ||||||
|  | 	for { | ||||||
|  | 		hdr, err := tr.Next() | ||||||
|  | 		if err == io.EOF { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		if _, isFile := snapshotFiles[hdr.Name]; !isFile { | ||||||
|  | 			t.Fatalf("File %s unexpectedly in tar", hdr.Name) | ||||||
|  | 		} | ||||||
|  | 		contents, _ := ioutil.ReadAll(tr) | ||||||
|  | 		if string(contents) != snapshotFiles[hdr.Name] { | ||||||
|  | 			t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSnapshotChangePermissions(t *testing.T) { | ||||||
|  | 	testDir, snapshotter, err := setUpTestDir() | ||||||
|  | 	defer os.RemoveAll(testDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	// Change permissions on a file
 | ||||||
|  | 	batPath := filepath.Join(testDir, "bar/bat") | ||||||
|  | 	if err := os.Chmod(batPath, 0600); err != nil { | ||||||
|  | 		t.Fatalf("Error changing permissions on %s: %v", batPath, err) | ||||||
|  | 	} | ||||||
|  | 	// Take another snapshot
 | ||||||
|  | 	contents, err := snapshotter.TakeSnapshot() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error taking snapshot of fs: %s", err) | ||||||
|  | 	} | ||||||
|  | 	// Check contents of the snapshot, make sure contents is equivalent to snapshotFiles
 | ||||||
|  | 	reader := bytes.NewReader(contents) | ||||||
|  | 	tr := tar.NewReader(reader) | ||||||
|  | 	snapshotFiles := map[string]string{ | ||||||
|  | 		batPath: "baz2", | ||||||
|  | 	} | ||||||
|  | 	for { | ||||||
|  | 		hdr, err := tr.Next() | ||||||
|  | 		if err == io.EOF { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		if _, isFile := snapshotFiles[hdr.Name]; !isFile { | ||||||
|  | 			t.Fatalf("File %s unexpectedly in tar", hdr.Name) | ||||||
|  | 		} | ||||||
|  | 		contents, _ := ioutil.ReadAll(tr) | ||||||
|  | 		if string(contents) != snapshotFiles[hdr.Name] { | ||||||
|  | 			t.Fatalf("Contents of %s incorrect, expected: %s, actual: %s", hdr.Name, snapshotFiles[hdr.Name], string(contents)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestEmptySnapshot(t *testing.T) { | ||||||
|  | 	testDir, snapshotter, err := setUpTestDir() | ||||||
|  | 	defer os.RemoveAll(testDir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal(err) | ||||||
|  | 	} | ||||||
|  | 	// Take snapshot with no changes
 | ||||||
|  | 	contents, err := snapshotter.TakeSnapshot() | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error taking snapshot of fs: %s", err) | ||||||
|  | 	} | ||||||
|  | 	// Since we took a snapshot with no changes, contents should be nil
 | ||||||
|  | 	if contents != nil { | ||||||
|  | 		t.Fatal("Contents should be nil, since no changes to the filesystem were made.") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setUpTestDir() (string, *Snapshotter, error) { | ||||||
|  | 	testDir, err := ioutil.TempDir("", "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		return testDir, nil, errors.Wrap(err, "setting up temp dir") | ||||||
|  | 	} | ||||||
|  | 	files := map[string]string{ | ||||||
|  | 		"foo":            "baz1", | ||||||
|  | 		"bar/bat":        "baz2", | ||||||
|  | 		"workspace/file": "file", | ||||||
|  | 	} | ||||||
|  | 	// Set up initial files
 | ||||||
|  | 	if err := testutil.SetupFiles(testDir, files); err != nil { | ||||||
|  | 		return testDir, nil, errors.Wrap(err, "setting up file system") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Take the initial snapshot
 | ||||||
|  | 	l := NewLayeredMap(util.Hasher()) | ||||||
|  | 	snapshotter := NewSnapshotter(l, testDir) | ||||||
|  | 	if err := snapshotter.Init(); err != nil { | ||||||
|  | 		return testDir, nil, errors.Wrap(err, "initializing snapshotter") | ||||||
|  | 	} | ||||||
|  | 	return testDir, snapshotter, nil | ||||||
|  | } | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
| 	"io" | 	"io" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -47,6 +48,17 @@ func ExtractFileSystemFromImage(img string) error { | ||||||
| 	return pkgutil.GetFileSystemFromReference(ref, imgSrc, constants.RootDir, whitelist) | 	return pkgutil.GetFileSystemFromReference(ref, imgSrc, constants.RootDir, whitelist) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // PathInWhitelist returns true if the path is whitelisted
 | ||||||
|  | func PathInWhitelist(path, directory string) bool { | ||||||
|  | 	for _, d := range whitelist { | ||||||
|  | 		dirPath := filepath.Join(directory, d) | ||||||
|  | 		if pkgutil.HasFilepathPrefix(path, dirPath) { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Get whitelist from roots of mounted files
 | // Get whitelist from roots of mounted files
 | ||||||
| // Each line of /proc/self/mountinfo is in the form:
 | // Each line of /proc/self/mountinfo is in the form:
 | ||||||
| // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 | // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,52 @@ | ||||||
|  | /* | ||||||
|  | Copyright 2018 Google LLC | ||||||
|  | 
 | ||||||
|  | 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 ( | ||||||
|  | 	"archive/tar" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // AddToTar adds the file i to tar w at path p
 | ||||||
|  | func AddToTar(p string, i os.FileInfo, w *tar.Writer) error { | ||||||
|  | 	linkDst := "" | ||||||
|  | 	if i.Mode()&os.ModeSymlink != 0 { | ||||||
|  | 		var err error | ||||||
|  | 		linkDst, err = os.Readlink(p) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	hdr, err := tar.FileInfoHeader(i, linkDst) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	hdr.Name = p | ||||||
|  | 	w.WriteHeader(hdr) | ||||||
|  | 	if !i.Mode().IsRegular() { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	r, err := os.Open(p) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := io.Copy(w, r); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -17,8 +17,12 @@ limitations under the License. | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/md5" | ||||||
|  | 	"encoding/hex" | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	"github.com/sirupsen/logrus" | 	"github.com/sirupsen/logrus" | ||||||
|  | 	"io" | ||||||
|  | 	"os" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // SetLogLevel sets the logrus logging level
 | // SetLogLevel sets the logrus logging level
 | ||||||
|  | @ -30,3 +34,30 @@ func SetLogLevel(logLevel string) error { | ||||||
| 	logrus.SetLevel(lvl) | 	logrus.SetLevel(lvl) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // Hasher returns a hash function, used in snapshotting to determine if a file has changed
 | ||||||
|  | func Hasher() func(string) string { | ||||||
|  | 	hasher := func(p string) string { | ||||||
|  | 		h := md5.New() | ||||||
|  | 		fi, err := os.Lstat(p) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 		h.Write([]byte(fi.Mode().String())) | ||||||
|  | 		h.Write([]byte(fi.ModTime().String())) | ||||||
|  | 
 | ||||||
|  | 		if fi.Mode().IsRegular() { | ||||||
|  | 			f, err := os.Open(p) | ||||||
|  | 			if err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
|  | 			defer f.Close() | ||||||
|  | 			if _, err := io.Copy(h, f); err != nil { | ||||||
|  | 				panic(err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return hex.EncodeToString(h.Sum(nil)) | ||||||
|  | 	} | ||||||
|  | 	return hasher | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -18,10 +18,27 @@ package testutil | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // SetupFiles creates files at path
 | ||||||
|  | func SetupFiles(path string, files map[string]string) error { | ||||||
|  | 	for p, c := range files { | ||||||
|  | 		path := filepath.Join(path, p) | ||||||
|  | 		if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		if err := ioutil.WriteFile(path, []byte(c), 0644); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func CheckErrorAndDeepEqual(t *testing.T, shouldErr bool, err error, expected, actual interface{}) { | func CheckErrorAndDeepEqual(t *testing.T, shouldErr bool, err error, expected, actual interface{}) { | ||||||
| 	if err := checkErr(shouldErr, err); err != nil { | 	if err := checkErr(shouldErr, err); err != nil { | ||||||
| 		t.Error(err) | 		t.Error(err) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue