From 5d6ef6335066e5921fcf8334990c2a856410dc93 Mon Sep 17 00:00:00 2001 From: "Justin Terry (VM)" Date: Fri, 17 Aug 2018 13:14:16 -0700 Subject: [PATCH] Introduce go-runhcs client bindings Signed-off-by: Justin Terry (VM) --- cmd/go-runhcs/LICENSE | 201 +++++++++++++++++++++++++++++++++ cmd/go-runhcs/NOTICE | 22 ++++ cmd/go-runhcs/runhcs.go | 125 ++++++++++++++++++++ cmd/go-runhcs/runhcs_create.go | 91 +++++++++++++++ cmd/go-runhcs/runhcs_delete.go | 33 ++++++ cmd/go-runhcs/runhcs_exec.go | 77 +++++++++++++ cmd/go-runhcs/runhcs_kill.go | 11 ++ cmd/go-runhcs/runhcs_start.go | 10 ++ cmd/go-runhcs/runhcs_test.go | 103 +++++++++++++++++ 9 files changed, 673 insertions(+) create mode 100644 cmd/go-runhcs/LICENSE create mode 100644 cmd/go-runhcs/NOTICE create mode 100644 cmd/go-runhcs/runhcs.go create mode 100644 cmd/go-runhcs/runhcs_create.go create mode 100644 cmd/go-runhcs/runhcs_delete.go create mode 100644 cmd/go-runhcs/runhcs_exec.go create mode 100644 cmd/go-runhcs/runhcs_kill.go create mode 100644 cmd/go-runhcs/runhcs_start.go create mode 100644 cmd/go-runhcs/runhcs_test.go diff --git a/cmd/go-runhcs/LICENSE b/cmd/go-runhcs/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/cmd/go-runhcs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cmd/go-runhcs/NOTICE b/cmd/go-runhcs/NOTICE new file mode 100644 index 0000000000..5f9d59f13c --- /dev/null +++ b/cmd/go-runhcs/NOTICE @@ -0,0 +1,22 @@ +go-runhcs is a fork of go-runc + +The following is runc's legal notice. + +--- + +runc + +Copyright 2012-2015 Docker, Inc. + +This product includes software developed at Docker, Inc. (http://www.docker.com). + +The following is courtesy of our legal counsel: + +Use and transfer of Docker may be subject to certain restrictions by the +United States and other governments. +It is your responsibility to ensure that your use and/or transfer does not +violate applicable laws. + +For more information, please see http://www.bis.doc.gov + +See also http://www.apache.org/dev/crypto.html and/or seek legal counsel. \ No newline at end of file diff --git a/cmd/go-runhcs/runhcs.go b/cmd/go-runhcs/runhcs.go new file mode 100644 index 0000000000..df580248ef --- /dev/null +++ b/cmd/go-runhcs/runhcs.go @@ -0,0 +1,125 @@ +package runhcs + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "sync" + + "github.com/containerd/go-runc" +) + +// Format is the type of log formatting options available. +type Format string + +const ( + none Format = "" + // Text is the default text log ouput. + Text Format = "text" + // JSON is the JSON formatted log output. + JSON Format = "json" + + command = "runhcs" +) + +var bytesBufferPool = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(nil) + }, +} + +func getBuf() *bytes.Buffer { + return bytesBufferPool.Get().(*bytes.Buffer) +} + +func putBuf(b *bytes.Buffer) { + b.Reset() + bytesBufferPool.Put(b) +} + +// Runhcs is the client to the runhcs cli +type Runhcs struct { + // Debug enables debug output for logging. + Debug bool + // Log sets the log file path where internal debug information is written. + Log string + // LogFormat sets the format used by logs. + LogFormat Format + // Owner sets the compute system owner property. + Owner string + // Root is the registry key root for storage of runhcs container state. + Root string +} + +func (r *Runhcs) args() []string { + var out []string + if r.Debug { + out = append(out, "--debug") + } + if r.Log != "" { + // TODO: JTERRY75 - Should we do abs here? + out = append(out, "--log", r.Log) + } + if r.LogFormat != none { + out = append(out, "--log-format", string(r.LogFormat)) + } + if r.Owner != "" { + out = append(out, "--owner", r.Owner) + } + if r.Root != "" { + out = append(out, "--root", r.Root) + } + return out +} + +func (r *Runhcs) command(context context.Context, args ...string) *exec.Cmd { + cmd := exec.CommandContext(context, command, append(r.args(), args...)...) + cmd.Env = os.Environ() + return cmd +} + +// runOrError will run the provided command. If an error is +// encountered and neither Stdout or Stderr was set the error and the +// stderr of the command will be returned in the format of : +// +func (r *Runhcs) runOrError(cmd *exec.Cmd) error { + if cmd.Stdout != nil || cmd.Stderr != nil { + ec, err := runc.Monitor.Start(cmd) + if err != nil { + return err + } + status, err := runc.Monitor.Wait(cmd, ec) + if err == nil && status != 0 { + err = fmt.Errorf("%s did not terminate sucessfully", cmd.Args[0]) + } + return err + } + data, err := cmdOutput(cmd, true) + if err != nil { + return fmt.Errorf("%s: %s", err, data) + } + return nil +} + +func cmdOutput(cmd *exec.Cmd, combined bool) ([]byte, error) { + b := getBuf() + defer putBuf(b) + + cmd.Stdout = b + if combined { + cmd.Stderr = b + } + ec, err := runc.Monitor.Start(cmd) + if err != nil { + return nil, err + } + + status, err := runc.Monitor.Wait(cmd, ec) + if err == nil && status != 0 { + err = fmt.Errorf("%s did not terminate sucessfully", cmd.Args[0]) + } + + return b.Bytes(), err +} diff --git a/cmd/go-runhcs/runhcs_create.go b/cmd/go-runhcs/runhcs_create.go new file mode 100644 index 0000000000..601eab8b22 --- /dev/null +++ b/cmd/go-runhcs/runhcs_create.go @@ -0,0 +1,91 @@ +package runhcs + +import ( + "context" + "fmt" + "path/filepath" + + runc "github.com/containerd/go-runc" +) + +// CreateOpts is set of options that can be used with the Create command. +type CreateOpts struct { + runc.IO + // PidFile is the path to the file to write the process id to. + PidFile string + // ShimLog is the path to the log file for the launched shim process. + ShimLog string + // VMLog is the path to the log file for the launched VM shim process. + VMLog string + // VMConsole is the path to the pipe for the VM's console (e.g. \\.\pipe\debugpipe) + VMConsole string +} + +func (opt *CreateOpts) args() ([]string, error) { + var out []string + if opt.PidFile != "" { + abs, err := filepath.Abs(opt.PidFile) + if err != nil { + return nil, err + } + out = append(out, "--pid-file", abs) + } + if opt.ShimLog != "" { + abs, err := filepath.Abs(opt.ShimLog) + if err != nil { + return nil, err + } + out = append(out, "--shim-log", abs) + } + if opt.VMLog != "" { + abs, err := filepath.Abs(opt.VMLog) + if err != nil { + return nil, err + } + out = append(out, "--vm-log", abs) + } + if opt.VMConsole != "" { + out = append(out, "--vm-console", opt.VMConsole) + } + return out, nil +} + +// Create creates a new container and returns its pid if it was created +// successfully. +func (r *Runhcs) Create(context context.Context, id, bundle string, opts *CreateOpts) error { + args := []string{"create", "--bundle", bundle} + if opts != nil { + oargs, err := opts.args() + if err != nil { + return err + } + args = append(args, oargs...) + } + cmd := r.command(context, append(args, id)...) + if opts != nil && opts.IO != nil { + opts.Set(cmd) + } + if cmd.Stdout == nil && cmd.Stderr == nil { + data, err := cmdOutput(cmd, true) + if err != nil { + return fmt.Errorf("%s: %s", err, data) + } + return nil + } + ec, err := runc.Monitor.Start(cmd) + if err != nil { + return err + } + if opts != nil && opts.IO != nil { + if c, ok := opts.IO.(runc.StartCloser); ok { + if err := c.CloseAfterStart(); err != nil { + return err + } + } + } + status, err := runc.Monitor.Wait(cmd, ec) + if err == nil && status != 0 { + err = fmt.Errorf("%s did not terminate sucessfully", cmd.Args[0]) + } + return nil +} diff --git a/cmd/go-runhcs/runhcs_delete.go b/cmd/go-runhcs/runhcs_delete.go new file mode 100644 index 0000000000..08b82bbd9a --- /dev/null +++ b/cmd/go-runhcs/runhcs_delete.go @@ -0,0 +1,33 @@ +package runhcs + +import ( + "context" +) + +// DeleteOpts is set of options that can be used with the Delete command. +type DeleteOpts struct { + // Force forcibly deletes the container if it is still running (uses SIGKILL). + Force bool +} + +func (opt *DeleteOpts) args() ([]string, error) { + var out []string + if opt.Force { + out = append(out, "--force") + } + return out, nil +} + +// Delete any resources held by the container often used with detached +// containers. +func (r *Runhcs) Delete(context context.Context, id string, opts *DeleteOpts) error { + args := []string{"delete"} + if opts != nil { + oargs, err := opts.args() + if err != nil { + return err + } + args = append(args, oargs...) + } + return r.runOrError(r.command(context, append(args, id)...)) +} diff --git a/cmd/go-runhcs/runhcs_exec.go b/cmd/go-runhcs/runhcs_exec.go new file mode 100644 index 0000000000..1203eaa8b7 --- /dev/null +++ b/cmd/go-runhcs/runhcs_exec.go @@ -0,0 +1,77 @@ +package runhcs + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/containerd/go-runc" +) + +// ExecOpts is set of options that can be used with the Exec command. +type ExecOpts struct { + runc.IO + // PidFile is the path to the file to write the process id to. + PidFile string + // ShimLog is the path to the log file for the launched shim process. + ShimLog string +} + +func (opt *ExecOpts) args() ([]string, error) { + var out []string + if opt.PidFile != "" { + abs, err := filepath.Abs(opt.PidFile) + if err != nil { + return nil, err + } + out = append(out, "--pid-file", abs) + } + if opt.ShimLog != "" { + abs, err := filepath.Abs(opt.ShimLog) + if err != nil { + return nil, err + } + out = append(out, "--shim-log", abs) + } + return out, nil +} + +// Exec executes an additional process inside the container based on the +// oci.Process spec found at processFile. +func (r *Runhcs) Exec(context context.Context, id, processFile string, opts *ExecOpts) error { + args := []string{"exec", "--process", processFile} + if opts != nil { + oargs, err := opts.args() + if err != nil { + return err + } + args = append(args, oargs...) + } + cmd := r.command(context, append(args, id)...) + if opts != nil && opts.IO != nil { + opts.Set(cmd) + } + if cmd.Stdout == nil && cmd.Stderr == nil { + data, err := cmdOutput(cmd, true) + if err != nil { + return fmt.Errorf("%s: %s", err, data) + } + return nil + } + ec, err := runc.Monitor.Start(cmd) + if err != nil { + return err + } + if opts != nil && opts.IO != nil { + if c, ok := opts.IO.(runc.StartCloser); ok { + if err := c.CloseAfterStart(); err != nil { + return err + } + } + } + status, err := runc.Monitor.Wait(cmd, ec) + if err == nil && status != 0 { + err = fmt.Errorf("%s did not terminate sucessfully", cmd.Args[0]) + } + return err +} diff --git a/cmd/go-runhcs/runhcs_kill.go b/cmd/go-runhcs/runhcs_kill.go new file mode 100644 index 0000000000..021e5b16fe --- /dev/null +++ b/cmd/go-runhcs/runhcs_kill.go @@ -0,0 +1,11 @@ +package runhcs + +import ( + "context" +) + +// Kill sends the specified signal (default: SIGTERM) to the container's init +// process. +func (r *Runhcs) Kill(context context.Context, id, signal string) error { + return r.runOrError(r.command(context, "kill", id, signal)) +} diff --git a/cmd/go-runhcs/runhcs_start.go b/cmd/go-runhcs/runhcs_start.go new file mode 100644 index 0000000000..ad3df746a8 --- /dev/null +++ b/cmd/go-runhcs/runhcs_start.go @@ -0,0 +1,10 @@ +package runhcs + +import ( + "context" +) + +// Start will start an already created container. +func (r *Runhcs) Start(context context.Context, id string) error { + return r.runOrError(r.command(context, "start", id)) +} diff --git a/cmd/go-runhcs/runhcs_test.go b/cmd/go-runhcs/runhcs_test.go new file mode 100644 index 0000000000..34d3bd0006 --- /dev/null +++ b/cmd/go-runhcs/runhcs_test.go @@ -0,0 +1,103 @@ +// +build runhcs_test + +package runhcs + +import ( + "bytes" + "context" + "io" + "os" + "path/filepath" + "sync" + "testing" + + runc "github.com/containerd/go-runc" +) + +func TestRunhcs_E2E(t *testing.T) { + rhcs := Runhcs{ + Debug: true, + } + + // TODO: JTERRY75 use this from assets dynamically + //dir, err := ioutil.TempDir("", "runhcs-bundle") + //if err != nil { + // t.Fatalf("failed to create tempdir with error: %v", err) + //} + //defer os.Remove(dir) + br := os.Getenv("RUNHCS_TEST_BUNDLE_ROOT") + if br == "" { + t.Fatal("You must set %RUNHCS_TEST_BUNDLE_ROOT% to the folder containing the test bundles") + return + } + // TODO: JTERRY75 create this spec dynamically once we can do the layer + // extraction in some way so we dont need hard coded bundle/config.json's + dir := filepath.Join(br, "runhcs-tee-test") + + ctx := context.TODO() + id := "runhcs-e2e-id" + + pio, err := runc.NewPipeIO() + if err != nil { + t.Fatalf("failed to create new pipe io with error: %v", err) + } + defer pio.Close() + + // Write our expected output + expected := "Hello go-runhcs-container!" + inbuff := bytes.NewBufferString(expected) + outbuff := &bytes.Buffer{} + errbuff := &bytes.Buffer{} + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + _, err := io.Copy(pio.Stdin(), inbuff) + if err != nil { + t.Errorf("failed to copy string to stdin pipe.") + } + pio.Stdin().Close() + }() + go func() { + _, err := io.Copy(outbuff, pio.Stdout()) + if err != nil { + t.Errorf("failed to copy string from stdout pipe") + } + wg.Done() + }() + go func() { + _, err := io.Copy(errbuff, pio.Stderr()) + if err != nil { + t.Errorf("failed to copy string from stderr pipe") + } + wg.Done() + }() + + copts := &CreateOpts{ + IO: pio, + PidFile: filepath.Join(dir, "pid-file.txt"), + ShimLog: filepath.Join(dir, "shim-log.txt"), + VMLog: filepath.Join(dir, "vm-log.txt"), + } + if err := rhcs.Create(ctx, id, dir, copts); err != nil { + t.Fatalf("failed to create container with error: %v", err) + } + defer func() { + if err := rhcs.Delete(ctx, id, &DeleteOpts{Force: true}); err != nil { + t.Fatalf("failed to delete container with error: %v", err) + } + }() + + if err := rhcs.Start(ctx, id); err != nil { + t.Fatalf("failed to start container with error: %v", err) + } + wg.Wait() + + outstring := outbuff.String() + if outstring != expected { + t.Fatalf("stdout string '%s' != expected '%s'", outstring, expected) + } + errstring := errbuff.String() + if errstring != expected { + t.Fatalf("stderr string '%s' != expected '%s'", errstring, expected) + } +}