Skip to content

Commit

Permalink
Add create layer cmd to create filesystem changeset
Browse files Browse the repository at this point in the history
Signed-off-by: Lei Jitang <[email protected]>
  • Loading branch information
coolljt0725 committed Sep 30, 2016
1 parent ffcdca5 commit a8bafa4
Show file tree
Hide file tree
Showing 10 changed files with 1,187 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/oci-create-runtime-bundle
/oci-unpack
/oci-image-validate
/oci-create-layer
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tools:
go build ./cmd/oci-create-runtime-bundle
go build ./cmd/oci-unpack
go build ./cmd/oci-image-validate
go build ./cmd/oci-create-layer

lint:
@echo "checking lint"
Expand Down
80 changes: 80 additions & 0 deletions cmd/oci-create-layer/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2016 The Linux Foundation
//
// 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 main

import (
"log"
"os"

"github.com/opencontainers/image-tools/image"
"github.com/spf13/cobra"
)

type layerCmd struct {
stdout *log.Logger
stderr *log.Logger
dest string
}

func main() {
stdout := log.New(os.Stdout, "", 0)
stderr := log.New(os.Stderr, "", 0)

cmd := newLayerCmd(stdout, stderr)
if err := cmd.Execute(); err != nil {
stderr.Println(err)
os.Exit(1)
}
}

func newLayerCmd(stdout, stderr *log.Logger) *cobra.Command {
v := &layerCmd{
stdout: stdout,
stderr: stderr,
}

cmd := &cobra.Command{
Use: "oci-create-layer [child] [parent]",
Short: "Create an OCI layer",
Long: `Create an OCI layer based on the changeset between filesystems.`,
Run: v.Run,
}
cmd.Flags().StringVar(
&v.dest, "dest", "",
`The dest specify a particular filename where the layer write to`,
)
return cmd
}

func (v *layerCmd) Run(cmd *cobra.Command, args []string) {
if len(args) != 1 && len(args) != 2 {
v.stderr.Print("One or two filesystems are required")
if err := cmd.Usage(); err != nil {
v.stderr.Println(err)
}
os.Exit(1)
}
var err error
if len(args) == 1 {
err = image.CreateLayer(args[0], "", v.dest)
} else {
err = image.CreateLayer(args[0], args[1], v.dest)
}
if err != nil {
v.stderr.Printf("create layer failed: %v", err)
os.Exit(1)
}
os.Exit(0)
}
283 changes: 283 additions & 0 deletions image/change.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
// Copyright 2016 The Linux Foundation
//
// 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 image

import (
"archive/tar"
"bytes"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"

"github.com/Sirupsen/logrus"
"github.com/opencontainers/image-tools/utils"
)

// ChangeType represents the change type.
type ChangeType int

const (
// ChangeModify represents the modify operation.
ChangeModify = iota
// ChangeAdd represents the add operation.
ChangeAdd
// ChangeDelete represents the delete operation.
ChangeDelete
)

// Change represents a change, it wraps the change type and path.
// It describes changes of the files in the path respect to the
// parent layers. The change could be modify, add, delete.
// This is used for layer diff.
type Change struct {
Path string
Kind ChangeType
}

// for sort.Sort
type changesByPath []Change

func (c changesByPath) Less(i, j int) bool { return c[i].Path < c[j].Path }
func (c changesByPath) Len() int { return len(c) }
func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] }

// Gnu tar and the go tar writer don't have sub-second mtime
// precision, which is problematic when we apply changes via tar
// files, we handle this by comparing for exact times, *or* same
// second count and either a or b having exactly 0 nanoseconds
func sameFsTime(a, b time.Time) bool {
return a == b ||
(a.Unix() == b.Unix() &&
(a.Nanosecond() == 0 || b.Nanosecond() == 0))
}

func sameFsTimeSpec(a, b syscall.Timespec) bool {
return a.Sec == b.Sec &&
(a.Nsec == b.Nsec || a.Nsec == 0 || b.Nsec == 0)
}

// FileInfo describes the information of a file.
type FileInfo struct {
parent *FileInfo
name string
stat *utils.StatT
children map[string]*FileInfo
capability []byte
added bool
}

func newRootFileInfo() *FileInfo {
// As this runs on the daemon side, file paths are OS specific.
root := &FileInfo{
name: string(os.PathSeparator),
children: make(map[string]*FileInfo),
}
return root
}

// LookUp looks up the file information of a file.
func (info *FileInfo) LookUp(path string) *FileInfo {
// As this runs on the daemon side, file paths are OS specific.
parent := info
if path == string(os.PathSeparator) {
return info
}

pathElements := strings.Split(path, string(os.PathSeparator))
for _, elem := range pathElements {
if elem != "" {
child := parent.children[elem]
if child == nil {
return nil
}
parent = child
}
}
return parent
}

