Skip to content

Commit a8bafa4

Browse files
committed
Add create layer cmd to create filesystem changeset
Signed-off-by: Lei Jitang <[email protected]>
1 parent ffcdca5 commit a8bafa4

File tree

10 files changed

+1187
-0
lines changed

10 files changed

+1187
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/oci-create-runtime-bundle
22
/oci-unpack
33
/oci-image-validate
4+
/oci-create-layer

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ tools:
2222
go build ./cmd/oci-create-runtime-bundle
2323
go build ./cmd/oci-unpack
2424
go build ./cmd/oci-image-validate
25+
go build ./cmd/oci-create-layer
2526

2627
lint:
2728
@echo "checking lint"

cmd/oci-create-layer/main.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"log"
19+
"os"
20+
21+
"github.com/opencontainers/image-tools/image"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
type layerCmd struct {
26+
stdout *log.Logger
27+
stderr *log.Logger
28+
dest string
29+
}
30+
31+
func main() {
32+
stdout := log.New(os.Stdout, "", 0)
33+
stderr := log.New(os.Stderr, "", 0)
34+
35+
cmd := newLayerCmd(stdout, stderr)
36+
if err := cmd.Execute(); err != nil {
37+
stderr.Println(err)
38+
os.Exit(1)
39+
}
40+
}
41+
42+
func newLayerCmd(stdout, stderr *log.Logger) *cobra.Command {
43+
v := &layerCmd{
44+
stdout: stdout,
45+
stderr: stderr,
46+
}
47+
48+
cmd := &cobra.Command{
49+
Use: "oci-create-layer [child] [parent]",
50+
Short: "Create an OCI layer",
51+
Long: `Create an OCI layer based on the changeset between filesystems.`,
52+
Run: v.Run,
53+
}
54+
cmd.Flags().StringVar(
55+
&v.dest, "dest", "",
56+
`The dest specify a particular filename where the layer write to`,
57+
)
58+
return cmd
59+
}
60+
61+
func (v *layerCmd) Run(cmd *cobra.Command, args []string) {
62+
if len(args) != 1 && len(args) != 2 {
63+
v.stderr.Print("One or two filesystems are required")
64+
if err := cmd.Usage(); err != nil {
65+
v.stderr.Println(err)
66+
}
67+
os.Exit(1)
68+
}
69+
var err error
70+
if len(args) == 1 {
71+
err = image.CreateLayer(args[0], "", v.dest)
72+
} else {
73+
err = image.CreateLayer(args[0], args[1], v.dest)
74+
}
75+
if err != nil {
76+
v.stderr.Printf("create layer failed: %v", err)
77+
os.Exit(1)
78+
}
79+
os.Exit(0)
80+
}

