github.com/lalkh/containerd@v1.4.3/oci/spec_opts_test.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package oci 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "io/ioutil" 26 "log" 27 "os" 28 "reflect" 29 "runtime" 30 "strings" 31 "testing" 32 33 "github.com/containerd/containerd/content" 34 "github.com/opencontainers/go-digest" 35 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 36 37 "github.com/containerd/containerd/containers" 38 "github.com/containerd/containerd/namespaces" 39 "github.com/opencontainers/runtime-spec/specs-go" 40 ) 41 42 type blob []byte 43 44 func (b blob) ReadAt(p []byte, off int64) (int, error) { 45 if off >= int64(len(b)) { 46 return 0, io.EOF 47 } 48 return copy(p, b[off:]), nil 49 } 50 51 func (b blob) Close() error { 52 return nil 53 } 54 55 func (b blob) Size() int64 { 56 return int64(len(b)) 57 } 58 59 type fakeImage struct { 60 config ocispec.Descriptor 61 blobs map[string]blob 62 } 63 64 func newFakeImage(config ocispec.Image) (Image, error) { 65 configBlob, err := json.Marshal(config) 66 if err != nil { 67 return nil, err 68 } 69 configDescriptor := ocispec.Descriptor{ 70 MediaType: ocispec.MediaTypeImageConfig, 71 Digest: digest.NewDigestFromBytes(digest.SHA256, configBlob), 72 } 73 74 return fakeImage{ 75 config: configDescriptor, 76 blobs: map[string]blob{ 77 configDescriptor.Digest.String(): configBlob, 78 }, 79 }, nil 80 } 81 82 func (i fakeImage) Config(ctx context.Context) (ocispec.Descriptor, error) { 83 return i.config, nil 84 } 85 86 func (i fakeImage) ContentStore() content.Store { 87 return i 88 } 89 90 func (i fakeImage) ReaderAt(ctx context.Context, dec ocispec.Descriptor) (content.ReaderAt, error) { 91 blob, found := i.blobs[dec.Digest.String()] 92 if !found { 93 return nil, errors.New("not found") 94 } 95 return blob, nil 96 } 97 98 func (i fakeImage) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) { 99 return content.Info{}, errors.New("not implemented") 100 } 101 102 func (i fakeImage) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) { 103 return content.Info{}, errors.New("not implemented") 104 } 105 106 func (i fakeImage) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error { 107 return errors.New("not implemented") 108 } 109 110 func (i fakeImage) Delete(ctx context.Context, dgst digest.Digest) error { 111 return errors.New("not implemented") 112 } 113 114 func (i fakeImage) Status(ctx context.Context, ref string) (content.Status, error) { 115 return content.Status{}, errors.New("not implemented") 116 } 117 118 func (i fakeImage) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) { 119 return nil, errors.New("not implemented") 120 } 121 122 func (i fakeImage) Abort(ctx context.Context, ref string) error { 123 return errors.New("not implemented") 124 } 125 126 func (i fakeImage) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) { 127 return nil, errors.New("not implemented") 128 } 129 130 func TestReplaceOrAppendEnvValues(t *testing.T) { 131 t.Parallel() 132 133 defaults := []string{ 134 "o=ups", "p=$e", "x=foo", "y=boo", "z", "t=", 135 } 136 overrides := []string{ 137 "x=bar", "y", "a=42", "o=", "e", "s=", 138 } 139 expected := []string{ 140 "o=", "p=$e", "x=bar", "z", "t=", "a=42", "s=", 141 } 142 143 defaultsOrig := make([]string, len(defaults)) 144 copy(defaultsOrig, defaults) 145 overridesOrig := make([]string, len(overrides)) 146 copy(overridesOrig, overrides) 147 148 results := replaceOrAppendEnvValues(defaults, overrides) 149 150 if err := assertEqualsStringArrays(defaults, defaultsOrig); err != nil { 151 t.Fatal(err) 152 } 153 if err := assertEqualsStringArrays(overrides, overridesOrig); err != nil { 154 t.Fatal(err) 155 } 156 157 if err := assertEqualsStringArrays(results, expected); err != nil { 158 t.Fatal(err) 159 } 160 } 161 162 func TestWithDefaultSpecForPlatform(t *testing.T) { 163 t.Parallel() 164 var ( 165 s Spec 166 c = containers.Container{ID: "TestWithDefaultSpecForPlatform"} 167 ctx = namespaces.WithNamespace(context.Background(), "test") 168 ) 169 170 platforms := []string{"linux/amd64", "windows/amd64"} 171 for _, p := range platforms { 172 if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpecForPlatform(p)); err != nil { 173 t.Fatal(err) 174 } 175 } 176 177 } 178 179 func Contains(a []string, x string) bool { 180 for _, n := range a { 181 if x == n { 182 return true 183 } 184 } 185 return false 186 } 187 188 func TestWithDefaultPathEnv(t *testing.T) { 189 t.Parallel() 190 s := Spec{} 191 s.Process = &specs.Process{ 192 Env: []string{}, 193 } 194 var ( 195 defaultUnixEnv = "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 196 ctx = namespaces.WithNamespace(context.Background(), "test") 197 ) 198 WithDefaultPathEnv(ctx, nil, nil, &s) 199 if !Contains(s.Process.Env, defaultUnixEnv) { 200 t.Fatal("default Unix Env not found") 201 } 202 } 203 204 func TestWithProcessCwd(t *testing.T) { 205 t.Parallel() 206 s := Spec{} 207 opts := []SpecOpts{ 208 WithProcessCwd("testCwd"), 209 } 210 var expectedCwd = "testCwd" 211 212 for _, opt := range opts { 213 if err := opt(nil, nil, nil, &s); err != nil { 214 t.Fatal(err) 215 } 216 } 217 if s.Process.Cwd != expectedCwd { 218 t.Fatal("Process has a wrong current working directory") 219 } 220 221 } 222 223 func TestWithEnv(t *testing.T) { 224 t.Parallel() 225 226 s := Spec{} 227 s.Process = &specs.Process{ 228 Env: []string{"DEFAULT=test"}, 229 } 230 231 WithEnv([]string{"env=1"})(context.Background(), nil, nil, &s) 232 233 if len(s.Process.Env) != 2 { 234 t.Fatal("didn't append") 235 } 236 237 WithEnv([]string{"env2=1"})(context.Background(), nil, nil, &s) 238 239 if len(s.Process.Env) != 3 { 240 t.Fatal("didn't append") 241 } 242 243 WithEnv([]string{"env2=2"})(context.Background(), nil, nil, &s) 244 245 if s.Process.Env[2] != "env2=2" { 246 t.Fatal("couldn't update") 247 } 248 249 WithEnv([]string{"env2"})(context.Background(), nil, nil, &s) 250 251 if len(s.Process.Env) != 2 { 252 t.Fatal("couldn't unset") 253 } 254 } 255 256 func TestWithMounts(t *testing.T) { 257 258 t.Parallel() 259 260 s := Spec{ 261 Mounts: []specs.Mount{ 262 { 263 Source: "default-source", 264 Destination: "default-dest", 265 }, 266 }, 267 } 268 269 WithMounts([]specs.Mount{ 270 { 271 Source: "new-source", 272 Destination: "new-dest", 273 }, 274 })(nil, nil, nil, &s) 275 276 if len(s.Mounts) != 2 { 277 t.Fatal("didn't append") 278 } 279 280 if s.Mounts[1].Source != "new-source" { 281 t.Fatal("invalid mount") 282 } 283 284 if s.Mounts[1].Destination != "new-dest" { 285 t.Fatal("invalid mount") 286 } 287 } 288 289 func TestWithDefaultSpec(t *testing.T) { 290 t.Parallel() 291 var ( 292 s Spec 293 c = containers.Container{ID: "TestWithDefaultSpec"} 294 ctx = namespaces.WithNamespace(context.Background(), "test") 295 ) 296 297 if err := ApplyOpts(ctx, nil, &c, &s, WithDefaultSpec()); err != nil { 298 t.Fatal(err) 299 } 300 301 var expected Spec 302 var err error 303 if runtime.GOOS == "windows" { 304 err = populateDefaultWindowsSpec(ctx, &expected, c.ID) 305 } else { 306 err = populateDefaultUnixSpec(ctx, &expected, c.ID) 307 } 308 if err != nil { 309 t.Fatal(err) 310 } 311 312 if reflect.DeepEqual(s, Spec{}) { 313 t.Fatalf("spec should not be empty") 314 } 315 316 if !reflect.DeepEqual(&s, &expected) { 317 t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected) 318 } 319 } 320 321 func TestWithSpecFromFile(t *testing.T) { 322 t.Parallel() 323 var ( 324 s Spec 325 c = containers.Container{ID: "TestWithDefaultSpec"} 326 ctx = namespaces.WithNamespace(context.Background(), "test") 327 ) 328 329 fp, err := ioutil.TempFile("", "testwithdefaultspec.json") 330 if err != nil { 331 t.Fatal(err) 332 } 333 defer func() { 334 if err := os.Remove(fp.Name()); err != nil { 335 log.Printf("failed to remove tempfile %v: %v", fp.Name(), err) 336 } 337 }() 338 defer fp.Close() 339 340 expected, err := GenerateSpec(ctx, nil, &c) 341 if err != nil { 342 t.Fatal(err) 343 } 344 345 p, err := json.Marshal(expected) 346 if err != nil { 347 t.Fatal(err) 348 } 349 350 if _, err := fp.Write(p); err != nil { 351 t.Fatal(err) 352 } 353 354 if err := ApplyOpts(ctx, nil, &c, &s, WithSpecFromFile(fp.Name())); err != nil { 355 t.Fatal(err) 356 } 357 358 if reflect.DeepEqual(s, Spec{}) { 359 t.Fatalf("spec should not be empty") 360 } 361 362 if !reflect.DeepEqual(&s, expected) { 363 t.Fatalf("spec from option differs from default: \n%#v != \n%#v", &s, expected) 364 } 365 } 366 367 func TestWithMemoryLimit(t *testing.T) { 368 var ( 369 ctx = namespaces.WithNamespace(context.Background(), "testing") 370 c = containers.Container{ID: t.Name()} 371 m = uint64(768 * 1024 * 1024) 372 o = WithMemoryLimit(m) 373 ) 374 // Test with all three supported scenarios 375 platforms := []string{"", "linux/amd64", "windows/amd64"} 376 for _, p := range platforms { 377 var spec *Spec 378 var err error 379 if p == "" { 380 t.Log("Testing GenerateSpec default platform") 381 spec, err = GenerateSpec(ctx, nil, &c, o) 382 383 // Convert the platform to the default based on GOOS like 384 // GenerateSpec does. 385 switch runtime.GOOS { 386 case "linux": 387 p = "linux/amd64" 388 case "windows": 389 p = "windows/amd64" 390 } 391 } else { 392 t.Logf("Testing GenerateSpecWithPlatform with platform: '%s'", p) 393 spec, err = GenerateSpecWithPlatform(ctx, nil, p, &c, o) 394 } 395 if err != nil { 396 t.Fatalf("failed to generate spec with: %v", err) 397 } 398 switch p { 399 case "linux/amd64": 400 if *spec.Linux.Resources.Memory.Limit != int64(m) { 401 t.Fatalf("spec.Linux.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Linux.Resources.Memory.Limit) 402 } 403 // If we are linux/amd64 on Windows GOOS it is LCOW 404 if runtime.GOOS == "windows" { 405 // Verify that we also set the Windows section. 406 if *spec.Windows.Resources.Memory.Limit != m { 407 t.Fatalf("for LCOW spec.Windows.Resources.Memory.Limit is also expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit) 408 } 409 } else { 410 if spec.Windows != nil { 411 t.Fatalf("spec.Windows section should not be set for linux/amd64 spec on non-windows platform") 412 } 413 } 414 case "windows/amd64": 415 if *spec.Windows.Resources.Memory.Limit != m { 416 t.Fatalf("spec.Windows.Resources.Memory.Limit expected: %v, got: %v", m, *spec.Windows.Resources.Memory.Limit) 417 } 418 if spec.Linux != nil { 419 t.Fatalf("spec.Linux section should not be set for windows/amd64 spec ever") 420 } 421 } 422 } 423 } 424 425 func isEqualStringArrays(values, expected []string) bool { 426 if len(values) != len(expected) { 427 return false 428 } 429 430 for i, x := range expected { 431 if values[i] != x { 432 return false 433 } 434 } 435 return true 436 } 437 438 func assertEqualsStringArrays(values, expected []string) error { 439 if !isEqualStringArrays(values, expected) { 440 return fmt.Errorf("expected %s, but found %s", expected, values) 441 } 442 return nil 443 } 444 445 func TestWithTTYSize(t *testing.T) { 446 t.Parallel() 447 s := Spec{} 448 opts := []SpecOpts{ 449 WithTTYSize(10, 20), 450 } 451 var ( 452 expectedWidth = uint(10) 453 expectedHeight = uint(20) 454 ) 455 456 for _, opt := range opts { 457 if err := opt(nil, nil, nil, &s); err != nil { 458 t.Fatal(err) 459 } 460 } 461 if s.Process.ConsoleSize.Height != expectedWidth && s.Process.ConsoleSize.Height != expectedHeight { 462 t.Fatal("Process Console has invalid size") 463 } 464 465 } 466 467 func TestWithUserNamespace(t *testing.T) { 468 t.Parallel() 469 s := Spec{} 470 471 opts := []SpecOpts{ 472 WithUserNamespace([]specs.LinuxIDMapping{ 473 { 474 ContainerID: 1, 475 HostID: 2, 476 Size: 10000, 477 }, 478 }, []specs.LinuxIDMapping{ 479 { 480 ContainerID: 2, 481 HostID: 3, 482 Size: 20000, 483 }, 484 }), 485 } 486 487 for _, opt := range opts { 488 if err := opt(nil, nil, nil, &s); err != nil { 489 t.Fatal(err) 490 } 491 } 492 493 expectedUIDMapping := specs.LinuxIDMapping{ 494 ContainerID: 1, 495 HostID: 2, 496 Size: 10000, 497 } 498 expectedGIDMapping := specs.LinuxIDMapping{ 499 ContainerID: 2, 500 HostID: 3, 501 Size: 20000, 502 } 503 504 if !(len(s.Linux.UIDMappings) == 1 && s.Linux.UIDMappings[0] == expectedUIDMapping) || !(len(s.Linux.GIDMappings) == 1 && s.Linux.GIDMappings[0] == expectedGIDMapping) { 505 t.Fatal("WithUserNamespace Cannot set the uid/gid mappings for the task") 506 } 507 508 } 509 func TestWithImageConfigArgs(t *testing.T) { 510 t.Parallel() 511 512 img, err := newFakeImage(ocispec.Image{ 513 Config: ocispec.ImageConfig{ 514 Env: []string{"z=bar", "y=baz"}, 515 Entrypoint: []string{"create", "--namespace=test"}, 516 Cmd: []string{"", "--debug"}, 517 }, 518 }) 519 if err != nil { 520 t.Fatal(err) 521 } 522 523 s := Spec{ 524 Version: specs.Version, 525 Root: &specs.Root{}, 526 Windows: &specs.Windows{}, 527 } 528 529 opts := []SpecOpts{ 530 WithEnv([]string{"x=foo", "y=boo"}), 531 WithProcessArgs("run", "--foo", "xyz", "--bar"), 532 WithImageConfigArgs(img, []string{"--boo", "bar"}), 533 } 534 535 expectedEnv := []string{"z=bar", "y=boo", "x=foo"} 536 expectedArgs := []string{"create", "--namespace=test", "--boo", "bar"} 537 538 for _, opt := range opts { 539 if err := opt(nil, nil, nil, &s); err != nil { 540 t.Fatal(err) 541 } 542 } 543 544 if err := assertEqualsStringArrays(s.Process.Env, expectedEnv); err != nil { 545 t.Fatal(err) 546 } 547 if err := assertEqualsStringArrays(s.Process.Args, expectedArgs); err != nil { 548 t.Fatal(err) 549 } 550 } 551 552 func TestAddCaps(t *testing.T) { 553 t.Parallel() 554 555 var s specs.Spec 556 557 if err := WithAddedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { 558 t.Fatal(err) 559 } 560 for i, cl := range [][]string{ 561 s.Process.Capabilities.Bounding, 562 s.Process.Capabilities.Effective, 563 s.Process.Capabilities.Permitted, 564 s.Process.Capabilities.Inheritable, 565 } { 566 if !capsContain(cl, "CAP_CHOWN") { 567 t.Errorf("cap list %d does not contain added cap", i) 568 } 569 } 570 } 571 572 func TestDropCaps(t *testing.T) { 573 t.Parallel() 574 575 var s specs.Spec 576 577 if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil { 578 t.Fatal(err) 579 } 580 if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { 581 t.Fatal(err) 582 } 583 584 for i, cl := range [][]string{ 585 s.Process.Capabilities.Bounding, 586 s.Process.Capabilities.Effective, 587 s.Process.Capabilities.Permitted, 588 s.Process.Capabilities.Inheritable, 589 } { 590 if capsContain(cl, "CAP_CHOWN") { 591 t.Errorf("cap list %d contains dropped cap", i) 592 } 593 } 594 595 // Add all capabilities back and drop a different cap. 596 if err := WithAllCapabilities(context.Background(), nil, nil, &s); err != nil { 597 t.Fatal(err) 598 } 599 if err := WithDroppedCapabilities([]string{"CAP_FOWNER"})(context.Background(), nil, nil, &s); err != nil { 600 t.Fatal(err) 601 } 602 603 for i, cl := range [][]string{ 604 s.Process.Capabilities.Bounding, 605 s.Process.Capabilities.Effective, 606 s.Process.Capabilities.Permitted, 607 s.Process.Capabilities.Inheritable, 608 } { 609 if capsContain(cl, "CAP_FOWNER") { 610 t.Errorf("cap list %d contains dropped cap", i) 611 } 612 if !capsContain(cl, "CAP_CHOWN") { 613 t.Errorf("cap list %d doesn't contain non-dropped cap", i) 614 } 615 } 616 617 // Drop all duplicated caps. 618 if err := WithCapabilities([]string{"CAP_CHOWN", "CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { 619 t.Fatal(err) 620 } 621 if err := WithDroppedCapabilities([]string{"CAP_CHOWN"})(context.Background(), nil, nil, &s); err != nil { 622 t.Fatal(err) 623 } 624 for i, cl := range [][]string{ 625 s.Process.Capabilities.Bounding, 626 s.Process.Capabilities.Effective, 627 s.Process.Capabilities.Permitted, 628 s.Process.Capabilities.Inheritable, 629 } { 630 if len(cl) != 0 { 631 t.Errorf("cap list %d is not empty", i) 632 } 633 } 634 } 635 636 func TestDevShmSize(t *testing.T) { 637 t.Parallel() 638 var ( 639 s Spec 640 c = containers.Container{ID: t.Name()} 641 ctx = namespaces.WithNamespace(context.Background(), "test") 642 ) 643 644 err := populateDefaultUnixSpec(ctx, &s, c.ID) 645 if err != nil { 646 t.Fatal(err) 647 } 648 649 expected := "1024k" 650 if err := WithDevShmSize(1024)(nil, nil, nil, &s); err != nil { 651 t.Fatal(err) 652 } 653 m := getShmMount(&s) 654 if m == nil { 655 t.Fatal("no shm mount found") 656 } 657 o := getShmSize(m.Options) 658 if o == "" { 659 t.Fatal("shm size not specified") 660 } 661 parts := strings.Split(o, "=") 662 if len(parts) != 2 { 663 t.Fatal("invalid size format") 664 } 665 size := parts[1] 666 if size != expected { 667 t.Fatalf("size %s not equal %s", size, expected) 668 } 669 } 670 671 func getShmMount(s *Spec) *specs.Mount { 672 for _, m := range s.Mounts { 673 if m.Source == "shm" && m.Type == "tmpfs" { 674 return &m 675 } 676 } 677 return nil 678 } 679 680 func getShmSize(opts []string) string { 681 for _, o := range opts { 682 if strings.HasPrefix(o, "size=") { 683 return o 684 } 685 } 686 return "" 687 }