func (info *FileInfo) path() string {
if info.parent == nil {
// As this runs on the daemon side, file paths are OS specific.
return string(os.PathSeparator)
}
return filepath.Join(info.parent.path(), info.name)
}

func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {

sizeAtEntry := len(*changes)

if oldInfo == nil {
// add
change := Change{
Path: info.path(),
Kind: ChangeAdd,
}
*changes = append(*changes, change)
info.added = true
}

// We make a copy so we can modify it to detect additions
// also, we only recurse on the old dir if the new info is a directory
// otherwise any previous delete/change is considered recursive
oldChildren := make(map[string]*FileInfo)
if oldInfo != nil && info.isDir() {
for k, v := range oldInfo.children {
oldChildren[k] = v
}
}

for name, newChild := range info.children {
oldChild, _ := oldChildren[name]
if oldChild != nil {
// change?
oldStat := oldChild.stat
newStat := newChild.stat
// Note: We can't compare inode or ctime or blocksize here, because these change
// when copying a file into a container. However, that is not generally a problem
// because any content change will change mtime, and any status change should
// be visible when actually comparing the stat fields. The only time this
// breaks down is if some code intentionally hides a change by setting
// back mtime
if statDifferent(oldStat, newStat) ||
bytes.Compare(oldChild.capability, newChild.capability) != 0 {
change := Change{
Path: newChild.path(),
Kind: ChangeModify,
}
*changes = append(*changes, change)
newChild.added = true
}

// Remove from copy so we can detect deletions
delete(oldChildren, name)
}

newChild.addChanges(oldChild, changes)
}
for _, oldChild := range oldChildren {
// delete
change := Change{
Path: oldChild.path(),
Kind: ChangeDelete,
}
*changes = append(*changes, change)
}

// If there were changes inside this directory, we need to add it, even if the directory
// itself wasn't changed. This is needed to properly save and restore filesystem permissions.
// As this runs on the daemon side, file paths are OS specific.
if len(*changes) > sizeAtEntry && info.isDir() && !info.added && info.path() != string(os.PathSeparator) {
change := Change{
Path: info.path(),
Kind: ChangeModify,
}
// Let's insert the directory entry before the recently added entries located inside this dir
*changes = append(*changes, change) // just to resize the slice, will be overwritten
copy((*changes)[sizeAtEntry+1:], (*changes)[sizeAtEntry:])
(*changes)[sizeAtEntry] = change
}

}

// Changes add changes to file information.
func (info *FileInfo) Changes(oldInfo *FileInfo) []Change {
var changes []Change

info.addChanges(oldInfo, &changes)

return changes
}

// ChangesDirs compares two directories and generates an array of Change objects describing the changes.
// If oldDir is "", then all files in newDir will be Add-Changes.
func ChangesDirs(newDir, oldDir string) ([]Change, error) {
var (
oldRoot, newRoot *FileInfo
)
if oldDir == "" {
emptyDir, err := ioutil.TempDir("", "empty")
if err != nil {
return nil, err
}
defer os.Remove(emptyDir)
oldDir = emptyDir
}
oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir)
if err != nil {
return nil, err
}

return newRoot.Changes(oldRoot), nil
}

// ExportChanges produces an Archive from the provided changes, relative to dir.
func exportChanges(dir string, changes []Change) (io.ReadCloser, error) {
reader, writer := io.Pipe()
go func() {
ta := &utils.TarAppender{
TarWriter: tar.NewWriter(writer),
Buffer: utils.BufioWriter32KPool.Get(nil),
SeenFiles: make(map[uint64]string),
}
// this buffer is needed for the duration of this piped stream
defer utils.BufioWriter32KPool.Put(ta.Buffer)

sort.Sort(changesByPath(changes))

// In general we log errors here but ignore them because
// during e.g. a diff operation the container can continue
// mutating the filesystem and we can see transient errors
// from this
for _, change := range changes {
if change.Kind == ChangeDelete {
whiteOutDir := filepath.Dir(change.Path)
whiteOutBase := filepath.Base(change.Path)
whiteOut := filepath.Join(whiteOutDir, ".wh."+whiteOutBase)
timestamp := time.Now()
hdr := &tar.Header{
Name: whiteOut[1:],
Size: 0,
ModTime: timestamp,
AccessTime: timestamp,
ChangeTime: timestamp,
}
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
logrus.Debugf("Can't write whiteout header: %s", err)
}
} else {
path := filepath.Join(dir, change.Path)
if err := ta.AddTarFile(path, change.Path[1:]); err != nil {
logrus.Debugf("Can't add file %s to tar: %s", path, err)
}
}
}

// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil {
logrus.Debugf("Can't close layer: %s", err)
}
if err := writer.Close(); err != nil {
logrus.Debugf("failed close Changes writer: %s", err)
}
}()
return reader, nil
}
Loading

0 comments on commit a8bafa4

Please sign in to comment.