Merge pull request #2585 from hashicorp/b-2554-container-exec

Execute exec/java script checks in containers
This commit is contained in:
Michael Schurter
2017-05-05 10:31:18 -07:00
committed by GitHub
18 changed files with 468 additions and 97 deletions

View File

@@ -186,13 +186,19 @@ func (r *AllocRunner) RestoreState() error {
continue
}
if err := tr.RestoreState(); err != nil {
r.logger.Printf("[ERR] client: failed to restore state for alloc %s task '%s': %v", r.alloc.ID, name, err)
if restartReason, err := tr.RestoreState(); err != nil {
r.logger.Printf("[ERR] client: failed to restore state for alloc %s task %q: %v", r.alloc.ID, name, err)
mErr.Errors = append(mErr.Errors, err)
} else if !r.alloc.TerminalStatus() {
// Only start if the alloc isn't in a terminal status.
go tr.Run()
// Restart task runner if RestoreState gave a reason
if restartReason != "" {
tr.Restart("upgrade", restartReason)
}
}
}
return mErr.ErrorOrNil()

View File

@@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"text/template"
"time"
@@ -477,6 +478,84 @@ func TestAllocRunner_SaveRestoreState_TerminalAlloc(t *testing.T) {
})
}
// TestAllocRunner_SaveRestoreState_Upgrade asserts that pre-0.6 exec tasks are
// restarted on upgrade.
func TestAllocRunner_SaveRestoreState_Upgrade(t *testing.T) {
alloc := mock.Alloc()
task := alloc.Job.TaskGroups[0].Tasks[0]
task.Driver = "mock_driver"
task.Config = map[string]interface{}{
"exit_code": "0",
"run_for": "10s",
}
upd, ar := testAllocRunnerFromAlloc(alloc, false)
// Hack in old version to cause an upgrade on RestoreState
origConfig := ar.config.Copy()
ar.config.Version = "0.5.6"
go ar.Run()
// Snapshot state
testutil.WaitForResult(func() (bool, error) {
return len(ar.tasks) == 1, nil
}, func(err error) {
t.Fatalf("task never started: %v", err)
})
err := ar.SaveState()
if err != nil {
t.Fatalf("err: %v", err)
}
// Create a new alloc runner
l2 := prefixedTestLogger("----- ar2: ")
ar2 := NewAllocRunner(l2, origConfig, upd.Update,
&structs.Allocation{ID: ar.alloc.ID}, ar.vaultClient,
ar.consulClient)
err = ar2.RestoreState()
if err != nil {
t.Fatalf("err: %v", err)
}
go ar2.Run()
testutil.WaitForResult(func() (bool, error) {
if len(ar2.tasks) != 1 {
return false, fmt.Errorf("Incorrect number of tasks")
}
if upd.Count < 3 {
return false, nil
}
for _, ev := range ar2.alloc.TaskStates["web"].Events {
if strings.HasSuffix(ev.RestartReason, pre06ScriptCheckReason) {
return true, nil
}
}
return false, fmt.Errorf("no restart with proper reason found")
}, func(err error) {
t.Fatalf("err: %v\nAllocs: %#v\nWeb State: %#v", err, upd.Allocs, ar2.alloc.TaskStates["web"])
})
// Destroy and wait
ar2.Destroy()
start := time.Now()
testutil.WaitForResult(func() (bool, error) {
alloc := ar2.Alloc()
if alloc.ClientStatus != structs.AllocClientStatusComplete {
return false, fmt.Errorf("Bad client status; got %v; want %v", alloc.ClientStatus, structs.AllocClientStatusComplete)
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v %#v %#v", err, upd.Allocs[0], ar.alloc.TaskStates)
})
if time.Since(start) > time.Duration(testutil.TestMultiplier()*5)*time.Second {
t.Fatalf("took too long to terminate")
}
}
// Ensure pre-#2132 state files containing the Context struct are properly
// migrated to the new format.
//

View File

@@ -259,7 +259,12 @@ func (h *execHandle) Update(task *structs.Task) error {
}
func (h *execHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error) {
return execChroot(ctx, h.taskDir.Dir, cmd, args)
deadline, ok := ctx.Deadline()
if !ok {
// No deadline set on context; default to 1 minute
deadline = time.Now().Add(time.Minute)
}
return h.executor.Exec(deadline, cmd, args)
}
func (h *execHandle) Signal(s os.Signal) error {

View File

@@ -283,7 +283,8 @@ func TestExecDriverUser(t *testing.T) {
}
}
// TestExecDriver_HandlerExec ensures the exec driver's handle properly executes commands inside the chroot.
// TestExecDriver_HandlerExec ensures the exec driver's handle properly
// executes commands inside the container.
func TestExecDriver_HandlerExec(t *testing.T) {
ctestutils.ExecCompatible(t)
task := &structs.Task{
@@ -315,20 +316,60 @@ func TestExecDriver_HandlerExec(t *testing.T) {
t.Fatalf("missing handle")
}
// Exec a command that should work
out, code, err := handle.Exec(context.TODO(), "/usr/bin/stat", []string{"/alloc"})
// Exec a command that should work and dump the environment
out, code, err := handle.Exec(context.Background(), "/bin/sh", []string{"-c", "env | grep NOMAD"})
if err != nil {
t.Fatalf("error exec'ing stat: %v", err)
}
if code != 0 {
t.Fatalf("expected `stat /alloc` to succeed but exit code was: %d", code)
}
if expected := 100; len(out) < expected {
t.Fatalf("expected at least %d bytes of output but found %d:\n%s", expected, len(out), out)
// Assert exec'd commands are run in a task-like environment
scriptEnv := make(map[string]string)
for _, line := range strings.Split(string(out), "\n") {
if line == "" {
continue
}
parts := strings.SplitN(string(line), "=", 2)
if len(parts) != 2 {
t.Fatalf("Invalid env var: %q", line)
}
scriptEnv[parts[0]] = parts[1]
}
if v, ok := scriptEnv["NOMAD_SECRETS_DIR"]; !ok || v != "/secrets" {
t.Errorf("Expected NOMAD_SECRETS_DIR=/secrets but found=%t value=%q", ok, v)
}
if v, ok := scriptEnv["NOMAD_ALLOC_ID"]; !ok || v != ctx.DriverCtx.allocID {
t.Errorf("Expected NOMAD_SECRETS_DIR=%q but found=%t value=%q", ok, v)
}
// Assert cgroup membership
out, code, err = handle.Exec(context.Background(), "/bin/cat", []string{"/proc/self/cgroup"})
if err != nil {
t.Fatalf("error exec'ing cat /proc/self/cgroup: %v", err)
}
if code != 0 {
t.Fatalf("expected `cat /proc/self/cgroup` to succeed but exit code was: %d", code)
}
found := false
for _, line := range strings.Split(string(out), "\n") {
// Every cgroup entry should be /nomad/$ALLOC_ID
if line == "" {
continue
}
if !strings.Contains(line, ":/nomad/") {
t.Errorf("Not a member of the alloc's cgroup: expected=...:/nomad/... -- found=%q", line)
continue
}
found = true
}
if !found {
t.Errorf("exec'd command isn't in the task's cgroup")
}
// Exec a command that should fail
out, code, err = handle.Exec(context.TODO(), "/usr/bin/stat", []string{"lkjhdsaflkjshowaisxmcvnlia"})
out, code, err = handle.Exec(context.Background(), "/usr/bin/stat", []string{"lkjhdsaflkjshowaisxmcvnlia"})
if err != nil {
t.Fatalf("error exec'ing stat: %v", err)
}

View File

@@ -1,6 +1,7 @@
package executor
import (
"context"
"fmt"
"io/ioutil"
"log"
@@ -15,6 +16,7 @@ import (
"syscall"
"time"
"github.com/armon/circbuf"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/go-ps"
"github.com/shirou/gopsutil/process"
@@ -57,6 +59,7 @@ type Executor interface {
Version() (*ExecutorVersion, error)
Stats() (*cstructs.TaskResourceUsage, error)
Signal(s os.Signal) error
Exec(deadline time.Time, cmd string, args []string) ([]byte, int, error)
}
// ExecutorContext holds context to configure the command user
@@ -203,8 +206,8 @@ func (e *UniversalExecutor) SetContext(ctx *ExecutorContext) error {
return nil
}
// LaunchCmd launches a process and returns it's state. It also configures an
// applies isolation on certain platforms.
// LaunchCmd launches the main process and returns its state. It also
// configures an applies isolation on certain platforms.
func (e *UniversalExecutor) LaunchCmd(command *ExecCommand) (*ProcessState, error) {
e.logger.Printf("[DEBUG] executor: launching command %v %v", command.Cmd, strings.Join(command.Args, " "))
@@ -283,6 +286,51 @@ func (e *UniversalExecutor) LaunchCmd(command *ExecCommand) (*ProcessState, erro
return &ProcessState{Pid: e.cmd.Process.Pid, ExitCode: -1, IsolationConfig: ic, Time: time.Now()}, nil
}
// Exec a command inside a container for exec and java drivers.
func (e *UniversalExecutor) Exec(deadline time.Time, name string, args []string) ([]byte, int, error) {
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
return ExecScript(ctx, e.cmd.Dir, e.ctx.TaskEnv, e.cmd.SysProcAttr, name, args)
}
// ExecScript executes cmd with args and returns the output, exit code, and
// error. Output is truncated to client/driver/structs.CheckBufSize
func ExecScript(ctx context.Context, dir string, env *env.TaskEnvironment, attrs *syscall.SysProcAttr,
name string, args []string) ([]byte, int, error) {
name = env.ReplaceEnv(name)
cmd := exec.CommandContext(ctx, name, env.ParseAndReplace(args)...)
// Copy runtime environment from the main command
cmd.SysProcAttr = attrs
cmd.Dir = dir
cmd.Env = env.EnvList()
// Capture output
buf, _ := circbuf.NewBuffer(int64(dstructs.CheckBufSize))
cmd.Stdout = buf
cmd.Stderr = buf
if err := cmd.Run(); err != nil {
exitErr, ok := err.(*exec.ExitError)
if !ok {
// Non-exit error, return it and let the caller treat
// it as a critical failure
return nil, 0, err
}
// Some kind of error happened; default to critical
exitCode := 2
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
}
// Don't return the exitError as the caller only needs the
// output and code.
return buf.Bytes(), exitCode, nil
}
return buf.Bytes(), 0, nil
}
// configureLoggers sets up the standard out/error file rotators
func (e *UniversalExecutor) configureLoggers() error {
e.rotatorLock.Lock()

View File

@@ -6,6 +6,7 @@ import (
"net/rpc"
"os"
"syscall"
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/client/driver/executor"
@@ -33,6 +34,17 @@ type LaunchCmdArgs struct {
Cmd *executor.ExecCommand
}
type ExecCmdArgs struct {
Deadline time.Time
Name string
Args []string
}
type ExecCmdReturn struct {
Output []byte
Code int
}
func (e *ExecutorRPC) LaunchCmd(cmd *executor.ExecCommand) (*executor.ProcessState, error) {
var ps *executor.ProcessState
err := e.client.Call("Plugin.LaunchCmd", LaunchCmdArgs{Cmd: cmd}, &ps)
@@ -91,6 +103,20 @@ func (e *ExecutorRPC) Signal(s os.Signal) error {
return e.client.Call("Plugin.Signal", &s, new(interface{}))
}
func (e *ExecutorRPC) Exec(deadline time.Time, name string, args []string) ([]byte, int, error) {
req := ExecCmdArgs{
Deadline: deadline,
Name: name,
Args: args,
}
var resp *ExecCmdReturn
err := e.client.Call("Plugin.Exec", req, &resp)
if resp == nil {
return nil, 0, err
}
return resp.Output, resp.Code, err
}
type ExecutorRPCServer struct {
Impl executor.Executor
logger *log.Logger
@@ -165,6 +191,16 @@ func (e *ExecutorRPCServer) Signal(args os.Signal, resp *interface{}) error {
return e.Impl.Signal(args)
}
func (e *ExecutorRPCServer) Exec(args ExecCmdArgs, result *ExecCmdReturn) error {
out, code, err := e.Impl.Exec(args.Deadline, args.Name, args.Args)
ret := &ExecCmdReturn{
Output: out,
Code: code,
}
*result = *ret
return err
}
type ExecutorPlugin struct {
logger *log.Logger
Impl *ExecutorRPCServer

View File

@@ -390,7 +390,12 @@ func (h *javaHandle) Update(task *structs.Task) error {
}
func (h *javaHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error) {
return execChroot(ctx, h.taskDir, cmd, args)
deadline, ok := ctx.Deadline()
if !ok {
// No deadline set on context; default to 1 minute
deadline = time.Now().Add(time.Minute)
}
return h.executor.Exec(deadline, cmd, args)
}
func (h *javaHandle) Signal(s os.Signal) error {

View File

@@ -10,7 +10,9 @@ import (
"time"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/env"
"github.com/hashicorp/nomad/client/driver/executor"
dstructs "github.com/hashicorp/nomad/client/driver/structs"
"github.com/hashicorp/nomad/client/fingerprint"
@@ -48,6 +50,8 @@ type rawExecHandle struct {
logger *log.Logger
waitCh chan *dstructs.WaitResult
doneCh chan struct{}
taskEnv *env.TaskEnvironment
taskDir *allocdir.TaskDir
}
// NewRawExecDriver is used to create a new raw exec driver
@@ -165,6 +169,8 @@ func (d *RawExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandl
logger: d.logger,
doneCh: make(chan struct{}),
waitCh: make(chan *dstructs.WaitResult, 1),
taskEnv: d.taskEnv,
taskDir: ctx.TaskDir,
}
go h.run()
return h, nil
@@ -212,6 +218,8 @@ func (d *RawExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, e
version: id.Version,
doneCh: make(chan struct{}),
waitCh: make(chan *dstructs.WaitResult, 1),
taskEnv: d.taskEnv,
taskDir: ctx.TaskDir,
}
go h.run()
return h, nil
@@ -247,7 +255,7 @@ func (h *rawExecHandle) Update(task *structs.Task) error {
}
func (h *rawExecHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte, int, error) {
return execChroot(ctx, "", cmd, args)
return executor.ExecScript(ctx, h.taskDir.Dir, h.taskEnv, nil, cmd, args)
}
func (h *rawExecHandle) Signal(s os.Signal) error {

View File

@@ -22,6 +22,7 @@ import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver/env"
"github.com/hashicorp/nomad/client/driver/executor"
dstructs "github.com/hashicorp/nomad/client/driver/structs"
cstructs "github.com/hashicorp/nomad/client/structs"
@@ -87,6 +88,8 @@ type RktDriverConfig struct {
// rktHandle is returned from Start/Open as a handle to the PID
type rktHandle struct {
uuid string
env *env.TaskEnvironment
taskDir *allocdir.TaskDir
pluginClient *plugin.Client
executorPid int
executor executor.Executor
@@ -474,6 +477,8 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e
maxKill := d.DriverContext.config.MaxKillTimeout
h := &rktHandle{
uuid: uuid,
env: d.taskEnv,
taskDir: ctx.TaskDir,
pluginClient: pluginClient,
executor: execIntf,
executorPid: ps.Pid,
@@ -514,6 +519,8 @@ func (d *RktDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error
// Return a driver handle
h := &rktHandle{
uuid: id.UUID,
env: d.taskEnv,
taskDir: ctx.TaskDir,
pluginClient: pluginClient,
executorPid: id.ExecutorPid,
executor: exec,
@@ -566,7 +573,7 @@ func (h *rktHandle) Exec(ctx context.Context, cmd string, args []string) ([]byte
enterArgs[1] = h.uuid
enterArgs[2] = cmd
copy(enterArgs[3:], args)
return execChroot(ctx, "", rktCmd, enterArgs)
return executor.ExecScript(ctx, h.taskDir.Dir, h.env, nil, rktCmd, enterArgs)
}
func (h *rktHandle) Signal(s os.Signal) error {

View File

@@ -22,7 +22,7 @@ import (
func TestRktVersionRegex(t *testing.T) {
if os.Getenv("NOMAD_TEST_RKT") == "" {
t.Skip("skipping rkt tests")
t.Skip("NOMAD_TEST_RKT unset, skipping")
}
input_rkt := "rkt version 0.8.1"

View File

@@ -1,7 +1,6 @@
package driver
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -9,10 +8,8 @@ import (
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/armon/circbuf"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/client/config"
@@ -181,36 +178,3 @@ func getExecutorUser(task *structs.Task) string {
}
return task.User
}
// execChroot executes cmd with args inside chroot if set and returns the
// output, exit code, and error. If chroot is an empty string the command is
// executed on the host.
func execChroot(ctx context.Context, chroot, name string, args []string) ([]byte, int, error) {
buf, _ := circbuf.NewBuffer(int64(cstructs.CheckBufSize))
cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = "/"
cmd.Stdout = buf
cmd.Stderr = buf
if chroot != "" {
setChroot(cmd, chroot)
}
if err := cmd.Run(); err != nil {
exitErr, ok := err.(*exec.ExitError)
if !ok {
// Non-exit error, return it and let the caller treat
// it as a critical failure
return nil, 0, err
}
// Some kind of error happened; default to critical
exitCode := 2
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode = status.ExitStatus()
}
// Don't return the exitError as the caller only needs the
// output and code.
return buf.Bytes(), exitCode, nil
}
return buf.Bytes(), 0, nil
}

View File

@@ -16,6 +16,7 @@ import (
"github.com/golang/snappy"
"github.com/hashicorp/consul-template/signals"
"github.com/hashicorp/go-multierror"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/client/allocdir"
"github.com/hashicorp/nomad/client/config"
"github.com/hashicorp/nomad/client/driver"
@@ -234,17 +235,20 @@ func (r *TaskRunner) stateFilePath() string {
return path
}
// RestoreState is used to restore our state
func (r *TaskRunner) RestoreState() error {
// RestoreState is used to restore our state. If a non-empty string is returned
// the task is restarted with the string as the reason. This is useful for
// backwards incompatible upgrades that need to restart tasks with a new
// executor.
func (r *TaskRunner) RestoreState() (string, error) {
// Load the snapshot
var snap taskRunnerState
if err := restoreState(r.stateFilePath(), &snap); err != nil {
return err
return "", err
}
// Restore fields
if snap.Task == nil {
return fmt.Errorf("task runner snapshot includes nil Task")
return "", fmt.Errorf("task runner snapshot includes nil Task")
} else {
r.task = snap.Task
}
@@ -255,7 +259,7 @@ func (r *TaskRunner) RestoreState() error {
r.setCreatedResources(snap.CreatedResources)
if err := r.setTaskEnv(); err != nil {
return fmt.Errorf("client: failed to create task environment for task %q in allocation %q: %v",
return "", fmt.Errorf("client: failed to create task environment for task %q in allocation %q: %v",
r.task.Name, r.alloc.ID, err)
}
@@ -265,7 +269,7 @@ func (r *TaskRunner) RestoreState() error {
data, err := ioutil.ReadFile(tokenPath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to read token for task %q in alloc %q: %v", r.task.Name, r.alloc.ID, err)
return "", fmt.Errorf("failed to read token for task %q in alloc %q: %v", r.task.Name, r.alloc.ID, err)
}
// Token file doesn't exist
@@ -276,10 +280,11 @@ func (r *TaskRunner) RestoreState() error {
}
// Restore the driver
restartReason := ""
if snap.HandleID != "" {
d, err := r.createDriver()
if err != nil {
return err
return "", err
}
ctx := driver.NewExecContext(r.taskDir)
@@ -289,7 +294,11 @@ func (r *TaskRunner) RestoreState() error {
if err != nil {
r.logger.Printf("[ERR] client: failed to open handle to task %q for alloc %q: %v",
r.task.Name, r.alloc.ID, err)
return nil
return "", nil
}
if pre06ScriptCheck(snap.Version, r.task.Driver, r.task.Services) {
restartReason = pre06ScriptCheckReason
}
if err := r.registerServices(d, handle); err != nil {
@@ -308,7 +317,40 @@ func (r *TaskRunner) RestoreState() error {
r.running = true
r.runningLock.Unlock()
}
return nil
return restartReason, nil
}
// ver06 is used for checking for pre-0.6 script checks
var ver06 = version.Must(version.NewVersion("0.6.0dev"))
// pre06ScriptCheckReason is the restart reason given when a pre-0.6 script
// check is found on an exec/java task.
const pre06ScriptCheckReason = "upgrading pre-0.6 script checks"
// pre06ScriptCheck returns true if version is prior to 0.6.0dev, has a script
// check, and uses exec or java drivers.
func pre06ScriptCheck(ver, driver string, services []*structs.Service) bool {
if driver != "exec" && driver != "java" && driver != "mock_driver" {
// Only exec and java are affected
return false
}
v, err := version.NewVersion(ver)
if err != nil {
// Treat it as old
return true
}
if !v.LessThan(ver06) {
// >= 0.6.0dev
return false
}
for _, service := range services {
for _, check := range service.Checks {
if check.Type == "script" {
return true
}
}
}
return false
}
// SaveState is used to snapshot our state

View File

@@ -370,7 +370,7 @@ func TestTaskRunner_SaveRestoreState(t *testing.T) {
tr2 := NewTaskRunner(ctx.tr.logger, ctx.tr.config, ctx.upd.Update,
ctx.tr.taskDir, ctx.tr.alloc, task2, ctx.tr.vaultClient, ctx.tr.consul)
tr2.restartTracker = noRestartsTracker()
if err := tr2.RestoreState(); err != nil {
if _, err := tr2.RestoreState(); err != nil {
t.Fatalf("err: %v", err)
}
go tr2.Run()
@@ -1521,3 +1521,49 @@ func TestTaskRunner_CleanupFail(t *testing.T) {
t.Fatalf("expected %#v but found: %#v", expected, ctx.tr.createdResources.Resources)
}
}
func TestTaskRunner_Pre06ScriptCheck(t *testing.T) {
run := func(ver, driver, checkType string, exp bool) (string, func(t *testing.T)) {
name := fmt.Sprintf("%s %s %s returns %t", ver, driver, checkType, exp)
return name, func(t *testing.T) {
services := []*structs.Service{
{
Checks: []*structs.ServiceCheck{
{
Type: checkType,
},
},
},
}
if act := pre06ScriptCheck(ver, driver, services); act != exp {
t.Errorf("expected %t received %t", exp, act)
}
}
}
t.Run(run("0.5.6", "exec", "script", true))
t.Run(run("0.5.6", "java", "script", true))
t.Run(run("0.5.6", "mock_driver", "script", true))
t.Run(run("0.5.9", "exec", "script", true))
t.Run(run("0.5.9", "java", "script", true))
t.Run(run("0.5.9", "mock_driver", "script", true))
t.Run(run("0.6.0dev", "exec", "script", false))
t.Run(run("0.6.0dev", "java", "script", false))
t.Run(run("0.6.0dev", "mock_driver", "script", false))
t.Run(run("0.6.0", "exec", "script", false))
t.Run(run("0.6.0", "java", "script", false))
t.Run(run("0.6.0", "mock_driver", "script", false))
t.Run(run("1.0.0", "exec", "script", false))
t.Run(run("1.0.0", "java", "script", false))
t.Run(run("1.0.0", "mock_driver", "script", false))
t.Run(run("0.5.6", "rkt", "script", false))
t.Run(run("0.5.6", "docker", "script", false))
t.Run(run("0.5.6", "qemu", "script", false))
t.Run(run("0.5.6", "raw_exec", "script", false))
t.Run(run("0.5.6", "invalid", "script", false))
t.Run(run("0.5.6", "exec", "tcp", false))
t.Run(run("0.5.6", "java", "tcp", false))
t.Run(run("0.5.6", "mock_driver", "tcp", false))
}

View File

@@ -1,11 +0,0 @@
language: go
go:
- 1.0
- 1.1
- 1.2
- 1.3
- 1.4
script:
- go test

View File

@@ -1,5 +1,5 @@
# Versioning Library for Go
[![Build Status](https://travis-ci.org/hashicorp/go-version.svg?branch=master)](https://travis-ci.org/hashicorp/go-version)
[![Build Status](https://travis-ci.org/hashicorp/go-version.svg?branch=master)](https://travis-ci.org/hashicorp/go-version)
go-version is a library for parsing versions and version constraints,
and verifying versions against a set of constraints. go-version

View File

@@ -37,7 +37,7 @@ func init() {
}
ops := make([]string, 0, len(constraintOperators))
for k, _ := range constraintOperators {
for k := range constraintOperators {
ops = append(ops, regexp.QuoteMeta(k))
}
@@ -142,15 +142,37 @@ func constraintLessThanEqual(v, c *Version) bool {
}
func constraintPessimistic(v, c *Version) bool {
// If the version being checked is naturally less than the constraint, then there
// is no way for the version to be valid against the constraint
if v.LessThan(c) {
return false
}
// We'll use this more than once, so grab the length now so it's a little cleaner
// to write the later checks
cs := len(c.segments)
// If the version being checked has less specificity than the constraint, then there
// is no way for the version to be valid against the constraint
if cs > len(v.segments) {
return false
}
// Check the segments in the constraint against those in the version. If the version
// being checked, at any point, does not have the same values in each index of the
// constraints segments, then it cannot be valid against the constraint.
for i := 0; i < c.si-1; i++ {
if v.segments[i] != c.segments[i] {
return false
}
}
// Check the last part of the segment in the constraint. If the version segment at
// this index is less than the constraints segment at this index, then it cannot
// be valid against the constraint
if c.segments[cs-1] > v.segments[cs-1] {
return false
}
// If nothing has rejected the version by now, it's valid
return true
}

View File

@@ -14,8 +14,8 @@ var versionRegexp *regexp.Regexp
// The raw regular expression string used for testing the validity
// of a version.
const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+){0,2})` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
`(-?([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`?`
@@ -23,7 +23,7 @@ const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+){0,2})` +
type Version struct {
metadata string
pre string
segments []int
segments []int64
si int
}
@@ -38,20 +38,23 @@ func NewVersion(v string) (*Version, error) {
if matches == nil {
return nil, fmt.Errorf("Malformed version: %s", v)
}
segmentsStr := strings.Split(matches[1], ".")
segments := make([]int, len(segmentsStr), 3)
segments := make([]int64, len(segmentsStr))
si := 0
for i, str := range segmentsStr {
val, err := strconv.ParseInt(str, 10, 32)
val, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return nil, fmt.Errorf(
"Error parsing version: %s", err)
}
segments[i] = int(val)
si += 1
segments[i] = int64(val)
si++
}
// Even though we could support more than three segments, if we
// got less than three, pad it with 0s. This is to cover the basic
// default usecase of semver, which is MAJOR.MINOR.PATCH at the minimum
for i := len(segments); i < 3; i++ {
segments = append(segments, 0)
}
@@ -86,8 +89,8 @@ func (v *Version) Compare(other *Version) int {
return 0
}
segmentsSelf := v.Segments()
segmentsOther := other.Segments()
segmentsSelf := v.Segments64()
segmentsOther := other.Segments64()
// If the segments are the same, we must compare on prerelease info
if reflect.DeepEqual(segmentsSelf, segmentsOther) {
@@ -106,21 +109,56 @@ func (v *Version) Compare(other *Version) int {
return comparePrereleases(preSelf, preOther)
}
// Get the highest specificity (hS), or if they're equal, just use segmentSelf length
lenSelf := len(segmentsSelf)
lenOther := len(segmentsOther)
hS := lenSelf
if lenSelf < lenOther {
hS = lenOther
}
// Compare the segments
for i := 0; i < len(segmentsSelf); i++ {
// Because a constraint could have more/less specificity than the version it's
// checking, we need to account for a lopsided or jagged comparison
for i := 0; i < hS; i++ {
if i > lenSelf-1 {
// This means Self had the lower specificity
// Check to see if the remaining segments in Other are all zeros
if !allZero(segmentsOther[i:]) {
// if not, it means that Other has to be greater than Self
return -1
}
break
} else if i > lenOther-1 {
// this means Other had the lower specificity
// Check to see if the remaining segments in Self are all zeros -
if !allZero(segmentsSelf[i:]) {
//if not, it means that Self has to be greater than Other
return 1
}
break
}
lhs := segmentsSelf[i]
rhs := segmentsOther[i]
if lhs == rhs {
continue
} else if lhs < rhs {
return -1
} else {
return 1
}
// Otherwis, rhs was > lhs, they're not equal
return 1
}
panic("should not be reached")
// if we got this far, they're equal
return 0
}
func allZero(segs []int64) bool {
for _, s := range segs {
if s != 0 {
return false
}
}
return true
}
func comparePart(preSelf string, preOther string) int {
@@ -128,24 +166,38 @@ func comparePart(preSelf string, preOther string) int {
return 0
}
selfNumeric := true
_, err := strconv.ParseInt(preSelf, 10, 64)
if err != nil {
selfNumeric = false
}
otherNumeric := true
_, err = strconv.ParseInt(preOther, 10, 64)
if err != nil {
otherNumeric = false
}
// if a part is empty, we use the other to decide
if preSelf == "" {
_, notIsNumeric := strconv.ParseInt(preOther, 10, 64)
if notIsNumeric == nil {
if otherNumeric {
return -1
}
return 1
}
if preOther == "" {
_, notIsNumeric := strconv.ParseInt(preSelf, 10, 64)
if notIsNumeric == nil {
if selfNumeric {
return 1
}
return -1
}
if preSelf > preOther {
if selfNumeric && !otherNumeric {
return -1
} else if !selfNumeric && otherNumeric {
return 1
} else if preSelf > preOther {
return 1
}
@@ -226,12 +278,25 @@ func (v *Version) Prerelease() string {
return v.pre
}
// Segments returns the numeric segments of the version as a slice.
// Segments returns the numeric segments of the version as a slice of ints.
//
// This excludes any metadata or pre-release information. For example,
// for a version "1.2.3-beta", segments will return a slice of
// 1, 2, 3.
func (v *Version) Segments() []int {
segmentSlice := make([]int, len(v.segments))
for i, v := range v.segments {
segmentSlice[i] = int(v)
}
return segmentSlice
}
// Segments64 returns the numeric segments of the version as a slice of int64s.
//
// This excludes any metadata or pre-release information. For example,
// for a version "1.2.3-beta", segments will return a slice of
// 1, 2, 3.
func (v *Version) Segments64() []int64 {
return v.segments
}
@@ -239,7 +304,13 @@ func (v *Version) Segments() []int {
// and metadata information.
func (v *Version) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d.%d.%d", v.segments[0], v.segments[1], v.segments[2])
fmtParts := make([]string, len(v.segments))
for i, s := range v.segments {
// We can ignore err here since we've pre-parsed the values in segments
str := strconv.FormatInt(s, 10)
fmtParts[i] = str
}
fmt.Fprintf(&buf, strings.Join(fmtParts, "."))
if v.pre != "" {
fmt.Fprintf(&buf, "-%s", v.pre)
}

4
vendor/vendor.json vendored
View File

@@ -743,8 +743,10 @@
"revision": "42a2b573b664dbf281bd48c3cc12c086b17a39ba"
},
{
"checksumSHA1": "tUGxc7rfX0cmhOOUDhMuAZ9rWsA=",
"path": "github.com/hashicorp/go-version",
"revision": "2e7f5ea8e27bb3fdf9baa0881d16757ac4637332"
"revision": "03c5bf6be031b6dd45afec16b1cf94fc8938bc77",
"revisionTime": "2017-02-02T08:07:59Z"
},
{
"checksumSHA1": "d9PxF1XQGLMJZRct2R8qVM/eYlE=",