-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add create layer cmd to create filesystem changeset
Signed-off-by: Lei Jitang <[email protected]>
- Loading branch information
1 parent
ffcdca5
commit a8bafa4
Showing
10 changed files
with
1,187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.