diff --git a/.gitignore b/.gitignore index e407015f6..39dc26c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -52,7 +52,6 @@ nomad_linux_amd64 nomad_darwin_amd64 TODO.md codecgen-*.generated.go -*.generated.go .terraform *.tfstate* diff --git a/.travis.yml b/.travis.yml index da9da6a13..a2c460253 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,16 +6,12 @@ language: go go: - 1.9.x +addons: + chrome: stable + git: depth: 300 -branches: - only: - - master - -matrix: - include: - matrix: include: - os: linux @@ -31,18 +27,20 @@ matrix: env: RUN_STATIC_CHECKS=1 SKIP_NOMAD_TESTS=1 - os: osx osx_image: xcode9.1 - -cache: - directories: - - ui/node_modules + allow_failures: + - os: osx + fast_finish: true before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ -z "$SKIP_NOMAD_TESTS" ]]; then sudo -E bash ./scripts/travis-mac-priv.sh ; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ -z "$SKIP_NOMAD_TESTS" ]]; then sudo -E bash ./scripts/travis-linux.sh ; fi + - if [[ "$RUN_UI_TESTS" ]]; then curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.0.1 ; fi + - if [[ "$RUN_UI_TESTS" ]]; then export PATH="$HOME/.yarn/bin:$PATH" ; fi install: - if [[ -z "$SKIP_NOMAD_TESTS" ]]; then make deps ; fi - if [[ "$RUN_STATIC_CHECKS" ]]; then make lint-deps ; fi + - if [[ "$RUN_UI_TESTS" ]]; then . $HOME/.nvm/nvm.sh && cd ui && nvm use && cd .. ; fi script: - sudo -E "PATH=$PATH" make travis diff --git a/CHANGELOG.md b/CHANGELOG.md index f746f5cc5..4f9f7f562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,26 @@ -## 0.7.1 (Unreleased) +## 0.8 (Unreleased) + +__BACKWARDS INCOMPATIBILITIES:__ + * discovery: Prevent absolute URLs in check paths. The documentation indicated + that absolute URLs are not allowed, but it was not enforced. Absolute URLs + in HTTP check paths will now fail to validate. [[GH-3685](https://github.com/hashicorp/nomad/issues/3685)] + +IMPROVEMENTS: + * discovery: Allow `check_restart` to be specified in the `service` stanza. + [[GH-3718](https://github.com/hashicorp/nomad/issues/3718)] + * driver/lxc: Add volumes config to LXC driver [GH-3687] + +BUG FIXES: + * core: Fix search endpoint forwarding for multi-region clusters [[GH-3680](https://github.com/hashicorp/nomad/issues/3680)] + * core: Fix an issue in which batch jobs with queued placements and lost + allocations could result in improper placement counts [[GH-3717](https://github.com/hashicorp/nomad/issues/3717)] + * client: Migrated ephemeral_disk's maintain directory permissions [[GH-3723](https://github.com/hashicorp/nomad/issues/3723)] + * client/vault: Recognize renewing non-renewable Vault lease as fatal [[GH-3727](https://github.com/hashicorp/nomad/issues/3727)] + * config: Revert minimum CPU limit back to 20 from 100. + * ui: Fix ui on non-leaders when ACLs are enabled [[GH-3722](https://github.com/hashicorp/nomad/issues/3722)] + * ui: Fix requests using client-side certificates in Firefox. [[GH-3728](https://github.com/hashicorp/nomad/pull/3728)] + +## 0.7.1 (December 19, 2017) __BACKWARDS INCOMPATIBILITIES:__ * client: The format of service IDs in Consul has changed. If you rely upon @@ -7,6 +29,8 @@ __BACKWARDS INCOMPATIBILITIES:__ * config: Nomad no longer parses Atlas configuration stanzas. Atlas has been deprecated since earlier this year. If you have an Atlas stanza in your config file it will have to be removed. + * config: Default minimum CPU configuration has been changed to 100 from 20. Jobs + using the old minimum value of 20 will have to be updated. * telemetry: Hostname is now emitted via a tag rather than within the key name. To maintain old behavior during an upgrade path specify `backwards_compatible_metrics` in the telemetry configuration. @@ -52,6 +76,9 @@ BUG FIXES: * core: Fix issue in which restoring periodic jobs could fail when a leader election occurs [[GH-3646](https://github.com/hashicorp/nomad/issues/3646)] + * core: Fix race condition in which rapid reprocessing of a blocked evaluation + may lead to the scheduler not seeing the results of the previous scheduling + event [[GH-3669](https://github.com/hashicorp/nomad/issues/3669)] * core: Fixed an issue where the leader server could get into a state where it was no longer performing the periodic leader loop duties after a barrier timeout error [[GH-3402](https://github.com/hashicorp/nomad/issues/3402)] diff --git a/GNUmakefile b/GNUmakefile index c2527210b..951e7a81b 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -208,7 +208,7 @@ dev: GOOS=$(shell go env GOOS) dev: GOARCH=$(shell go env GOARCH) dev: GOPATH=$(shell go env GOPATH) dev: DEV_TARGET=pkg/$(GOOS)_$(GOARCH)$(if $(HAS_LXC),-lxc)/nomad -dev: vendorfmt ## Build for the current development platform +dev: vendorfmt changelogfmt ## Build for the current development platform @echo "==> Removing old development build..." @rm -f $(PROJECT_ROOT)/$(DEV_TARGET) @rm -f $(PROJECT_ROOT)/bin/nomad @@ -283,8 +283,8 @@ static-assets: ## Compile the static routes to serve alongside the API .PHONY: test-ui test-ui: ## Run Nomad UI test suite @echo "--> Installing JavaScript assets" + @cd ui && npm rebuild node-sass @cd ui && yarn install - @cd ui && npm install phantomjs-prebuilt @echo "--> Running ember tests" @cd ui && phantomjs --version @cd ui && npm test diff --git a/api/resources.go b/api/resources.go index 8c547ee7e..1abcf209d 100644 --- a/api/resources.go +++ b/api/resources.go @@ -49,7 +49,7 @@ func DefaultResources() *Resources { // IN nomad/structs/structs.go and should be kept in sync. func MinResources() *Resources { return &Resources{ - CPU: helper.IntToPtr(100), + CPU: helper.IntToPtr(20), MemoryMB: helper.IntToPtr(10), IOPS: helper.IntToPtr(0), } diff --git a/api/tasks.go b/api/tasks.go index 7dc2950b1..a7e3de40a 100644 --- a/api/tasks.go +++ b/api/tasks.go @@ -128,15 +128,15 @@ func (c *CheckRestart) Merge(o *CheckRestart) *CheckRestart { return nc } - if nc.Limit == 0 { + if o.Limit > 0 { nc.Limit = o.Limit } - if nc.Grace == nil { + if o.Grace != nil { nc.Grace = o.Grace } - if nc.IgnoreWarnings { + if o.IgnoreWarnings { nc.IgnoreWarnings = o.IgnoreWarnings } @@ -185,13 +185,11 @@ func (s *Service) Canonicalize(t *Task, tg *TaskGroup, job *Job) { s.AddressMode = "auto" } - s.CheckRestart.Canonicalize() - // Canonicallize CheckRestart on Checks and merge Service.CheckRestart // into each check. - for _, c := range s.Checks { - c.CheckRestart.Canonicalize() - c.CheckRestart = c.CheckRestart.Merge(s.CheckRestart) + for i, check := range s.Checks { + s.Checks[i].CheckRestart = s.CheckRestart.Merge(check.CheckRestart) + s.Checks[i].CheckRestart.Canonicalize() } } diff --git a/api/tasks_test.go b/api/tasks_test.go index d870eab27..7542c6094 100644 --- a/api/tasks_test.go +++ b/api/tasks_test.go @@ -3,6 +3,7 @@ package api import ( "reflect" "testing" + "time" "github.com/hashicorp/nomad/helper" "github.com/stretchr/testify/assert" @@ -266,3 +267,51 @@ func TestTaskGroup_Canonicalize_Update(t *testing.T) { tg.Canonicalize(job) assert.Nil(t, tg.Update) } + +// TestService_CheckRestart asserts Service.CheckRestart settings are properly +// inherited by Checks. +func TestService_CheckRestart(t *testing.T) { + job := &Job{Name: helper.StringToPtr("job")} + tg := &TaskGroup{Name: helper.StringToPtr("group")} + task := &Task{Name: "task"} + service := &Service{ + CheckRestart: &CheckRestart{ + Limit: 11, + Grace: helper.TimeToPtr(11 * time.Second), + IgnoreWarnings: true, + }, + Checks: []ServiceCheck{ + { + Name: "all-set", + CheckRestart: &CheckRestart{ + Limit: 22, + Grace: helper.TimeToPtr(22 * time.Second), + IgnoreWarnings: true, + }, + }, + { + Name: "some-set", + CheckRestart: &CheckRestart{ + Limit: 33, + Grace: helper.TimeToPtr(33 * time.Second), + }, + }, + { + Name: "unset", + }, + }, + } + + service.Canonicalize(task, tg, job) + assert.Equal(t, service.Checks[0].CheckRestart.Limit, 22) + assert.Equal(t, *service.Checks[0].CheckRestart.Grace, 22*time.Second) + assert.True(t, service.Checks[0].CheckRestart.IgnoreWarnings) + + assert.Equal(t, service.Checks[1].CheckRestart.Limit, 33) + assert.Equal(t, *service.Checks[1].CheckRestart.Grace, 33*time.Second) + assert.True(t, service.Checks[1].CheckRestart.IgnoreWarnings) + + assert.Equal(t, service.Checks[2].CheckRestart.Limit, 11) + assert.Equal(t, *service.Checks[2].CheckRestart.Grace, 11*time.Second) + assert.True(t, service.Checks[2].CheckRestart.IgnoreWarnings) +} diff --git a/client/alloc_watcher.go b/client/alloc_watcher.go index 03aa948f2..ecac7190f 100644 --- a/client/alloc_watcher.go +++ b/client/alloc_watcher.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "sync" + "syscall" "time" "github.com/hashicorp/consul/lib" @@ -452,6 +453,9 @@ func (p *remotePrevAlloc) streamAllocDir(ctx context.Context, resp io.ReadCloser tr := tar.NewReader(resp) defer resp.Close() + // Cache effective uid as we only run Chown if we're root + euid := syscall.Geteuid() + canceled := func() bool { select { case <-ctx.Done(): @@ -495,7 +499,15 @@ func (p *remotePrevAlloc) streamAllocDir(ctx context.Context, resp io.ReadCloser // If the header is for a directory we create the directory if hdr.Typeflag == tar.TypeDir { - os.MkdirAll(filepath.Join(dest, hdr.Name), os.FileMode(hdr.Mode)) + name := filepath.Join(dest, hdr.Name) + os.MkdirAll(name, os.FileMode(hdr.Mode)) + + // Can't change owner if not root or on Windows. + if euid == 0 { + if err := os.Chown(name, hdr.Uid, hdr.Gid); err != nil { + return fmt.Errorf("error chowning directory %v", err) + } + } continue } // If the header is for a symlink we create the symlink @@ -517,9 +529,13 @@ func (p *remotePrevAlloc) streamAllocDir(ctx context.Context, resp io.ReadCloser f.Close() return fmt.Errorf("error chmoding file %v", err) } - if err := f.Chown(hdr.Uid, hdr.Gid); err != nil { - f.Close() - return fmt.Errorf("error chowning file %v", err) + + // Can't change owner if not root or on Windows. + if euid == 0 { + if err := f.Chown(hdr.Uid, hdr.Gid); err != nil { + f.Close() + return fmt.Errorf("error chowning file %v", err) + } } // We write in chunks so that we can test if the client diff --git a/client/alloc_watcher_test.go b/client/alloc_watcher_test.go index 0ecb46865..45972e759 100644 --- a/client/alloc_watcher_test.go +++ b/client/alloc_watcher_test.go @@ -10,11 +10,13 @@ import ( "os" "path/filepath" "strings" + "syscall" "testing" "time" "github.com/hashicorp/nomad/client/allocdir" "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/testutil" "github.com/hashicorp/nomad/nomad/mock" ) @@ -73,6 +75,7 @@ func TestPrevAlloc_LocalPrevAlloc(t *testing.T) { // TestPrevAlloc_StreamAllocDir_Ok asserts that streaming a tar to an alloc dir // works. func TestPrevAlloc_StreamAllocDir_Ok(t *testing.T) { + testutil.RequireRoot(t) t.Parallel() dir, err := ioutil.TempDir("", "") if err != nil { @@ -80,18 +83,29 @@ func TestPrevAlloc_StreamAllocDir_Ok(t *testing.T) { } defer os.RemoveAll(dir) - if err := os.Mkdir(filepath.Join(dir, "foo"), 0777); err != nil { + // Create foo/ + fooDir := filepath.Join(dir, "foo") + if err := os.Mkdir(fooDir, 0777); err != nil { t.Fatalf("err: %v", err) } - dirInfo, err := os.Stat(filepath.Join(dir, "foo")) + + // Change ownership of foo/ to test #3702 (any non-root user is fine) + const uid, gid = 1, 1 + if err := os.Chown(fooDir, uid, gid); err != nil { + t.Fatalf("err : %v", err) + } + + dirInfo, err := os.Stat(fooDir) if err != nil { t.Fatalf("err: %v", err) } - f, err := os.Create(filepath.Join(dir, "foo", "bar")) + + // Create foo/bar + f, err := os.Create(filepath.Join(fooDir, "bar")) if err != nil { t.Fatalf("err: %v", err) } - if _, err := f.WriteString("foo"); err != nil { + if _, err := f.WriteString("123"); err != nil { t.Fatalf("err: %v", err) } if err := f.Chmod(0644); err != nil { @@ -102,6 +116,8 @@ func TestPrevAlloc_StreamAllocDir_Ok(t *testing.T) { t.Fatalf("err: %v", err) } f.Close() + + // Create foo/baz -> bar symlink if err := os.Symlink("bar", filepath.Join(dir, "foo", "baz")); err != nil { t.Fatalf("err: %v", err) } @@ -181,6 +197,11 @@ func TestPrevAlloc_StreamAllocDir_Ok(t *testing.T) { if fi.Mode() != dirInfo.Mode() { t.Fatalf("mode: %v", fi.Mode()) } + stat := fi.Sys().(*syscall.Stat_t) + if stat.Uid != uid || stat.Gid != gid { + t.Fatalf("foo/ has incorrect ownership: expected %d:%d found %d:%d", + uid, gid, stat.Uid, stat.Gid) + } fi1, err := os.Stat(filepath.Join(dir1, "bar")) if err != nil { diff --git a/client/driver/driver_test.go b/client/driver/driver_test.go index c2d59541c..7e9a43534 100644 --- a/client/driver/driver_test.go +++ b/client/driver/driver_test.go @@ -24,13 +24,6 @@ var basicResources = &structs.Resources{ CPU: 250, MemoryMB: 256, DiskMB: 20, - Networks: []*structs.NetworkResource{ - { - IP: "0.0.0.0", - ReservedPorts: []structs.Port{{Label: "main", Value: 12345}}, - DynamicPorts: []structs.Port{{Label: "HTTP", Value: 43330}}, - }, - }, } func init() { diff --git a/client/driver/executor/executor_linux_test.go b/client/driver/executor/executor_linux_test.go index 905d88000..ad73f13e3 100644 --- a/client/driver/executor/executor_linux_test.go +++ b/client/driver/executor/executor_linux_test.go @@ -69,7 +69,7 @@ func TestExecutor_IsolationAndConstraints(t *testing.T) { execCmd.FSIsolation = true execCmd.ResourceLimits = true - execCmd.User = dstructs.DefaultUnpriviledgedUser + execCmd.User = dstructs.DefaultUnprivilegedUser executor := NewExecutor(log.New(os.Stdout, "", log.LstdFlags)) diff --git a/client/driver/java.go b/client/driver/java.go index 0c660547c..8c162e0cd 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -116,7 +116,7 @@ func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, // Only enable if we are root and cgroups are mounted when running on linux systems. if runtime.GOOS == "linux" && (syscall.Geteuid() != 0 || !cgroupsMounted(node)) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { - d.logger.Printf("[DEBUG] driver.java: root priviledges and mounted cgroups required on linux, disabling") + d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling") } delete(node.Attributes, "driver.java") d.fingerprintSuccess = helper.BoolToPtr(false) diff --git a/client/driver/lxc.go b/client/driver/lxc.go index 36c6e0e99..fefb6f2fb 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -31,6 +31,11 @@ const ( // Config.Options map. lxcConfigOption = "driver.lxc.enable" + // lxcVolumesConfigOption is the key for enabling the use of + // custom bind volumes to arbitrary host paths + lxcVolumesConfigOption = "lxc.volumes.enabled" + lxcVolumesConfigDefault = true + // containerMonitorIntv is the interval at which the driver checks if the // container is still alive containerMonitorIntv = 2 * time.Second @@ -69,6 +74,7 @@ type LxcDriverConfig struct { TemplateArgs []string `mapstructure:"template_args"` LogLevel string `mapstructure:"log_level"` Verbosity string + Volumes []string `mapstructure:"volumes"` } // NewLxcDriver returns a new instance of the LXC driver @@ -137,6 +143,10 @@ func (d *LxcDriver) Validate(config map[string]interface{}) error { Type: fields.TypeString, Required: false, }, + "volumes": { + Type: fields.TypeArray, + Required: false, + }, }, } @@ -144,6 +154,21 @@ func (d *LxcDriver) Validate(config map[string]interface{}) error { return err } + volumes, _ := fd.GetOk("volumes") + for _, volDesc := range volumes.([]interface{}) { + volStr := volDesc.(string) + paths := strings.Split(volStr, ":") + if len(paths) != 2 { + return fmt.Errorf("invalid volume bind mount entry: '%s'", volStr) + } + if len(paths[0]) == 0 || len(paths[1]) == 0 { + return fmt.Errorf("invalid volume bind mount entry: '%s'", volStr) + } + if paths[1][0] == '/' { + return fmt.Errorf("unsupported absolute container mount point: '%s'", paths[1]) + } + } + return nil } @@ -170,6 +195,12 @@ func (d *LxcDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, e } node.Attributes["driver.lxc.version"] = version node.Attributes["driver.lxc"] = "1" + + // Advertise if this node supports lxc volumes + if d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) { + node.Attributes["driver."+lxcVolumesConfigOption] = "1" + } + return true, nil } @@ -250,6 +281,25 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, fmt.Sprintf("%s alloc none rw,bind,create=dir", ctx.TaskDir.SharedAllocDir), fmt.Sprintf("%s secrets none rw,bind,create=dir", ctx.TaskDir.SecretsDir), } + + volumesEnabled := d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) + + for _, volDesc := range driverConfig.Volumes { + // the format was checked in Validate() + paths := strings.Split(volDesc, ":") + + if filepath.IsAbs(paths[0]) { + if !volumesEnabled { + return nil, fmt.Errorf("absolute bind-mount volume in config but '%v' is false", lxcVolumesConfigOption) + } + } else { + // Relative source paths are treated as relative to alloc dir + paths[0] = filepath.Join(ctx.TaskDir.Dir, paths[0]) + } + + mounts = append(mounts, fmt.Sprintf("%s %s none rw,bind,create=dir", paths[0], paths[1])) + } + for _, mnt := range mounts { if err := c.SetConfigItem("lxc.mount.entry", mnt); err != nil { return nil, fmt.Errorf("error setting bind mount %q error: %v", mnt, err) diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index ddf8260fd..ddc78193c 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -3,13 +3,17 @@ package driver import ( + "bytes" "fmt" + "io/ioutil" "os" + "os/exec" "path/filepath" "testing" "time" "github.com/hashicorp/nomad/client/config" + ctestutil "github.com/hashicorp/nomad/client/testutil" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" lxc "gopkg.in/lxc/go-lxc.v2" @@ -61,17 +65,32 @@ func TestLxcDriver_Start_Wait(t *testing.T) { if !lxcPresent(t) { t.Skip("lxc not present") } + ctestutil.RequireRoot(t) task := &structs.Task{ Name: "foo", Driver: "lxc", Config: map[string]interface{}{ "template": "/usr/share/lxc/templates/lxc-busybox", + "volumes": []string{"/tmp/:mnt/tmp"}, }, KillTimeout: 10 * time.Second, Resources: structs.DefaultResources(), } + testFileContents := []byte("this should be visible under /mnt/tmp") + tmpFile, err := ioutil.TempFile("/tmp", "testlxcdriver_start_wait") + if err != nil { + t.Fatalf("error writing temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + if _, err := tmpFile.Write(testFileContents); err != nil { + t.Fatalf("error writing temp file: %v", err) + } + if err := tmpFile.Close(); err != nil { + t.Fatalf("error closing temp file: %v", err) + } + ctx := testDriverContexts(t, task) defer ctx.AllocDir.Destroy() d := NewLxcDriver(ctx.DriverCtx) @@ -104,7 +123,7 @@ func TestLxcDriver_Start_Wait(t *testing.T) { // Look for mounted directories in their proper location containerName := fmt.Sprintf("%s-%s", task.Name, ctx.DriverCtx.allocID) - for _, mnt := range []string{"alloc", "local", "secrets"} { + for _, mnt := range []string{"alloc", "local", "secrets", "mnt/tmp"} { fullpath := filepath.Join(lxcHandle.lxcPath, containerName, "rootfs", mnt) stat, err := os.Stat(fullpath) if err != nil { @@ -115,6 +134,16 @@ func TestLxcDriver_Start_Wait(t *testing.T) { } } + // Test that /mnt/tmp/$tempFile exists in the container: + mountedContents, err := exec.Command("lxc-attach", "-n", containerName, "--", "cat", filepath.Join("/mnt/", tmpFile.Name())).Output() + if err != nil { + t.Fatalf("err reading temp file in bind mount: %v", err) + } + + if !bytes.Equal(mountedContents, testFileContents) { + t.Fatalf("contents of temp bind mounted file did not match, was '%s'", mountedContents) + } + // Desroy the container if err := sresp.Handle.Kill(); err != nil { t.Fatalf("err: %v", err) @@ -137,6 +166,7 @@ func TestLxcDriver_Open_Wait(t *testing.T) { if !lxcPresent(t) { t.Skip("lxc not present") } + ctestutil.RequireRoot(t) task := &structs.Task{ Name: "foo", @@ -197,3 +227,98 @@ func TestLxcDriver_Open_Wait(t *testing.T) { func lxcPresent(t *testing.T) bool { return lxc.Version() != "" } + +func TestLxcDriver_Volumes_ConfigValidation(t *testing.T) { + if !testutil.IsTravis() { + t.Parallel() + } + if !lxcPresent(t) { + t.Skip("lxc not present") + } + ctestutil.RequireRoot(t) + + brokenVolumeConfigs := [][]string{ + { + "foo:/var", + }, + { + ":", + }, + { + "abc:", + }, + { + ":def", + }, + { + "abc:def:ghi", + }, + } + + for _, bc := range brokenVolumeConfigs { + if err := testVolumeConfig(t, bc); err == nil { + t.Fatalf("error expected in validate for config %+v", bc) + } + } + if err := testVolumeConfig(t, []string{"abc:def"}); err != nil { + t.Fatalf("error in validate for syntactically valid config abc:def") + } +} + +func testVolumeConfig(t *testing.T, volConfig []string) error { + task := &structs.Task{ + Name: "voltest", + Driver: "lxc", + KillTimeout: 10 * time.Second, + Resources: structs.DefaultResources(), + Config: map[string]interface{}{ + "template": "busybox", + }, + } + task.Config["volumes"] = volConfig + + ctx := testDriverContexts(t, task) + defer ctx.AllocDir.Destroy() + + driver := NewLxcDriver(ctx.DriverCtx) + + err := driver.Validate(task.Config) + return err + +} + +func TestLxcDriver_Start_NoVolumes(t *testing.T) { + if !testutil.IsTravis() { + t.Parallel() + } + if !lxcPresent(t) { + t.Skip("lxc not present") + } + ctestutil.RequireRoot(t) + + task := &structs.Task{ + Name: "foo", + Driver: "lxc", + Config: map[string]interface{}{ + "template": "/usr/share/lxc/templates/lxc-busybox", + "volumes": []string{"/tmp/:mnt/tmp"}, + }, + KillTimeout: 10 * time.Second, + Resources: structs.DefaultResources(), + } + + ctx := testDriverContexts(t, task) + defer ctx.AllocDir.Destroy() + + ctx.DriverCtx.config.Options = map[string]string{lxcVolumesConfigOption: "false"} + + d := NewLxcDriver(ctx.DriverCtx) + + if _, err := d.Prestart(ctx.ExecCtx, task); err != nil { + t.Fatalf("prestart err: %v", err) + } + _, err := d.Start(ctx.ExecCtx, task) + if err == nil { + t.Fatalf("expected error in start, got nil.") + } +} diff --git a/client/driver/qemu_test.go b/client/driver/qemu_test.go index b0978ec9c..ebab5dae6 100644 --- a/client/driver/qemu_test.go +++ b/client/driver/qemu_test.go @@ -127,6 +127,7 @@ func TestQemuDriver_GracefulShutdown(t *testing.T) { t.Parallel() } ctestutils.QemuCompatible(t) + ctestutils.RequireRoot(t) task := &structs.Task{ Name: "linux", Driver: "qemu", diff --git a/client/driver/structs/structs.go b/client/driver/structs/structs.go index aed1e831a..6966205f7 100644 --- a/client/driver/structs/structs.go +++ b/client/driver/structs/structs.go @@ -7,7 +7,7 @@ import ( const ( // The default user that the executor uses to run tasks - DefaultUnpriviledgedUser = "nobody" + DefaultUnprivilegedUser = "nobody" // CheckBufSize is the size of the check output result CheckBufSize = 4 * 1024 diff --git a/client/driver/utils.go b/client/driver/utils.go index 23a64c89c..5fba8071f 100644 --- a/client/driver/utils.go +++ b/client/driver/utils.go @@ -178,7 +178,7 @@ func GetAbsolutePath(bin string) (string, error) { // dstructs.DefaultUnprivilegedUser if none was given. func getExecutorUser(task *structs.Task) string { if task.User == "" { - return dstructs.DefaultUnpriviledgedUser + return dstructs.DefaultUnprivilegedUser } return task.User } diff --git a/client/testutil/driver_compatible.go b/client/testutil/driver_compatible.go index 996fca131..97f994c44 100644 --- a/client/testutil/driver_compatible.go +++ b/client/testutil/driver_compatible.go @@ -7,6 +7,13 @@ import ( "testing" ) +// RequireRoot skips tests unless running on a Unix as root. +func RequireRoot(t *testing.T) { + if syscall.Geteuid() != 0 { + t.Skip("Must run as root on Unix") + } +} + func ExecCompatible(t *testing.T) { if runtime.GOOS != "linux" || syscall.Geteuid() != 0 { t.Skip("Test only available running as root on linux") diff --git a/client/vaultclient/vaultclient.go b/client/vaultclient/vaultclient.go index 7fe7958ed..af78eea64 100644 --- a/client/vaultclient/vaultclient.go +++ b/client/vaultclient/vaultclient.go @@ -428,6 +428,7 @@ func (c *vaultClient) renew(req *vaultClientRenewalRequest) error { fatal := false if renewalErr != nil && (strings.Contains(renewalErr.Error(), "lease not found or lease is not renewable") || + strings.Contains(renewalErr.Error(), "lease is not renewable") || strings.Contains(renewalErr.Error(), "token not found") || strings.Contains(renewalErr.Error(), "permission denied")) { fatal = true diff --git a/client/vaultclient/vaultclient_test.go b/client/vaultclient/vaultclient_test.go index dc0b63aa8..7322d3351 100644 --- a/client/vaultclient/vaultclient_test.go +++ b/client/vaultclient/vaultclient_test.go @@ -3,6 +3,7 @@ package vaultclient import ( "log" "os" + "strings" "testing" "time" @@ -197,3 +198,85 @@ func TestVaultClient_Heap(t *testing.T) { } } + +func TestVaultClient_RenewNonRenewableLease(t *testing.T) { + t.Parallel() + v := testutil.NewTestVault(t) + defer v.Stop() + + logger := log.New(os.Stderr, "TEST: ", log.Lshortfile|log.LstdFlags) + v.Config.ConnectionRetryIntv = 100 * time.Millisecond + v.Config.TaskTokenTTL = "4s" + c, err := NewVaultClient(v.Config, logger, nil) + if err != nil { + t.Fatalf("failed to build vault client: %v", err) + } + + c.Start() + defer c.Stop() + + // Sleep a little while to ensure that the renewal loop is active + time.Sleep(time.Duration(testutil.TestMultiplier()) * time.Second) + + tcr := &vaultapi.TokenCreateRequest{ + Policies: []string{"foo", "bar"}, + TTL: "2s", + DisplayName: "derived-for-task", + Renewable: new(bool), + } + + c.client.SetToken(v.Config.Token) + + if err := c.client.SetAddress(v.Config.Addr); err != nil { + t.Fatal(err) + } + + secret, err := c.client.Auth().Token().Create(tcr) + if err != nil { + t.Fatalf("failed to create vault token: %v", err) + } + + if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatal("failed to derive a wrapped vault token") + } + + _, err = c.RenewToken(secret.Auth.ClientToken, secret.Auth.LeaseDuration) + if err == nil { + t.Fatalf("expected error, got nil") + } else if !strings.Contains(err.Error(), "lease is not renewable") { + t.Fatalf("expected \"%s\" in error message, got \"%v\"", "lease is not renewable", err) + } +} + +func TestVaultClient_RenewNonExistentLease(t *testing.T) { + t.Parallel() + v := testutil.NewTestVault(t) + defer v.Stop() + + logger := log.New(os.Stderr, "TEST: ", log.Lshortfile|log.LstdFlags) + v.Config.ConnectionRetryIntv = 100 * time.Millisecond + v.Config.TaskTokenTTL = "4s" + c, err := NewVaultClient(v.Config, logger, nil) + if err != nil { + t.Fatalf("failed to build vault client: %v", err) + } + + c.Start() + defer c.Stop() + + // Sleep a little while to ensure that the renewal loop is active + time.Sleep(time.Duration(testutil.TestMultiplier()) * time.Second) + + c.client.SetToken(v.Config.Token) + + if err := c.client.SetAddress(v.Config.Addr); err != nil { + t.Fatal(err) + } + + _, err = c.RenewToken(c.client.Token(), 10) + if err == nil { + t.Fatalf("expected error, got nil") + } else if !strings.Contains(err.Error(), "lease not found") { + t.Fatalf("expected \"%s\" in error message, got \"%v\"", "lease not found", err) + } +} diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 51110ab60..400b57615 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -1,6 +1,7 @@ package agent import ( + "fmt" "io/ioutil" "net" "os" @@ -9,6 +10,7 @@ import ( "testing" "time" + "github.com/hashicorp/consul/lib/freeport" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs/config" ) @@ -541,7 +543,8 @@ func TestConfig_Listener(t *testing.T) { } // Works with valid inputs - ln, err := config.Listener("tcp", "127.0.0.1", 24000) + ports := freeport.GetT(t, 2) + ln, err := config.Listener("tcp", "127.0.0.1", ports[0]) if err != nil { t.Fatalf("err: %s", err) } @@ -550,20 +553,22 @@ func TestConfig_Listener(t *testing.T) { if net := ln.Addr().Network(); net != "tcp" { t.Fatalf("expected tcp, got: %q", net) } - if addr := ln.Addr().String(); addr != "127.0.0.1:24000" { - t.Fatalf("expected 127.0.0.1:4646, got: %q", addr) + want := fmt.Sprintf("127.0.0.1:%d", ports[0]) + if addr := ln.Addr().String(); addr != want { + t.Fatalf("expected %q, got: %q", want, addr) } // Falls back to default bind address if non provided config.BindAddr = "0.0.0.0" - ln, err = config.Listener("tcp4", "", 24000) + ln, err = config.Listener("tcp4", "", ports[1]) if err != nil { t.Fatalf("err: %s", err) } ln.Close() - if addr := ln.Addr().String(); addr != "0.0.0.0:24000" { - t.Fatalf("expected 0.0.0.0:24000, got: %q", addr) + want = fmt.Sprintf("0.0.0.0:%d", ports[1]) + if addr := ln.Addr().String(); addr != want { + t.Fatalf("expected %q, got: %q", want, addr) } } diff --git a/command/agent/consul/client.go b/command/agent/consul/client.go index c22cb8d74..b8db963ae 100644 --- a/command/agent/consul/client.go +++ b/command/agent/consul/client.go @@ -663,7 +663,7 @@ func (c *ServiceClient) checkRegs(ops *operations, allocID, serviceID string, se ip, port, err := getAddress(addrMode, portLabel, task.Resources.Networks, net) if err != nil { - return nil, fmt.Errorf("unable to get address for check %q: %v", check.Name, err) + return nil, fmt.Errorf("error getting address for check %q: %v", check.Name, err) } checkReg, err := createCheckReg(serviceID, checkID, check, ip, port) @@ -1036,6 +1036,11 @@ func createCheckReg(serviceID, checkID string, check *structs.ServiceCheck, host chkReg.Timeout = check.Timeout.String() chkReg.Interval = check.Interval.String() + // Require an address for http or tcp checks + if port == 0 && check.RequiresPort() { + return nil, fmt.Errorf("%s checks require an address", check.Type) + } + switch check.Type { case structs.ServiceCheckHTTP: proto := check.Protocol @@ -1089,9 +1094,15 @@ func isOldNomadService(id string) bool { return strings.HasPrefix(id, prefix) } -// getAddress returns the ip and port to use for a service or check. An error -// is returned if an ip and port cannot be determined. +// getAddress returns the IP and port to use for a service or check. If no port +// label is specified (an empty value), zero values are returned because no +// address could be resolved. func getAddress(addrMode, portLabel string, networks structs.Networks, driverNet *cstructs.DriverNetwork) (string, int, error) { + // No port label specified, no address can be assembled + if portLabel == "" { + return "", 0, nil + } + switch addrMode { case structs.AddressModeAuto: if driverNet.Advertise() { diff --git a/command/agent/consul/int_test.go b/command/agent/consul/int_test.go index ba9975532..ee0fff748 100644 --- a/command/agent/consul/int_test.go +++ b/command/agent/consul/int_test.go @@ -87,8 +87,17 @@ func TestConsul_Integration(t *testing.T) { task.Config = map[string]interface{}{ "run_for": "1h", } + // Choose a port that shouldn't be in use - task.Resources.Networks[0].ReservedPorts = []structs.Port{{Label: "http", Value: 3}} + netResource := &structs.NetworkResource{ + Device: "eth0", + IP: "127.0.0.1", + MBits: 50, + ReservedPorts: []structs.Port{{Label: "http", Value: 3}}, + } + alloc.Resources.Networks[0] = netResource + alloc.TaskResources["web"].Networks[0] = netResource + task.Resources.Networks[0] = netResource task.Services = []*structs.Service{ { Name: "httpd", @@ -96,13 +105,12 @@ func TestConsul_Integration(t *testing.T) { Tags: []string{"nomad", "test", "http"}, Checks: []*structs.ServiceCheck{ { - Name: "httpd-http-check", - Type: "http", - Path: "/", - Protocol: "http", - PortLabel: "http", - Interval: 9000 * time.Hour, - Timeout: 1, // fail as fast as possible + Name: "httpd-http-check", + Type: "http", + Path: "/", + Protocol: "http", + Interval: 9000 * time.Hour, + Timeout: 1, // fail as fast as possible }, { Name: "httpd-script-check", diff --git a/command/agent/consul/unit_test.go b/command/agent/consul/unit_test.go index 88acc4ee1..4d8123b88 100644 --- a/command/agent/consul/unit_test.go +++ b/command/agent/consul/unit_test.go @@ -1566,8 +1566,13 @@ func TestGetAddress(t *testing.T) { { Name: "InvalidMode", Mode: "invalid-mode", + PortLabel: "80", ErrContains: "invalid address mode", }, + { + Name: "EmptyIsOk", + Mode: structs.AddressModeHost, + }, } for _, tc := range cases { diff --git a/command/agent/http.go b/command/agent/http.go index eafd5309e..8dbf62fc8 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -11,6 +11,7 @@ import ( "net/http/pprof" "os" "strconv" + "strings" "time" "github.com/NYTimes/gziphandler" @@ -284,17 +285,22 @@ func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Reque if err != nil { s.logger.Printf("[ERR] http: Request %v, error: %v", reqURL, err) code := 500 + errMsg := err.Error() if http, ok := err.(HTTPCodedError); ok { code = http.Code() } else { - switch err.Error() { - case structs.ErrPermissionDenied.Error(), structs.ErrTokenNotFound.Error(): + // RPC errors get wrapped, so manually unwrap by only looking at their suffix + if strings.HasSuffix(errMsg, structs.ErrPermissionDenied.Error()) { + errMsg = structs.ErrPermissionDenied.Error() + code = 403 + } else if strings.HasSuffix(errMsg, structs.ErrTokenNotFound.Error()) { + errMsg = structs.ErrTokenNotFound.Error() code = 403 } } resp.WriteHeader(code) - resp.Write([]byte(err.Error())) + resp.Write([]byte(errMsg)) return } diff --git a/command/agent/http_test.go b/command/agent/http_test.go index 5d4004c18..6c4e637eb 100644 --- a/command/agent/http_test.go +++ b/command/agent/http_test.go @@ -225,15 +225,28 @@ func TestPermissionDenied(t *testing.T) { }) defer s.Shutdown() - resp := httptest.NewRecorder() - handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - return nil, structs.ErrPermissionDenied + { + resp := httptest.NewRecorder() + handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return nil, structs.ErrPermissionDenied + } + + req, _ := http.NewRequest("GET", "/v1/job/foo", nil) + s.Server.wrap(handler)(resp, req) + assert.Equal(t, resp.Code, 403) } - urlStr := "/v1/job/foo" - req, _ := http.NewRequest("GET", urlStr, nil) - s.Server.wrap(handler)(resp, req) - assert.Equal(t, resp.Code, 403) + // When remote RPC is used the errors have "rpc error: " prependend + { + resp := httptest.NewRecorder() + handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + return nil, fmt.Errorf("rpc error: %v", structs.ErrPermissionDenied) + } + + req, _ := http.NewRequest("GET", "/v1/job/foo", nil) + s.Server.wrap(handler)(resp, req) + assert.Equal(t, resp.Code, 403) + } } func TestTokenNotFound(t *testing.T) { diff --git a/command/agent/job_endpoint_test.go b/command/agent/job_endpoint_test.go index 019a82ae0..b595e28ab 100644 --- a/command/agent/job_endpoint_test.go +++ b/command/agent/job_endpoint_test.go @@ -1212,6 +1212,10 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { Name: "serviceA", Tags: []string{"1", "2"}, PortLabel: "foo", + CheckRestart: &api.CheckRestart{ + Limit: 4, + Grace: helper.TimeToPtr(11 * time.Second), + }, Checks: []api.ServiceCheck{ { Id: "hello", @@ -1228,10 +1232,17 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { InitialStatus: "ok", CheckRestart: &api.CheckRestart{ Limit: 3, - Grace: helper.TimeToPtr(10 * time.Second), IgnoreWarnings: true, }, }, + { + Id: "check2id", + Name: "check2", + Type: "tcp", + PortLabel: "foo", + Interval: 4 * time.Second, + Timeout: 2 * time.Second, + }, }, }, }, @@ -1425,10 +1436,21 @@ func TestJobs_ApiJobToStructsJob(t *testing.T) { InitialStatus: "ok", CheckRestart: &structs.CheckRestart{ Limit: 3, - Grace: 10 * time.Second, + Grace: 11 * time.Second, IgnoreWarnings: true, }, }, + { + Name: "check2", + Type: "tcp", + PortLabel: "foo", + Interval: 4 * time.Second, + Timeout: 2 * time.Second, + CheckRestart: &structs.CheckRestart{ + Limit: 4, + Grace: 11 * time.Second, + }, + }, }, }, }, diff --git a/demo/tls/GNUmakefile b/demo/tls/GNUmakefile new file mode 100644 index 000000000..a33ae4a6e --- /dev/null +++ b/demo/tls/GNUmakefile @@ -0,0 +1,56 @@ +SHELL = bash + +.PHONY: all +all: \ + ca.pem ca-key.pem ca.csr \ + client.pem client-key.pem client.csr \ + dev.pem dev-key.pem dev.csr \ + server.pem server-key.pem server.csr \ + user.pem user-key.pem user.csr user.pfx + +.PHONY: bootstrap +bootstrap: ## Install dependencies + @echo "==> Updating cfssl..." + go get -u github.com/cloudflare/cfssl/cmd/... + +clean: ## Remove generated files + @echo "==> Removing generated files..." + rm -f \ + ca.pem ca-key.pem ca.csr \ + client.pem client-key.pem client.csr \ + dev.pem dev-key.pem dev.csr \ + server.pem server-key.pem server.csr \ + user.pem user-key.pem user.csr user.pfx + +# Generate Nomad certificate authority +ca.pem ca-key.pem ca.csr: + @echo "==> Removing generated files..." + cfssl gencert -initca ca-csr.json | cfssljson -bare ca + +# Generate Nomad server certificate +server.pem server-key.pem server.csr: + @echo "==> Generating Nomad server certificate..." + cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=cfssl.json \ + -hostname="server.global.nomad,localhost,127.0.0.1" csr.json \ + | cfssljson -bare server + +# Generate Nomad client node certificate +client.pem client-key.pem client.csr: + @echo "==> Generating Nomad client node certificate..." + cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=cfssl.json \ + -hostname="client.global.nomad,localhost,127.0.0.1" csr.json \ + | cfssljson -bare client + +# Generate Nomad combined server and client node certificate +dev.pem dev-key.pem dev.csr: + @echo "==> Generating Nomad server and client node certificate..." + cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=cfssl.json \ + -hostname="server.global.nomad,client.global.nomad,localhost,127.0.0.1" csr.json \ + | cfssljson -bare dev + +# Generate certificates for users (CLI and browsers) +user.pem user-key.pem user.csr user.pfx: + @echo "==> Generating Nomad user certificates..." + cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=cfssl-user.json \ + csr.json | cfssljson -bare user + openssl pkcs12 -export -inkey user-key.pem -in user.pem -out user.pfx -password pass: diff --git a/demo/tls/README.md b/demo/tls/README.md new file mode 100644 index 000000000..8219fed47 --- /dev/null +++ b/demo/tls/README.md @@ -0,0 +1,57 @@ +Demo TLS Configuration +====================== + +**Do _NOT_ use in production. For testing purposes only.** + +See [Securing Nomad](https://www.nomadproject.io/guides/securing-nomad.html) +for a full guide. + +This directory contains sample TLS certificates and configuration to ease +testing of TLS related features. There is a makefile to generate certificates, +and pre-generated are available for use. + +## Files + +| Generated? | File | Description | +| - | ------------- | ---| +| ◻️ | `GNUmakefile` | Makefile to generate certificates | +| ◻️ | `tls-*.hcl` | Nomad TLS configurations | +| ◻️ | `cfssl*.json` | cfssl configuration files | +| ◻️ | `csr*.json` | cfssl certificate generation configurations | +| ☑️ | `ca*.pem` | Certificate Authority certificate and key | +| ☑️ | `client*.pem` | Nomad client node certificate and key | +| ☑️ | `dev*.pem` | Nomad certificate and key for dev agents | +| ☑️ | `server*.pem` | Nomad server certificate and key | +| ☑️ | `user*.pem` | Nomad user (CLI) certificate and key | +| ☑️ | `user.pfx` | Nomad browser PKCS #12 certificate and key *(blank password)* | + +## Usage + +### Agent + +To run a TLS-enabled Nomad agent include the `tls.hcl` configuration file with +either the `-dev` flag or your own configuration file. If you're not running +the `nomad agent` command from *this* directory you will have to edit the paths +in `tls.hcl`. + +```sh +# Run the dev agent with TLS enabled +nomad agent -dev -config=tls-dev.hcl + +# Run a *server* agent with your configuration and TLS enabled +nomad agent -config=path/to/custom.hcl -config=tls-server.hcl + +# Run a *client* agent with your configuration and TLS enabled +nomad agent -config=path/to/custom.hcl -config=tls-client.hcl +``` + +### Browser + +To access the Nomad Web UI when TLS is enabled you will need to import two +certificate files into your browser: + +- `ca.pem` must be imported as a Certificate Authority +- `user.pfx` must be imported as a Client certificate. The password is blank. + +When you access the UI via https://localhost:4646/ you will be prompted to +select the user certificate you imported. diff --git a/demo/tls/ca-csr.json b/demo/tls/ca-csr.json new file mode 100644 index 000000000..ded502e0a --- /dev/null +++ b/demo/tls/ca-csr.json @@ -0,0 +1,19 @@ +{ + "CN": "example.nomad", + "hosts": [ + "example.nomad" + ], + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "US", + "ST": "CA", + "L": "San Francisco", + "OU": "Nomad Demo" + } + ] +} + diff --git a/demo/tls/ca-key.pem b/demo/tls/ca-key.pem new file mode 100644 index 000000000..cc95d7c21 --- /dev/null +++ b/demo/tls/ca-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKsrq20VeBrZ0VOqMJSvvU6E+w7RAbUR7D5RkZSgNKJQoAoGCCqGSM49 +AwEHoUQDQgAEn/hg7ktoFRazpDTMTkN1mEJoCo/wJOlI7XD98WE1wr6U/4q0Wh9F +YuNyfCb2rK2nSrLKra/1R+z3Q+trXJt2cQ== +-----END EC PRIVATE KEY----- diff --git a/demo/tls/ca.csr b/demo/tls/ca.csr new file mode 100644 index 000000000..01f02b2d8 --- /dev/null +++ b/demo/tls/ca.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBRjCB7AIBADBfMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xEzARBgNVBAsTCk5vbWFkIERlbW8xFjAUBgNVBAMTDWV4 +YW1wbGUubm9tYWQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASf+GDuS2gVFrOk +NMxOQ3WYQmgKj/Ak6UjtcP3xYTXCvpT/irRaH0Vi43J8JvasradKssqtr/VH7PdD +62tcm3ZxoCswKQYJKoZIhvcNAQkOMRwwGjAYBgNVHREEETAPgg1leGFtcGxlLm5v +bWFkMAoGCCqGSM49BAMCA0kAMEYCIQDP+rv/peK1JGFzXOzdLmfjjEg2vOFWGccz +iAy63lDurgIhAIF//KajKrghaC1JXmsrqnVHuP40KZLOcAv54Q4PgH1h +-----END CERTIFICATE REQUEST----- diff --git a/demo/tls/ca.pem b/demo/tls/ca.pem new file mode 100644 index 000000000..945638503 --- /dev/null +++ b/demo/tls/ca.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICAzCCAaigAwIBAgIUN0nEio761fu7oRc04wRmlxxY3gowCgYIKoZIzj0EAwIw +XzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFk +MB4XDTE4MDEwOTE4MDgwMFoXDTIzMDEwODE4MDgwMFowXzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpO +b21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFkMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEn/hg7ktoFRazpDTMTkN1mEJoCo/wJOlI7XD98WE1wr6U/4q0 +Wh9FYuNyfCb2rK2nSrLKra/1R+z3Q+trXJt2caNCMEAwDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFKaOK4q82ysmZ7dYMhjbZyphHxx3 +MAoGCCqGSM49BAMCA0kAMEYCIQCLoeQKyg1PsyMzETrw3pBA3H3wXU81peHT1t74 +R63a2gIhALIeUT188aOaLtUMgPaWd7wE14BDhSpLp602jVGCNFkH +-----END CERTIFICATE----- diff --git a/demo/tls/cfssl-user.json b/demo/tls/cfssl-user.json new file mode 100644 index 000000000..0fa751cee --- /dev/null +++ b/demo/tls/cfssl-user.json @@ -0,0 +1,12 @@ +{ + "signing": { + "default": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + } + } +} diff --git a/demo/tls/cfssl.json b/demo/tls/cfssl.json new file mode 100644 index 000000000..6e438c9b9 --- /dev/null +++ b/demo/tls/cfssl.json @@ -0,0 +1,13 @@ +{ + "signing": { + "default": { + "expiry": "87600h", + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } +} diff --git a/demo/tls/client-key.pem b/demo/tls/client-key.pem new file mode 100644 index 000000000..75a665adc --- /dev/null +++ b/demo/tls/client-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIGCce4MNcD+MHx1hQWOARCLQWCPJVhWzrAiI1QV7ftYKoAoGCCqGSM49 +AwEHoUQDQgAEDotF3nv9Stt9Zp5sBv3BNk4936BFBH6eyGAIULRlqSJQUrbc97cf +hcdwrVU0hDJcM98Bpd0R3OhqU7j86rc0FQ== +-----END EC PRIVATE KEY----- diff --git a/demo/tls/client.csr b/demo/tls/client.csr new file mode 100644 index 000000000..eb2821868 --- /dev/null +++ b/demo/tls/client.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBRDCB6wIBADBHMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xEzARBgNVBAsTCk5vbWFkIERlbW8wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAAQOi0Xee/1K231mnmwG/cE2Tj3foEUEfp7IYAhQtGWpIlBS +ttz3tx+Fx3CtVTSEMlwz3wGl3RHc6GpTuPzqtzQVoEIwQAYJKoZIhvcNAQkOMTMw +MTAvBgNVHREEKDAmghNjbGllbnQuZ2xvYmFsLm5vbWFkgglsb2NhbGhvc3SHBH8A +AAEwCgYIKoZIzj0EAwIDSAAwRQIgRr+uu2A1NPkhso3QFWuq9IFf8eCkU6yzkmJI +9R7JZRQCIQDTj2mN3OqJAl1LsMRc2rmD1J7Fp+GvnGmSDT4fcdQ9zA== +-----END CERTIFICATE REQUEST----- diff --git a/demo/tls/client.pem b/demo/tls/client.pem new file mode 100644 index 000000000..67fb7ed54 --- /dev/null +++ b/demo/tls/client.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWjCCAgCgAwIBAgIUDYX/mI1EZQPtc/6kc7Kv2epWDwQwCgYIKoZIzj0EAwIw +XzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFk +MB4XDTE4MDEwOTE4MDgwMFoXDTI4MDEwNzE4MDgwMFowRzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpO +b21hZCBEZW1vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDotF3nv9Stt9Zp5s +Bv3BNk4936BFBH6eyGAIULRlqSJQUrbc97cfhcdwrVU0hDJcM98Bpd0R3OhqU7j8 +6rc0FaOBsTCBrjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFO2ys/83g7JgjwZf5KY4 +nOQojbV1MB8GA1UdIwQYMBaAFKaOK4q82ysmZ7dYMhjbZyphHxx3MC8GA1UdEQQo +MCaCE2NsaWVudC5nbG9iYWwubm9tYWSCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO +PQQDAgNIADBFAiEAu+R+nZv0QXbo5c+vEA+b8wryMWqK9TSkMZmh/BwMriwCIHIJ +o/vUarVvgFLy+9ZITDYgtQxMWGLjm8brPyDiXNEA +-----END CERTIFICATE----- diff --git a/demo/tls/csr.json b/demo/tls/csr.json new file mode 100644 index 000000000..4f8ae5938 --- /dev/null +++ b/demo/tls/csr.json @@ -0,0 +1,10 @@ +{ + "names": [ + { + "C": "US", + "ST": "CA", + "L": "San Francisco", + "OU": "Nomad Demo" + } + ] +} diff --git a/demo/tls/dev-key.pem b/demo/tls/dev-key.pem new file mode 100644 index 000000000..381f686fd --- /dev/null +++ b/demo/tls/dev-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ/MkDicoe6ohduiDoGOwqGXlk2V13fZBwKRB8Ns+2hkoAoGCCqGSM49 +AwEHoUQDQgAEmjMddkSmrwZ5qamlGgn0NpbV09qvhAFmaBtawpGXa3LlPzvauHfm +lRcSEzHzkS1M6NT5eAKjJG8yojGHR78cXQ== +-----END EC PRIVATE KEY----- diff --git a/demo/tls/dev.csr b/demo/tls/dev.csr new file mode 100644 index 000000000..960bde4b4 --- /dev/null +++ b/demo/tls/dev.csr @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBWTCCAQACAQAwRzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQH +Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMFkwEwYHKoZIzj0C +AQYIKoZIzj0DAQcDQgAEmjMddkSmrwZ5qamlGgn0NpbV09qvhAFmaBtawpGXa3Ll +PzvauHfmlRcSEzHzkS1M6NT5eAKjJG8yojGHR78cXaBXMFUGCSqGSIb3DQEJDjFI +MEYwRAYDVR0RBD0wO4ITc2VydmVyLmdsb2JhbC5ub21hZIITY2xpZW50Lmdsb2Jh +bC5ub21hZIIJbG9jYWxob3N0hwR/AAABMAoGCCqGSM49BAMCA0cAMEQCIEPHMv5p +xoNybtEQVprQrq5ymLX3rm1ZMkjH0EiJjk/AAiAsM2DTQtK8LnL0YKVbbmBNBX5g +1JQeTRt/kW7yKq0OeA== +-----END CERTIFICATE REQUEST----- diff --git a/demo/tls/dev.pem b/demo/tls/dev.pem new file mode 100644 index 000000000..ed6e67266 --- /dev/null +++ b/demo/tls/dev.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICbjCCAhWgAwIBAgIUc5S8QB/Kai23mJkU23YD4hoO7zkwCgYIKoZIzj0EAwIw +XzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFk +MB4XDTE4MDEwOTE4MDgwMFoXDTI4MDEwNzE4MDgwMFowRzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpO +b21hZCBEZW1vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmjMddkSmrwZ5qaml +Ggn0NpbV09qvhAFmaBtawpGXa3LlPzvauHfmlRcSEzHzkS1M6NT5eAKjJG8yojGH +R78cXaOBxjCBwzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFBng/OMDB+a/pXc07ZYb +I6OODU5ZMB8GA1UdIwQYMBaAFKaOK4q82ysmZ7dYMhjbZyphHxx3MEQGA1UdEQQ9 +MDuCE3NlcnZlci5nbG9iYWwubm9tYWSCE2NsaWVudC5nbG9iYWwubm9tYWSCCWxv +Y2FsaG9zdIcEfwAAATAKBggqhkjOPQQDAgNHADBEAiAKiyqdAvtQewpuEXLU2VuP +Ifdn+7XK82AoTjOW/BbB0gIgNLusqAft2j7mqDT/LNpUTsl6E7O068METh4I9JlT +nEQ= +-----END CERTIFICATE----- diff --git a/demo/tls/server-key.pem b/demo/tls/server-key.pem new file mode 100644 index 000000000..9ab93fa5a --- /dev/null +++ b/demo/tls/server-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIP5t9f7rjG4tWmGaDkfIul+OiMEcCOp4aK9oOGQPFcv3oAoGCCqGSM49 +AwEHoUQDQgAErP0oL1Eo7dnxsUbaM0O1zTa2XLQTQrt8sfYQKuSxq5f1w3GxgUYJ +wHEpQRK34cNfvZZ1piAde/wBK8rAKCzhoQ== +-----END EC PRIVATE KEY----- diff --git a/demo/tls/server.csr b/demo/tls/server.csr new file mode 100644 index 000000000..647048909 --- /dev/null +++ b/demo/tls/server.csr @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBRTCB6wIBADBHMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xEzARBgNVBAsTCk5vbWFkIERlbW8wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAASs/SgvUSjt2fGxRtozQ7XNNrZctBNCu3yx9hAq5LGrl/XD +cbGBRgnAcSlBErfhw1+9lnWmIB17/AErysAoLOGhoEIwQAYJKoZIhvcNAQkOMTMw +MTAvBgNVHREEKDAmghNzZXJ2ZXIuZ2xvYmFsLm5vbWFkgglsb2NhbGhvc3SHBH8A +AAEwCgYIKoZIzj0EAwIDSQAwRgIhAMpGeIRtFaCxn2Yp8EqRgRT3OnECUv6Mi4+d +Hwn42L2UAiEAzISsF4+Dkemn6KRrOXTv7Anam8fTeoAdqokWV3j4ELQ= +-----END CERTIFICATE REQUEST----- diff --git a/demo/tls/server.pem b/demo/tls/server.pem new file mode 100644 index 000000000..50f6a7706 --- /dev/null +++ b/demo/tls/server.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWjCCAgCgAwIBAgIUJSWExbHzjFPPc/1Eiod55vk+11IwCgYIKoZIzj0EAwIw +XzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFk +MB4XDTE4MDEwOTE4MDgwMFoXDTI4MDEwNzE4MDgwMFowRzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpO +b21hZCBEZW1vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErP0oL1Eo7dnxsUba +M0O1zTa2XLQTQrt8sfYQKuSxq5f1w3GxgUYJwHEpQRK34cNfvZZ1piAde/wBK8rA +KCzhoaOBsTCBrjAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG +CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFLK3byFY7RGvoyYtJ9sM +DUKbriNRMB8GA1UdIwQYMBaAFKaOK4q82ysmZ7dYMhjbZyphHxx3MC8GA1UdEQQo +MCaCE3NlcnZlci5nbG9iYWwubm9tYWSCCWxvY2FsaG9zdIcEfwAAATAKBggqhkjO +PQQDAgNIADBFAiB7aohsv0AOs7dnL9zrUNoeU6/B90+BntrRtk8+NHTpnQIhAL7W +EpQ9vbAxQ/FouOPC5lLd94yYkMbbUmoke3H2vKkd +-----END CERTIFICATE----- diff --git a/demo/tls/tls-client.hcl b/demo/tls/tls-client.hcl new file mode 100644 index 000000000..ee129e1b1 --- /dev/null +++ b/demo/tls/tls-client.hcl @@ -0,0 +1,11 @@ +tls { + http = true + rpc = true + + ca_file = "ca.pem" + cert_file = "client.pem" + key_file = "client-key.pem" + + verify_server_hostname = true + verify_https_client = true +} diff --git a/demo/tls/tls-dev.hcl b/demo/tls/tls-dev.hcl new file mode 100644 index 000000000..e41ba8f32 --- /dev/null +++ b/demo/tls/tls-dev.hcl @@ -0,0 +1,11 @@ +tls { + http = true + rpc = true + + ca_file = "ca.pem" + cert_file = "dev.pem" + key_file = "dev-key.pem" + + verify_server_hostname = true + verify_https_client = true +} diff --git a/demo/tls/tls-server.hcl b/demo/tls/tls-server.hcl new file mode 100644 index 000000000..9e1a80269 --- /dev/null +++ b/demo/tls/tls-server.hcl @@ -0,0 +1,11 @@ +tls { + http = true + rpc = true + + ca_file = "ca.pem" + cert_file = "server.pem" + key_file = "server-key.pem" + + verify_server_hostname = true + verify_https_client = true +} diff --git a/demo/tls/user-key.pem b/demo/tls/user-key.pem new file mode 100644 index 000000000..6e7fa6b42 --- /dev/null +++ b/demo/tls/user-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEILshv6hNINiqJk7iPOBr1rL519YdPah78vK/uTrJm+eYoAoGCCqGSM49 +AwEHoUQDQgAES0uuEUedpQxKop5YTUgtywlx7vWJ5dN5PTa2MRoccEhKTVTg1IxW +S8OJxffyTIYXxAtTiDA4JVStchBf1rl2LQ== +-----END EC PRIVATE KEY----- diff --git a/demo/tls/user.csr b/demo/tls/user.csr new file mode 100644 index 000000000..d83211583 --- /dev/null +++ b/demo/tls/user.csr @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBATCBqQIBADBHMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcT +DVNhbiBGcmFuY2lzY28xEzARBgNVBAsTCk5vbWFkIERlbW8wWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARLS64RR52lDEqinlhNSC3LCXHu9Ynl03k9NrYxGhxwSEpN +VODUjFZLw4nF9/JMhhfEC1OIMDglVK1yEF/WuXYtoAAwCgYIKoZIzj0EAwIDRwAw +RAIgL01k8EVmO9UBLTa5VDTzPmmOBJuB2GAL7KIUc20BVnQCIFNUx7+KblsI6E5Q +qOIZN1QUMPCGedKufHQvZJ9iX5S3 +-----END CERTIFICATE REQUEST----- diff --git a/demo/tls/user.pem b/demo/tls/user.pem new file mode 100644 index 000000000..d92772350 --- /dev/null +++ b/demo/tls/user.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICHjCCAcOgAwIBAgIUeB9kcy9/5oLhHCm0PmBiBe6pybwwCgYIKoZIzj0EAwIw +XzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRMwEQYDVQQLEwpOb21hZCBEZW1vMRYwFAYDVQQDEw1leGFtcGxlLm5vbWFk +MB4XDTE4MDEwOTE4MDgwMFoXDTI4MDEwNzE4MDgwMFowRzELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMwEQYDVQQLEwpO +b21hZCBEZW1vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAES0uuEUedpQxKop5Y +TUgtywlx7vWJ5dN5PTa2MRoccEhKTVTg1IxWS8OJxffyTIYXxAtTiDA4JVStchBf +1rl2LaN1MHMwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwG +A1UdEwEB/wQCMAAwHQYDVR0OBBYEFIjrKUYag+vlAh5h1eJwhsdekvgGMB8GA1Ud +IwQYMBaAFKaOK4q82ysmZ7dYMhjbZyphHxx3MAoGCCqGSM49BAMCA0kAMEYCIQC6 +AZ/eZTHXKOU1sxLTRsK3FHn88DKBqXhHJG/2rbMWEwIhALCC5fi/lTP1lB/EDm1E +j4gRnSu3V03XWZhK6QcdQhr1 +-----END CERTIFICATE----- diff --git a/demo/tls/user.pfx b/demo/tls/user.pfx new file mode 100644 index 000000000..35de38c9d Binary files /dev/null and b/demo/tls/user.pfx differ diff --git a/helper/testlog/testlog.go b/helper/testlog/testlog.go new file mode 100644 index 000000000..7f6c6cb04 --- /dev/null +++ b/helper/testlog/testlog.go @@ -0,0 +1,46 @@ +// Package testlog creates a *log.Logger backed by *testing.T to ease logging +// in tests. This allows logs from components being tested to only be printed +// if the test fails (or the verbose flag is specified). +package testlog + +import ( + "io" + "log" +) + +// LogPrinter is the methods of testing.T (or testing.B) needed by the test +// logger. +type LogPrinter interface { + Logf(format string, args ...interface{}) +} + +// writer implements io.Writer on top of a Logger. +type writer struct { + t LogPrinter +} + +// Write to an underlying Logger. Never returns an error. +func (w *writer) Write(p []byte) (n int, err error) { + w.t.Logf(string(p)) + return len(p), nil +} + +// NewWriter creates a new io.Writer backed by a Logger. +func NewWriter(t LogPrinter) io.Writer { + return &writer{t} +} + +// New returns a new test logger. See https://golang.org/pkg/log/#New +func New(t LogPrinter, prefix string, flag int) *log.Logger { + return log.New(&writer{t}, prefix, flag) +} + +// WithPrefix returns a new test logger with the Lmicroseconds flag set. +func WithPrefix(t LogPrinter, prefix string) *log.Logger { + return New(t, prefix, log.Lmicroseconds) +} + +// NewLog logger with "TEST" prefix and the Lmicroseconds flag. +func Logger(t LogPrinter) *log.Logger { + return WithPrefix(t, "TEST ") +} diff --git a/jobspec/parse.go b/jobspec/parse.go index d25f38bd0..babe41b17 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -912,6 +912,7 @@ func parseServices(jobName string, taskGroupName string, task *api.Task, service "port", "check", "address_mode", + "check_restart", } if err := helper.CheckHCLKeys(o.Val, valid); err != nil { return multierror.Prefix(err, fmt.Sprintf("service (%d) ->", idx)) diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 2dfc890d4..4134e9ee4 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -631,6 +631,42 @@ func TestParse(t *testing.T) { }, false, }, + { + "service-check-restart.hcl", + &api.Job{ + ID: helper.StringToPtr("service_check_restart"), + Name: helper.StringToPtr("service_check_restart"), + Type: helper.StringToPtr("service"), + TaskGroups: []*api.TaskGroup{ + { + Name: helper.StringToPtr("group"), + Tasks: []*api.Task{ + { + Name: "task", + Services: []*api.Service{ + { + Name: "http-service", + CheckRestart: &api.CheckRestart{ + Limit: 3, + Grace: helper.TimeToPtr(10 * time.Second), + IgnoreWarnings: true, + }, + Checks: []api.ServiceCheck{ + { + Name: "random-check", + Type: "tcp", + PortLabel: "9001", + }, + }, + }, + }, + }, + }, + }, + }, + }, + false, + }, } for _, tc := range cases { diff --git a/jobspec/test-fixtures/service-check-restart.hcl b/jobspec/test-fixtures/service-check-restart.hcl new file mode 100644 index 000000000..d34f70003 --- /dev/null +++ b/jobspec/test-fixtures/service-check-restart.hcl @@ -0,0 +1,21 @@ +job "service_check_restart" { + type = "service" + group "group" { + task "task" { + service { + name = "http-service" + check_restart { + limit = 3 + grace = "10s" + ignore_warnings = true + } + check { + name = "random-check" + type = "tcp" + port = "9001" + } + } + } + } +} + diff --git a/nomad/eval_endpoint_test.go b/nomad/eval_endpoint_test.go index 6955182c5..ea0c42a01 100644 --- a/nomad/eval_endpoint_test.go +++ b/nomad/eval_endpoint_test.go @@ -286,6 +286,73 @@ func TestEvalEndpoint_Dequeue_WaitIndex(t *testing.T) { } } +func TestEvalEndpoint_Dequeue_UpdateWaitIndex(t *testing.T) { + // test enqueueing an eval, updating a plan result for the same eval and de-queueing the eval + t.Parallel() + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + + alloc := mock.Alloc() + job := alloc.Job + alloc.Job = nil + + state := s1.fsm.State() + + if err := state.UpsertJob(999, job); err != nil { + t.Fatalf("err: %v", err) + } + + eval := mock.Eval() + eval.JobID = job.ID + + // Create an eval + if err := state.UpsertEvals(1, []*structs.Evaluation{eval}); err != nil { + t.Fatalf("err: %v", err) + } + + s1.evalBroker.Enqueue(eval) + + // Create a plan result and apply it with a later index + res := structs.ApplyPlanResultsRequest{ + AllocUpdateRequest: structs.AllocUpdateRequest{ + Alloc: []*structs.Allocation{alloc}, + Job: job, + }, + EvalID: eval.ID, + } + assert := assert.New(t) + err := state.UpsertPlanResults(1000, &res) + assert.Nil(err) + + // Dequeue the eval + get := &structs.EvalDequeueRequest{ + Schedulers: defaultSched, + SchedulerVersion: scheduler.SchedulerVersion, + WriteRequest: structs.WriteRequest{Region: "global"}, + } + var resp structs.EvalDequeueResponse + if err := msgpackrpc.CallWithCodec(codec, "Eval.Dequeue", get, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + // Ensure outstanding + token, ok := s1.evalBroker.Outstanding(eval.ID) + if !ok { + t.Fatalf("should be outstanding") + } + if token != resp.Token { + t.Fatalf("bad token: %#v %#v", token, resp.Token) + } + + if resp.WaitIndex != 1000 { + t.Fatalf("bad wait index; got %d; want %d", resp.WaitIndex, 1000) + } +} + func TestEvalEndpoint_Dequeue_Version_Mismatch(t *testing.T) { t.Parallel() s1 := testServer(t, func(c *Config) { diff --git a/nomad/fsm.go b/nomad/fsm.go index f1e84fae8..61c14bfe4 100644 --- a/nomad/fsm.go +++ b/nomad/fsm.go @@ -1123,7 +1123,7 @@ func (n *nomadFSM) Restore(old io.ReadCloser) error { return nil } -// reconcileSummaries re-calculates the queued allocations for every job that we +// reconcileQueuedAllocations re-calculates the queued allocations for every job that we // created a Job Summary during the snap shot restore func (n *nomadFSM) reconcileQueuedAllocations(index uint64) error { // Get all the jobs @@ -1161,7 +1161,7 @@ func (n *nomadFSM) reconcileQueuedAllocations(index uint64) error { Status: structs.EvalStatusPending, AnnotatePlan: true, } - + snap.UpsertEvals(100, []*structs.Evaluation{eval}) // Create the scheduler and run it sched, err := scheduler.NewScheduler(eval.Type, n.logger, snap, planner) if err != nil { diff --git a/nomad/fsm_test.go b/nomad/fsm_test.go index 63a48f3eb..90c0b6c12 100644 --- a/nomad/fsm_test.go +++ b/nomad/fsm_test.go @@ -945,9 +945,14 @@ func TestFSM_UpsertAllocs_StrippedResources(t *testing.T) { fsm := testFSM(t) alloc := mock.Alloc() + + // Need to remove mock dynamic port from alloc as it won't be computed + // in this test + alloc.TaskResources["web"].Networks[0].DynamicPorts[0].Value = 0 + fsm.State().UpsertJobSummary(1, mock.JobSummary(alloc.JobID)) job := alloc.Job - resources := alloc.Resources + origResources := alloc.Resources alloc.Resources = nil req := structs.AllocUpdateRequest{ Job: job, @@ -974,10 +979,10 @@ func TestFSM_UpsertAllocs_StrippedResources(t *testing.T) { alloc.AllocModifyIndex = out.AllocModifyIndex // Resources should be recomputed - resources.DiskMB = alloc.Job.TaskGroups[0].EphemeralDisk.SizeMB - alloc.Resources = resources + origResources.DiskMB = alloc.Job.TaskGroups[0].EphemeralDisk.SizeMB + alloc.Resources = origResources if !reflect.DeepEqual(alloc, out) { - t.Fatalf("bad: %#v %#v", alloc, out) + t.Fatalf("not equal: % #v", pretty.Diff(alloc, out)) } } @@ -1214,6 +1219,10 @@ func TestFSM_ApplyPlanResults(t *testing.T) { alloc.DeploymentID = d.ID + eval := mock.Eval() + eval.JobID = job.ID + fsm.State().UpsertEvals(1, []*structs.Evaluation{eval}) + fsm.State().UpsertJobSummary(1, mock.JobSummary(alloc.JobID)) req := structs.ApplyPlanResultsRequest{ AllocUpdateRequest: structs.AllocUpdateRequest{ @@ -1221,6 +1230,7 @@ func TestFSM_ApplyPlanResults(t *testing.T) { Alloc: []*structs.Allocation{alloc}, }, Deployment: d, + EvalID: eval.ID, } buf, err := structs.Encode(structs.ApplyPlanResultsRequestType, req) if err != nil { @@ -1234,32 +1244,32 @@ func TestFSM_ApplyPlanResults(t *testing.T) { // Verify the allocation is registered ws := memdb.NewWatchSet() + assert := assert.New(t) out, err := fsm.State().AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } + assert.Nil(err) alloc.CreateIndex = out.CreateIndex alloc.ModifyIndex = out.ModifyIndex alloc.AllocModifyIndex = out.AllocModifyIndex // Job should be re-attached alloc.Job = job - if !reflect.DeepEqual(alloc, out) { - t.Fatalf("bad: %#v %#v", alloc, out) - } + assert.Equal(alloc, out) dout, err := fsm.State().DeploymentByID(ws, d.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if tg, ok := dout.TaskGroups[alloc.TaskGroup]; !ok || tg.PlacedAllocs != 1 { - t.Fatalf("err: %v %v", tg, err) - } + assert.Nil(err) + tg, ok := dout.TaskGroups[alloc.TaskGroup] + assert.True(ok) + assert.NotNil(tg) + assert.Equal(1, tg.PlacedAllocs) // Ensure that the original job is used evictAlloc := alloc.Copy() job = mock.Job() job.Priority = 123 + eval = mock.Eval() + eval.JobID = job.ID + + fsm.State().UpsertEvals(2, []*structs.Evaluation{eval}) evictAlloc.Job = nil evictAlloc.DesiredStatus = structs.AllocDesiredStatusEvict @@ -1268,28 +1278,28 @@ func TestFSM_ApplyPlanResults(t *testing.T) { Job: job, Alloc: []*structs.Allocation{evictAlloc}, }, + EvalID: eval.ID, } buf, err = structs.Encode(structs.ApplyPlanResultsRequestType, req2) - if err != nil { - t.Fatalf("err: %v", err) - } + assert.Nil(err) - resp = fsm.Apply(makeLog(buf)) - if resp != nil { - t.Fatalf("resp: %v", resp) - } + log := makeLog(buf) + //set the index to something other than 1 + log.Index = 25 + resp = fsm.Apply(log) + assert.Nil(resp) // Verify we are evicted out, err = fsm.State().AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if out.DesiredStatus != structs.AllocDesiredStatusEvict { - t.Fatalf("alloc found!") - } - if out.Job == nil || out.Job.Priority == 123 { - t.Fatalf("bad job") - } + assert.Nil(err) + assert.Equal(structs.AllocDesiredStatusEvict, out.DesiredStatus) + assert.NotNil(out.Job) + assert.NotEqual(123, out.Job.Priority) + + evalOut, err := fsm.State().EvalByID(ws, eval.ID) + assert.Nil(err) + assert.Equal(log.Index, evalOut.ModifyIndex) + } func TestFSM_DeploymentStatusUpdate(t *testing.T) { diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index a47588ad8..41857f92f 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -1088,6 +1088,8 @@ func (j *Job) Plan(args *structs.JobPlanRequest, reply *structs.JobPlanResponse) AnnotatePlan: true, } + snap.UpsertEvals(100, []*structs.Evaluation{eval}) + // Create an in-memory Planner that returns no errors and stores the // submitted plan and created evals. planner := &scheduler.Harness{ diff --git a/nomad/mock/mock.go b/nomad/mock/mock.go index d0c889b09..c4921a644 100644 --- a/nomad/mock/mock.go +++ b/nomad/mock/mock.go @@ -292,7 +292,7 @@ func Alloc() *structs.Allocation { IP: "192.168.0.100", ReservedPorts: []structs.Port{{Label: "admin", Value: 5000}}, MBits: 50, - DynamicPorts: []structs.Port{{Label: "http"}}, + DynamicPorts: []structs.Port{{Label: "http", Value: 9876}}, }, }, }, diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 5d2c29bcf..44f78e2c8 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -135,6 +135,7 @@ func (s *Server) applyPlan(plan *structs.Plan, result *structs.PlanResult, snap }, Deployment: result.Deployment, DeploymentUpdates: result.DeploymentUpdates, + EvalID: plan.EvalID, } for _, updateList := range result.NodeUpdate { req.Alloc = append(req.Alloc, updateList...) diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index 7d3195d92..93e44e617 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/hashicorp/raft" + "github.com/stretchr/testify/assert" ) const ( @@ -65,7 +66,7 @@ func TestPlanApply_applyPlan(t *testing.T) { defer s1.Shutdown() testutil.WaitForLeader(t, s1.RPC) - // Register ndoe + // Register node node := mock.Node() testRegisterNode(t, s1, node) @@ -91,6 +92,13 @@ func TestPlanApply_applyPlan(t *testing.T) { // Register alloc, deployment and deployment update alloc := mock.Alloc() s1.State().UpsertJobSummary(1000, mock.JobSummary(alloc.JobID)) + // Create an eval + eval := mock.Eval() + eval.JobID = alloc.JobID + if err := s1.State().UpsertEvals(1, []*structs.Evaluation{eval}); err != nil { + t.Fatalf("err: %v", err) + } + planRes := &structs.PlanResult{ NodeAllocation: map[string][]*structs.Allocation{ node.ID: {alloc}, @@ -110,73 +118,55 @@ func TestPlanApply_applyPlan(t *testing.T) { Job: alloc.Job, Deployment: dnew, DeploymentUpdates: updates, + EvalID: eval.ID, } // Apply the plan future, err := s1.applyPlan(plan, planRes, snap) - if err != nil { - t.Fatalf("err: %v", err) - } + assert := assert.New(t) + assert.Nil(err) // Verify our optimistic snapshot is updated ws := memdb.NewWatchSet() - if out, err := snap.AllocByID(ws, alloc.ID); err != nil || out == nil { - t.Fatalf("bad: %v %v", out, err) - } + allocOut, err := snap.AllocByID(ws, alloc.ID) + assert.Nil(err) + assert.NotNil(allocOut) - if out, err := snap.DeploymentByID(ws, plan.Deployment.ID); err != nil || out == nil { - t.Fatalf("bad: %v %v", out, err) - } + deploymentOut, err := snap.DeploymentByID(ws, plan.Deployment.ID) + assert.Nil(err) + assert.NotNil(deploymentOut) // Check plan does apply cleanly index, err := planWaitFuture(future) - if err != nil { - t.Fatalf("err: %v", err) - } - if index == 0 { - t.Fatalf("bad: %d", index) - } + assert.Nil(err) + assert.NotEqual(0, index) // Lookup the allocation fsmState := s1.fsm.State() - out, err := fsmState.AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("missing alloc") - } - - if out.CreateTime <= 0 { - t.Fatalf("invalid create time %v", out.CreateTime) - } - if out.ModifyTime <= 0 { - t.Fatalf("invalid modify time %v", out.CreateTime) - } - if out.CreateTime != out.ModifyTime { - t.Fatalf("create time %v modify time %v must be equal", out.CreateTime, out.ModifyTime) - } + allocOut, err = fsmState.AllocByID(ws, alloc.ID) + assert.Nil(err) + assert.NotNil(allocOut) + assert.True(allocOut.CreateTime > 0) + assert.True(allocOut.ModifyTime > 0) + assert.Equal(allocOut.CreateTime, allocOut.ModifyTime) // Lookup the new deployment dout, err := fsmState.DeploymentByID(ws, plan.Deployment.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if dout == nil { - t.Fatalf("missing deployment") - } + assert.Nil(err) + assert.NotNil(dout) // Lookup the updated deployment dout2, err := fsmState.DeploymentByID(ws, oldDeployment.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if dout2 == nil { - t.Fatalf("missing deployment") - } - if dout2.Status != desiredStatus || dout2.StatusDescription != desiredStatusDescription { - t.Fatalf("bad status: %#v", dout2) - } + assert.Nil(err) + assert.NotNil(dout2) + assert.Equal(desiredStatus, dout2.Status) + assert.Equal(desiredStatusDescription, dout2.StatusDescription) + + // Lookup updated eval + evalOut, err := fsmState.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.Equal(index, evalOut.ModifyIndex) // Evict alloc, Register alloc2 allocEvict := new(structs.Allocation) @@ -197,60 +187,43 @@ func TestPlanApply_applyPlan(t *testing.T) { // Snapshot the state snap, err = s1.State().Snapshot() - if err != nil { - t.Fatalf("err: %v", err) - } + assert.Nil(err) // Apply the plan plan = &structs.Plan{ - Job: job, + Job: job, + EvalID: eval.ID, } future, err = s1.applyPlan(plan, planRes, snap) - if err != nil { - t.Fatalf("err: %v", err) - } + assert.Nil(err) // Check that our optimistic view is updated - if out, _ := snap.AllocByID(ws, allocEvict.ID); out.DesiredStatus != structs.AllocDesiredStatusEvict { - t.Fatalf("bad: %#v", out) - } + out, _ := snap.AllocByID(ws, allocEvict.ID) + assert.Equal(structs.AllocDesiredStatusEvict, out.DesiredStatus) // Verify plan applies cleanly index, err = planWaitFuture(future) - if err != nil { - t.Fatalf("err: %v", err) - } - if index == 0 { - t.Fatalf("bad: %d", index) - } + assert.Nil(err) + assert.NotEqual(0, index) // Lookup the allocation - out, err = s1.fsm.State().AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if out.DesiredStatus != structs.AllocDesiredStatusEvict { - t.Fatalf("should be evicted alloc: %#v", out) - } - if out.Job == nil { - t.Fatalf("missing job") - } - - if out.ModifyTime <= 0 { - t.Fatalf("must have valid modify time but was %v", out.ModifyTime) - } + allocOut, err = s1.fsm.State().AllocByID(ws, alloc.ID) + assert.Nil(err) + assert.Equal(structs.AllocDesiredStatusEvict, allocOut.DesiredStatus) + assert.NotNil(allocOut.Job) + assert.True(allocOut.ModifyTime > 0) // Lookup the allocation - out, err = s1.fsm.State().AllocByID(ws, alloc2.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - if out == nil { - t.Fatalf("missing alloc") - } - if out.Job == nil { - t.Fatalf("missing job") - } + allocOut, err = s1.fsm.State().AllocByID(ws, alloc2.ID) + assert.Nil(err) + assert.NotNil(allocOut) + assert.NotNil(allocOut.Job) + + // Lookup updated eval + evalOut, err = fsmState.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.Equal(index, evalOut.ModifyIndex) } func TestPlanApply_EvalPlan_Simple(t *testing.T) { diff --git a/nomad/search_endpoint.go b/nomad/search_endpoint.go index 61931a5e8..9eb892dd3 100644 --- a/nomad/search_endpoint.go +++ b/nomad/search_endpoint.go @@ -2,7 +2,9 @@ package nomad import ( "strings" + "time" + metrics "github.com/armon/go-metrics" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/nomad/state" @@ -114,6 +116,11 @@ func roundUUIDDownIfOdd(prefix string, context structs.Context) string { // PrefixSearch is used to list matches for a given prefix, and returns // matching jobs, evaluations, allocations, and/or nodes. func (s *Search) PrefixSearch(args *structs.SearchRequest, reply *structs.SearchResponse) error { + if done, err := s.srv.forward("Search.PrefixSearch", args, args, reply); done { + return err + } + defer metrics.MeasureSince([]string{"nomad", "search", "prefix_search"}, time.Now()) + aclObj, err := s.srv.ResolveToken(args.AuthToken) if err != nil { return err diff --git a/nomad/search_endpoint_test.go b/nomad/search_endpoint_test.go index 7cd6fe6b2..2631b6958 100644 --- a/nomad/search_endpoint_test.go +++ b/nomad/search_endpoint_test.go @@ -712,3 +712,47 @@ func TestSearch_PrefixSearch_RoundDownToEven(t *testing.T) { assert.Equal(1, len(resp.Matches[structs.Jobs])) assert.Equal(job.ID, resp.Matches[structs.Jobs][0]) } + +func TestSearch_PrefixSearch_MultiRegion(t *testing.T) { + assert := assert.New(t) + + jobName := "exampleexample" + + t.Parallel() + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 + c.Region = "foo" + }) + defer s1.Shutdown() + + s2 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 + c.Region = "bar" + }) + defer s2.Shutdown() + + testJoin(t, s1, s2) + testutil.WaitForLeader(t, s1.RPC) + + job := registerAndVerifyJob(s1, t, jobName, 0) + + req := &structs.SearchRequest{ + Prefix: "", + Context: structs.Jobs, + QueryOptions: structs.QueryOptions{ + Region: "foo", + Namespace: job.Namespace, + }, + } + + codec := rpcClient(t, s2) + + var resp structs.SearchResponse + if err := msgpackrpc.CallWithCodec(codec, "Search.PrefixSearch", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + assert.Equal(1, len(resp.Matches[structs.Jobs])) + assert.Equal(job.ID, resp.Matches[structs.Jobs][0]) + assert.Equal(uint64(jobIndex), resp.Index) +} diff --git a/nomad/serf.go b/nomad/serf.go index 907183710..2bfc11ba1 100644 --- a/nomad/serf.go +++ b/nomad/serf.go @@ -154,7 +154,7 @@ func (s *Server) maybeBootstrap() { if err := s.connPool.RPC(s.config.Region, server.Addr, server.MajorVersion, "Status.Peers", req, &peers); err != nil { nextRetry := (1 << attempt) * peerRetryBase - s.logger.Printf("[ERR] consul: Failed to confirm peer status for %s: %v. Retrying in "+ + s.logger.Printf("[ERR] nomad: Failed to confirm peer status for %s: %v. Retrying in "+ "%v...", server.Name, err, nextRetry.String()) time.Sleep(nextRetry) } else { diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 0091055b3..f837a6106 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -196,6 +196,16 @@ func (s *StateStore) UpsertPlanResults(index uint64, results *structs.ApplyPlanR return err } + // COMPAT: Nomad versions before 0.7.1 did not include the eval ID when + // applying the plan. Thus while we are upgrading, we ignore updating the + // modify index of evaluations from older plans. + if results.EvalID != "" { + // Update the modify index of the eval id + if err := s.updateEvalModifyIndex(txn, index, results.EvalID); err != nil { + return err + } + } + txn.Commit() return nil } @@ -1486,6 +1496,34 @@ func (s *StateStore) nestedUpsertEval(txn *memdb.Txn, index uint64, eval *struct return nil } +// updateEvalModifyIndex is used to update the modify index of an evaluation that has been +// through a scheduler pass. This is done as part of plan apply. It ensures that when a subsequent +// scheduler workers process a re-queued evaluation it sees any partial updates from the plan apply. +func (s *StateStore) updateEvalModifyIndex(txn *memdb.Txn, index uint64, evalID string) error { + // Lookup the evaluation + existing, err := txn.First("evals", "id", evalID) + if err != nil { + return fmt.Errorf("eval lookup failed: %v", err) + } + if existing == nil { + err := fmt.Errorf("unable to find eval id %q", evalID) + s.logger.Printf("[ERR] state_store: %v", err) + return err + } + eval := existing.(*structs.Evaluation).Copy() + // Update the indexes + eval.ModifyIndex = index + + // Insert the eval + if err := txn.Insert("evals", eval); err != nil { + return fmt.Errorf("eval insert failed: %v", err) + } + if err := txn.Insert("index", &IndexEntry{"evals", index}); err != nil { + return fmt.Errorf("index update failed: %v", err) + } + return nil +} + // DeleteEval is used to delete an evaluation func (s *StateStore) DeleteEval(index uint64, evals []string, allocs []string) error { txn := s.db.Txn(true) diff --git a/nomad/state/state_store_test.go b/nomad/state/state_store_test.go index 38081ccfc..08179bdfd 100644 --- a/nomad/state/state_store_test.go +++ b/nomad/state/state_store_test.go @@ -100,40 +100,43 @@ func TestStateStore_UpsertPlanResults_AllocationsCreated_Denormalized(t *testing t.Fatalf("err: %v", err) } + eval := mock.Eval() + eval.JobID = job.ID + + // Create an eval + if err := state.UpsertEvals(1, []*structs.Evaluation{eval}); err != nil { + t.Fatalf("err: %v", err) + } + // Create a plan result res := structs.ApplyPlanResultsRequest{ AllocUpdateRequest: structs.AllocUpdateRequest{ Alloc: []*structs.Allocation{alloc}, Job: job, }, + EvalID: eval.ID, } - + assert := assert.New(t) err := state.UpsertPlanResults(1000, &res) - if err != nil { - t.Fatalf("err: %v", err) - } + assert.Nil(err) ws := memdb.NewWatchSet() out, err := state.AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if !reflect.DeepEqual(alloc, out) { - t.Fatalf("bad: %#v %#v", alloc, out) - } + assert.Nil(err) + assert.Equal(alloc, out) index, err := state.Index("allocs") - if err != nil { - t.Fatalf("err: %v", err) - } - if index != 1000 { - t.Fatalf("bad: %d", index) - } + assert.Nil(err) + assert.EqualValues(1000, index) if watchFired(ws) { t.Fatalf("bad") } + + evalOut, err := state.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.EqualValues(1000, evalOut.ModifyIndex) } // This test checks that the deployment is created and allocations count towards @@ -154,6 +157,14 @@ func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) { t.Fatalf("err: %v", err) } + eval := mock.Eval() + eval.JobID = job.ID + + // Create an eval + if err := state.UpsertEvals(1, []*structs.Evaluation{eval}); err != nil { + t.Fatalf("err: %v", err) + } + // Create a plan result res := structs.ApplyPlanResultsRequest{ AllocUpdateRequest: structs.AllocUpdateRequest{ @@ -161,6 +172,7 @@ func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) { Job: job, }, Deployment: d, + EvalID: eval.ID, } err := state.UpsertPlanResults(1000, &res) @@ -169,31 +181,24 @@ func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) { } ws := memdb.NewWatchSet() + assert := assert.New(t) out, err := state.AllocByID(ws, alloc.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if !reflect.DeepEqual(alloc, out) { - t.Fatalf("bad: %#v %#v", alloc, out) - } + assert.Nil(err) + assert.Equal(alloc, out) dout, err := state.DeploymentByID(ws, d.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if dout == nil { - t.Fatalf("bad: nil deployment") - } + assert.Nil(err) + assert.NotNil(dout) tg, ok := dout.TaskGroups[alloc.TaskGroup] - if !ok { - t.Fatalf("bad: nil deployment state") - } - if tg == nil || tg.PlacedAllocs != 2 { - t.Fatalf("bad: %v", dout) - } + assert.True(ok) + assert.NotNil(tg) + assert.Equal(2, tg.PlacedAllocs) + + evalOut, err := state.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.EqualValues(1000, evalOut.ModifyIndex) if watchFired(ws) { t.Fatalf("bad") @@ -215,6 +220,7 @@ func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) { Job: job, }, Deployment: d2, + EvalID: eval.ID, } err = state.UpsertPlanResults(1001, &res) @@ -223,21 +229,18 @@ func TestStateStore_UpsertPlanResults_Deployment(t *testing.T) { } dout, err = state.DeploymentByID(ws, d2.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if dout == nil { - t.Fatalf("bad: nil deployment") - } + assert.Nil(err) + assert.NotNil(dout) tg, ok = dout.TaskGroups[alloc.TaskGroup] - if !ok { - t.Fatalf("bad: nil deployment state") - } - if tg == nil || tg.PlacedAllocs != 2 { - t.Fatalf("bad: %v", dout) - } + assert.True(ok) + assert.NotNil(tg) + assert.Equal(2, tg.PlacedAllocs) + + evalOut, err = state.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.EqualValues(1001, evalOut.ModifyIndex) } // This test checks that deployment updates are applied correctly @@ -258,6 +261,13 @@ func TestStateStore_UpsertPlanResults_DeploymentUpdates(t *testing.T) { t.Fatalf("err: %v", err) } + eval := mock.Eval() + eval.JobID = job.ID + + // Create an eval + if err := state.UpsertEvals(1, []*structs.Evaluation{eval}); err != nil { + t.Fatalf("err: %v", err) + } alloc := mock.Alloc() alloc.Job = nil @@ -280,41 +290,37 @@ func TestStateStore_UpsertPlanResults_DeploymentUpdates(t *testing.T) { }, Deployment: dnew, DeploymentUpdates: []*structs.DeploymentStatusUpdate{update}, + EvalID: eval.ID, } err := state.UpsertPlanResults(1000, &res) if err != nil { t.Fatalf("err: %v", err) } - + assert := assert.New(t) ws := memdb.NewWatchSet() // Check the deployments are correctly updated. dout, err := state.DeploymentByID(ws, dnew.ID) - if err != nil { - t.Fatalf("err: %v", err) - } - - if dout == nil { - t.Fatalf("bad: nil deployment") - } + assert.Nil(err) + assert.NotNil(dout) tg, ok := dout.TaskGroups[alloc.TaskGroup] - if !ok { - t.Fatalf("bad: nil deployment state") - } - if tg == nil || tg.PlacedAllocs != 1 { - t.Fatalf("bad: %v", dout) - } + assert.True(ok) + assert.NotNil(tg) + assert.Equal(1, tg.PlacedAllocs) doutstandingout, err := state.DeploymentByID(ws, doutstanding.ID) - if err != nil || doutstandingout == nil { - t.Fatalf("bad: %v %v", err, doutstandingout) - } - if doutstandingout.Status != update.Status || doutstandingout.StatusDescription != update.StatusDescription || doutstandingout.ModifyIndex != 1000 { - t.Fatalf("bad: %v", doutstandingout) - } + assert.Nil(err) + assert.NotNil(doutstandingout) + assert.Equal(update.Status, doutstandingout.Status) + assert.Equal(update.StatusDescription, doutstandingout.StatusDescription) + assert.EqualValues(1000, doutstandingout.ModifyIndex) + evalOut, err := state.EvalByID(ws, eval.ID) + assert.Nil(err) + assert.NotNil(evalOut) + assert.EqualValues(1000, evalOut.ModifyIndex) if watchFired(ws) { t.Fatalf("bad") } diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 7556d0826..c820caa6d 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "net" + "net/url" "os" "path/filepath" "reflect" @@ -515,6 +516,14 @@ type ApplyPlanResultsRequest struct { // deployments. This allows the scheduler to cancel any unneeded deployment // because the job is stopped or the update block is removed. DeploymentUpdates []*DeploymentStatusUpdate + + // EvalID is the eval ID of the plan being applied. The modify index of the + // evaluation is updated as part of applying the plan to ensure that subsequent + // scheduling events for the same job will wait for the index that last produced + // state changes. This is necessary for blocked evaluations since they can be + // processed many times, potentially making state updates, without the state of + // the evaluation itself being updated. + EvalID string } // AllocUpdateRequest is used to submit changes to allocations, either @@ -1062,7 +1071,7 @@ type Node struct { // SecretID is an ID that is only known by the Node and the set of Servers. // It is not accessible via the API and is used to authenticate nodes - // conducting priviledged activities. + // conducting privileged activities. SecretID string // Datacenter for this node @@ -1246,7 +1255,7 @@ func DefaultResources() *Resources { // api/resources.go and should be kept in sync. func MinResources() *Resources { return &Resources{ - CPU: 100, + CPU: 20, MemoryMB: 10, IOPS: 0, } @@ -2930,6 +2939,13 @@ func (sc *ServiceCheck) validate() error { if sc.Path == "" { return fmt.Errorf("http type must have a valid http path") } + url, err := url.Parse(sc.Path) + if err != nil { + return fmt.Errorf("http type must have a valid http path") + } + if url.IsAbs() { + return fmt.Errorf("http type must have a relative http path") + } case ServiceCheckScript: if sc.Command == "" { @@ -3124,8 +3140,8 @@ func (s *Service) Validate() error { } for _, c := range s.Checks { - if s.PortLabel == "" && c.RequiresPort() { - mErr.Errors = append(mErr.Errors, fmt.Errorf("check %s invalid: check requires a port but the service %+q has no port", c.Name, s.Name)) + if s.PortLabel == "" && c.PortLabel == "" && c.RequiresPort() { + mErr.Errors = append(mErr.Errors, fmt.Errorf("check %s invalid: check requires a port but neither check nor service %+q have a port", c.Name, s.Name)) continue } @@ -3570,8 +3586,16 @@ func validateServices(t *Task) error { } } + // Iterate over a sorted list of keys to make error listings stable + keys := make([]string, 0, len(servicePorts)) + for p := range servicePorts { + keys = append(keys, p) + } + sort.Strings(keys) + // Ensure all ports referenced in services exist. - for servicePort, services := range servicePorts { + for _, servicePort := range keys { + services := servicePorts[servicePort] _, ok := portLabels[servicePort] if !ok { names := make([]string, 0, len(services)) @@ -3620,7 +3644,7 @@ type Template struct { DestPath string // EmbeddedTmpl store the raw template. This is useful for smaller templates - // where they are embedded in the job file rather than sent as an artificat + // where they are embedded in the job file rather than sent as an artifact EmbeddedTmpl string // ChangeMode indicates what should be done if the template is re-rendered @@ -3790,7 +3814,9 @@ func (ts *TaskState) Copy() *TaskState { return copy } -// Successful returns whether a task finished successfully. +// Successful returns whether a task finished successfully. This doesn't really +// have meaning on a non-batch allocation because a service and system +// allocation should not finish. func (ts *TaskState) Successful() bool { l := len(ts.Events) if ts.State != TaskStateDead || l == 0 { @@ -4996,9 +5022,25 @@ func (a *Allocation) Terminated() bool { } // RanSuccessfully returns whether the client has ran the allocation and all -// tasks finished successfully +// tasks finished successfully. Critically this function returns whether the +// allocation has ran to completion and not just that the alloc has converged to +// its desired state. That is to say that a batch allocation must have finished +// with exit code 0 on all task groups. This doesn't really have meaning on a +// non-batch allocation because a service and system allocation should not +// finish. func (a *Allocation) RanSuccessfully() bool { - return a.ClientStatus == AllocClientStatusComplete + // Handle the case the client hasn't started the allocation. + if len(a.TaskStates) == 0 { + return false + } + + // Check to see if all the tasks finised successfully in the allocation + allSuccess := true + for _, state := range a.TaskStates { + allSuccess = allSuccess && state.Successful() + } + + return allSuccess } // ShouldMigrate returns if the allocation needs data migration diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index 26b2d2a02..c4e52038f 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -1230,60 +1230,129 @@ func TestTask_Validate_Service_Check(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } + + check2 := ServiceCheck{ + Name: "check-name-2", + Type: ServiceCheckHTTP, + Interval: 10 * time.Second, + Timeout: 2 * time.Second, + Path: "/foo/bar", + } + + err = check2.validate() + if err != nil { + t.Fatalf("err: %v", err) + } + + check2.Path = "" + err = check2.validate() + if err == nil { + t.Fatal("Expected an error") + } + if !strings.Contains(err.Error(), "valid http path") { + t.Fatalf("err: %v", err) + } + + check2.Path = "http://www.example.com" + err = check2.validate() + if err == nil { + t.Fatal("Expected an error") + } + if !strings.Contains(err.Error(), "relative http path") { + t.Fatalf("err: %v", err) + } } // TestTask_Validate_Service_Check_AddressMode asserts that checks do not // inherit address mode but do inherit ports. func TestTask_Validate_Service_Check_AddressMode(t *testing.T) { - task := &Task{ - Resources: &Resources{ - Networks: []*NetworkResource{ - { - DynamicPorts: []Port{ - { - Label: "http", - Value: 9999, + getTask := func(s *Service) *Task { + return &Task{ + Resources: &Resources{ + Networks: []*NetworkResource{ + { + DynamicPorts: []Port{ + { + Label: "http", + Value: 9999, + }, }, }, }, }, - }, - Services: []*Service{ - { + Services: []*Service{s}, + } + } + + cases := []struct { + Service *Service + ErrContains string + }{ + { + Service: &Service{ Name: "invalid-driver", PortLabel: "80", AddressMode: "host", }, - { - Name: "http-driver", + ErrContains: `port label "80" referenced`, + }, + { + Service: &Service{ + Name: "http-driver-fail-1", PortLabel: "80", AddressMode: "driver", Checks: []*ServiceCheck{ { - // Should fail Name: "invalid-check-1", Type: "tcp", Interval: time.Second, Timeout: time.Second, }, + }, + }, + ErrContains: `check "invalid-check-1" cannot use a numeric port`, + }, + { + Service: &Service{ + Name: "http-driver-fail-2", + PortLabel: "80", + AddressMode: "driver", + Checks: []*ServiceCheck{ { - // Should fail Name: "invalid-check-2", Type: "tcp", PortLabel: "80", Interval: time.Second, Timeout: time.Second, }, + }, + }, + ErrContains: `check "invalid-check-2" cannot use a numeric port`, + }, + { + Service: &Service{ + Name: "http-driver-fail-3", + PortLabel: "80", + AddressMode: "driver", + Checks: []*ServiceCheck{ { - // Should fail Name: "invalid-check-3", Type: "tcp", PortLabel: "missing-port-label", Interval: time.Second, Timeout: time.Second, }, + }, + }, + ErrContains: `port label "missing-port-label" referenced`, + }, + { + Service: &Service{ + Name: "http-driver-passes", + PortLabel: "80", + AddressMode: "driver", + Checks: []*ServiceCheck{ { - // Should pass Name: "valid-script-check", Type: "script", Command: "ok", @@ -1291,7 +1360,6 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) { Timeout: time.Second, }, { - // Should pass Name: "valid-host-check", Type: "tcp", PortLabel: "http", @@ -1299,7 +1367,6 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) { Timeout: time.Second, }, { - // Should pass Name: "valid-driver-check", Type: "tcp", AddressMode: "driver", @@ -1309,23 +1376,65 @@ func TestTask_Validate_Service_Check_AddressMode(t *testing.T) { }, }, }, - } - err := validateServices(task) - if err == nil { - t.Fatalf("expected errors but task validated successfully") - } - errs := err.(*multierror.Error).Errors - if expected := 4; len(errs) != expected { - for i, err := range errs { - t.Logf("errs[%d] -> %s", i, err) - } - t.Fatalf("expected %d errors but found %d", expected, len(errs)) + { + Service: &Service{ + Name: "empty-address-3673-passes-1", + Checks: []*ServiceCheck{ + { + Name: "valid-port-label", + Type: "tcp", + PortLabel: "http", + Interval: time.Second, + Timeout: time.Second, + }, + { + Name: "empty-is-ok", + Type: "script", + Command: "ok", + Interval: time.Second, + Timeout: time.Second, + }, + }, + }, + }, + { + Service: &Service{ + Name: "empty-address-3673-passes-2", + }, + }, + { + Service: &Service{ + Name: "empty-address-3673-fails", + Checks: []*ServiceCheck{ + { + Name: "empty-is-not-ok", + Type: "tcp", + Interval: time.Second, + Timeout: time.Second, + }, + }, + }, + ErrContains: `invalid: check requires a port but neither check nor service`, + }, } - assert.Contains(t, errs[0].Error(), `check "invalid-check-1" cannot use a numeric port`) - assert.Contains(t, errs[1].Error(), `check "invalid-check-2" cannot use a numeric port`) - assert.Contains(t, errs[2].Error(), `port label "80" referenced`) - assert.Contains(t, errs[3].Error(), `port label "missing-port-label" referenced`) + for _, tc := range cases { + tc := tc + task := getTask(tc.Service) + t.Run(tc.Service.Name, func(t *testing.T) { + err := validateServices(task) + if err == nil && tc.ErrContains == "" { + // Ok! + return + } + if err == nil { + t.Fatalf("no error returned. expected: %s", tc.ErrContains) + } + if !strings.Contains(err.Error(), tc.ErrContains) { + t.Fatalf("expected %q but found: %v", tc.ErrContains, err) + } + }) + } } func TestTask_Validate_Service_Check_CheckRestart(t *testing.T) { diff --git a/nomad/worker_test.go b/nomad/worker_test.go index 627887e31..faa9cc104 100644 --- a/nomad/worker_test.go +++ b/nomad/worker_test.go @@ -341,6 +341,7 @@ func TestWorker_SubmitPlan(t *testing.T) { eval1 := mock.Eval() eval1.JobID = job.ID s1.fsm.State().UpsertJob(1000, job) + s1.fsm.State().UpsertEvals(1000, []*structs.Evaluation{eval1}) // Create the register request s1.evalBroker.Enqueue(eval1) diff --git a/scheduler/generic_sched_test.go b/scheduler/generic_sched_test.go index 02296ff76..39eff3773 100644 --- a/scheduler/generic_sched_test.go +++ b/scheduler/generic_sched_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestServiceSched_JobRegister(t *testing.T) { @@ -35,8 +36,11 @@ func TestServiceSched_JobRegister(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -118,7 +122,9 @@ func TestServiceSched_JobRegister_StickyAllocs(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewServiceScheduler, eval); err != nil { @@ -149,7 +155,9 @@ func TestServiceSched_JobRegister_StickyAllocs(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) h1 := NewHarnessWithState(t, h.State) if err := h1.Process(NewServiceScheduler, eval); err != nil { t.Fatalf("err: %v", err) @@ -206,8 +214,11 @@ func TestServiceSched_JobRegister_DiskConstraints(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -275,8 +286,11 @@ func TestServiceSched_JobRegister_DistinctHosts(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -364,8 +378,11 @@ func TestServiceSched_JobRegister_DistinctProperty(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -456,7 +473,9 @@ func TestServiceSched_JobRegister_DistinctProperty_TaskGroup(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -548,7 +567,9 @@ func TestServiceSched_JobRegister_DistinctProperty_TaskGroup_Incr(t *testing.T) Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation assert.Nil(h.Process(NewServiceScheduler, eval), "Process") @@ -602,7 +623,9 @@ func TestServiceSched_JobRegister_Annotate(t *testing.T) { TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, AnnotatePlan: true, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -679,7 +702,9 @@ func TestServiceSched_JobRegister_CountZero(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -720,8 +745,11 @@ func TestServiceSched_JobRegister_AllocFail(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -802,8 +830,11 @@ func TestServiceSched_JobRegister_CreateBlockedEval(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -899,8 +930,9 @@ func TestServiceSched_JobRegister_FeasibleAndInfeasibleTG(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -1016,8 +1048,11 @@ func TestServiceSched_Plan_Partial_Progress(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -1245,7 +1280,9 @@ func TestServiceSched_JobModify(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1329,7 +1366,9 @@ func TestServiceSched_JobModify_IncrCount_NodeLimit(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1436,7 +1475,9 @@ func TestServiceSched_JobModify_CountZero(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1530,7 +1571,9 @@ func TestServiceSched_JobModify_Rolling(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1635,7 +1678,9 @@ func TestServiceSched_JobModify_Rolling_FullNode(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1736,7 +1781,9 @@ func TestServiceSched_JobModify_Canaries(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1844,7 +1891,9 @@ func TestServiceSched_JobModify_InPlace(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1958,7 +2007,9 @@ func TestServiceSched_JobModify_DistinctProperty(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2044,7 +2095,9 @@ func TestServiceSched_JobDeregister_Purged(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobDeregister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2111,7 +2164,9 @@ func TestServiceSched_JobDeregister_Stopped(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobDeregister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2201,7 +2256,9 @@ func TestServiceSched_NodeDown(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2267,7 +2324,9 @@ func TestServiceSched_NodeUpdate(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2318,7 +2377,9 @@ func TestServiceSched_NodeDrain(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2419,8 +2480,11 @@ func TestServiceSched_NodeDrain_Down(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -2493,7 +2557,9 @@ func TestServiceSched_NodeDrain_Queued_Allocations(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2550,7 +2616,9 @@ func TestServiceSched_NodeDrain_UpdateStrategy(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2608,7 +2676,9 @@ func TestServiceSched_RetryLimit(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -2664,7 +2734,9 @@ func TestBatchSched_Run_CompleteAlloc(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -2719,7 +2791,9 @@ func TestBatchSched_Run_FailedAlloc(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -2752,6 +2826,94 @@ func TestBatchSched_Run_FailedAlloc(t *testing.T) { h.AssertEvalStatus(t, structs.EvalStatusComplete) } +func TestBatchSched_Run_LostAlloc(t *testing.T) { + h := NewHarness(t) + + // Create a node + node := mock.Node() + noErr(t, h.State.UpsertNode(h.NextIndex(), node)) + + // Create a job + job := mock.Job() + job.ID = "my-job" + job.Type = structs.JobTypeBatch + job.TaskGroups[0].Count = 3 + noErr(t, h.State.UpsertJob(h.NextIndex(), job)) + + // Desired = 3 + // Mark one as lost and then schedule + // [(0, run, running), (1, run, running), (1, stop, lost)] + + // Create two running allocations + var allocs []*structs.Allocation + for i := 0; i <= 1; i++ { + alloc := mock.Alloc() + alloc.Job = job + alloc.JobID = job.ID + alloc.NodeID = node.ID + alloc.Name = fmt.Sprintf("my-job.web[%d]", i) + alloc.ClientStatus = structs.AllocClientStatusRunning + allocs = append(allocs, alloc) + } + + // Create a failed alloc + alloc := mock.Alloc() + alloc.Job = job + alloc.JobID = job.ID + alloc.NodeID = node.ID + alloc.Name = "my-job.web[1]" + alloc.DesiredStatus = structs.AllocDesiredStatusStop + alloc.ClientStatus = structs.AllocClientStatusComplete + allocs = append(allocs, alloc) + noErr(t, h.State.UpsertAllocs(h.NextIndex(), allocs)) + + // Create a mock evaluation to register the job + eval := &structs.Evaluation{ + Namespace: structs.DefaultNamespace, + ID: uuid.Generate(), + Priority: job.Priority, + TriggeredBy: structs.EvalTriggerJobRegister, + JobID: job.ID, + Status: structs.EvalStatusPending, + } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + + // Process the evaluation + err := h.Process(NewBatchScheduler, eval) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Ensure a plan + if len(h.Plans) != 1 { + t.Fatalf("bad: %#v", h.Plans) + } + + // Lookup the allocations by JobID + ws := memdb.NewWatchSet() + out, err := h.State.AllocsByJob(ws, job.Namespace, job.ID, false) + noErr(t, err) + + // Ensure a replacement alloc was placed. + if len(out) != 4 { + t.Fatalf("bad: %#v", out) + } + + // Assert that we have the correct number of each alloc name + expected := map[string]int{ + "my-job.web[0]": 1, + "my-job.web[1]": 2, + "my-job.web[2]": 1, + } + actual := make(map[string]int, 3) + for _, alloc := range out { + actual[alloc.Name] += 1 + } + require.Equal(t, actual, expected) + + h.AssertEvalStatus(t, structs.EvalStatusComplete) +} + func TestBatchSched_Run_FailedAllocQueuedAllocations(t *testing.T) { h := NewHarness(t) @@ -2781,7 +2943,9 @@ func TestBatchSched_Run_FailedAllocQueuedAllocations(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -2841,7 +3005,9 @@ func TestBatchSched_ReRun_SuccessfullyFinishedAlloc(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -2904,7 +3070,9 @@ func TestBatchSched_JobModify_InPlace_Terminal(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -2985,7 +3153,9 @@ func TestBatchSched_JobModify_Destructive_Terminal(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewBatchScheduler, eval) @@ -3039,8 +3209,11 @@ func TestBatchSched_NodeDrain_Running_OldJob(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewBatchScheduler, eval) if err != nil { @@ -3093,6 +3266,16 @@ func TestBatchSched_NodeDrain_Complete(t *testing.T) { alloc.NodeID = node.ID alloc.Name = "my-job.web[0]" alloc.ClientStatus = structs.AllocClientStatusComplete + alloc.TaskStates = make(map[string]*structs.TaskState) + alloc.TaskStates["web"] = &structs.TaskState{ + State: structs.TaskStateDead, + Events: []*structs.TaskEvent{ + { + Type: structs.TaskTerminated, + ExitCode: 0, + }, + }, + } noErr(t, h.State.UpsertAllocs(h.NextIndex(), []*structs.Allocation{alloc})) // Create a mock evaluation to register the job @@ -3102,8 +3285,11 @@ func TestBatchSched_NodeDrain_Complete(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewBatchScheduler, eval) if err != nil { @@ -3154,8 +3340,11 @@ func TestBatchSched_ScaleDown_SameName(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewBatchScheduler, eval) if err != nil { @@ -3197,7 +3386,9 @@ func TestGenericSched_ChainedAlloc(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewServiceScheduler, eval); err != nil { t.Fatalf("err: %v", err) @@ -3226,7 +3417,10 @@ func TestGenericSched_ChainedAlloc(t *testing.T) { Priority: job1.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job1.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval1})) + // Process the evaluation if err := h1.Process(NewServiceScheduler, eval1); err != nil { t.Fatalf("err: %v", err) @@ -3287,8 +3481,11 @@ func TestServiceSched_NodeDrain_Sticky(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: alloc.Job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -3344,8 +3541,11 @@ func TestServiceSched_CancelDeployment_Stopped(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobDeregister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -3413,8 +3613,11 @@ func TestServiceSched_CancelDeployment_NewerJob(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) + // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { diff --git a/scheduler/system_sched_test.go b/scheduler/system_sched_test.go index 8fe30b6cc..8947d2c5d 100644 --- a/scheduler/system_sched_test.go +++ b/scheduler/system_sched_test.go @@ -32,7 +32,9 @@ func TestSystemSched_JobRegister(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -105,7 +107,9 @@ func TestSystemeSched_JobRegister_StickyAllocs(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewSystemScheduler, eval); err != nil { @@ -134,7 +138,9 @@ func TestSystemeSched_JobRegister_StickyAllocs(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) h1 := NewHarnessWithState(t, h.State) if err := h1.Process(NewSystemScheduler, eval); err != nil { t.Fatalf("err: %v", err) @@ -181,7 +187,9 @@ func TestSystemSched_JobRegister_EphemeralDiskConstraint(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewSystemScheduler, eval); err != nil { @@ -207,7 +215,9 @@ func TestSystemSched_JobRegister_EphemeralDiskConstraint(t *testing.T) { Priority: job1.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job1.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h1.Process(NewSystemScheduler, eval1); err != nil { @@ -241,8 +251,9 @@ func TestSystemSched_ExhaustResources(t *testing.T) { Priority: svcJob.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: svcJob.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) if err != nil { @@ -260,8 +271,9 @@ func TestSystemSched_ExhaustResources(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewSystemScheduler, eval1); err != nil { t.Fatalf("err: %v", err) @@ -307,7 +319,9 @@ func TestSystemSched_JobRegister_Annotate(t *testing.T) { TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, AnnotatePlan: true, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -405,8 +419,9 @@ func TestSystemSched_JobRegister_AddNode(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) if err != nil { @@ -472,8 +487,9 @@ func TestSystemSched_JobRegister_AllocFail(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) if err != nil { @@ -542,7 +558,9 @@ func TestSystemSched_JobModify(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -633,8 +651,9 @@ func TestSystemSched_JobModify_Rolling(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } - + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) if err != nil { @@ -728,7 +747,9 @@ func TestSystemSched_JobModify_InPlace(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -822,7 +843,9 @@ func TestSystemSched_JobDeregister_Purged(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobDeregister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -894,7 +917,9 @@ func TestSystemSched_JobDeregister_Stopped(t *testing.T) { Priority: 50, TriggeredBy: structs.EvalTriggerJobDeregister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -956,7 +981,9 @@ func TestSystemSched_NodeDown(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1021,7 +1048,9 @@ func TestSystemSched_NodeDrain_Down(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewServiceScheduler, eval) @@ -1080,7 +1109,9 @@ func TestSystemSched_NodeDrain(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1143,7 +1174,9 @@ func TestSystemSched_NodeUpdate(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1180,7 +1213,9 @@ func TestSystemSched_RetryLimit(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1230,7 +1265,9 @@ func TestSystemSched_Queued_With_Constraints(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1264,7 +1301,9 @@ func TestSystemSched_ChainedAlloc(t *testing.T) { Priority: job.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation if err := h.Process(NewSystemScheduler, eval); err != nil { t.Fatalf("err: %v", err) @@ -1299,7 +1338,9 @@ func TestSystemSched_ChainedAlloc(t *testing.T) { Priority: job1.Priority, TriggeredBy: structs.EvalTriggerJobRegister, JobID: job1.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval1})) // Process the evaluation if err := h1.Process(NewSystemScheduler, eval1); err != nil { t.Fatalf("err: %v", err) @@ -1389,7 +1430,9 @@ func TestSystemSched_PlanWithDrainedNode(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) @@ -1460,7 +1503,9 @@ func TestSystemSched_QueuedAllocsMultTG(t *testing.T) { TriggeredBy: structs.EvalTriggerNodeUpdate, JobID: job.ID, NodeID: node.ID, + Status: structs.EvalStatusPending, } + noErr(t, h.State.UpsertEvals(h.NextIndex(), []*structs.Evaluation{eval})) // Process the evaluation err := h.Process(NewSystemScheduler, eval) diff --git a/scheduler/testing.go b/scheduler/testing.go index fb631d444..a04b99ce8 100644 --- a/scheduler/testing.go +++ b/scheduler/testing.go @@ -122,6 +122,7 @@ func (h *Harness) SubmitPlan(plan *structs.Plan) (*structs.PlanResult, State, er }, Deployment: plan.Deployment, DeploymentUpdates: plan.DeploymentUpdates, + EvalID: plan.EvalID, } // Apply the full plan diff --git a/scripts/travis.sh b/scripts/travis.sh index d17aa91e6..8e500f793 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -9,7 +9,7 @@ trap 'kill ${PING_LOOP_PID}' EXIT HUP INT QUIT TERM if [ "$RUN_STATIC_CHECKS" ]; then make check - if [ "$TRAVIS_OS_NAME" == "linux" ]; then + if [ "$TRAVIS_OS_NAME" == "linux" ]; then make checkscripts fi fi diff --git a/scripts/vagrant-linux-priv-consul.sh b/scripts/vagrant-linux-priv-consul.sh index 94027cac1..c9a0ba5b8 100755 --- a/scripts/vagrant-linux-priv-consul.sh +++ b/scripts/vagrant-linux-priv-consul.sh @@ -2,7 +2,7 @@ set -o errexit -VERSION=1.0.0 +VERSION=1.0.2 DOWNLOAD=https://releases.hashicorp.com/consul/${VERSION}/consul_${VERSION}_linux_amd64.zip function install_consul() { diff --git a/scripts/vagrant-linux-priv-rkt.sh b/scripts/vagrant-linux-priv-rkt.sh index 07375133f..ad7c26af3 100755 --- a/scripts/vagrant-linux-priv-rkt.sh +++ b/scripts/vagrant-linux-priv-rkt.sh @@ -3,7 +3,7 @@ set -o errexit VERSION=1.27.0 -DOWNLOAD=https://github.com/coreos/rkt/releases/download/v${VERSION}/rkt-v${VERSION}.tar.gz +DOWNLOAD=https://github.com/rkt/rkt/releases/download/v${VERSION}/rkt-v${VERSION}.tar.gz function install_rkt() { if [[ -e /usr/local/bin/rkt ]] ; then diff --git a/scripts/vagrant-linux-priv-vault.sh b/scripts/vagrant-linux-priv-vault.sh index 6882c865e..11a2b47ea 100755 --- a/scripts/vagrant-linux-priv-vault.sh +++ b/scripts/vagrant-linux-priv-vault.sh @@ -2,7 +2,7 @@ set -o errexit -VERSION=0.7.0 +VERSION=0.9.1 DOWNLOAD=https://releases.hashicorp.com/vault/${VERSION}/vault_${VERSION}_linux_amd64.zip function install_vault() { diff --git a/terraform/aws/README.md b/terraform/aws/README.md index 3cd916c35..6f2f0ac33 100644 --- a/terraform/aws/README.md +++ b/terraform/aws/README.md @@ -43,7 +43,7 @@ a custom AMI: ```bash region = "us-east-1" -ami = "ami-6ce26316" +ami = "ami-d42d74ae" instance_type = "t2.medium" key_name = "KEY_NAME" server_count = "3" diff --git a/terraform/aws/env/us-east/terraform.tfvars b/terraform/aws/env/us-east/terraform.tfvars index 328a9f574..bf4b90236 100644 --- a/terraform/aws/env/us-east/terraform.tfvars +++ b/terraform/aws/env/us-east/terraform.tfvars @@ -1,5 +1,5 @@ region = "us-east-1" -ami = "ami-6ce26316" +ami = "ami-d42d74ae" instance_type = "t2.medium" key_name = "KEY_NAME" server_count = "3" diff --git a/terraform/shared/scripts/setup.sh b/terraform/shared/scripts/setup.sh index b60573317..da6ade089 100644 --- a/terraform/shared/scripts/setup.sh +++ b/terraform/shared/scripts/setup.sh @@ -6,17 +6,17 @@ cd /ops CONFIGDIR=/ops/shared/config -CONSULVERSION=1.0.0 +CONSULVERSION=1.0.2 CONSULDOWNLOAD=https://releases.hashicorp.com/consul/${CONSULVERSION}/consul_${CONSULVERSION}_linux_amd64.zip CONSULCONFIGDIR=/etc/consul.d CONSULDIR=/opt/consul -VAULTVERSION=0.8.3 +VAULTVERSION=0.9.1 VAULTDOWNLOAD=https://releases.hashicorp.com/vault/${VAULTVERSION}/vault_${VAULTVERSION}_linux_amd64.zip VAULTCONFIGDIR=/etc/vault.d VAULTDIR=/opt/vault -NOMADVERSION=0.7.0 +NOMADVERSION=0.7.1 NOMADDOWNLOAD=https://releases.hashicorp.com/nomad/${NOMADVERSION}/nomad_${NOMADVERSION}_linux_amd64.zip NOMADCONFIGDIR=/etc/nomad.d NOMADDIR=/opt/nomad diff --git a/ui/.nvmrc b/ui/.nvmrc new file mode 100644 index 000000000..45a4fb75d --- /dev/null +++ b/ui/.nvmrc @@ -0,0 +1 @@ +8 diff --git a/ui/.prettierrc b/ui/.prettierrc new file mode 100644 index 000000000..7f9eaa64f --- /dev/null +++ b/ui/.prettierrc @@ -0,0 +1,3 @@ +printWidth: 100 +singleQuote: true +trailingComma: es5 diff --git a/ui/.travis.yml b/ui/.travis.yml deleted file mode 100644 index 385f003d4..000000000 --- a/ui/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -language: node_js -node_js: - - "6" - -sudo: false - -cache: - directories: - - $HOME/.npm - -before_install: - - npm config set spin false - - npm install -g phantomjs-prebuilt - - phantomjs --version - -install: - - npm install - -script: - - npm test diff --git a/ui/app/adapters/application.js b/ui/app/adapters/application.js index 1e5c97333..bcf223383 100644 --- a/ui/app/adapters/application.js +++ b/ui/app/adapters/application.js @@ -1,15 +1,14 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { computed, get } from '@ember/object'; import RESTAdapter from 'ember-data/adapters/rest'; import codesForError from '../utils/codes-for-error'; -const { get, computed, inject } = Ember; - export const namespace = 'v1'; export default RESTAdapter.extend({ namespace, - token: inject.service(), + token: service(), headers: computed('token.secret', function() { const token = this.get('token.secret'); diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 4d18d1625..80398adac 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -1,10 +1,10 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import RSVP from 'rsvp'; +import { assign } from '@ember/polyfills'; import ApplicationAdapter from './application'; -const { RSVP, inject, assign } = Ember; - export default ApplicationAdapter.extend({ - system: inject.service(), + system: service(), shouldReloadAll: () => true, diff --git a/ui/app/adapters/token.js b/ui/app/adapters/token.js index 0bd103334..32315037a 100644 --- a/ui/app/adapters/token.js +++ b/ui/app/adapters/token.js @@ -1,10 +1,8 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; import { default as ApplicationAdapter, namespace } from './application'; -const { inject } = Ember; - export default ApplicationAdapter.extend({ - store: inject.service(), + store: service(), namespace: namespace + '/acl', diff --git a/ui/app/app.js b/ui/app/app.js index 831ad6106..7d6bae3ce 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -1,16 +1,14 @@ -import Ember from 'ember'; +import Application from '@ember/application'; import Resolver from './resolver'; import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; let App; -Ember.MODEL_FACTORY_INJECTIONS = true; - -App = Ember.Application.extend({ +App = Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, - Resolver + Resolver, }); loadInitializers(App, config.modulePrefix); diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index a258f5b24..5625c092f 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -1,10 +1,10 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; +import { run } from '@ember/runloop'; import { lazyClick } from '../helpers/lazy-click'; -const { Component, inject, run } = Ember; - export default Component.extend({ - store: inject.service(), + store: service(), tagName: 'tr', diff --git a/ui/app/components/allocation-status-bar.js b/ui/app/components/allocation-status-bar.js index 0a3734626..7f2d1e2fc 100644 --- a/ui/app/components/allocation-status-bar.js +++ b/ui/app/components/allocation-status-bar.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import { computed } from '@ember/object'; import DistributionBar from './distribution-bar'; -const { computed } = Ember; - export default DistributionBar.extend({ layoutName: 'components/distribution-bar', diff --git a/ui/app/components/attributes-section.js b/ui/app/components/attributes-section.js index f2503c9fc..479865264 100644 --- a/ui/app/components/attributes-section.js +++ b/ui/app/components/attributes-section.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: '', diff --git a/ui/app/components/client-node-row.js b/ui/app/components/client-node-row.js index e2f9dd19c..2775ed58f 100644 --- a/ui/app/components/client-node-row.js +++ b/ui/app/components/client-node-row.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import Component from '@ember/component'; import { lazyClick } from '../helpers/lazy-click'; -const { Component } = Ember; - export default Component.extend({ tagName: 'tr', classNames: ['client-node-row', 'is-interactive'], diff --git a/ui/app/components/distribution-bar.js b/ui/app/components/distribution-bar.js index 105273df6..46d47d8f4 100644 --- a/ui/app/components/distribution-bar.js +++ b/ui/app/components/distribution-bar.js @@ -1,10 +1,13 @@ -import Ember from 'ember'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { run } from '@ember/runloop'; +import { assign } from '@ember/polyfills'; +import { guidFor } from '@ember/object/internals'; import d3 from 'npm:d3-selection'; import 'npm:d3-transition'; import WindowResizable from '../mixins/window-resizable'; import styleStringProperty from '../utils/properties/style-string'; -const { Component, computed, run, assign, guidFor } = Ember; const sumAggregate = (total, val) => total + val; export default Component.extend(WindowResizable, { @@ -96,7 +99,7 @@ export default Component.extend(WindowResizable, { }); slices = slices.merge(slicesEnter); - slices.attr('class', d => d.className || `slice-${filteredData.indexOf(d)}`); + slices.attr('class', d => d.className || `slice-${_data.indexOf(d)}`); const setWidth = d => `${width * d.percent - (d.index === sliceCount - 1 || d.index === 0 ? 1 : 2)}px` const setOffset = d => `${width * d.offset + (d.index === 0 ? 0 : 1)}px` diff --git a/ui/app/components/freestyle/sg-boxed-section.js b/ui/app/components/freestyle/sg-boxed-section.js new file mode 100644 index 000000000..4e4f4fa7d --- /dev/null +++ b/ui/app/components/freestyle/sg-boxed-section.js @@ -0,0 +1,27 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + variants: computed(() => [ + { + key: 'Normal', + title: 'Normal', + slug: '', + }, + { + key: 'Info', + title: 'Info', + slug: 'is-info', + }, + { + key: 'Warning', + title: 'Warning', + slug: 'is-warning', + }, + { + key: 'Danger', + title: 'Danger', + slug: 'is-danger', + }, + ]), +}); diff --git a/ui/app/components/freestyle/sg-colors.js b/ui/app/components/freestyle/sg-colors.js new file mode 100644 index 000000000..40d77fcd2 --- /dev/null +++ b/ui/app/components/freestyle/sg-colors.js @@ -0,0 +1,97 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + nomadTheme: computed(() => [ + { + name: 'Primary', + base: '#25ba81', + }, + { + name: 'Primary Dark', + base: '#1d9467', + }, + { + name: 'Text', + base: '#0a0a0a', + }, + { + name: 'Link', + base: '#1563ff', + }, + { + name: 'Gray', + base: '#bbc4d1', + }, + { + name: 'Off-white', + base: '#f5f5f5', + }, + ]), + + productColors: computed(() => [ + { + name: 'Consul Pink', + base: '#ff0087', + }, + { + name: 'Consul Pink Dark', + base: '#c62a71', + }, + { + name: 'Packer Blue', + base: '#1daeff', + }, + { + name: 'Packer Blue Dark', + base: '#1d94dd', + }, + { + name: 'Terraform Purple', + base: '#5c4ee5', + }, + { + name: 'Terraform Purple Dark', + base: '#4040b2', + }, + { + name: 'Vagrant Blue', + base: '#1563ff', + }, + { + name: 'Vagrant Blue Dark', + base: '#104eb2', + }, + { + name: 'Nomad Green', + base: '#25ba81', + }, + { + name: 'Nomad Green Dark', + base: '#1d9467', + }, + { + name: 'Nomad Green Darker', + base: '#16704d', + }, + ]), + + emotiveColors: computed(() => [ + { + name: 'Success', + base: '#23d160', + }, + { + name: 'Warning', + base: '#fa8e23', + }, + { + name: 'Danger', + base: '#c84034', + }, + { + name: 'Info', + base: '#1563ff', + }, + ]), +}); diff --git a/ui/app/components/freestyle/sg-distribution-bar-jumbo.js b/ui/app/components/freestyle/sg-distribution-bar-jumbo.js new file mode 100644 index 000000000..9176d6b7c --- /dev/null +++ b/ui/app/components/freestyle/sg-distribution-bar-jumbo.js @@ -0,0 +1,13 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + distributionBarData: computed(() => { + return [ + { label: 'one', value: 10 }, + { label: 'two', value: 20 }, + { label: 'three', value: 0 }, + { label: 'four', value: 35 }, + ]; + }), +}); diff --git a/ui/app/components/freestyle/sg-distribution-bar.js b/ui/app/components/freestyle/sg-distribution-bar.js new file mode 100644 index 000000000..ab43ceb03 --- /dev/null +++ b/ui/app/components/freestyle/sg-distribution-bar.js @@ -0,0 +1,43 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + timerTicks: 0, + + startTimer: function() { + this.set( + 'timer', + setInterval(() => { + this.incrementProperty('timerTicks'); + }, 500) + ); + }.on('init'), + + willDestroy() { + clearInterval(this.get('timer')); + }, + + distributionBarData: computed(() => { + return [ + { label: 'one', value: 10 }, + { label: 'two', value: 20 }, + { label: 'three', value: 30 }, + ]; + }), + + distributionBarDataWithClasses: computed(() => { + return [ + { label: 'Queued', value: 10, className: 'queued' }, + { label: 'Complete', value: 20, className: 'complete' }, + { label: 'Failed', value: 30, className: 'failed' }, + ]; + }), + + distributionBarDataRotating: computed('timerTicks', () => { + return [ + { label: 'one', value: Math.round(Math.random() * 50) }, + { label: 'two', value: Math.round(Math.random() * 50) }, + { label: 'three', value: Math.round(Math.random() * 50) }, + ]; + }), +}); diff --git a/ui/app/components/gutter-menu.js b/ui/app/components/gutter-menu.js index 9ab098cbb..7a5086b76 100644 --- a/ui/app/components/gutter-menu.js +++ b/ui/app/components/gutter-menu.js @@ -1,9 +1,9 @@ -import Ember from 'ember'; - -const { Component, inject, computed } = Ember; +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; export default Component.extend({ - system: inject.service(), + system: service(), sortedNamespaces: computed('system.namespaces.@each.name', function() { const namespaces = this.get('system.namespaces').toArray() || []; diff --git a/ui/app/components/job-deployment.js b/ui/app/components/job-deployment.js index 1a2476be8..feb13f17d 100644 --- a/ui/app/components/job-deployment.js +++ b/ui/app/components/job-deployment.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ classNames: ['job-deployment', 'boxed-section'], diff --git a/ui/app/components/job-deployment/deployment-metrics.js b/ui/app/components/job-deployment/deployment-metrics.js index f2503c9fc..479865264 100644 --- a/ui/app/components/job-deployment/deployment-metrics.js +++ b/ui/app/components/job-deployment/deployment-metrics.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: '', diff --git a/ui/app/components/job-deployments-stream.js b/ui/app/components/job-deployments-stream.js index 95bd4f198..0ddb5fedb 100644 --- a/ui/app/components/job-deployments-stream.js +++ b/ui/app/components/job-deployments-stream.js @@ -1,17 +1,16 @@ -import Ember from 'ember'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; import moment from 'moment'; -const { Component, computed } = Ember; - export default Component.extend({ tagName: 'ol', classNames: ['timeline'], deployments: computed(() => []), - sortedDeployments: computed('deployments.@each.version.submitTime', function() { + sortedDeployments: computed('deployments.@each.versionSubmitTime', function() { return this.get('deployments') - .sortBy('version.submitTime') + .sortBy('versionSubmitTime') .reverse(); }), diff --git a/ui/app/components/job-diff-fields-and-objects.js b/ui/app/components/job-diff-fields-and-objects.js index f2503c9fc..479865264 100644 --- a/ui/app/components/job-diff-fields-and-objects.js +++ b/ui/app/components/job-diff-fields-and-objects.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: '', diff --git a/ui/app/components/job-diff.js b/ui/app/components/job-diff.js index 2bba7310e..79371f333 100644 --- a/ui/app/components/job-diff.js +++ b/ui/app/components/job-diff.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Component, computed } = Ember; +import { equal } from '@ember/object/computed'; +import Component from '@ember/component'; export default Component.extend({ classNames: ['job-diff'], @@ -10,7 +9,7 @@ export default Component.extend({ verbose: true, - isEdited: computed.equal('diff.Type', 'Edited'), - isAdded: computed.equal('diff.Type', 'Added'), - isDeleted: computed.equal('diff.Type', 'Deleted'), + isEdited: equal('diff.Type', 'Edited'), + isAdded: equal('diff.Type', 'Added'), + isDeleted: equal('diff.Type', 'Deleted'), }); diff --git a/ui/app/components/job-row.js b/ui/app/components/job-row.js index e370cfba9..db9e6e369 100644 --- a/ui/app/components/job-row.js +++ b/ui/app/components/job-row.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import Component from '@ember/component'; import { lazyClick } from '../helpers/lazy-click'; -const { Component } = Ember; - export default Component.extend({ tagName: 'tr', classNames: ['job-row', 'is-interactive'], diff --git a/ui/app/components/job-version.js b/ui/app/components/job-version.js index 0ffe77803..978111a93 100644 --- a/ui/app/components/job-version.js +++ b/ui/app/components/job-version.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Component, computed } = Ember; +import Component from '@ember/component'; +import { computed } from '@ember/object'; const changeTypes = ['Added', 'Deleted', 'Edited']; diff --git a/ui/app/components/job-versions-stream.js b/ui/app/components/job-versions-stream.js index 8ac011376..b2a92d71a 100644 --- a/ui/app/components/job-versions-stream.js +++ b/ui/app/components/job-versions-stream.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; import moment from 'moment'; -const { Component, computed } = Ember; - export default Component.extend({ tagName: 'ol', classNames: ['timeline'], diff --git a/ui/app/components/json-viewer.js b/ui/app/components/json-viewer.js index 2d8442e33..cf966757d 100644 --- a/ui/app/components/json-viewer.js +++ b/ui/app/components/json-viewer.js @@ -1,8 +1,8 @@ -import Ember from 'ember'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { run } from '@ember/runloop'; import JSONFormatterPkg from 'npm:json-formatter-js'; -const { Component, computed, run } = Ember; - // json-formatter-js is packaged in a funny way that ember-cli-browserify // doesn't unwrap properly. const { default: JSONFormatter } = JSONFormatterPkg; diff --git a/ui/app/components/list-pagination.js b/ui/app/components/list-pagination.js index c18301d4a..c04aaa9f4 100644 --- a/ui/app/components/list-pagination.js +++ b/ui/app/components/list-pagination.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Component, computed } = Ember; +import Component from '@ember/component'; +import { computed } from '@ember/object'; export default Component.extend({ source: computed(() => []), @@ -31,9 +30,11 @@ export default Component.extend({ const lowerBound = Math.max(1, page - spread); const upperBound = Math.min(lastPage, page + spread) + 1; - return Array(upperBound - lowerBound).fill(null).map((_, index) => ({ - pageNumber: lowerBound + index, - })); + return Array(upperBound - lowerBound) + .fill(null) + .map((_, index) => ({ + pageNumber: lowerBound + index, + })); }), list: computed('source.[]', 'page', 'size', function() { diff --git a/ui/app/components/list-pagination/list-pager.js b/ui/app/components/list-pagination/list-pager.js index f2503c9fc..479865264 100644 --- a/ui/app/components/list-pagination/list-pager.js +++ b/ui/app/components/list-pagination/list-pager.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: '', diff --git a/ui/app/components/list-table.js b/ui/app/components/list-table.js index 39cf15fe6..0b6d63416 100644 --- a/ui/app/components/list-table.js +++ b/ui/app/components/list-table.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Component, computed } = Ember; +import Component from '@ember/component'; +import { computed } from '@ember/object'; export default Component.extend({ tagName: 'table', diff --git a/ui/app/components/list-table/sort-by.js b/ui/app/components/list-table/sort-by.js index 9edcb61e7..cd6e3fc2c 100644 --- a/ui/app/components/list-table/sort-by.js +++ b/ui/app/components/list-table/sort-by.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Component, computed } = Ember; +import Component from '@ember/component'; +import { computed } from '@ember/object'; export default Component.extend({ tagName: 'th', diff --git a/ui/app/components/list-table/table-body.js b/ui/app/components/list-table/table-body.js index 4c756a699..782917851 100644 --- a/ui/app/components/list-table/table-body.js +++ b/ui/app/components/list-table/table-body.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: 'tbody', diff --git a/ui/app/components/list-table/table-head.js b/ui/app/components/list-table/table-head.js index c1f0c7825..92a17d670 100644 --- a/ui/app/components/list-table/table-head.js +++ b/ui/app/components/list-table/table-head.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Component } = Ember; +import Component from '@ember/component'; export default Component.extend({ tagName: 'thead', diff --git a/ui/app/components/search-box.js b/ui/app/components/search-box.js index 19aa76bef..8066f3e0b 100644 --- a/ui/app/components/search-box.js +++ b/ui/app/components/search-box.js @@ -1,13 +1,13 @@ -import Ember from 'ember'; - -const { Component, computed, run } = Ember; +import { reads } from '@ember/object/computed'; +import Component from '@ember/component'; +import { run } from '@ember/runloop'; export default Component.extend({ // Passed to the component (mutable) searchTerm: null, // Used as a debounce buffer - _searchTerm: computed.reads('searchTerm'), + _searchTerm: reads('searchTerm'), // Used to throttle sets to searchTerm debounce: 150, diff --git a/ui/app/components/server-agent-row.js b/ui/app/components/server-agent-row.js index f853ada96..98d401fe0 100644 --- a/ui/app/components/server-agent-row.js +++ b/ui/app/components/server-agent-row.js @@ -1,13 +1,15 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; import { lazyClick } from '../helpers/lazy-click'; -const { Component, inject, computed } = Ember; - export default Component.extend({ - // TODO Switch back to the router service style when it is no longer feature-flagged + // TODO Switch back to the router service once the service behaves more like Route + // https://github.com/emberjs/ember.js/issues/15801 // router: inject.service('router'), - _router: inject.service('-routing'), - router: computed.alias('_router.router'), + _router: service('-routing'), + router: alias('_router.router'), tagName: 'tr', classNames: ['server-agent-row', 'is-interactive'], @@ -15,7 +17,8 @@ export default Component.extend({ agent: null, isActive: computed('agent', 'router.currentURL', function() { - // TODO Switch back to the router service style when it is no longer feature-flagged + // TODO Switch back to the router service once the service behaves more like Route + // https://github.com/emberjs/ember.js/issues/15801 // const targetURL = this.get('router').urlFor('servers.server', this.get('agent')); // const currentURL = `${this.get('router.rootURL').slice(0, -1)}${this.get('router.currentURL')}`; diff --git a/ui/app/components/task-group-row.js b/ui/app/components/task-group-row.js index 4fb239637..98753d615 100644 --- a/ui/app/components/task-group-row.js +++ b/ui/app/components/task-group-row.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import Component from '@ember/component'; import { lazyClick } from '../helpers/lazy-click'; -const { Component } = Ember; - export default Component.extend({ tagName: 'tr', diff --git a/ui/app/components/task-log.js b/ui/app/components/task-log.js index 1b066db73..277023859 100644 --- a/ui/app/components/task-log.js +++ b/ui/app/components/task-log.js @@ -1,12 +1,13 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { run } from '@ember/runloop'; import { task } from 'ember-concurrency'; import { logger } from 'nomad-ui/utils/classes/log'; import WindowResizable from 'nomad-ui/mixins/window-resizable'; -const { Component, computed, inject, run } = Ember; - export default Component.extend(WindowResizable, { - token: inject.service(), + token: service(), classNames: ['boxed-section', 'task-log'], diff --git a/ui/app/controllers/allocations/allocation.js b/ui/app/controllers/allocations/allocation.js index 1ee88420b..75ceaaec3 100644 --- a/ui/app/controllers/allocations/allocation.js +++ b/ui/app/controllers/allocations/allocation.js @@ -1,5 +1,3 @@ -import Ember from 'ember'; - -const { Controller } = Ember; +import Controller from '@ember/controller'; export default Controller.extend({}); diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js index 5a764f2a6..810d0f728 100644 --- a/ui/app/controllers/allocations/allocation/index.js +++ b/ui/app/controllers/allocations/allocation/index.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller from '@ember/controller'; import Sortable from 'nomad-ui/mixins/sortable'; -const { Controller, computed } = Ember; - export default Controller.extend(Sortable, { queryParams: { sortProperty: 'sort', @@ -12,6 +11,6 @@ export default Controller.extend(Sortable, { sortProperty: 'name', sortDescending: false, - listToSort: computed.alias('model.states'), - sortedStates: computed.alias('listSorted'), + listToSort: alias('model.states'), + sortedStates: alias('listSorted'), }); diff --git a/ui/app/controllers/allocations/allocation/task/index.js b/ui/app/controllers/allocations/allocation/task/index.js index 7374af8fd..790e190ec 100644 --- a/ui/app/controllers/allocations/allocation/task/index.js +++ b/ui/app/controllers/allocations/allocation/task/index.js @@ -1,9 +1,9 @@ -import Ember from 'ember'; - -const { Controller, computed } = Ember; +import { alias } from '@ember/object/computed'; +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; export default Controller.extend({ - network: computed.alias('model.resources.networks.firstObject'), + network: alias('model.resources.networks.firstObject'), ports: computed('network.reservedPorts.[]', 'network.dynamicPorts.[]', function() { return (this.get('network.reservedPorts') || []) .map(port => ({ diff --git a/ui/app/controllers/application.js b/ui/app/controllers/application.js index ee8bee9ee..fc3c5e58e 100644 --- a/ui/app/controllers/application.js +++ b/ui/app/controllers/application.js @@ -1,10 +1,12 @@ +import { inject as service } from '@ember/service'; +import Controller from '@ember/controller'; +import { run } from '@ember/runloop'; +import { observer, computed } from '@ember/object'; import Ember from 'ember'; import codesForError from '../utils/codes-for-error'; -const { Controller, computed, inject, run, observer } = Ember; - export default Controller.extend({ - config: inject.service(), + config: service(), error: null, @@ -33,7 +35,7 @@ export default Controller.extend({ run.next(() => { throw this.get('error'); }); - } else { + } else if (!Ember.testing) { run.next(() => { // eslint-disable-next-line console.warn('UNRECOVERABLE ERROR:', this.get('error')); diff --git a/ui/app/controllers/clients.js b/ui/app/controllers/clients.js index 41e5c9e2f..f4d0631dc 100644 --- a/ui/app/controllers/clients.js +++ b/ui/app/controllers/clients.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Controller } = Ember; +import Controller from '@ember/controller'; export default Controller.extend({ isForbidden: false, diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index ed463e813..008169f5e 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -1,9 +1,9 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; -const { Controller, computed } = Ember; - export default Controller.extend(Sortable, Searchable, { queryParams: { currentPage: 'page', @@ -20,9 +20,9 @@ export default Controller.extend(Sortable, Searchable, { searchProps: computed(() => ['shortId', 'name']), - listToSort: computed.alias('model.allocations'), - listToSearch: computed.alias('listSorted'), - sortedAllocations: computed.alias('listSearched'), + listToSort: alias('model.allocations'), + listToSearch: alias('listSorted'), + sortedAllocations: alias('listSearched'), actions: { gotoAllocation(allocation) { diff --git a/ui/app/controllers/clients/index.js b/ui/app/controllers/clients/index.js index eedb9ebe8..ac19f9d9d 100644 --- a/ui/app/controllers/clients/index.js +++ b/ui/app/controllers/clients/index.js @@ -1,14 +1,14 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; +import { computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(Sortable, Searchable, { - clientsController: inject.controller('clients'), + clientsController: controller('clients'), - nodes: computed.alias('model.nodes'), - agents: computed.alias('model.agents'), + nodes: alias('model.nodes'), + agents: alias('model.agents'), queryParams: { currentPage: 'page', @@ -25,11 +25,11 @@ export default Controller.extend(Sortable, Searchable, { searchProps: computed(() => ['id', 'name', 'datacenter']), - listToSort: computed.alias('nodes'), - listToSearch: computed.alias('listSorted'), - sortedNodes: computed.alias('listSearched'), + listToSort: alias('nodes'), + listToSearch: alias('listSorted'), + sortedNodes: alias('listSearched'), - isForbidden: computed.alias('clientsController.isForbidden'), + isForbidden: alias('clientsController.isForbidden'), actions: { gotoNode(node) { diff --git a/ui/app/controllers/freestyle.js b/ui/app/controllers/freestyle.js index 71e5bde38..a5809f613 100644 --- a/ui/app/controllers/freestyle.js +++ b/ui/app/controllers/freestyle.js @@ -1,47 +1,6 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; import FreestyleController from 'ember-freestyle/controllers/freestyle'; -const { inject, computed } = Ember; - export default FreestyleController.extend({ - emberFreestyle: inject.service(), - - timerTicks: 0, - - startTimer: function() { - this.set( - 'timer', - setInterval(() => { - this.incrementProperty('timerTicks'); - }, 500) - ); - }.on('init'), - - stopTimer: function() { - clearInterval(this.get('timer')); - }.on('willDestroy'), - - distributionBarData: computed(() => { - return [ - { label: 'one', value: 10 }, - { label: 'two', value: 20 }, - { label: 'three', value: 30 }, - ]; - }), - - distributionBarDataWithClasses: computed(() => { - return [ - { label: 'Queued', value: 10, className: 'queued' }, - { label: 'Complete', value: 20, className: 'complete' }, - { label: 'Failed', value: 30, className: 'failed' }, - ]; - }), - - distributionBarDataRotating: computed('timerTicks', () => { - return [ - { label: 'one', value: Math.round(Math.random() * 50) }, - { label: 'two', value: Math.round(Math.random() * 50) }, - { label: 'three', value: Math.round(Math.random() * 50) }, - ]; - }), + emberFreestyle: service(), }); diff --git a/ui/app/controllers/jobs.js b/ui/app/controllers/jobs.js index 88f3d60e1..e7a69a7d8 100644 --- a/ui/app/controllers/jobs.js +++ b/ui/app/controllers/jobs.js @@ -1,9 +1,10 @@ -import Ember from 'ember'; - -const { Controller, inject, observer, run } = Ember; +import { inject as service } from '@ember/service'; +import Controller from '@ember/controller'; +import { observer } from '@ember/object'; +import { run } from '@ember/runloop'; export default Controller.extend({ - system: inject.service(), + system: service(), queryParams: { jobNamespace: 'namespace', diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index 079d30dc3..d0d06e9ee 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -1,18 +1,19 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { alias, filterBy } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; +import { computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(Sortable, Searchable, { - system: inject.service(), - jobsController: inject.controller('jobs'), + system: service(), + jobsController: controller('jobs'), - isForbidden: computed.alias('jobsController.isForbidden'), + isForbidden: alias('jobsController.isForbidden'), - pendingJobs: computed.filterBy('model', 'status', 'pending'), - runningJobs: computed.filterBy('model', 'status', 'running'), - deadJobs: computed.filterBy('model', 'status', 'dead'), + pendingJobs: filterBy('model', 'status', 'pending'), + runningJobs: filterBy('model', 'status', 'running'), + deadJobs: filterBy('model', 'status', 'dead'), queryParams: { currentPage: 'page', @@ -42,9 +43,9 @@ export default Controller.extend(Sortable, Searchable, { } ), - listToSort: computed.alias('filteredJobs'), - listToSearch: computed.alias('listSorted'), - sortedJobs: computed.alias('listSearched'), + listToSort: alias('filteredJobs'), + listToSearch: alias('listSorted'), + sortedJobs: alias('listSearched'), isShowingDeploymentDetails: false, diff --git a/ui/app/controllers/jobs/job.js b/ui/app/controllers/jobs/job.js index fab3c3204..5b8865a11 100644 --- a/ui/app/controllers/jobs/job.js +++ b/ui/app/controllers/jobs/job.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Controller, computed } = Ember; +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; export default Controller.extend({ breadcrumbs: computed('model.{name,id}', function() { diff --git a/ui/app/controllers/jobs/job/definition.js b/ui/app/controllers/jobs/job/definition.js index 95144a831..b105b72b0 100644 --- a/ui/app/controllers/jobs/job/definition.js +++ b/ui/app/controllers/jobs/job/definition.js @@ -1,12 +1,11 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(WithNamespaceResetting, { - jobController: inject.controller('jobs.job'), + jobController: controller('jobs.job'), - job: computed.alias('model.job'), + job: alias('model.job'), - breadcrumbs: computed.alias('jobController.breadcrumbs'), + breadcrumbs: alias('jobController.breadcrumbs'), }); diff --git a/ui/app/controllers/jobs/job/deployments.js b/ui/app/controllers/jobs/job/deployments.js index a8c6b1761..0540c98b1 100644 --- a/ui/app/controllers/jobs/job/deployments.js +++ b/ui/app/controllers/jobs/job/deployments.js @@ -1,13 +1,12 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(WithNamespaceResetting, { - jobController: inject.controller('jobs.job'), + jobController: controller('jobs.job'), - job: computed.alias('model'), - deployments: computed.alias('model.deployments'), + job: alias('model'), + deployments: alias('model.deployments'), - breadcrumbs: computed.alias('jobController.breadcrumbs'), + breadcrumbs: alias('jobController.breadcrumbs'), }); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 2738cccf1..97b97efb5 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,12 +1,13 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; +import { computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(Sortable, WithNamespaceResetting, { - system: inject.service(), - jobController: inject.controller('jobs.job'), + system: service(), + jobController: controller('jobs.job'), queryParams: { currentPage: 'page', @@ -20,15 +21,15 @@ export default Controller.extend(Sortable, WithNamespaceResetting, { sortProperty: 'name', sortDescending: false, - breadcrumbs: computed.alias('jobController.breadcrumbs'), - job: computed.alias('model'), + breadcrumbs: alias('jobController.breadcrumbs'), + job: alias('model'), taskGroups: computed('model.taskGroups.[]', function() { return this.get('model.taskGroups') || []; }), - listToSort: computed.alias('taskGroups'), - sortedTaskGroups: computed.alias('listSorted'), + listToSort: alias('taskGroups'), + sortedTaskGroups: alias('listSorted'), sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() { return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse(); diff --git a/ui/app/controllers/jobs/job/loading.js b/ui/app/controllers/jobs/job/loading.js index bb159e836..2251e2d75 100644 --- a/ui/app/controllers/jobs/job/loading.js +++ b/ui/app/controllers/jobs/job/loading.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; - -const { Controller, computed, inject } = Ember; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; export default Controller.extend({ - jobController: inject.controller('jobs.job'), - breadcrumbs: computed.alias('jobController.breadcrumbs'), + jobController: controller('jobs.job'), + breadcrumbs: alias('jobController.breadcrumbs'), }); diff --git a/ui/app/controllers/jobs/job/task-group.js b/ui/app/controllers/jobs/job/task-group.js index 4fc628201..2e2ab25e7 100644 --- a/ui/app/controllers/jobs/job/task-group.js +++ b/ui/app/controllers/jobs/job/task-group.js @@ -1,12 +1,12 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; +import { computed } from '@ember/object'; import Sortable from 'nomad-ui/mixins/sortable'; import Searchable from 'nomad-ui/mixins/searchable'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, { - jobController: inject.controller('jobs.job'), + jobController: controller('jobs.job'), queryParams: { currentPage: 'page', @@ -27,9 +27,9 @@ export default Controller.extend(Sortable, Searchable, WithNamespaceResetting, { return this.get('model.allocations') || []; }), - listToSort: computed.alias('allocations'), - listToSearch: computed.alias('listSorted'), - sortedAllocations: computed.alias('listSearched'), + listToSort: alias('allocations'), + listToSearch: alias('listSorted'), + sortedAllocations: alias('listSearched'), breadcrumbs: computed('jobController.breadcrumbs.[]', 'model.{name}', function() { return this.get('jobController.breadcrumbs').concat([ diff --git a/ui/app/controllers/jobs/job/versions.js b/ui/app/controllers/jobs/job/versions.js index 3b2420213..eb669a22b 100644 --- a/ui/app/controllers/jobs/job/versions.js +++ b/ui/app/controllers/jobs/job/versions.js @@ -1,13 +1,12 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; -const { Controller, computed, inject } = Ember; - export default Controller.extend(WithNamespaceResetting, { - jobController: inject.controller('jobs.job'), + jobController: controller('jobs.job'), - job: computed.alias('model'), - versions: computed.alias('model.versions'), + job: alias('model'), + versions: alias('model.versions'), - breadcrumbs: computed.alias('jobController.breadcrumbs'), + breadcrumbs: alias('jobController.breadcrumbs'), }); diff --git a/ui/app/controllers/servers.js b/ui/app/controllers/servers.js index a8d0e1f8a..5e6f1bea4 100644 --- a/ui/app/controllers/servers.js +++ b/ui/app/controllers/servers.js @@ -1,11 +1,10 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import Controller from '@ember/controller'; import Sortable from 'nomad-ui/mixins/sortable'; -const { Controller, computed } = Ember; - export default Controller.extend(Sortable, { - nodes: computed.alias('model.nodes'), - agents: computed.alias('model.agents'), + nodes: alias('model.nodes'), + agents: alias('model.agents'), queryParams: { currentPage: 'page', @@ -21,6 +20,6 @@ export default Controller.extend(Sortable, { isForbidden: false, - listToSort: computed.alias('agents'), - sortedAgents: computed.alias('listSorted'), + listToSort: alias('agents'), + sortedAgents: alias('listSorted'), }); diff --git a/ui/app/controllers/servers/index.js b/ui/app/controllers/servers/index.js index adf0eee4d..ee49e8f72 100644 --- a/ui/app/controllers/servers/index.js +++ b/ui/app/controllers/servers/index.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; - -const { Controller, computed, inject } = Ember; +import { alias } from '@ember/object/computed'; +import Controller, { inject as controller } from '@ember/controller'; export default Controller.extend({ - serversController: inject.controller('servers'), - isForbidden: computed.alias('serversController.isForbidden'), + serversController: controller('servers'), + isForbidden: alias('serversController.isForbidden'), }); diff --git a/ui/app/controllers/servers/server.js b/ui/app/controllers/servers/server.js index 94f5f0bdd..dd455b3ad 100644 --- a/ui/app/controllers/servers/server.js +++ b/ui/app/controllers/servers/server.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Controller, computed } = Ember; +import Controller from '@ember/controller'; +import { computed } from '@ember/object'; export default Controller.extend({ activeTab: 'tags', diff --git a/ui/app/controllers/settings/tokens.js b/ui/app/controllers/settings/tokens.js index 2aad23469..be1db757d 100644 --- a/ui/app/controllers/settings/tokens.js +++ b/ui/app/controllers/settings/tokens.js @@ -1,12 +1,13 @@ -import Ember from 'ember'; - -const { Controller, inject, computed, getOwner } = Ember; +import { inject as service } from '@ember/service'; +import { reads } from '@ember/object/computed'; +import Controller from '@ember/controller'; +import { getOwner } from '@ember/application'; export default Controller.extend({ - token: inject.service(), - store: inject.service(), + token: service(), + store: service(), - secret: computed.reads('token.secret'), + secret: reads('token.secret'), tokenIsValid: false, tokenIsInvalid: false, diff --git a/ui/app/helpers/css-class.js b/ui/app/helpers/css-class.js index a23d3b90d..d7f30127c 100644 --- a/ui/app/helpers/css-class.js +++ b/ui/app/helpers/css-class.js @@ -1,4 +1,4 @@ -import Ember from 'ember'; +import { helper } from '@ember/component/helper'; /** * CSS Class @@ -12,4 +12,4 @@ export function cssClass([updateType]) { return updateType.replace(/\//g, '-').dasherize(); } -export default Ember.Helper.helper(cssClass); +export default helper(cssClass); diff --git a/ui/app/helpers/format-bytes.js b/ui/app/helpers/format-bytes.js index dc3ab3e15..b2c69ed06 100644 --- a/ui/app/helpers/format-bytes.js +++ b/ui/app/helpers/format-bytes.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Helper } = Ember; +import Helper from '@ember/component/helper'; const UNITS = ['Bytes', 'KiB', 'MiB']; diff --git a/ui/app/helpers/format-percentage.js b/ui/app/helpers/format-percentage.js index ff4358bc7..cfd409c13 100644 --- a/ui/app/helpers/format-percentage.js +++ b/ui/app/helpers/format-percentage.js @@ -1,4 +1,4 @@ -import Ember from 'ember'; +import { helper } from '@ember/component/helper'; /** * Percentage Calculator @@ -35,4 +35,4 @@ function safeNumber(value) { return isNaN(value) ? 0 : +value; } -export default Ember.Helper.helper(formatPercentage); +export default helper(formatPercentage); diff --git a/ui/app/helpers/is-object.js b/ui/app/helpers/is-object.js index 0b7dae86d..97dd42e65 100644 --- a/ui/app/helpers/is-object.js +++ b/ui/app/helpers/is-object.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Helper } = Ember; +import Helper from '@ember/component/helper'; export function isObject([value]) { const isObject = !Array.isArray(value) && value !== null && typeof value === 'object'; diff --git a/ui/app/helpers/lazy-click.js b/ui/app/helpers/lazy-click.js index f9d12dcd2..de96d1c0e 100644 --- a/ui/app/helpers/lazy-click.js +++ b/ui/app/helpers/lazy-click.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Helper, $ } = Ember; +import Helper from '@ember/component/helper'; +import $ from 'jquery'; /** * Lazy Click Event diff --git a/ui/app/helpers/pluralize.js b/ui/app/helpers/pluralize.js index 161b366a4..fcab84cbe 100644 --- a/ui/app/helpers/pluralize.js +++ b/ui/app/helpers/pluralize.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Helper } = Ember; +import Helper from '@ember/component/helper'; export function pluralize([term, count]) { return count === 1 ? term : term.pluralize(); diff --git a/ui/app/helpers/x-icon.js b/ui/app/helpers/x-icon.js index d3c670e54..f53a50543 100644 --- a/ui/app/helpers/x-icon.js +++ b/ui/app/helpers/x-icon.js @@ -1,4 +1,4 @@ -import Ember from 'ember'; +import { helper } from '@ember/component/helper'; import { inlineSvg } from 'ember-inline-svg/helpers/inline-svg'; // Generated at compile-time by ember-inline-svg @@ -18,4 +18,4 @@ export function xIcon(params, options) { return inlineSvg(SVGs, name, { class: classes }); } -export default Ember.Helper.helper(xIcon); +export default helper(xIcon); diff --git a/ui/app/mixins/searchable.js b/ui/app/mixins/searchable.js index 99a929eed..26557f75a 100644 --- a/ui/app/mixins/searchable.js +++ b/ui/app/mixins/searchable.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Mixin, computed, get } = Ember; +import Mixin from '@ember/object/mixin'; +import { get, computed } from '@ember/object'; /** Searchable mixin diff --git a/ui/app/mixins/sortable.js b/ui/app/mixins/sortable.js index 269075cd8..e6a78aaf9 100644 --- a/ui/app/mixins/sortable.js +++ b/ui/app/mixins/sortable.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Mixin, computed } = Ember; +import Mixin from '@ember/object/mixin'; +import { computed } from '@ember/object'; /** Sortable mixin diff --git a/ui/app/mixins/window-resizable.js b/ui/app/mixins/window-resizable.js index 3f3c5b7aa..6a7055bdf 100644 --- a/ui/app/mixins/window-resizable.js +++ b/ui/app/mixins/window-resizable.js @@ -1,8 +1,8 @@ -import Ember from 'ember'; +import Mixin from '@ember/object/mixin'; +import { run } from '@ember/runloop'; +import $ from 'jquery'; -const { run, $ } = Ember; - -export default Ember.Mixin.create({ +export default Mixin.create({ setupWindowResize: function() { run.scheduleOnce('afterRender', this, () => { this.set('_windowResizeHandler', this.get('windowResizeHandler').bind(this)); diff --git a/ui/app/mixins/with-forbidden-state.js b/ui/app/mixins/with-forbidden-state.js index 4b1397e09..85f27676a 100644 --- a/ui/app/mixins/with-forbidden-state.js +++ b/ui/app/mixins/with-forbidden-state.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Mixin } = Ember; +import Mixin from '@ember/object/mixin'; export default Mixin.create({ setupController(controller) { diff --git a/ui/app/mixins/with-model-error-handling.js b/ui/app/mixins/with-model-error-handling.js index 585c61595..484a80148 100644 --- a/ui/app/mixins/with-model-error-handling.js +++ b/ui/app/mixins/with-model-error-handling.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import Mixin from '@ember/object/mixin'; import notifyError from 'nomad-ui/utils/notify-error'; -const { Mixin } = Ember; - export default Mixin.create({ model() { return this._super(...arguments).catch(notifyError(this)); diff --git a/ui/app/mixins/with-namespace-resetting.js b/ui/app/mixins/with-namespace-resetting.js index c40407568..ab57f3e3e 100644 --- a/ui/app/mixins/with-namespace-resetting.js +++ b/ui/app/mixins/with-namespace-resetting.js @@ -1,10 +1,10 @@ -import Ember from 'ember'; - -const { Mixin, inject } = Ember; +import { inject as controller } from '@ember/controller'; +import { inject as service } from '@ember/service'; +import Mixin from '@ember/object/mixin'; export default Mixin.create({ - system: inject.service(), - jobsController: inject.controller('jobs'), + system: service(), + jobsController: controller('jobs'), actions: { gotoJobs(namespace) { diff --git a/ui/app/models/agent.js b/ui/app/models/agent.js index aea6c7ee5..63bd7d2b0 100644 --- a/ui/app/models/agent.js +++ b/ui/app/models/agent.js @@ -1,11 +1,10 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -const { computed, inject } = Ember; - export default Model.extend({ - system: inject.service(), + system: service(), name: attr('string'), address: attr('string'), diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 6cf164a41..617c1d3f7 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -1,4 +1,7 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { readOnly } from '@ember/object/computed'; +import { computed } from '@ember/object'; +import RSVP from 'rsvp'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; @@ -7,8 +10,6 @@ import PromiseObject from '../utils/classes/promise-object'; import timeout from '../utils/timeout'; import shortUUIDProperty from '../utils/properties/short-uuid'; -const { computed, RSVP, inject } = Ember; - const STATUS_ORDER = { pending: 1, running: 2, @@ -18,7 +19,7 @@ const STATUS_ORDER = { }; export default Model.extend({ - token: inject.service(), + token: service(), shortId: shortUUIDProperty('id'), job: belongsTo('job'), @@ -56,7 +57,7 @@ export default Model.extend({ return taskGroups && taskGroups.findBy('name', this.get('taskGroupName')); }), - memoryUsed: computed.readOnly('stats.ResourceUsage.MemoryStats.RSS'), + memoryUsed: readOnly('stats.ResourceUsage.MemoryStats.RSS'), cpuUsed: computed('stats.ResourceUsage.CpuStats.TotalTicks', function() { return Math.floor(this.get('stats.ResourceUsage.CpuStats.TotalTicks') || 0); }), diff --git a/ui/app/models/deployment.js b/ui/app/models/deployment.js index 2a952afb7..02dfb4c81 100644 --- a/ui/app/models/deployment.js +++ b/ui/app/models/deployment.js @@ -1,4 +1,5 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; +import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo, hasMany } from 'ember-data/relationships'; @@ -6,8 +7,6 @@ import { fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; import sumAggregation from '../utils/properties/sum-aggregation'; -const { computed } = Ember; - export default Model.extend({ shortId: shortUUIDProperty('id'), @@ -34,6 +33,9 @@ export default Model.extend({ return (this.get('job.versions') || []).findBy('number', this.get('versionNumber')); }), + // Dependent keys can only go one level past an @each so an alias is needed + versionSubmitTime: alias('version.submitTime'), + placedCanaries: sumAggregation('taskGroupSummaries', 'placedCanaries'), desiredCanaries: sumAggregation('taskGroupSummaries', 'desiredCanaries'), desiredTotal: sumAggregation('taskGroupSummaries', 'desiredTotal'), diff --git a/ui/app/models/evaluation.js b/ui/app/models/evaluation.js index 8afdf15ae..5fcb3550a 100644 --- a/ui/app/models/evaluation.js +++ b/ui/app/models/evaluation.js @@ -1,12 +1,10 @@ -import Ember from 'ember'; +import { bool } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; -const { computed } = Ember; - export default Model.extend({ shortId: shortUUIDProperty('id'), priority: attr('number'), @@ -16,7 +14,7 @@ export default Model.extend({ statusDescription: attr('string'), failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }), - hasPlacementFailures: computed.bool('failedTGAllocs.length'), + hasPlacementFailures: bool('failedTGAllocs.length'), // TEMPORARY: https://github.com/emberjs/data/issues/5209 originalJobId: attr('string'), diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 4f33983ac..511f89856 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -1,12 +1,11 @@ -import Ember from 'ember'; +import { collect, sum, bool, equal } from '@ember/object/computed'; +import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import sumAggregation from '../utils/properties/sum-aggregation'; -const { computed } = Ember; - export default Model.extend({ region: attr('string'), name: attr('string'), @@ -35,7 +34,7 @@ export default Model.extend({ failedAllocs: sumAggregation('taskGroupSummaries', 'failedAllocs'), lostAllocs: sumAggregation('taskGroupSummaries', 'lostAllocs'), - allocsList: computed.collect( + allocsList: collect( 'queuedAllocs', 'startingAllocs', 'runningAllocs', @@ -44,7 +43,7 @@ export default Model.extend({ 'lostAllocs' ), - totalAllocs: computed.sum('allocsList'), + totalAllocs: sum('allocsList'), pendingChildren: attr('number'), runningChildren: attr('number'), @@ -56,7 +55,7 @@ export default Model.extend({ evaluations: hasMany('evaluations'), namespace: belongsTo('namespace'), - hasPlacementFailures: computed.bool('latestFailureEvaluation'), + hasPlacementFailures: bool('latestFailureEvaluation'), latestEvaluation: computed('evaluations.@each.modifyIndex', 'evaluations.isPending', function() { const evaluations = this.get('evaluations'); @@ -82,7 +81,7 @@ export default Model.extend({ } ), - supportsDeployments: computed.equal('type', 'service'), + supportsDeployments: equal('type', 'service'), runningDeployment: computed('deployments.@each.status', function() { return this.get('deployments').findBy('status', 'running'); diff --git a/ui/app/models/namespace.js b/ui/app/models/namespace.js index 7b05135e8..b5b5f5750 100644 --- a/ui/app/models/namespace.js +++ b/ui/app/models/namespace.js @@ -1,11 +1,9 @@ -import Ember from 'ember'; +import { readOnly } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -const { computed } = Ember; - export default Model.extend({ - name: computed.readOnly('id'), + name: readOnly('id'), hash: attr('string'), description: attr('string'), }); diff --git a/ui/app/models/node-attributes.js b/ui/app/models/node-attributes.js index 6bfaba7ca..c893d8500 100644 --- a/ui/app/models/node-attributes.js +++ b/ui/app/models/node-attributes.js @@ -1,9 +1,8 @@ -import Ember from 'ember'; +import { get, computed } from '@ember/object'; import attr from 'ember-data/attr'; import Fragment from 'ember-data-model-fragments/fragment'; import flat from 'npm:flat'; -const { computed, get } = Ember; const { unflatten } = flat; export default Fragment.extend({ @@ -12,10 +11,12 @@ export default Fragment.extend({ attributesStructured: computed('attributes', function() { // `unflatten` doesn't sort keys before unflattening, so manual preprocessing is necessary. const original = this.get('attributes'); - const attrs = Object.keys(original).sort().reduce((obj, key) => { - obj[key] = original[key]; - return obj; - }, {}); + const attrs = Object.keys(original) + .sort() + .reduce((obj, key) => { + obj[key] = original[key]; + return obj; + }, {}); return unflatten(attrs, { overwrite: true }); }), diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 6966acfb9..56e489e1d 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -1,4 +1,4 @@ -import Ember from 'ember'; +import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; @@ -6,8 +6,6 @@ import { fragment } from 'ember-data-model-fragments/attributes'; import shortUUIDProperty from '../utils/properties/short-uuid'; import ipParts from '../utils/ip-parts'; -const { computed } = Ember; - export default Model.extend({ // Available from list response name: attr('string'), diff --git a/ui/app/models/task-event.js b/ui/app/models/task-event.js index 1a26daadc..3d6f464f3 100644 --- a/ui/app/models/task-event.js +++ b/ui/app/models/task-event.js @@ -1,10 +1,9 @@ -import Ember from 'ember'; +import { computed } from '@ember/object'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; import moment from 'moment'; -const { computed } = Ember; const displayProps = [ 'message', 'validationError', diff --git a/ui/app/models/task-group-deployment-summary.js b/ui/app/models/task-group-deployment-summary.js index 9a6b0eab3..9d5c04035 100644 --- a/ui/app/models/task-group-deployment-summary.js +++ b/ui/app/models/task-group-deployment-summary.js @@ -1,10 +1,8 @@ -import Ember from 'ember'; +import { gt } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -const { computed } = Ember; - export default Fragment.extend({ deployment: fragmentOwner(), @@ -12,7 +10,7 @@ export default Fragment.extend({ autoRevert: attr('boolean'), promoted: attr('boolean'), - requiresPromotion: computed.gt('desiredCanaries', 0), + requiresPromotion: gt('desiredCanaries', 0), placedCanaries: attr('number'), desiredCanaries: attr('number'), diff --git a/ui/app/models/task-group-summary.js b/ui/app/models/task-group-summary.js index 5d937f73b..9e713b8ab 100644 --- a/ui/app/models/task-group-summary.js +++ b/ui/app/models/task-group-summary.js @@ -1,10 +1,8 @@ -import Ember from 'ember'; +import { collect, sum } from '@ember/object/computed'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner } from 'ember-data-model-fragments/attributes'; -const { computed } = Ember; - export default Fragment.extend({ job: fragmentOwner(), name: attr('string'), @@ -16,7 +14,7 @@ export default Fragment.extend({ failedAllocs: attr('number'), lostAllocs: attr('number'), - allocsList: computed.collect( + allocsList: collect( 'queuedAllocs', 'startingAllocs', 'runningAllocs', @@ -25,5 +23,5 @@ export default Fragment.extend({ 'lostAllocs' ), - totalAllocs: computed.sum('allocsList'), + totalAllocs: sum('allocsList'), }); diff --git a/ui/app/models/task-group.js b/ui/app/models/task-group.js index b3998a4cd..e5ea67d0e 100644 --- a/ui/app/models/task-group.js +++ b/ui/app/models/task-group.js @@ -1,11 +1,9 @@ -import Ember from 'ember'; +import { computed } from '@ember/object'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; import sumAggregation from '../utils/properties/sum-aggregation'; -const { computed } = Ember; - export default Fragment.extend({ job: fragmentOwner(), diff --git a/ui/app/models/task-state.js b/ui/app/models/task-state.js index a5b3bff22..217e87459 100644 --- a/ui/app/models/task-state.js +++ b/ui/app/models/task-state.js @@ -1,10 +1,9 @@ -import Ember from 'ember'; +import { none } from '@ember/object/computed'; +import { computed } from '@ember/object'; import Fragment from 'ember-data-model-fragments/fragment'; import attr from 'ember-data/attr'; import { fragment, fragmentOwner, fragmentArray } from 'ember-data-model-fragments/attributes'; -const { computed } = Ember; - export default Fragment.extend({ name: attr('string'), state: attr('string'), @@ -12,7 +11,7 @@ export default Fragment.extend({ finishedAt: attr('date'), failed: attr('boolean'), - isActive: computed.none('finishedAt'), + isActive: none('finishedAt'), allocation: fragmentOwner(), task: computed('allocation.taskGroup.tasks.[]', function() { diff --git a/ui/app/models/token.js b/ui/app/models/token.js index 07243dd8a..37db199ae 100644 --- a/ui/app/models/token.js +++ b/ui/app/models/token.js @@ -1,10 +1,8 @@ -import Ember from 'ember'; +import { alias } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { hasMany } from 'ember-data/relationships'; -const { computed } = Ember; - export default Model.extend({ secret: attr('string'), name: attr('string'), @@ -14,5 +12,5 @@ export default Model.extend({ policies: hasMany('policy'), policyNames: attr(), - accessor: computed.alias('id'), + accessor: alias('id'), }); diff --git a/ui/app/router.js b/ui/app/router.js index bddf8813d..494a2173d 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -1,7 +1,7 @@ -import Ember from 'ember'; +import EmberRouter from '@ember/routing/router'; import config from './config/environment'; -const Router = Ember.Router.extend({ +const Router = EmberRouter.extend({ location: config.locationType, rootURL: config.rootURL, }); diff --git a/ui/app/routes/allocations/allocation.js b/ui/app/routes/allocations/allocation.js index 60eff663b..17aa8b10c 100644 --- a/ui/app/routes/allocations/allocation.js +++ b/ui/app/routes/allocations/allocation.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; +import Route from '@ember/routing/route'; import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling'; -const { Route } = Ember; - export default Route.extend(WithModelErrorHandling); diff --git a/ui/app/routes/allocations/allocation/task.js b/ui/app/routes/allocations/allocation/task.js index 4a57f1ad2..dcf2bda10 100644 --- a/ui/app/routes/allocations/allocation/task.js +++ b/ui/app/routes/allocations/allocation/task.js @@ -1,9 +1,9 @@ -import Ember from 'ember'; - -const { Route, inject, Error: EmberError } = Ember; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import EmberError from '@ember/error'; export default Route.extend({ - store: inject.service(), + store: service(), model({ name }) { const allocation = this.modelFor('allocations.allocation'); diff --git a/ui/app/routes/allocations/allocation/task/logs.js b/ui/app/routes/allocations/allocation/task/logs.js index 5e4767eb5..399258f21 100644 --- a/ui/app/routes/allocations/allocation/task/logs.js +++ b/ui/app/routes/allocations/allocation/task/logs.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ model() { diff --git a/ui/app/routes/application.js b/ui/app/routes/application.js index 0a2e57c39..f7437a18c 100644 --- a/ui/app/routes/application.js +++ b/ui/app/routes/application.js @@ -1,9 +1,8 @@ -import Ember from 'ember'; - -const { Route, inject } = Ember; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; export default Route.extend({ - config: inject.service(), + config: service(), resetController(controller, isExiting) { if (isExiting) { diff --git a/ui/app/routes/clients.js b/ui/app/routes/clients.js index f2915a3a9..49559c8c9 100644 --- a/ui/app/routes/clients.js +++ b/ui/app/routes/clients.js @@ -1,12 +1,12 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; -const { Route, inject, RSVP } = Ember; - export default Route.extend(WithForbiddenState, { - store: inject.service(), - system: inject.service(), + store: service(), + system: service(), beforeModel() { return this.get('system.leader'); diff --git a/ui/app/routes/clients/client.js b/ui/app/routes/clients/client.js index 9571d975a..1be621e47 100644 --- a/ui/app/routes/clients/client.js +++ b/ui/app/routes/clients/client.js @@ -1,10 +1,9 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; import notifyError from 'nomad-ui/utils/notify-error'; -const { Route, inject } = Ember; - export default Route.extend({ - store: inject.service(), + store: service(), model() { return this._super(...arguments).catch(notifyError(this)); diff --git a/ui/app/routes/freestyle.js b/ui/app/routes/freestyle.js new file mode 100644 index 000000000..21534ffe7 --- /dev/null +++ b/ui/app/routes/freestyle.js @@ -0,0 +1,18 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import RSVP from 'rsvp'; + +export default Route.extend({ + emberFreestyle: service(), + + beforeModel() { + let emberFreestyle = this.get('emberFreestyle'); + + return emberFreestyle.ensureHljs().then(() => { + return RSVP.all([ + emberFreestyle.ensureHljsLanguage('handlebars'), + emberFreestyle.ensureHljsLanguage('htmlbars'), + ]); + }); + }, +}); diff --git a/ui/app/routes/index.js b/ui/app/routes/index.js index f07c338c8..7de6fff38 100644 --- a/ui/app/routes/index.js +++ b/ui/app/routes/index.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ redirect() { diff --git a/ui/app/routes/jobs.js b/ui/app/routes/jobs.js index 9d51a55b6..745e326e2 100644 --- a/ui/app/routes/jobs.js +++ b/ui/app/routes/jobs.js @@ -1,12 +1,12 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import { run } from '@ember/runloop'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; -const { Route, inject, run } = Ember; - export default Route.extend(WithForbiddenState, { - system: inject.service(), - store: inject.service(), + system: service(), + store: service(), beforeModel() { return this.get('system.namespaces'); diff --git a/ui/app/routes/jobs/index.js b/ui/app/routes/jobs/index.js index 8962abd90..0a8317fea 100644 --- a/ui/app/routes/jobs/index.js +++ b/ui/app/routes/jobs/index.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ actions: { diff --git a/ui/app/routes/jobs/job.js b/ui/app/routes/jobs/job.js index 4317a1e35..558ea55e4 100644 --- a/ui/app/routes/jobs/job.js +++ b/ui/app/routes/jobs/job.js @@ -1,10 +1,10 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; import notifyError from 'nomad-ui/utils/notify-error'; -const { Route, RSVP, inject } = Ember; - export default Route.extend({ - store: inject.service(), + store: service(), serialize(model) { return { job_name: model.get('plainId') }; @@ -13,7 +13,7 @@ export default Route.extend({ model(params, transition) { const namespace = transition.queryParams.namespace || this.get('system.activeNamespace.id'); const name = params.job_name; - const fullId = JSON.stringify([name, namespace]); + const fullId = JSON.stringify([name, namespace || 'default']); return this.get('store') .findRecord('job', fullId, { reload: true }) .then(job => { diff --git a/ui/app/routes/jobs/job/definition.js b/ui/app/routes/jobs/job/definition.js index 4f24dbc60..8730f83b8 100644 --- a/ui/app/routes/jobs/job/definition.js +++ b/ui/app/routes/jobs/job/definition.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ model() { diff --git a/ui/app/routes/jobs/job/deployments.js b/ui/app/routes/jobs/job/deployments.js index a048c898a..41363eff6 100644 --- a/ui/app/routes/jobs/job/deployments.js +++ b/ui/app/routes/jobs/job/deployments.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Route, RSVP } = Ember; +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; export default Route.extend({ model() { diff --git a/ui/app/routes/jobs/job/task-group.js b/ui/app/routes/jobs/job/task-group.js index 2be6107ab..def6e57ea 100644 --- a/ui/app/routes/jobs/job/task-group.js +++ b/ui/app/routes/jobs/job/task-group.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ model({ name }) { diff --git a/ui/app/routes/jobs/job/versions.js b/ui/app/routes/jobs/job/versions.js index 637d663c3..6debc85db 100644 --- a/ui/app/routes/jobs/job/versions.js +++ b/ui/app/routes/jobs/job/versions.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; - -const { Route } = Ember; +import Route from '@ember/routing/route'; export default Route.extend({ model() { diff --git a/ui/app/routes/not-found.js b/ui/app/routes/not-found.js index 1974ee9ba..a7df89c01 100644 --- a/ui/app/routes/not-found.js +++ b/ui/app/routes/not-found.js @@ -1,6 +1,5 @@ -import Ember from 'ember'; - -const { Route, Error: EmberError } = Ember; +import Route from '@ember/routing/route'; +import EmberError from '@ember/error'; export default Route.extend({ model() { diff --git a/ui/app/routes/servers.js b/ui/app/routes/servers.js index f2915a3a9..49559c8c9 100644 --- a/ui/app/routes/servers.js +++ b/ui/app/routes/servers.js @@ -1,12 +1,12 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import Route from '@ember/routing/route'; +import RSVP from 'rsvp'; import WithForbiddenState from 'nomad-ui/mixins/with-forbidden-state'; import notifyForbidden from 'nomad-ui/utils/notify-forbidden'; -const { Route, inject, RSVP } = Ember; - export default Route.extend(WithForbiddenState, { - store: inject.service(), - system: inject.service(), + store: service(), + system: service(), beforeModel() { return this.get('system.leader'); diff --git a/ui/app/routes/servers/server.js b/ui/app/routes/servers/server.js index 60eff663b..17aa8b10c 100644 --- a/ui/app/routes/servers/server.js +++ b/ui/app/routes/servers/server.js @@ -1,6 +1,4 @@ -import Ember from 'ember'; +import Route from '@ember/routing/route'; import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling'; -const { Route } = Ember; - export default Route.extend(WithModelErrorHandling); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 33cb3ebf5..8a7a5d43b 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -1,10 +1,9 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; import ApplicationSerializer from './application'; -const { get, inject } = Ember; - export default ApplicationSerializer.extend({ - system: inject.service(), + system: service(), attrs: { taskGroupName: 'TaskGroup', diff --git a/ui/app/serializers/application.js b/ui/app/serializers/application.js index bdec30fc1..3d63f5b63 100644 --- a/ui/app/serializers/application.js +++ b/ui/app/serializers/application.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import { makeArray } from '@ember/array'; import JSONSerializer from 'ember-data/serializers/json'; -const { makeArray } = Ember; - export default JSONSerializer.extend({ primaryKey: 'ID', diff --git a/ui/app/serializers/deployment.js b/ui/app/serializers/deployment.js index 04e5475a0..2a21f484e 100644 --- a/ui/app/serializers/deployment.js +++ b/ui/app/serializers/deployment.js @@ -1,8 +1,7 @@ -import Ember from 'ember'; +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; -const { get, assign } = Ember; - export default ApplicationSerializer.extend({ attrs: { versionNumber: 'JobVersion', diff --git a/ui/app/serializers/evaluation.js b/ui/app/serializers/evaluation.js index 76a2b9b3c..e5faad410 100644 --- a/ui/app/serializers/evaluation.js +++ b/ui/app/serializers/evaluation.js @@ -1,10 +1,10 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; -const { inject, get, assign } = Ember; - export default ApplicationSerializer.extend({ - system: inject.service(), + system: service(), normalize(typeHash, hash) { hash.FailedTGAllocs = Object.keys(hash.FailedTGAllocs || {}).map(key => { diff --git a/ui/app/serializers/job-version.js b/ui/app/serializers/job-version.js index 490b6d740..f05809b3f 100644 --- a/ui/app/serializers/job-version.js +++ b/ui/app/serializers/job-version.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; -const { assign } = Ember; - export default ApplicationSerializer.extend({ attrs: { number: 'Version', diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index 74eeb78b0..e09c95cd8 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -1,9 +1,8 @@ -import Ember from 'ember'; +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; import queryString from 'npm:query-string'; -const { get, assign } = Ember; - export default ApplicationSerializer.extend({ attrs: { parameterized: 'ParameterizedJob', diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js index 376f15d12..e0ecfc9a7 100644 --- a/ui/app/serializers/node.js +++ b/ui/app/serializers/node.js @@ -1,10 +1,8 @@ -import Ember from 'ember'; +import { inject as service } from '@ember/service'; import ApplicationSerializer from './application'; -const { inject } = Ember; - export default ApplicationSerializer.extend({ - config: inject.service(), + config: service(), attrs: { httpAddr: 'HTTPAddr', diff --git a/ui/app/serializers/task-group.js b/ui/app/serializers/task-group.js index 107ddf11e..a834250c9 100644 --- a/ui/app/serializers/task-group.js +++ b/ui/app/serializers/task-group.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import { copy } from '@ember/object/internals'; import ApplicationSerializer from './application'; -const { copy } = Ember; - export default ApplicationSerializer.extend({ normalize(typeHash, hash) { // Provide EphemeralDisk to each task diff --git a/ui/app/serializers/token.js b/ui/app/serializers/token.js index ede185aed..7ac46e611 100644 --- a/ui/app/serializers/token.js +++ b/ui/app/serializers/token.js @@ -1,8 +1,6 @@ -import Ember from 'ember'; +import { copy } from '@ember/object/internals'; import ApplicationSerializer from './application'; -const { copy } = Ember; - export default ApplicationSerializer.extend({ primaryKey: 'AccessorID', diff --git a/ui/app/services/config.js b/ui/app/services/config.js index 2c586d97c..80e38e1ea 100644 --- a/ui/app/services/config.js +++ b/ui/app/services/config.js @@ -1,14 +1,14 @@ -import Ember from 'ember'; +import { equal } from '@ember/object/computed'; +import Service from '@ember/service'; +import { get } from '@ember/object'; import config from '../config/environment'; -const { Service, get, computed } = Ember; - export default Service.extend({ unknownProperty(path) { return get(config, path); }, - isDev: computed.equal('environment', 'development'), - isProd: computed.equal('environment', 'production'), - isTest: computed.equal('environment', 'test'), + isDev: equal('environment', 'development'), + isProd: equal('environment', 'production'), + isTest: equal('environment', 'test'), }); diff --git a/ui/app/services/ember-freestyle.js b/ui/app/services/ember-freestyle.js index fa276c8d9..a30217765 100644 --- a/ui/app/services/ember-freestyle.js +++ b/ui/app/services/ember-freestyle.js @@ -1,5 +1,5 @@ import EmberFreestyle from 'ember-freestyle/services/ember-freestyle'; export default EmberFreestyle.extend({ - defaultTheme: 'monokai-sublime', + defaultTheme: 'solarized-light', }); diff --git a/ui/app/services/system.js b/ui/app/services/system.js index 111e3bfc2..558b411a9 100644 --- a/ui/app/services/system.js +++ b/ui/app/services/system.js @@ -1,12 +1,11 @@ -import Ember from 'ember'; +import Service, { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; import PromiseObject from '../utils/classes/promise-object'; import { namespace } from '../adapters/application'; -const { Service, computed, inject } = Ember; - export default Service.extend({ - token: inject.service(), - store: inject.service(), + token: service(), + store: service(), leader: computed(function() { const token = this.get('token'); diff --git a/ui/app/services/token.js b/ui/app/services/token.js index be1da0c90..4ce27d9c3 100644 --- a/ui/app/services/token.js +++ b/ui/app/services/token.js @@ -1,8 +1,8 @@ -import Ember from 'ember'; +import Service from '@ember/service'; +import { computed } from '@ember/object'; +import { assign } from '@ember/polyfills'; import fetch from 'nomad-ui/utils/fetch'; -const { Service, computed, assign } = Ember; - export default Service.extend({ secret: computed({ get() { @@ -19,7 +19,7 @@ export default Service.extend({ }, }), - authorizedRequest(url, options = {}) { + authorizedRequest(url, options = { credentials: 'include' }) { const headers = {}; const token = this.get('secret'); diff --git a/ui/app/styles/app.scss b/ui/app/styles/app.scss index 444a30182..952709aee 100644 --- a/ui/app/styles/app.scss +++ b/ui/app/styles/app.scss @@ -1,5 +1,8 @@ -@import "./core"; -@import "./components"; -@import "./charts"; +@import './core'; +@import './components'; +@import './charts'; -@import "ember-power-select"; +@import 'ember-power-select'; + +// Only necessary in dev +@import './styleguide.scss'; diff --git a/ui/app/styles/charts/colors.scss b/ui/app/styles/charts/colors.scss index 4f3c6335f..e204c723f 100644 --- a/ui/app/styles/charts/colors.scss +++ b/ui/app/styles/charts/colors.scss @@ -10,7 +10,8 @@ $lost: $dark; fill: $queued; } - .starting, .pending { + .starting, + .pending { .layer-0 { fill: $starting; } @@ -58,7 +59,8 @@ $lost: $dark; background: $queued; } - &.starting, &.pending { + &.starting, + &.pending { background: repeating-linear-gradient( -45deg, $starting, diff --git a/ui/app/styles/charts/distribution-bar.scss b/ui/app/styles/charts/distribution-bar.scss index a59c041fb..fcd4a7824 100644 --- a/ui/app/styles/charts/distribution-bar.scss +++ b/ui/app/styles/charts/distribution-bar.scss @@ -21,8 +21,7 @@ opacity: 0; } - $color-sequence: $orange, $yellow, $green, $turquoise, $blue, $purple, - $red; + $color-sequence: $orange, $yellow, $green, $turquoise, $blue, $purple, $red; @for $i from 1 through length($color-sequence) { .slice-#{$i - 1} { diff --git a/ui/app/styles/charts/tooltip.scss b/ui/app/styles/charts/tooltip.scss index c8e17f908..580e0ab3b 100644 --- a/ui/app/styles/charts/tooltip.scss +++ b/ui/app/styles/charts/tooltip.scss @@ -18,7 +18,7 @@ &::before { pointer-events: none; display: inline-block; - content: ""; + content: ''; width: 0; height: 0; border-top: 7px solid $grey; @@ -34,7 +34,7 @@ &::after { pointer-events: none; display: inline-block; - content: ""; + content: ''; width: 0; height: 0; border-top: 6px solid $white; diff --git a/ui/app/styles/components.scss b/ui/app/styles/components.scss index cb2162c2f..afd92f4e0 100644 --- a/ui/app/styles/components.scss +++ b/ui/app/styles/components.scss @@ -1,6 +1,5 @@ @import "./components/badge"; @import "./components/boxed-section"; -@import "./components/breadcrumbs"; @import "./components/cli-window"; @import "./components/ember-power-select"; @import "./components/empty-message"; @@ -12,6 +11,7 @@ @import "./components/loading-spinner"; @import "./components/metrics"; @import "./components/node-status-light"; +@import "./components/page-layout"; @import "./components/simple-list"; @import "./components/status-text"; @import "./components/timeline"; diff --git a/ui/app/styles/components/breadcrumbs.scss b/ui/app/styles/components/breadcrumbs.scss deleted file mode 100644 index c3ec634a6..000000000 --- a/ui/app/styles/components/breadcrumbs.scss +++ /dev/null @@ -1,26 +0,0 @@ -.breadcrumbs { - .breadcrumb { - color: $white; - opacity: 0.7; - text-decoration: none; - - + .breadcrumb { - margin-left: 15px; - &::before { - content: "/"; - color: $primary; - position: relative; - left: -7px; - } - } - - &:last-child { - opacity: 1; - } - } - - a.breadcrumb:hover { - color: $primary-invert; - opacity: 1; - } -} diff --git a/ui/app/styles/components/json-viewer.scss b/ui/app/styles/components/json-viewer.scss index dd215bc97..4c6b1431f 100644 --- a/ui/app/styles/components/json-viewer.scss +++ b/ui/app/styles/components/json-viewer.scss @@ -13,7 +13,9 @@ $url-color: blue ) { font-family: monospace; - &, a, a:hover { + &, + a, + a:hover { color: $default-color; text-decoration: none; } @@ -31,10 +33,10 @@ display: none; } &.json-formatter-object:after { - content: "No properties"; + content: 'No properties'; } &.json-formatter-array:after { - content: "[]"; + content: '[]'; } } } @@ -100,7 +102,7 @@ // Inline preview on hover (optional) > a > .json-formatter-preview-text { opacity: 0; - transition: opacity .15s ease-in; + transition: opacity 0.15s ease-in; font-style: italic; } diff --git a/ui/app/styles/core/page-layout.scss b/ui/app/styles/components/page-layout.scss similarity index 92% rename from ui/app/styles/core/page-layout.scss rename to ui/app/styles/components/page-layout.scss index 26c8de78e..d463be113 100644 --- a/ui/app/styles/core/page-layout.scss +++ b/ui/app/styles/components/page-layout.scss @@ -1,8 +1,3 @@ -html, body, body > .ember-view { - height: 100%; - width: 100%; -} - .page-layout { height: 100%; display: flex; diff --git a/ui/app/styles/components/timeline.scss b/ui/app/styles/components/timeline.scss index bc3ab077a..553257c6c 100644 --- a/ui/app/styles/components/timeline.scss +++ b/ui/app/styles/components/timeline.scss @@ -4,7 +4,7 @@ z-index: $z-base; &::before { - content: " "; + content: ' '; position: absolute; display: block; top: 0; @@ -32,7 +32,7 @@ } &::before { - content: " "; + content: ' '; position: absolute; display: block; width: 10px; diff --git a/ui/app/styles/components/tooltip.scss b/ui/app/styles/components/tooltip.scss index bcb851033..68842c133 100644 --- a/ui/app/styles/components/tooltip.scss +++ b/ui/app/styles/components/tooltip.scss @@ -31,7 +31,7 @@ pointer-events: none; display: block; opacity: 0; - content: ""; + content: ''; width: 0; height: 0; border-top: 6px solid $black; diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index 3021d6be0..e4391415b 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -1,60 +1,28 @@ // Utils +@import "./utils/reset.scss"; @import "./utils/z-indices"; +@import "./utils/product-colors"; +@import "./utils/bumper"; // Start with Bulma variables as a foundation @import "bulma/sass/utilities/initial-variables"; // Override variables where appropriate -@import "./utils/product-colors"; - -$orange: #fa8e23; -$green: #2eb039; -$blue: $vagrant-blue; -$purple: $terraform-purple; -$red: #c84034; -$grey-blue: #bbc4d1; - -$primary: $nomad-green; -$warning: $orange; -$warning-invert: $white; -$danger: $red; -$dark: #234; - -$radius: 2px; - -$body-size: 14px; -$title-size: 1.75rem; -$size-5: 1.15rem; -$size-4: 1.3rem; -$size-7: 0.85rem; - -$title-weight: $weight-semibold; - -$family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - -$text: $black; - -$header-height: 112px; -$gutter-width: 250px; - -$icon-dimensions: 1.25rem; -$icon-dimensions-small: 1rem; -$icon-dimensions-medium: 1.5rem; -$icon-dimensions-large: 2.5rem; +@import "./core/variables.scss"; // Bring in the rest of Bulma @import "bulma/bulma"; // Override Bulma details where appropriate @import "./core/buttons"; +@import "./core/breadcrumb"; @import "./core/columns"; @import "./core/forms"; @import "./core/icon"; @import "./core/level"; @import "./core/menu"; @import "./core/message"; -@import "./core/nav"; +@import "./core/navbar"; @import "./core/notification"; @import "./core/pagination"; @import "./core/progress"; @@ -64,7 +32,3 @@ $icon-dimensions-large: 2.5rem; @import "./core/tag"; @import "./core/title"; @import "./core/typography"; - -// Add unique core extensions -@import "./core/page-layout"; -@import "./core/bumper"; diff --git a/ui/app/styles/core/breadcrumb.scss b/ui/app/styles/core/breadcrumb.scss new file mode 100644 index 000000000..182decee3 --- /dev/null +++ b/ui/app/styles/core/breadcrumb.scss @@ -0,0 +1,15 @@ +.breadcrumb { + a { + text-decoration: none; + opacity: 0.7; + + &:hover { + text-decoration: none; + opacity: 1; + } + } + + li.is-active a { + opacity: 1; + } +} diff --git a/ui/app/styles/core/forms.scss b/ui/app/styles/core/forms.scss index 741dd892e..19ad29950 100644 --- a/ui/app/styles/core/forms.scss +++ b/ui/app/styles/core/forms.scss @@ -4,7 +4,12 @@ border-color: $grey-blue; color: $text; - &:hover, &.is-hovered, &:active, &.is-active, &:focus, &.is-focused { + &:hover, + &.is-hovered, + &:active, + &.is-active, + &:focus, + &.is-focused { border-color: darken($grey-blue, 5%); } @@ -14,7 +19,8 @@ } } -.input, .textarea { +.input, +.textarea { @include input; box-shadow: none; padding: 0.75em 1.5em; diff --git a/ui/app/styles/core/message.scss b/ui/app/styles/core/message.scss index b5e71e6b7..dba6ffa4b 100644 --- a/ui/app/styles/core/message.scss +++ b/ui/app/styles/core/message.scss @@ -1,8 +1,8 @@ .message { - background: $body-background; + background: $body-background-color; .message-header { - background: $body-background; + background: $body-background-color; color: $text; font-size: $size-5; font-weight: $weight-semibold; diff --git a/ui/app/styles/core/nav.scss b/ui/app/styles/core/navbar.scss similarity index 71% rename from ui/app/styles/core/nav.scss rename to ui/app/styles/core/navbar.scss index b1c6d6079..39232665d 100644 --- a/ui/app/styles/core/nav.scss +++ b/ui/app/styles/core/navbar.scss @@ -1,21 +1,18 @@ -.nav { +.navbar { &.is-primary { - background: linear-gradient( - to right, - $nomad-green-darker, - $nomad-green-dark - ); + background: linear-gradient(to right, $nomad-green-darker, $nomad-green-dark); height: 3.5rem; color: $primary-invert; padding-left: 20px; padding-right: 20px; - .nav-item { + .navbar-item { color: rgba($primary-invert, 0.8); text-decoration: none; &:hover { color: $primary-invert; + background: transparent; } &.is-active, @@ -24,14 +21,14 @@ border-bottom-color: $primary-invert; } - + .nav-item { + + .navbar-item { position: relative; &::before { width: 1px; height: 1em; background: rgba($primary-invert, 0.5); - content: " "; + content: ' '; display: block; position: absolute; left: 0px; @@ -44,6 +41,15 @@ max-height: 26px; } } + + .navbar-end > a.navbar-item { + color: rgba($primary-invert, 0.8); + + &:hover { + color: $primary-invert; + background: transparent; + } + } } &.is-secondary { @@ -53,12 +59,12 @@ font-weight: $weight-semibold; color: $primary-invert; - .nav-item { + .navbar-item { font-size: $size-4; } } - .nav-item { + .navbar-item { &.is-gutter { width: $gutter-width; } diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 171379635..ecfacdd86 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -3,6 +3,7 @@ border-radius: $radius; border: 1px solid $grey-blue; border-collapse: separate; + width: 100%; &.is-fixed { table-layout: fixed; @@ -146,7 +147,7 @@ position: relative; &::after { - content: ""; + content: ''; width: 10px; right: 1.5em; top: 0.75em; @@ -156,11 +157,11 @@ } &.asc::after { - content: "⬇"; + content: '⬇'; } &.desc::after { - content: "⬆"; + content: '⬆'; } } @@ -185,7 +186,7 @@ &::after { position: absolute; - content: ""; + content: ''; width: 3px; top: 0; bottom: 0; diff --git a/ui/app/styles/core/tag.scss b/ui/app/styles/core/tag.scss index de4f25c74..cbf2bbe3b 100644 --- a/ui/app/styles/core/tag.scss +++ b/ui/app/styles/core/tag.scss @@ -1,4 +1,6 @@ -.tag { +// Strange selector mirrors selector used in Bulma +// https://github.com/jgthms/bulma/issues/912 +.tag:not(body) { text-transform: uppercase; border-radius: 200px; font-weight: $weight-normal; diff --git a/ui/app/styles/core/variables.scss b/ui/app/styles/core/variables.scss new file mode 100644 index 000000000..bef639661 --- /dev/null +++ b/ui/app/styles/core/variables.scss @@ -0,0 +1,41 @@ +$orange: #fa8e23; +$green: #2eb039; +$blue: $vagrant-blue; +$purple: $terraform-purple; +$red: #c84034; +$grey-blue: #bbc4d1; + +$primary: $nomad-green; +$warning: $orange; +$warning-invert: $white; +$danger: $red; +$info: $blue; +$dark: #234; + +$radius: 2px; + +$body-size: 14px; +$title-size: 1.75rem; +$size-5: 1.15rem; +$size-4: 1.3rem; +$size-7: 0.85rem; + +$title-weight: $weight-semibold; + +$family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, + Cantarell, 'Helvetica Neue', sans-serif; + +$text: $black; + +$header-height: 112px; +$gutter-width: 250px; + +$icon-dimensions: 1.25rem; +$icon-dimensions-small: 1rem; +$icon-dimensions-medium: 1.5rem; +$icon-dimensions-large: 2.5rem; + +$breadcrumb-item-color: $white; +$breadcrumb-item-hover-color: $white; +$breadcrumb-item-active-color: $white; +$breadcrumb-item-separator-color: $primary; diff --git a/ui/app/styles/styleguide.scss b/ui/app/styles/styleguide.scss new file mode 100644 index 000000000..f77df0655 --- /dev/null +++ b/ui/app/styles/styleguide.scss @@ -0,0 +1,44 @@ +#styleguide { + .mock-content { + display: flex; + height: 250px; + + .mock-image, + .mock-copy { + height: 100%; + width: 100%; + margin: 1em; + } + + .mock-image { + background: linear-gradient( + to top right, + transparent 0%, + transparent 49%, + $grey-blue 49%, + $grey-blue 51%, + transparent 51%, + transparent 100% + ), + linear-gradient( + to bottom right, + transparent 0%, + transparent 49%, + $grey-blue 49%, + $grey-blue 51%, + transparent 51%, + transparent 100% + ); + } + + .mock-copy { + background: repeating-linear-gradient( + to bottom, + $grey-blue, + $grey-blue 5px, + transparent 5px, + transparent 14px + ); + } + } +} diff --git a/ui/app/styles/core/bumper.scss b/ui/app/styles/utils/bumper.scss similarity index 100% rename from ui/app/styles/core/bumper.scss rename to ui/app/styles/utils/bumper.scss diff --git a/ui/app/styles/utils/product-colors.scss b/ui/app/styles/utils/product-colors.scss index 689b9e84e..bbf4b63aa 100644 --- a/ui/app/styles/utils/product-colors.scss +++ b/ui/app/styles/utils/product-colors.scss @@ -1,15 +1,15 @@ -$consul-pink: #FF0087; -$consul-pink-dark: #C62A71; +$consul-pink: #ff0087; +$consul-pink-dark: #c62a71; -$packer-blue: #1DAEFF; -$packer-blue-dark: #1D94DD; +$packer-blue: #1daeff; +$packer-blue-dark: #1d94dd; -$terraform-purple: #5C4EE5; -$terraform-purple-dark: #4040B2; +$terraform-purple: #5c4ee5; +$terraform-purple-dark: #4040b2; -$vagrant-blue: #1563FF; -$vagrant-blue-dark: #104EB2; +$vagrant-blue: #1563ff; +$vagrant-blue-dark: #104eb2; -$nomad-green: #25BA81; +$nomad-green: #25ba81; $nomad-green-dark: #1d9467; -$nomad-green-darker: #16704D; +$nomad-green-darker: #16704d; diff --git a/ui/app/styles/utils/reset.scss b/ui/app/styles/utils/reset.scss new file mode 100644 index 000000000..42d609252 --- /dev/null +++ b/ui/app/styles/utils/reset.scss @@ -0,0 +1,6 @@ +html, +body, +body > .ember-view { + height: 100%; + width: 100%; +} diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 96cb194b5..9778b2637 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -1,25 +1,25 @@ {{#global-header class="page-header"}} - - {{#link-to "allocations.allocation" model class="breadcrumb"}} - {{model.shortId}} - {{/link-to}} +