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