diff --git a/validate/error.go b/validate/error.go index 0311c2304..46e70d971 100644 --- a/validate/error.go +++ b/validate/error.go @@ -15,6 +15,13 @@ type ErrorCode int const ( // DefaultFilesystems represents the error code of default filesystems test. DefaultFilesystems ErrorCode = iota + + // CreateWithID represents the error code of 'create' lifecyle test with 'id' provided. + CreateWithID + // CreateWithUniqueID represents the error code of 'create' lifecyle test with unique 'id' provided. + CreateWithUniqueID + // CreateNewContainer represents the error code 'create' lifecyle test that creates new container. + CreateNewContainer ) type errorTemplate struct { @@ -29,6 +36,24 @@ var ociErrors = map[ErrorCode]errorTemplate{ return fmt.Sprintf(referenceTemplate, version, "config-linux.md#default-filesystems"), nil }, }, + CreateWithID: errorTemplate{ + Level: rfc2119.Must, + Reference: func(version string) (reference string, err error) { + return fmt.Sprintf(referenceTemplate, version, "runtime.md#create"), nil + }, + }, + CreateWithUniqueID: errorTemplate{ + Level: rfc2119.Must, + Reference: func(version string) (reference string, err error) { + return fmt.Sprintf(referenceTemplate, version, "runtime.md#create"), nil + }, + }, + CreateNewContainer: errorTemplate{ + Level: rfc2119.Must, + Reference: func(version string) (reference string, err error) { + return fmt.Sprintf(referenceTemplate, version, "runtime.md#create"), nil + }, + }, } // NewError creates an Error referencing a spec violation. The error diff --git a/validation/container.go b/validation/container.go new file mode 100644 index 000000000..0e3d193b9 --- /dev/null +++ b/validation/container.go @@ -0,0 +1,115 @@ +package validation + +import ( + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + + rspecs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/runtime-tools/generate" +) + +type Runtime struct { + RuntimeCommand string + BundleDir string + ID string +} + +func NewRuntime(runtimeCommand string, bundleDir string) (Runtime, error) { + var r Runtime + var err error + r.RuntimeCommand, err = exec.LookPath(runtimeCommand) + if err != nil { + return Runtime{}, err + } + + r.BundleDir = bundleDir + return r, err +} + +func (r *Runtime) SetConfig(g *generate.Generator) error { + if r.BundleDir == "" { + return errors.New("Please set the bundle directory first") + } + return g.SaveToFile(filepath.Join(r.BundleDir, "config.json"), generate.ExportOptions{}) +} + +func (r *Runtime) SetID(id string) { + r.ID = id +} + +func (r *Runtime) Create() error { + var args []string + args = append(args, "create") + if r.ID != "" { + args = append(args, r.ID) + } + + // TODO: following the spec, we need define the bundle, but 'runc' does not.. + // if r.BundleDir != "" { + // args = append(args, r.BundleDir) + // } + cmd := exec.Command(r.RuntimeCommand, args...) + cmd.Dir = r.BundleDir + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (r *Runtime) Start() error { + var args []string + args = append(args, "start") + if r.ID != "" { + args = append(args, r.ID) + } + + cmd := exec.Command(r.RuntimeCommand, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (r *Runtime) State() (rspecs.State, error) { + var args []string + args = append(args, "state") + if r.ID != "" { + args = append(args, r.ID) + } + + out, err := exec.Command(r.RuntimeCommand, args...).Output() + if err != nil { + return rspecs.State{}, err + } + + var state rspecs.State + err = json.Unmarshal(out, &state) + return state, err +} + +func (r *Runtime) Delete() error { + var args []string + args = append(args, "delete") + if r.ID != "" { + args = append(args, r.ID) + } + + cmd := exec.Command(r.RuntimeCommand, args...) + return cmd.Run() +} + +func (r *Runtime) Clean(removeBundle bool) error { + err := r.Delete() + if err != nil { + return err + } + + if removeBundle { + os.RemoveAll(r.BundleDir) + } + + return nil +} diff --git a/validation/validation_test.go b/validation/validation_test.go index d2ef9c501..769d5b585 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -1,7 +1,6 @@ package validation import ( - "fmt" "io/ioutil" "os" "os/exec" @@ -9,8 +8,12 @@ import ( "testing" "github.com/mrunalp/fileutils" - "github.com/opencontainers/runtime-tools/generate" + rspecs "github.com/opencontainers/runtime-spec/specs-go" "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + + "github.com/opencontainers/runtime-tools/generate" + "github.com/opencontainers/runtime-tools/validate" ) var ( @@ -24,81 +27,106 @@ func init() { } } -func runtimeValidate(runtime string, g *generate.Generator) error { - // Find the runtime binary in the PATH - runtimePath, err := exec.LookPath(runtime) +func prepareBundle() (string, error) { + // Setup a temporary test directory + bundleDir, err := ioutil.TempDir("", "ocitest") if err != nil { - return err + return "", err } - // Setup a temporary test directory - tmpDir, err := ioutil.TempDir("", "ocitest") + // Untar the root fs + untarCmd := exec.Command("tar", "-xf", "../rootfs.tar.gz", "-C", bundleDir) + _, err = untarCmd.CombinedOutput() if err != nil { - return err + os.RemoveAll(bundleDir) + return "", err } - defer os.RemoveAll(tmpDir) - // Create bundle directory for the test container - bundleDir := tmpDir + "/busybox" - if err := os.MkdirAll(bundleDir, 0755); err != nil { + return bundleDir, nil +} + +func getDefaultGenerator() *generate.Generator { + g := generate.New() + g.SetRootPath(".") + g.SetProcessArgs([]string{"/runtimetest"}) + return &g +} + +func runtimeInsideValidate(g *generate.Generator) error { + bundleDir, err := prepareBundle() + if err != nil { return err } - - // Untar the root fs - untarCmd := exec.Command("tar", "-xf", "../rootfs.tar.gz", "-C", bundleDir) - output, err := untarCmd.CombinedOutput() + r, err := NewRuntime(runtime, bundleDir) if err != nil { - fmt.Println(string(output)) + os.RemoveAll(bundleDir) return err } - - // Copy the runtimetest binary to the rootfs - err = fileutils.CopyFile("../runtimetest", filepath.Join(bundleDir, "runtimetest")) + defer r.Clean(true) + err = r.SetConfig(g) if err != nil { return err } - - // Generate test configuration - err = g.SaveToFile(filepath.Join(bundleDir, "config.json"), generate.ExportOptions{}) + err = fileutils.CopyFile("../runtimetest", filepath.Join(r.BundleDir, "runtimetest")) if err != nil { return err } - // TODO: Use a library to split run into create/start - // Launch the OCI runtime - containerID := uuid.NewV4() - runtimeCmd := exec.Command(runtimePath, "run", containerID.String()) - runtimeCmd.Dir = bundleDir - runtimeCmd.Stdin = os.Stdin - runtimeCmd.Stdout = os.Stdout - runtimeCmd.Stderr = os.Stderr - if err = runtimeCmd.Run(); err != nil { + r.SetID(uuid.NewV4().String()) + err = r.Create() + if err != nil { return err } - - return nil -} - -func getDefaultGenerator() *generate.Generator { - g := generate.New() - g.SetRootPath(".") - g.SetProcessArgs([]string{"/runtimetest"}) - return &g + return r.Start() } func TestValidateBasic(t *testing.T) { g := getDefaultGenerator() - if err := runtimeValidate(runtime, g); err != nil { - t.Errorf("%s failed validation: %v", runtime, err) - } + assert.Nil(t, runtimeInsideValidate(g)) } func TestValidateSysctls(t *testing.T) { g := getDefaultGenerator() g.AddLinuxSysctl("net.ipv4.ip_forward", "1") - if err := runtimeValidate(runtime, g); err != nil { - t.Errorf("%s failed validation: %v", runtime, err) + assert.Nil(t, runtimeInsideValidate(g)) +} + +func TestValidateCreate(t *testing.T) { + g := generate.New() + g.SetRootPath(".") + g.SetProcessArgs([]string{"ls"}) + + bundleDir, err := prepareBundle() + assert.Nil(t, err) + + r, err := NewRuntime(runtime, bundleDir) + assert.Nil(t, err) + defer r.Clean(true) + + err = r.SetConfig(&g) + assert.Nil(t, err) + + containerID := uuid.NewV4().String() + cases := []struct { + id string + errExpected bool + err error + }{ + {"", false, validate.NewError(validate.CreateWithID, "'Create' MUST generate an error if the ID is not provided", rspecs.Version)}, + {containerID, true, validate.NewError(validate.CreateNewContainer, "'Create' MUST create a new container", rspecs.Version)}, + {containerID, false, validate.NewError(validate.CreateWithUniqueID, "'Create' MUST generate an error if the ID provided is not unique", rspecs.Version)}, + } + + for _, c := range cases { + r.SetID(c.id) + err := r.Create() + assert.Equal(t, c.errExpected, err == nil, c.err.Error()) + + if err == nil { + state, _ := r.State() + assert.Equal(t, c.id, state.ID, c.err.Error()) + } } }