1// +build !windows
2
3/*
4   Copyright The containerd Authors.
5
6   Licensed under the Apache License, Version 2.0 (the "License");
7   you may not use this file except in compliance with the License.
8   You may obtain a copy of the License at
9
10       http://www.apache.org/licenses/LICENSE-2.0
11
12   Unless required by applicable law or agreed to in writing, software
13   distributed under the License is distributed on an "AS IS" BASIS,
14   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   See the License for the specific language governing permissions and
16   limitations under the License.
17*/
18
19package continuity
20
21import (
22	"bytes"
23	_ "crypto/sha256"
24	"errors"
25	"fmt"
26	"io/ioutil"
27	"math/rand"
28	"os"
29	"path/filepath"
30	"sort"
31	"syscall"
32	"testing"
33
34	"github.com/containerd/continuity/devices"
35	"github.com/opencontainers/go-digest"
36)
37
38// Hard things:
39//  1. Groups/gid - no standard library support.
40//  2. xattrs - must choose package to provide this.
41//  3. ADS - no clue where to start.
42
43func TestWalkFS(t *testing.T) {
44	rand.Seed(1)
45
46	// Testing:
47	// 1. Setup different files:
48	//		- links
49	//			- sibling directory - relative
50	//			- sibling directory - absolute
51	//			- parent directory - absolute
52	//			- parent directory - relative
53	//		- illegal links
54	//			- parent directory - relative, out of root
55	//			- parent directory - absolute, out of root
56	//		- regular files
57	//		- character devices
58	//		- what about sticky bits?
59	// 2. Build the manifest.
60	// 3. Verify expected result.
61	testResources := []dresource{
62		{
63			path: "a",
64			mode: 0644,
65		},
66		{
67			kind:   rhardlink,
68			path:   "a-hardlink",
69			target: "a",
70		},
71		{
72			kind: rdirectory,
73			path: "b",
74			mode: 0755,
75		},
76		{
77			kind:   rhardlink,
78			path:   "b/a-hardlink",
79			target: "a",
80		},
81		{
82			path: "b/a",
83			mode: 0600 | os.ModeSticky,
84		},
85		{
86			kind: rdirectory,
87			path: "c",
88			mode: 0755,
89		},
90		{
91			path: "c/a",
92			mode: 0644,
93		},
94		{
95			kind:   rrelsymlink,
96			path:   "c/ca-relsymlink",
97			mode:   0600,
98			target: "a",
99		},
100		{
101			kind:   rrelsymlink,
102			path:   "c/a-relsymlink",
103			mode:   0600,
104			target: "../a",
105		},
106		{
107			kind:   rabssymlink,
108			path:   "c/a-abssymlink",
109			mode:   0600,
110			target: "a",
111		},
112		// TODO(stevvooe): Make sure we can test this case and get proper
113		// errors when it is encountered.
114		// {
115		// 	// create a bad symlink and make sure we don't include it.
116		// 	kind:   relsymlink,
117		// 	path:   "c/a-badsymlink",
118		// 	mode:   0600,
119		// 	target: "../../..",
120		// },
121
122		// TODO(stevvooe): Must add tests for xattrs, with symlinks,
123		// directories and regular files.
124
125		{
126			kind: rnamedpipe,
127			path: "fifo",
128			mode: 0666 | os.ModeNamedPipe,
129		},
130
131		{
132			kind: rdirectory,
133			path: "/dev",
134			mode: 0755,
135		},
136
137		// NOTE(stevvooe): Below here, we add a few simple character devices.
138		// Block devices are untested but should be nearly the same as
139		// character devices.
140		// devNullResource,
141		// devZeroResource,
142	}
143
144	root, err := ioutil.TempDir("", "continuity-test-")
145	if err != nil {
146		t.Fatalf("error creating temporary directory: %v", err)
147	}
148
149	defer os.RemoveAll(root)
150
151	generateTestFiles(t, root, testResources)
152
153	ctx, err := NewContext(root)
154	if err != nil {
155		t.Fatalf("error getting context: %v", err)
156	}
157
158	m, err := BuildManifest(ctx)
159	if err != nil {
160		t.Fatalf("error building manifest: %v", err)
161	}
162
163	var b bytes.Buffer
164	MarshalText(&b, m)
165	t.Log(b.String())
166
167	// TODO(dmcgowan): always verify, currently hard links not supported
168	//if err := VerifyManifest(ctx, m); err != nil {
169	//	t.Fatalf("error verifying manifest: %v")
170	//}
171
172	expectedResources, err := expectedResourceList(root, testResources)
173	if err != nil {
174		// TODO(dmcgowan): update function to panic, this would mean test setup error
175		t.Fatalf("error creating resource list: %v", err)
176	}
177
178	// Diff resources
179	diff := diffResourceList(expectedResources, m.Resources)
180	if diff.HasDiff() {
181		t.Log("Resource list difference")
182		for _, a := range diff.Additions {
183			t.Logf("Unexpected resource: %#v", a)
184		}
185		for _, d := range diff.Deletions {
186			t.Logf("Missing resource: %#v", d)
187		}
188		for _, u := range diff.Updates {
189			t.Logf("Changed resource:\n\tExpected: %#v\n\tActual:   %#v", u.Original, u.Updated)
190		}
191
192		t.FailNow()
193	}
194}
195
196// TODO(stevvooe): At this time, we have a nice testing framework to define
197// and build resources. This will likely be a pre-cursor to the packages
198// public interface.
199type kind int
200
201func (k kind) String() string {
202	switch k {
203	case rfile:
204		return "file"
205	case rdirectory:
206		return "directory"
207	case rhardlink:
208		return "hardlink"
209	case rchardev:
210		return "chardev"
211	case rnamedpipe:
212		return "namedpipe"
213	}
214
215	panic(fmt.Sprintf("unknown kind: %v", int(k)))
216}
217
218const (
219	rfile kind = iota
220	rdirectory
221	rhardlink
222	rrelsymlink
223	rabssymlink
224	rchardev
225	rnamedpipe
226)
227
228type dresource struct {
229	kind         kind
230	path         string
231	mode         os.FileMode
232	target       string // hard/soft link target
233	digest       digest.Digest
234	size         int
235	uid          int64
236	gid          int64
237	major, minor int
238}
239
240func generateTestFiles(t *testing.T, root string, resources []dresource) {
241	for i, resource := range resources {
242		p := filepath.Join(root, resource.path)
243		switch resource.kind {
244		case rfile:
245			size := rand.Intn(4 << 20)
246			d := make([]byte, size)
247			randomBytes(d)
248			dgst := digest.FromBytes(d)
249			resources[i].digest = dgst
250			resources[i].size = size
251
252			// this relies on the proper directory parent being defined.
253			if err := ioutil.WriteFile(p, d, resource.mode); err != nil {
254				t.Fatalf("error writing %q: %v", p, err)
255			}
256		case rdirectory:
257			if err := os.Mkdir(p, resource.mode); err != nil {
258				t.Fatalf("error creating directory %q: %v", p, err)
259			}
260		case rhardlink:
261			target := filepath.Join(root, resource.target)
262			if err := os.Link(target, p); err != nil {
263				t.Fatalf("error creating hardlink: %v", err)
264			}
265		case rrelsymlink:
266			if err := os.Symlink(resource.target, p); err != nil {
267				t.Fatalf("error creating symlink: %v", err)
268			}
269		case rabssymlink:
270			// for absolute links, we join with root.
271			target := filepath.Join(root, resource.target)
272
273			if err := os.Symlink(target, p); err != nil {
274				t.Fatalf("error creating symlink: %v", err)
275			}
276		case rchardev, rnamedpipe:
277			if err := devices.Mknod(p, resource.mode, resource.major, resource.minor); err != nil {
278				t.Fatalf("error creating device %q: %v", p, err)
279			}
280		default:
281			t.Fatalf("unknown resource type: %v", resource.kind)
282		}
283
284		st, err := os.Lstat(p)
285		if err != nil {
286			t.Fatalf("error statting after creation: %v", err)
287		}
288		resources[i].uid = int64(st.Sys().(*syscall.Stat_t).Uid)
289		resources[i].gid = int64(st.Sys().(*syscall.Stat_t).Gid)
290		resources[i].mode = st.Mode()
291
292		// TODO: Readback and join xattr
293	}
294
295	// log the test root for future debugging
296	if err := filepath.Walk(root, func(p string, fi os.FileInfo, err error) error {
297		if fi.Mode()&os.ModeSymlink != 0 {
298			target, err := os.Readlink(p)
299			if err != nil {
300				return err
301			}
302			t.Log(fi.Mode(), p, "->", target)
303		} else {
304			t.Log(fi.Mode(), p)
305		}
306
307		return nil
308	}); err != nil {
309		t.Fatalf("error walking created root: %v", err)
310	}
311
312	var b bytes.Buffer
313	if err := tree(&b, root); err != nil {
314		t.Fatalf("error running tree: %v", err)
315	}
316	t.Logf("\n%s", b.String())
317}
318
319func randomBytes(p []byte) {
320	for i := range p {
321		p[i] = byte(rand.Intn(1<<8 - 1))
322	}
323}
324
325// expectedResourceList sorts the set of resources into the order
326// expected in the manifest and collapses hardlinks
327func expectedResourceList(root string, resources []dresource) ([]Resource, error) {
328	resourceMap := map[string]Resource{}
329	paths := []string{}
330	for _, r := range resources {
331		absPath := r.path
332		if !filepath.IsAbs(absPath) {
333			absPath = "/" + absPath
334		}
335		switch r.kind {
336		case rfile:
337			f := &regularFile{
338				resource: resource{
339					paths: []string{absPath},
340					mode:  r.mode,
341					uid:   r.uid,
342					gid:   r.gid,
343				},
344				size:    int64(r.size),
345				digests: []digest.Digest{r.digest},
346			}
347			resourceMap[absPath] = f
348			paths = append(paths, absPath)
349		case rdirectory:
350			d := &directory{
351				resource: resource{
352					paths: []string{absPath},
353					mode:  r.mode,
354					uid:   r.uid,
355					gid:   r.gid,
356				},
357			}
358			resourceMap[absPath] = d
359			paths = append(paths, absPath)
360		case rhardlink:
361			targetPath := r.target
362			if !filepath.IsAbs(targetPath) {
363				targetPath = "/" + targetPath
364			}
365			target, ok := resourceMap[targetPath]
366			if !ok {
367				return nil, errors.New("must specify target before hardlink for test resources")
368			}
369			rf, ok := target.(*regularFile)
370			if !ok {
371				return nil, errors.New("hardlink target must be regular file")
372			}
373			// TODO(dmcgowan): full merge
374			rf.paths = append(rf.paths, absPath)
375			// TODO(dmcgowan): check if first path is now different, changes source order and should update
376			// resource map key, to avoid canonically ordered first should be regular file
377			sort.Stable(sort.StringSlice(rf.paths))
378		case rrelsymlink, rabssymlink:
379			targetPath := r.target
380			if r.kind == rabssymlink && !filepath.IsAbs(r.target) {
381				// for absolute links, we join with root.
382				targetPath = filepath.Join(root, targetPath)
383			}
384			s := &symLink{
385				resource: resource{
386					paths: []string{absPath},
387					mode:  r.mode,
388					uid:   r.uid,
389					gid:   r.gid,
390				},
391				target: targetPath,
392			}
393			resourceMap[absPath] = s
394			paths = append(paths, absPath)
395		case rchardev:
396			d := &device{
397				resource: resource{
398					paths: []string{absPath},
399					mode:  r.mode,
400					uid:   r.uid,
401					gid:   r.gid,
402				},
403				major: uint64(r.major),
404				minor: uint64(r.minor),
405			}
406			resourceMap[absPath] = d
407			paths = append(paths, absPath)
408		case rnamedpipe:
409			p := &namedPipe{
410				resource: resource{
411					paths: []string{absPath},
412					mode:  r.mode,
413					uid:   r.uid,
414					gid:   r.gid,
415				},
416			}
417			resourceMap[absPath] = p
418			paths = append(paths, absPath)
419		default:
420			return nil, fmt.Errorf("unknown resource type: %v", r.kind)
421		}
422	}
423
424	if len(resourceMap) < len(paths) {
425		return nil, errors.New("resource list has duplicated paths")
426	}
427
428	sort.Strings(paths)
429
430	manifestResources := make([]Resource, len(paths))
431	for i, p := range paths {
432		manifestResources[i] = resourceMap[p]
433	}
434
435	return manifestResources, nil
436}
437