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 := ®ularFile{ 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