image/change.go

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// Copyright 2016 The Linux Foundation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package image
16+
17+
import (
18+
"archive/tar"
19+
"bytes"
20+
"io"
21+
"io/ioutil"
22+
"os"
23+
"path/filepath"
24+
"sort"
25+
"strings"
26+
"syscall"
27+
"time"
28+
29+
"github.com/Sirupsen/logrus"
30+
"github.com/opencontainers/image-tools/utils"
31+
)
32+
33+
// ChangeType represents the change type.
34+
type ChangeType int
35+
36+
const (
37+
// ChangeModify represents the modify operation.
38+
ChangeModify = iota
39+
// ChangeAdd represents the add operation.
40+
ChangeAdd
41+
// ChangeDelete represents the delete operation.
42+
ChangeDelete
43+
)
44+
45+
// Change represents a change, it wraps the change type and path.
46+
// It describes changes of the files in the path respect to the
47+
// parent layers. The change could be modify, add, delete.
48+
// This is used for layer diff.
49+
type Change struct {
50+
Path string
51+
Kind ChangeType
52+
}
53+
54+
// for sort.Sort
55+
type changesByPath []Change
56+
57+
func (c changesByPath) Less(i, j int) bool { return c[i].Path < c[j].Path }
58+
func (c changesByPath) Len() int { return len(c) }
59+
func (c changesByPath) Swap(i, j int) { c[j], c[i] = c[i], c[j] }
60+
61+
// Gnu tar and the go tar writer don't have sub-second mtime
62+
// precision, which is problematic when we apply changes via tar
63+
// files, we handle this by comparing for exact times, *or* same
64+
// second count and either a or b having exactly 0 nanoseconds
65+
func sameFsTime(a, b time.Time) bool {
66+
return a == b ||
67+
(a.Unix() == b.Unix() &&
68+
(a.Nanosecond() == 0 || b.Nanosecond() == 0))
69+
}
70+
71+
func sameFsTimeSpec(a, b syscall.Timespec) bool {
72+
return a.Sec == b.Sec &&
73+
(a.Nsec == b.Nsec || a.Nsec == 0 || b.Nsec == 0)
74+
}
75+
76+
// FileInfo describes the information of a file.
77+
type FileInfo struct {
78+
parent *FileInfo
79+
name string
80+
stat *utils.StatT
81+
children map[string]*FileInfo
82+
capability []byte
83+
added bool
84+
}
85+
86+
func newRootFileInfo() *FileInfo {
87+
// As this runs on the daemon side, file paths are OS specific.
88+
root := &FileInfo{
89+
name: string(os.PathSeparator),
90+
children: make(map[string]*FileInfo),
91+
}
92+
return root
93+
}
94+
95+
// LookUp looks up the file information of a file.
96+
func (info *FileInfo) LookUp(path string) *FileInfo {
97+
// As this runs on the daemon side, file paths are OS specific.
98+
parent := info
99+
if path == string(os.PathSeparator) {
100+
return info
101+
}
102+
103+
pathElements := strings.Split(path, string(os.PathSeparator))
104+
for _, elem := range pathElements {
105+
if elem != "" {
106+
child := parent.children[elem]
107+
if child == nil {
108+
return nil
109+
}
110+
parent = child
111+
}
112+
}
113+
return parent
114+
}
115+
116+
func (info *FileInfo) path() string {
117+
if info.parent == nil {
118+
// As this runs on the daemon side, file paths are OS specific.
119+
return string(os.PathSeparator)
120+
}
121+
return filepath.Join(info.parent.path(), info.name)
122+
}
123+
124+
func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
125+
126+
sizeAtEntry := len(*changes)
127+
128+
if oldInfo == nil {
129+
// add
130+
change := Change{
131+
Path: info.path(),
132+
Kind: ChangeAdd,
133+
}
134+
*changes = append(*changes, change)
135+
info.added = true
136+
}
137+
138+
// We make a copy so we can modify it to detect additions
139+
// also, we only recurse on the old dir if the new info is a directory
140+
// otherwise any previous delete/change is considered recursive
141+
oldChildren := make(map[string]*FileInfo)
142+
if oldInfo != nil && info.isDir() {
143+
for k, v := range oldInfo.children {
144+
oldChildren[k] = v
145+
}
146+
}
147+
148+
for name, newChild := range info.children {
149+
oldChild, _ := oldChildren[name]
150+
if oldChild != nil {
151+
// change?
152+
oldStat := oldChild.stat
153+
newStat := newChild.stat
154+
// Note: We can't compare inode or ctime or blocksize here, because these change
155+
// when copying a file into a container. However, that is not generally a problem
156+
// because any content change will change mtime, and any status change should
157+
// be visible when actually comparing the stat fields. The only time this
158+
// breaks down is if some code intentionally hides a change by setting
159+
// back mtime
160+
if statDifferent(oldStat, newStat) ||
161+
bytes.Compare(oldChild.capability, newChild.capability) != 0 {
162+
change := Change{
163+
Path: newChild.path(),
164+
Kind: ChangeModify,
165+
}
166+
*changes = append(*changes, change)
167+
newChild.added = true
168+
}
169+
170+
// Remove from copy so we can detect deletions
171+
delete(oldChildren, name)
172+
}
173+
174+
newChild.addChanges(oldChild, changes)
175+
}
176+
for _, oldChild := range oldChildren {
177+
// delete
178+
change := Change{
179+
Path: oldChild.path(),
180+
Kind: ChangeDelete,
181+
}
182+
*changes = append(*changes, change)
183+
}
184+
185+
// If there were changes inside this directory, we need to add it, even if the directory
186+
// itself wasn't changed. This is needed to properly save and restore filesystem permissions.
187+
// As this runs on the daemon side, file paths are OS specific.
188+
if len(*changes) > sizeAtEntry && info.isDir() && !info.added && info.path() != string(os.PathSeparator) {
189+
change := Change{
190+
Path: info.path(),
191+
Kind: ChangeModify,
192+
}
193+
// Let's insert the directory entry before the recently added entries located inside this dir
194+
*changes = append(*changes, change) // just to resize the slice, will be overwritten
195+
copy((*changes)[sizeAtEntry+1:], (*changes)[sizeAtEntry:])
196+
(*changes)[sizeAtEntry] = change
197+
}
198+
199+
}
200+
201+
// Changes add changes to file information.
202+
func (info *FileInfo) Changes(oldInfo *FileInfo) []Change {
203+
var changes []Change
204+
205+
info.addChanges(oldInfo, &changes)
206+
207+
return changes
208+
}
209+
210+
// ChangesDirs compares two directories and generates an array of Change objects describing the changes.
211+
// If oldDir is "", then all files in newDir will be Add-Changes.
212+
func ChangesDirs(newDir, oldDir string) ([]Change, error) {
213+
var (
214+
oldRoot, newRoot *FileInfo
215+
)
216+
if oldDir == "" {
217+
emptyDir, err := ioutil.TempDir("", "empty")
218+
if err != nil {
219+
return nil, err
220+
}
221+
defer os.Remove(emptyDir)
222+
oldDir = emptyDir
223+
}
224+
oldRoot, newRoot, err := collectFileInfoForChanges(oldDir, newDir)
225+
if err != nil {
226+
return nil, err
227+
}
228+
229+
return newRoot.Changes(oldRoot), nil
230+
}
231+
232+
// ExportChanges produces an Archive from the provided changes, relative to dir.
233+
func exportChanges(dir string, changes []Change) (io.ReadCloser, error) {
234+
reader, writer := io.Pipe()
235+
go func() {
236+
ta := &utils.TarAppender{
237+
TarWriter: tar.NewWriter(writer),
238+
Buffer: utils.BufioWriter32KPool.Get(nil),
239+
SeenFiles: make(map[uint64]string),
240+
}
241+
// this buffer is needed for the duration of this piped stream
242+
defer utils.BufioWriter32KPool.Put(ta.Buffer)
243+
244+
sort.Sort(changesByPath(changes))
245+
246+
// In general we log errors here but ignore them because
247+
// during e.g. a diff operation the container can continue
248+
// mutating the filesystem and we can see transient errors
249+
// from this
250+
for _, change := range changes {
251+
if change.Kind == ChangeDelete {
252+
whiteOutDir := filepath.Dir(change.Path)
253+
whiteOutBase := filepath.Base(change.Path)
254+
whiteOut := filepath.Join(whiteOutDir, ".wh."+whiteOutBase)
255+
timestamp := time.Now()
256+
hdr := &tar.Header{
257+
Name: whiteOut[1:],
258+
Size: 0,
259+
ModTime: timestamp,
260+
AccessTime: timestamp,
261+
ChangeTime: timestamp,
262+
}
263+
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
264+
logrus.Debugf("Can't write whiteout header: %s", err)
265+
}
266+
} else {
267+
path := filepath.Join(dir, change.Path)
268+
if err := ta.AddTarFile(path, change.Path[1:]); err != nil {
269+
logrus.Debugf("Can't add file %s to tar: %s", path, err)
270+
}
271+
}
272+
}
273+
274+
// Make sure to check the error on Close.
275+
if err := ta.TarWriter.Close(); err != nil {
276+
logrus.Debugf("Can't close layer: %s", err)
277+
}
278+
if err := writer.Close(); err != nil {
279+
logrus.Debugf("failed close Changes writer: %s", err)
280+
}
281+
}()
282+
return reader, nil
283+
}

0 commit comments

Comments
 (0)