| package runconfig |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "runtime" |
| "strings" |
| "testing" |
| |
| "os" |
| |
| "github.com/docker/docker/api/types/container" |
| networktypes "github.com/docker/docker/api/types/network" |
| "github.com/docker/docker/api/types/strslice" |
| "github.com/gotestyourself/gotestyourself/skip" |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| ) |
| |
| type f struct { |
| file string |
| entrypoint strslice.StrSlice |
| } |
| |
| func TestDecodeContainerConfig(t *testing.T) { |
| |
| var ( |
| fixtures []f |
| image string |
| ) |
| |
| //TODO: Should run for Solaris |
| if runtime.GOOS == "solaris" { |
| t.Skip() |
| } |
| |
| if runtime.GOOS != "windows" { |
| image = "ubuntu" |
| fixtures = []f{ |
| {"fixtures/unix/container_config_1_14.json", strslice.StrSlice{}}, |
| {"fixtures/unix/container_config_1_17.json", strslice.StrSlice{"bash"}}, |
| {"fixtures/unix/container_config_1_19.json", strslice.StrSlice{"bash"}}, |
| } |
| } else { |
| image = "windows" |
| fixtures = []f{ |
| {"fixtures/windows/container_config_1_19.json", strslice.StrSlice{"cmd"}}, |
| } |
| } |
| |
| for _, f := range fixtures { |
| b, err := ioutil.ReadFile(f.file) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| c, h, _, err := decodeContainerConfig(bytes.NewReader(b)) |
| if err != nil { |
| t.Fatal(fmt.Errorf("Error parsing %s: %v", f, err)) |
| } |
| |
| if c.Image != image { |
| t.Fatalf("Expected %s image, found %s\n", image, c.Image) |
| } |
| |
| if len(c.Entrypoint) != len(f.entrypoint) { |
| t.Fatalf("Expected %v, found %v\n", f.entrypoint, c.Entrypoint) |
| } |
| |
| if h != nil && h.Memory != 1000 { |
| t.Fatalf("Expected memory to be 1000, found %d\n", h.Memory) |
| } |
| } |
| } |
| |
| // TestDecodeContainerConfigIsolation validates isolation passed |
| // to the daemon in the hostConfig structure. Note this is platform specific |
| // as to what level of container isolation is supported. |
| func TestDecodeContainerConfigIsolation(t *testing.T) { |
| |
| // An Invalid isolation level |
| if _, _, _, err := callDecodeContainerConfigIsolation("invalid"); err != nil { |
| if !strings.Contains(err.Error(), `Invalid isolation: "invalid"`) { |
| t.Fatal(err) |
| } |
| } |
| |
| // Blank isolation (== default) |
| if _, _, _, err := callDecodeContainerConfigIsolation(""); err != nil { |
| t.Fatal("Blank isolation should have succeeded") |
| } |
| |
| // Default isolation |
| if _, _, _, err := callDecodeContainerConfigIsolation("default"); err != nil { |
| t.Fatal("default isolation should have succeeded") |
| } |
| |
| // Process isolation (Valid on Windows only) |
| if runtime.GOOS == "windows" { |
| if _, _, _, err := callDecodeContainerConfigIsolation("process"); err != nil { |
| t.Fatal("process isolation should have succeeded") |
| } |
| } else { |
| if _, _, _, err := callDecodeContainerConfigIsolation("process"); err != nil { |
| if !strings.Contains(err.Error(), `Invalid isolation: "process"`) { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| // Hyper-V Containers isolation (Valid on Windows only) |
| if runtime.GOOS == "windows" { |
| if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { |
| t.Fatal("hyperv isolation should have succeeded") |
| } |
| } else { |
| if _, _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { |
| if !strings.Contains(err.Error(), `Invalid isolation: "hyperv"`) { |
| t.Fatal(err) |
| } |
| } |
| } |
| } |
| |
| // callDecodeContainerConfigIsolation is a utility function to call |
| // DecodeContainerConfig for validating isolation |
| func callDecodeContainerConfigIsolation(isolation string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { |
| var ( |
| b []byte |
| err error |
| ) |
| w := ContainerConfigWrapper{ |
| Config: &container.Config{}, |
| HostConfig: &container.HostConfig{ |
| NetworkMode: "none", |
| Isolation: container.Isolation(isolation)}, |
| } |
| if b, err = json.Marshal(w); err != nil { |
| return nil, nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) |
| } |
| return decodeContainerConfig(bytes.NewReader(b)) |
| } |
| |
| func TestDecodeContainerConfigWithVolumes(t *testing.T) { |
| var testcases = []decodeConfigTestcase{ |
| { |
| doc: "no paths volume", |
| wrapper: containerWrapperWithVolume(":"), |
| expectedErr: `invalid volume specification: ':'`, |
| }, |
| { |
| doc: "no paths bind", |
| wrapper: containerWrapperWithBind(":"), |
| expectedErr: `invalid volume specification: ':'`, |
| }, |
| { |
| doc: "no paths or mode volume", |
| wrapper: containerWrapperWithVolume("::"), |
| expectedErr: `invalid volume specification: '::'`, |
| }, |
| { |
| doc: "no paths or mode bind", |
| wrapper: containerWrapperWithBind("::"), |
| expectedErr: `invalid volume specification: '::'`, |
| }, |
| } |
| for _, testcase := range testcases { |
| t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) |
| } |
| } |
| |
| func TestDecodeContainerConfigWithVolumesUnix(t *testing.T) { |
| skip.IfCondition(t, runtime.GOOS == "windows") |
| |
| baseErr := `invalid mount config for type "volume": invalid specification: ` |
| var testcases = []decodeConfigTestcase{ |
| { |
| doc: "root to root volume", |
| wrapper: containerWrapperWithVolume("/:/"), |
| expectedErr: `invalid volume specification: '/:/'`, |
| }, |
| { |
| doc: "root to root bind", |
| wrapper: containerWrapperWithBind("/:/"), |
| expectedErr: `invalid volume specification: '/:/'`, |
| }, |
| { |
| doc: "no destination path volume", |
| wrapper: containerWrapperWithVolume(`/tmp:`), |
| expectedErr: ` invalid volume specification: '/tmp:'`, |
| }, |
| { |
| doc: "no destination path bind", |
| wrapper: containerWrapperWithBind(`/tmp:`), |
| expectedErr: ` invalid volume specification: '/tmp:'`, |
| }, |
| { |
| doc: "no destination path or mode volume", |
| wrapper: containerWrapperWithVolume(`/tmp::`), |
| expectedErr: `invalid mount config for type "bind": field Target must not be empty`, |
| }, |
| { |
| doc: "no destination path or mode bind", |
| wrapper: containerWrapperWithBind(`/tmp::`), |
| expectedErr: `invalid mount config for type "bind": field Target must not be empty`, |
| }, |
| { |
| doc: "too many sections volume", |
| wrapper: containerWrapperWithVolume(`/tmp:/tmp:/tmp:/tmp`), |
| expectedErr: `invalid volume specification: '/tmp:/tmp:/tmp:/tmp'`, |
| }, |
| { |
| doc: "too many sections bind", |
| wrapper: containerWrapperWithBind(`/tmp:/tmp:/tmp:/tmp`), |
| expectedErr: `invalid volume specification: '/tmp:/tmp:/tmp:/tmp'`, |
| }, |
| { |
| doc: "just root volume", |
| wrapper: containerWrapperWithVolume("/"), |
| expectedErr: baseErr + `destination can't be '/'`, |
| }, |
| { |
| doc: "just root bind", |
| wrapper: containerWrapperWithBind("/"), |
| expectedErr: baseErr + `destination can't be '/'`, |
| }, |
| { |
| doc: "bind mount passed as a volume", |
| wrapper: containerWrapperWithVolume(`/foo:/bar`), |
| expectedConfig: &container.Config{ |
| Volumes: map[string]struct{}{`/foo:/bar`: {}}, |
| }, |
| expectedHostConfig: &container.HostConfig{NetworkMode: "default"}, |
| }, |
| } |
| for _, testcase := range testcases { |
| t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) |
| } |
| } |
| |
| type decodeConfigTestcase struct { |
| doc string |
| wrapper ContainerConfigWrapper |
| expectedErr string |
| expectedConfig *container.Config |
| expectedHostConfig *container.HostConfig |
| goos string |
| } |
| |
| func runDecodeContainerConfigTestCase(testcase decodeConfigTestcase) func(t *testing.T) { |
| return func(t *testing.T) { |
| raw := marshal(t, testcase.wrapper, testcase.doc) |
| config, hostConfig, _, err := decodeContainerConfig(bytes.NewReader(raw)) |
| if testcase.expectedErr != "" { |
| if !assert.Error(t, err) { |
| return |
| } |
| assert.Contains(t, err.Error(), testcase.expectedErr) |
| return |
| } |
| assert.NoError(t, err) |
| assert.Equal(t, testcase.expectedConfig, config) |
| assert.Equal(t, testcase.expectedHostConfig, hostConfig) |
| } |
| } |
| |
| func TestDecodeContainerConfigWithVolumesWindows(t *testing.T) { |
| skip.IfCondition(t, runtime.GOOS != "windows") |
| |
| tmpDir := os.Getenv("TEMP") |
| systemDrive := os.Getenv("SystemDrive") |
| var testcases = []decodeConfigTestcase{ |
| { |
| doc: "root to root volume", |
| wrapper: containerWrapperWithVolume(systemDrive + `\:c:\`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "root to root bind", |
| wrapper: containerWrapperWithBind(systemDrive + `\:c:\`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no destination path volume", |
| wrapper: containerWrapperWithVolume(tmpDir + `\:`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no destination path bind", |
| wrapper: containerWrapperWithBind(tmpDir + `\:`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no destination path or mode volume", |
| wrapper: containerWrapperWithVolume(tmpDir + `\::`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no destination path or mode bind", |
| wrapper: containerWrapperWithBind(tmpDir + `\::`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "too many sections volume", |
| wrapper: containerWrapperWithVolume(tmpDir + ":" + tmpDir + ":" + tmpDir + ":" + tmpDir), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "too many sections bind", |
| wrapper: containerWrapperWithBind(tmpDir + ":" + tmpDir + ":" + tmpDir + ":" + tmpDir), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no drive letter volume", |
| wrapper: containerWrapperWithVolume(`\tmp`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "no drive letter bind", |
| wrapper: containerWrapperWithBind(`\tmp`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "root to c-drive volume", |
| wrapper: containerWrapperWithVolume(systemDrive + `\:c:`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "root to c-drive bind", |
| wrapper: containerWrapperWithBind(systemDrive + `\:c:`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "container path without driver letter volume", |
| wrapper: containerWrapperWithVolume(`c:\windows:\somewhere`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| { |
| doc: "container path without driver letter bind", |
| wrapper: containerWrapperWithBind(`c:\windows:\somewhere`), |
| expectedErr: `invalid volume specification: `, |
| }, |
| } |
| |
| for _, testcase := range testcases { |
| t.Run(testcase.doc, runDecodeContainerConfigTestCase(testcase)) |
| } |
| } |
| |
| func marshal(t *testing.T, w ContainerConfigWrapper, doc string) []byte { |
| b, err := json.Marshal(w) |
| require.NoError(t, err, "%s: failed to encode config wrapper", doc) |
| return b |
| } |
| |
| func containerWrapperWithVolume(volume string) ContainerConfigWrapper { |
| return ContainerConfigWrapper{ |
| Config: &container.Config{ |
| Volumes: map[string]struct{}{ |
| volume: {}, |
| }, |
| }, |
| HostConfig: &container.HostConfig{}, |
| } |
| } |
| |
| func containerWrapperWithBind(bind string) ContainerConfigWrapper { |
| return ContainerConfigWrapper{ |
| Config: &container.Config{ |
| Volumes: map[string]struct{}{}, |
| }, |
| HostConfig: &container.HostConfig{ |
| Binds: []string{bind}, |
| }, |
| } |
| } |