From cae618d7df4ab8278bdaeb89c556bfbe1ff3885f Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Fri, 6 Oct 2017 17:32:26 +0100 Subject: [PATCH 01/79] Added Vault documentation for integrating with Identity Systems --- website/source/guides/acl.html.markdown | 137 ++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index c6e8d55a8..d8dc18c06 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -347,3 +347,140 @@ Error bootstrapping: Unexpected response code: 500 (Invalid bootstrap reset inde This is because the reset file is in place, but with the incorrect index. The reset file can be deleted, but Nomad will not reset the bootstrap until the index is corrected. +## Vault Integration +Hashicorp Vault has a secret backend for generating short lived Nomad tokens. As Vault has a number of +authentication backends, it could provide a workflow where a user or orchestration system authenticates +using an pre-existing identity service (LDAP, Okta, Amazon IAM, etc. ...) in order to obtain a short-lived +Nomad token. + +~> Hashicorp Vault is a standalone product with it's own set of deployment and configuration best +practices. Please review Vault's documentation before deploying it in production. + +For evaluation purposes, a Vault server in "dev" mode can be used. + +``` +$ vault server -dev +==> Vault server configuration: + + Cgo: disabled + Cluster Address: https://127.0.0.1:8201 + Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", tls: "disabled") + Log Level: info + Mlock: supported: false, enabled: false + Redirect Address: http://127.0.0.1:8200 + Storage: inmem + Version: Vault v0.8.3 + Version Sha: a393b20cb6d96c73e52eb5af776c892b8107a45d + +==> WARNING: Dev mode is enabled! + +In this mode, Vault is completely in-memory and unsealed. +Vault is configured to only have a single unseal key. The root +token has already been authenticated with the CLI, so you can +immediately begin using the Vault CLI. + +The only step you need to take is to set the following +environment variables: + + export VAULT_ADDR='http://127.0.0.1:8200' + +The unseal key and root token are reproduced below in case you +want to seal/unseal the Vault or play with authentication. + +Unseal Key: YzFfPgnLl9R1f6bLU7tGqi/PIDhDaAV/tlNDMV5Rrq0= +Root Token: f84b587e-5882-bba1-a3f0-d1a3d90ca105 + +``` + +### Pre-requisites +- Nomad ACL system bootstrapped. +- A management token (the bootstrap token can be used, but for production systems it's recommended to +have a separate token) +- A set of policies created in Nomad +- An unsealed Vault server + +### Configuration +Mount the "nomad" secret backend in Vault + +``` +$ vault mount nomad +Successfully mounted 'nomad' at 'nomad'! +``` + +Configure access with the right address and management token +``` +$ vault write nomad/config/access \ + address=http://127.0.0.1:4646 \ + token=adf4238a-882b-9ddc-4a9d-5b6758e4159e +Success! Data written to: nomad/config/access +``` + +Vault secret backends have the concept of roles, configuration unit that group one or more policies +to a potential identity based on Vault's policy. The name of the role is specified on the path, while +the mapping to Nomad policies is done by naming them in a comma separated list, for example: + +``` +$ vault write nomad/roles/role-name policy=policyone,policytwo +Success! Data written to: nomad/roles/role-name +``` + +Alternatively, to create management tokens, or global tokens: + +``` +$ vault write nomad/roles/role-name token_type=management global=true +Success! Data written to: nomad/roles/role-name +``` + +A Vault policy is required to allow different identities to get tokens associated with a particular +role: + +``` +$ echo 'path "nomad/creds/role-name" { + capabilities = ["read"] +}' | vault policy-write nomad-user-policy - +Policy 'nomad-user-policy' written. +``` + +If you have an existing authentication backend (like LDAP), follow the relevant instructions to create +a role available on the [Authentication backends page](https://www.vaultproject.io/docs/auth/index.html). +Otherwise, for testing purposes, a token can be generated associated with the policy: + +``` +$ vault token-create -policy=nomad-user-policy +Key Value +--- ----- +token deedfa83-99b5-34a1-278d-e8fb76809a5b +token_accessor fd185371-7d80-8011-4f45-1bb3af2c2733 +token_duration 768h0m0s +token_renewable true +token_policies [nomad-user-policy] +``` + +Finally obtain a Nomad Token using the existing Vault Token: + +``` +$ vault read nomad/creds/role-name +Key Value +--- ----- +lease_id nomad/creds/test/6fb22e25-0cd1-b4c9-494e-aba330c317b9 +lease_duration 768h0m0s +lease_renewable true +accessor_id 10b8fb49-7024-2126-8683-ab355b581db2 +secret_id 8898d19c-e5b3-35e4-649e-4153d63fbea9 +``` + +Verify that the token is created correctly in Nomad, referring to it by its accessor: + + +``` +$ nomad acl token info 10b8fb49-7024-2126-8683-ab355b581db2 +Accessor ID = 10b8fb49-7024-2126-8683-ab355b581db2 +Secret ID = 8898d19c-e5b3-35e4-649e-4153d63fbea9 +Name = Vault test root 1507307164169530060 +Type = management +Global = true +Policies = n/a +Create Time = 2017-10-06 16:26:04.170633207 +0000 UTC +Create Index = 228 +Modify Index = 228 +``` From 70bb4851a5bd9e7970aade5d4d026bafbe131404 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Fri, 6 Oct 2017 17:34:35 +0100 Subject: [PATCH 02/79] Minor cosmetic fix --- website/source/guides/acl.html.markdown | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index d8dc18c06..c98b60e8e 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -400,14 +400,15 @@ have a separate token) - An unsealed Vault server ### Configuration -Mount the "nomad" secret backend in Vault +Mount the "nomad" secret backend in Vault: ``` $ vault mount nomad Successfully mounted 'nomad' at 'nomad'! ``` -Configure access with the right address and management token +Configure access with the right address and management token: + ``` $ vault write nomad/config/access \ address=http://127.0.0.1:4646 \ From 71c0bdacd98841c7f61bc7f51af568f31c3b75c9 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 7 Nov 2017 15:11:41 +0000 Subject: [PATCH 03/79] Changed roles for role as the path changed in Vault --- website/source/guides/acl.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index c98b60e8e..cf1fb69f9 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -421,14 +421,14 @@ to a potential identity based on Vault's policy. The name of the role is specifi the mapping to Nomad policies is done by naming them in a comma separated list, for example: ``` -$ vault write nomad/roles/role-name policy=policyone,policytwo +$ vault write nomad/role/role-name policy=policyone,policytwo Success! Data written to: nomad/roles/role-name ``` Alternatively, to create management tokens, or global tokens: ``` -$ vault write nomad/roles/role-name token_type=management global=true +$ vault write nomad/role/role-name token_type=management global=true Success! Data written to: nomad/roles/role-name ``` From 3868e88518576da7c1bf759ecdcfc85a12ffd8e0 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Thu, 21 Dec 2017 08:58:35 +0000 Subject: [PATCH 04/79] Update `node-status` verbose command to include node address. This change updates the `nomad node-status -verbose` command to also include the addreess of the node. This is helpful for cluster administrators to quickly discover information and access nodes when required. --- api/nodes.go | 1 + command/node_status.go | 6 +++--- nomad/structs/structs.go | 2 ++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/api/nodes.go b/api/nodes.go index e1ef5e2aa..194affde4 100644 --- a/api/nodes.go +++ b/api/nodes.go @@ -151,6 +151,7 @@ type HostDiskStats struct { // NodeListStub is a subset of information returned during // node list operations. type NodeListStub struct { + Address string ID string Datacenter string Name string diff --git a/command/node_status.go b/command/node_status.go index e14ea6fca..52dd7d15d 100644 --- a/command/node_status.go +++ b/command/node_status.go @@ -180,7 +180,7 @@ func (c *NodeStatusCommand) Run(args []string) int { out[0] = "ID|DC|Name|Class|" if c.verbose { - out[0] += "Version|" + out[0] += "Address|Version|" } out[0] += "Drain|Status" @@ -196,8 +196,8 @@ func (c *NodeStatusCommand) Run(args []string) int { node.Name, node.NodeClass) if c.verbose { - out[i+1] += fmt.Sprintf("|%s", - node.Version) + out[i+1] += fmt.Sprintf("|%s|%s", + node.Address, node.Version) } out[i+1] += fmt.Sprintf("|%v|%s", node.Drain, diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index fa994e194..a79f1071f 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -1172,6 +1172,7 @@ func (n *Node) TerminalStatus() bool { // Stub returns a summarized version of the node func (n *Node) Stub() *NodeListStub { return &NodeListStub{ + Address: n.Attributes["unique.network.ip-address"], ID: n.ID, Datacenter: n.Datacenter, Name: n.Name, @@ -1188,6 +1189,7 @@ func (n *Node) Stub() *NodeListStub { // NodeListStub is used to return a subset of job information // for the job list type NodeListStub struct { + Address string ID string Datacenter string Name string From 68a2e81e7b712af6449e490fcd2017b26698388a Mon Sep 17 00:00:00 2001 From: James Rasell Date: Thu, 11 Jan 2018 07:17:58 +0000 Subject: [PATCH 05/79] Use advertise addr not 'unique.network.ip' in verbose node-status --- nomad/structs/structs.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index a79f1071f..6e3754d55 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -1171,8 +1171,11 @@ func (n *Node) TerminalStatus() bool { // Stub returns a summarized version of the node func (n *Node) Stub() *NodeListStub { + + addr, _, _ := net.SplitHostPort(n.HTTPAddr) + return &NodeListStub{ - Address: n.Attributes["unique.network.ip-address"], + Address: addr, ID: n.ID, Datacenter: n.Datacenter, Name: n.Name, From 0e9bae7bb4637f57ff94c61bb7afbd19913d9c0b Mon Sep 17 00:00:00 2001 From: Michael McCracken Date: Thu, 18 Jan 2018 16:03:03 -0800 Subject: [PATCH 06/79] lxc: cleanup partially configured containers after errors in Start If there are any errors in container setup after c.Create() in Start(), the container will be left around, with no way to clean it up because the handle will not be created or returned from Start. Added a wrapper that checks for errors and performs appropriate cleanup. Returning a cleanup function from a wrapped function instead of just doing the cleanup before returning the error helps to ensure that future changes that might add or change error exits can't forget to consider a cleanup function. Adds a check to the invalid config test case to check that a container created with an invalid config doesn't get left behind. Signed-off-by: Michael McCracken --- client/driver/lxc.go | 45 ++++++++++++++++++++++++++++----------- client/driver/lxc_test.go | 12 +++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/client/driver/lxc.go b/client/driver/lxc.go index fefb6f2fb..144845644 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -210,9 +210,20 @@ func (d *LxcDriver) Prestart(*ExecContext, *structs.Task) (*PrestartResponse, er // Start starts the LXC Driver func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, error) { + sresp, err, errCleanup := d.StartWithCleanup(ctx, task) + if err != nil { + if cleanupErr := errCleanup(); cleanupErr != nil { + d.logger.Printf("[ERR] error occured:\n%v\nwhile cleaning up from error in Start: %v", cleanupErr, err) + } + } + return sresp, err +} + +func (d *LxcDriver) StartWithCleanup(ctx *ExecContext, task *structs.Task) (*StartResponse, error, func() error) { + noCleanup := func() error { return nil } var driverConfig LxcDriverConfig if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { - return nil, err + return nil, err, noCleanup } lxcPath := lxc.DefaultConfigPath() if path := d.config.Read("driver.lxc.path"); path != "" { @@ -222,7 +233,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, containerName := fmt.Sprintf("%s-%s", task.Name, d.DriverContext.allocID) c, err := lxc.NewContainer(containerName, lxcPath) if err != nil { - return nil, fmt.Errorf("unable to initialize container: %v", err) + return nil, fmt.Errorf("unable to initialize container: %v", err), noCleanup } var verbosity lxc.Verbosity @@ -232,7 +243,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, case "", "quiet": verbosity = lxc.Quiet default: - return nil, fmt.Errorf("lxc driver config 'verbosity' can only be either quiet or verbose") + return nil, fmt.Errorf("lxc driver config 'verbosity' can only be either quiet or verbose"), noCleanup } c.SetVerbosity(verbosity) @@ -249,7 +260,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, case "", "error": logLevel = lxc.ERROR default: - return nil, fmt.Errorf("lxc driver config 'log_level' can only be trace, debug, info, warn or error") + return nil, fmt.Errorf("lxc driver config 'log_level' can only be trace, debug, info, warn or error"), noCleanup } c.SetLogLevel(logLevel) @@ -267,12 +278,12 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, } if err := c.Create(options); err != nil { - return nil, fmt.Errorf("unable to create container: %v", err) + return nil, fmt.Errorf("unable to create container: %v", err), noCleanup } // Set the network type to none if err := c.SetConfigItem("lxc.network.type", "none"); err != nil { - return nil, fmt.Errorf("error setting network type configuration: %v", err) + return nil, fmt.Errorf("error setting network type configuration: %v", err), c.Destroy } // Bind mount the shared alloc dir and task local dir in the container @@ -290,7 +301,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, if filepath.IsAbs(paths[0]) { if !volumesEnabled { - return nil, fmt.Errorf("absolute bind-mount volume in config but '%v' is false", lxcVolumesConfigOption) + return nil, fmt.Errorf("absolute bind-mount volume in config but '%v' is false", lxcVolumesConfigOption), c.Destroy } } else { // Relative source paths are treated as relative to alloc dir @@ -302,21 +313,31 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, 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) + return nil, fmt.Errorf("error setting bind mount %q error: %v", mnt, err), c.Destroy } } // Start the container if err := c.Start(); err != nil { - return nil, fmt.Errorf("unable to start container: %v", err) + return nil, fmt.Errorf("unable to start container: %v", err), c.Destroy + } + + stopAndDestroyCleanup := func() error { + if err := c.Stop(); err != nil { + return err + } + if err := c.Destroy(); err != nil { + return err + } + return nil } // Set the resource limits if err := c.SetMemoryLimit(lxc.ByteSize(task.Resources.MemoryMB) * lxc.MB); err != nil { - return nil, fmt.Errorf("unable to set memory limits: %v", err) + return nil, fmt.Errorf("unable to set memory limits: %v", err), stopAndDestroyCleanup } if err := c.SetCgroupItem("cpu.shares", strconv.Itoa(task.Resources.CPU)); err != nil { - return nil, fmt.Errorf("unable to set cpu shares: %v", err) + return nil, fmt.Errorf("unable to set cpu shares: %v", err), stopAndDestroyCleanup } h := lxcDriverHandle{ @@ -335,7 +356,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, go h.run() - return &StartResponse{Handle: &h}, nil + return &StartResponse{Handle: &h}, nil, noCleanup } func (d *LxcDriver) Cleanup(*ExecContext, *CreatedResources) error { return nil } diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index ddc78193c..b1d94cf20 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -310,6 +310,7 @@ func TestLxcDriver_Start_NoVolumes(t *testing.T) { ctx := testDriverContexts(t, task) defer ctx.AllocDir.Destroy() + // set lxcVolumesConfigOption to false to disallow absolute paths as the source for the bind mount ctx.DriverCtx.config.Options = map[string]string{lxcVolumesConfigOption: "false"} d := NewLxcDriver(ctx.DriverCtx) @@ -317,8 +318,19 @@ func TestLxcDriver_Start_NoVolumes(t *testing.T) { if _, err := d.Prestart(ctx.ExecCtx, task); err != nil { t.Fatalf("prestart err: %v", err) } + + // expect the "absolute bind-mount volume in config.. " error _, err := d.Start(ctx.ExecCtx, task) if err == nil { t.Fatalf("expected error in start, got nil.") } + + // Because the container was created but not started before + // the expected error, we can test that the destroy-only + // cleanup is done here. + containerName := fmt.Sprintf("%s-%s", task.Name, ctx.DriverCtx.allocID) + if err := exec.Command("bash", "-c", fmt.Sprintf("lxc-ls -1 | grep -q %s", containerName)).Run(); err == nil { + t.Fatalf("error, container '%s' is still around", containerName) + } + } From 61d7f93a2901e35c8b7ce889aee2fe176152e20a Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 19 Jan 2018 15:38:57 +0000 Subject: [PATCH 07/79] Make task-events use the `message` from the API rather than hardcoded The api has changed to send through a message for task-events. Use that instead of rendering a message from within js dependent on the task event type --- ui/app/models/task-event.js | 117 ------------------ .../allocations/allocation/index.hbs | 4 +- .../allocations/allocation/task/index.hbs | 4 +- 3 files changed, 4 insertions(+), 121 deletions(-) diff --git a/ui/app/models/task-event.js b/ui/app/models/task-event.js index 3d6f464f3..10377b021 100644 --- a/ui/app/models/task-event.js +++ b/ui/app/models/task-event.js @@ -1,27 +1,6 @@ -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 displayProps = [ - 'message', - 'validationError', - 'setupError', - 'driverError', - 'downloadError', - 'killReason', - 'killTimeout', - 'killError', - 'exitCode', - 'signal', - 'startDelay', - 'restartReason', - 'failedSibling', - 'taskSignal', - 'taskSignalReason', - 'driverMessage', -]; export default Fragment.extend({ state: fragmentOwner(), @@ -48,100 +27,4 @@ export default Fragment.extend({ vaultError: attr('string'), message: attr('string'), failedSibling: attr('string'), - - displayMessage: computed(...displayProps, function() { - let desc = ''; - switch (this.get('type')) { - case 'Task Setup': - desc = this.get('message'); - break; - case 'Started': - desc = 'Task started by client'; - break; - case 'Received': - desc = 'Task received by client'; - break; - case 'Failed Validation': - desc = this.get('validationError') || 'Validation of task failed'; - break; - case 'Setup Failure': - desc = this.get('setupError') || 'Task setup failed'; - break; - case 'Driver Failure': - desc = this.get('driverError') || 'Failed to start task'; - break; - case 'Downloading Artifacts': - desc = 'Client is downloading artifacts'; - break; - case 'Failed Artifact Download': - desc = this.get('downloadError') || 'Failed to download artifacts'; - break; - case 'Killing': - desc = - this.get('killReason') || - (this.get('killTimeout') && - `Sent interrupt. Waiting ${this.get('killTimeout')} before force killing`); - break; - case 'Killed': - desc = this.get('killError') || 'Task successfully killed'; - break; - case 'Terminated': - var parts = [`Exit Code: ${this.get('exitCode')}`]; - if (this.get('signal')) { - parts.push(`Signal: ${this.get('signal')}`); - } - if (this.get('message')) { - parts.push(`Exit Message: ${this.get('message')}`); - } - desc = parts.join(', '); - break; - case 'Restarting': - var timerMessage = `Task restarting in ${moment - .duration(this.get('startDelay') / 1000000, 'ms') - .humanize()}`; - if (this.get('restartReason') && this.get('restartReason') !== 'Restart within policy') { - desc = `${this.get('restartReason')} - ${timerMessage}`; - } else { - desc = timerMessage; - } - break; - case 'Not Restarting': - desc = this.get('restartReason') || 'Task exceeded restart policy'; - break; - case 'Sibling Task Failed': - desc = this.get('failedSibling') - ? `Task's sibling ${this.get('failedSibling')} failed` - : "Task's sibling failed"; - break; - case 'Signaling': - var signal = this.get('taskSignal'); - var reason = this.get('taskSignalReason'); - - if (!signal && !reason) { - desc = 'Task being sent a signal'; - } else if (!signal) { - desc = reason; - } else if (!reason) { - desc = `Task being sent signal ${signal}`; - } else { - desc = `Task being sent signal ${signal}: ${reason}`; - } - - break; - case 'Restart Signaled': - desc = this.get('restartReason') || 'Task signaled to restart'; - break; - case 'Driver': - desc = this.get('driverMessage'); - break; - case 'Leader Task Dead': - desc = 'Leader Task in Group dead'; - break; - case 'Generic': - desc = this.get('message'); - break; - } - - return desc; - }), }); diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 9778b2637..659329b65 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -50,8 +50,8 @@ {{row.model.state}} - {{#if row.model.events.lastObject.displayMessage}} - {{row.model.events.lastObject.displayMessage}} + {{#if row.model.events.lastObject.message}} + {{row.model.events.lastObject.message}} {{else}} No message {{/if}} diff --git a/ui/app/templates/allocations/allocation/task/index.hbs b/ui/app/templates/allocations/allocation/task/index.hbs index aebe8eeed..60a311bfa 100644 --- a/ui/app/templates/allocations/allocation/task/index.hbs +++ b/ui/app/templates/allocations/allocation/task/index.hbs @@ -83,8 +83,8 @@ {{moment-format row.model.time "MM/DD/YY HH:mm:ss"}} {{row.model.type}} - {{#if row.model.displayMessage}} - {{row.model.displayMessage}} + {{#if row.model.message}} + {{row.model.message}} {{else}} No message {{/if}} From 3bc103eb7a4c9aa66fa0d9cc5506966841336007 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Jan 2018 13:26:34 +0100 Subject: [PATCH 08/79] Introducing @schmichael suggestions --- website/source/guides/acl.html.markdown | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index fd3381e21..09c5cb3b4 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -377,7 +377,7 @@ using an pre-existing identity service (LDAP, Okta, Amazon IAM, etc. ...) in ord Nomad token. ~> Hashicorp Vault is a standalone product with it's own set of deployment and configuration best -practices. Please review Vault's documentation before deploying it in production. +practices. Please review [Vault's documentation](https://www.vaultproject.io/docs/index.html) before deploying it in production. For evaluation purposes, a Vault server in "dev" mode can be used. @@ -420,7 +420,7 @@ Root Token: f84b587e-5882-bba1-a3f0-d1a3d90ca105 - A management token (the bootstrap token can be used, but for production systems it's recommended to have a separate token) - A set of policies created in Nomad -- An unsealed Vault server +- An unsealed Vault server (Vault running in `dev` mode is unsealed automatically upon startup) ### Configuration Mount the "nomad" secret backend in Vault: @@ -439,23 +439,24 @@ $ vault write nomad/config/access \ Success! Data written to: nomad/config/access ``` -Vault secret backends have the concept of roles, configuration unit that group one or more policies -to a potential identity based on Vault's policy. The name of the role is specified on the path, while -the mapping to Nomad policies is done by naming them in a comma separated list, for example: +Vault secret backends have the concept of roles, which are configuration units that group one or more +Vault policies to a potential identity attribute, (Like an LDAP Group membership). The name of the role +is specified on the path, while the mapping to policies is done by naming them in a comma separated list, +for example: ``` $ vault write nomad/role/role-name policy=policyone,policytwo Success! Data written to: nomad/roles/role-name ``` -Alternatively, to create management tokens, or global tokens: +Similarly, to create management tokens, or global tokens: ``` -$ vault write nomad/role/role-name token_type=management global=true +$ vault write nomad/role/role-name type=management global=true Success! Data written to: nomad/roles/role-name ``` -A Vault policy is required to allow different identities to get tokens associated with a particular +Create a Vault policy to allow different identities to get tokens associated with a particular role: ``` @@ -467,7 +468,7 @@ Policy 'nomad-user-policy' written. If you have an existing authentication backend (like LDAP), follow the relevant instructions to create a role available on the [Authentication backends page](https://www.vaultproject.io/docs/auth/index.html). -Otherwise, for testing purposes, a token can be generated associated with the policy: +Otherwise, for testing purposes, a Vault token can be generated associated with the policy: ``` $ vault token-create -policy=nomad-user-policy @@ -477,7 +478,7 @@ token deedfa83-99b5-34a1-278d-e8fb76809a5b token_accessor fd185371-7d80-8011-4f45-1bb3af2c2733 token_duration 768h0m0s token_renewable true -token_policies [nomad-user-policy] +token_policies [default nomad-user-policy] ``` Finally obtain a Nomad Token using the existing Vault Token: @@ -493,8 +494,7 @@ accessor_id 10b8fb49-7024-2126-8683-ab355b581db2 secret_id 8898d19c-e5b3-35e4-649e-4153d63fbea9 ``` -Verify that the token is created correctly in Nomad, referring to it by its accessor: - +Verify that the token is created correctly in Nomad, looking it up by its accessor: ``` $ nomad acl token info 10b8fb49-7024-2126-8683-ab355b581db2 @@ -508,3 +508,6 @@ Create Time = 2017-10-06 16:26:04.170633207 +0000 UTC Create Index = 228 Modify Index = 228 ``` + +Any user or process with access to Vault can now obtain short lived Nomad Tokens in order to +carry out operations, thus centralising the access to Nomad tokens. From 81f64eea27e7bf254add89f29a099665cc92881b Mon Sep 17 00:00:00 2001 From: Michael McCracken Date: Tue, 23 Jan 2018 15:03:09 -0800 Subject: [PATCH 09/79] review cleanup don't export an internal function, and simplify some code Signed-off-by: Michael McCracken --- client/driver/lxc.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/driver/lxc.go b/client/driver/lxc.go index 144845644..c4a175f04 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -210,16 +210,16 @@ func (d *LxcDriver) Prestart(*ExecContext, *structs.Task) (*PrestartResponse, er // Start starts the LXC Driver func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, error) { - sresp, err, errCleanup := d.StartWithCleanup(ctx, task) + sresp, err, errCleanup := d.startWithCleanup(ctx, task) if err != nil { if cleanupErr := errCleanup(); cleanupErr != nil { - d.logger.Printf("[ERR] error occured:\n%v\nwhile cleaning up from error in Start: %v", cleanupErr, err) + d.logger.Printf("[ERR] error occured while cleaning up from error in Start: %v", cleanupErr) } } return sresp, err } -func (d *LxcDriver) StartWithCleanup(ctx *ExecContext, task *structs.Task) (*StartResponse, error, func() error) { +func (d *LxcDriver) startWithCleanup(ctx *ExecContext, task *structs.Task) (*StartResponse, error, func() error) { noCleanup := func() error { return nil } var driverConfig LxcDriverConfig if err := mapstructure.WeakDecode(task.Config, &driverConfig); err != nil { @@ -326,10 +326,7 @@ func (d *LxcDriver) StartWithCleanup(ctx *ExecContext, task *structs.Task) (*Sta if err := c.Stop(); err != nil { return err } - if err := c.Destroy(); err != nil { - return err - } - return nil + return c.Destroy() } // Set the resource limits From 5e8151d700654e433d4d1ef5c48be180796ce651 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 24 Jan 2018 09:09:53 -0500 Subject: [PATCH 10/79] refactor Fingerprint to request/response construct --- client/client.go | 85 +++++++++-- client/driver/docker.go | 22 ++- client/driver/docker_test.go | 35 +++-- client/driver/exec_default.go | 7 +- client/driver/exec_linux.go | 17 +-- client/driver/exec_test.go | 16 ++- client/driver/java.go | 27 ++-- client/driver/java_test.go | 19 ++- client/driver/lxc.go | 17 +-- client/driver/lxc_test.go | 47 +++++-- client/driver/mock_driver.go | 7 +- client/driver/qemu.go | 15 +- client/driver/qemu_test.go | 33 +++-- client/driver/raw_exec.go | 15 +- client/driver/raw_exec_test.go | 26 ++-- client/driver/rkt.go | 24 ++-- client/driver/rkt_nonlinux.go | 5 +- client/driver/rkt_test.go | 21 ++- client/fingerprint/arch.go | 9 +- client/fingerprint/arch_test.go | 17 ++- client/fingerprint/cgroup.go | 7 +- client/fingerprint/cgroup_linux.go | 17 ++- client/fingerprint/cgroup_test.go | 126 ++++++++++------- client/fingerprint/consul.go | 45 +++--- client/fingerprint/consul_test.go | 45 +++--- client/fingerprint/cpu.go | 32 ++--- client/fingerprint/cpu_test.go | 80 +++++++---- client/fingerprint/env_aws.go | 39 +++-- client/fingerprint/env_aws_test.go | 188 +++++++++++++++---------- client/fingerprint/env_gce.go | 38 +++-- client/fingerprint/env_gce_test.go | 69 +++++---- client/fingerprint/fingerprint.go | 7 +- client/fingerprint/fingerprint_test.go | 41 ++++-- client/fingerprint/host.go | 19 ++- client/fingerprint/host_test.go | 17 ++- client/fingerprint/memory.go | 16 +-- client/fingerprint/memory_test.go | 19 ++- client/fingerprint/network.go | 22 ++- client/fingerprint/network_test.go | 98 +++++++++---- client/fingerprint/nomad.go | 11 +- client/fingerprint/nomad_test.go | 20 ++- client/fingerprint/signal.go | 9 +- client/fingerprint/signal_test.go | 4 +- client/fingerprint/storage.go | 29 ++-- client/fingerprint/storage_test.go | 16 +-- client/fingerprint/vault.go | 35 ++--- client/fingerprint/vault_test.go | 24 ++-- client/structs/structs.go | 14 ++ 48 files changed, 907 insertions(+), 644 deletions(-) diff --git a/client/client.go b/client/client.go index 314b9dda4..005b478f0 100644 --- a/client/client.go +++ b/client/client.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/nomad/client/driver" "github.com/hashicorp/nomad/client/fingerprint" "github.com/hashicorp/nomad/client/stats" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/client/vaultclient" "github.com/hashicorp/nomad/command/agent/consul" "github.com/hashicorp/nomad/helper" @@ -949,15 +950,31 @@ func (c *Client) fingerprint() error { return err } - c.configLock.Lock() - applies, err := f.Fingerprint(c.config, c.config.Node) - c.configLock.Unlock() + request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err = f.Fingerprint(request, response) if err != nil { return err } - if applies { + + // if an attribute should be skipped, remove it from the list which we will + // later apply to the node + for _, e := range skipped { + delete(response.Attributes, e) + } + + for name := range response.Attributes { applied = append(applied, name) } + + // add the diff found from each fingerprinter + c.updateNodeFromFingerprint(response) + p, period := f.Periodic() if p { // TODO: If more periodic fingerprinters are added, then @@ -966,6 +983,7 @@ func (c *Client) fingerprint() error { go c.fingerprintPeriodic(name, f, period) } } + c.logger.Printf("[DEBUG] client: applied fingerprints %v", applied) if len(skipped) != 0 { c.logger.Printf("[DEBUG] client: fingerprint modules skipped due to white/blacklist: %v", skipped) @@ -979,11 +997,21 @@ func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d t for { select { case <-time.After(d): - c.configLock.Lock() - if _, err := f.Fingerprint(c.config, c.config.Node); err != nil { - c.logger.Printf("[DEBUG] client: periodic fingerprinting for %v failed: %v", name, err) + request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - c.configLock.Unlock() + + err := f.Fingerprint(request, response) + + if err != nil { + c.logger.Printf("[DEBUG] client: periodic fingerprinting for %v failed: %v", name, err) + } else { + c.updateNodeFromFingerprint(response) + } + case <-c.shutdownCh: return } @@ -1017,16 +1045,30 @@ func (c *Client) setupDrivers() error { if err != nil { return err } - c.configLock.Lock() - applies, err := d.Fingerprint(c.config, c.config.Node) - c.configLock.Unlock() + + request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err = d.Fingerprint(request, response) if err != nil { return err } - if applies { + + // remove attributes we are supposed to skip + for _, attr := range skipped { + delete(response.Attributes, attr) + } + + for name := range response.Attributes { avail = append(avail, name) } + c.updateNodeFromFingerprint(response) + p, period := d.Periodic() if p { go c.fingerprintPeriodic(name, d, period) @@ -1035,6 +1077,7 @@ func (c *Client) setupDrivers() error { } c.logger.Printf("[DEBUG] client: available drivers %v", avail) + c.logger.Printf("[DEBUG] client: skipped attributes %v", skipped) if len(skipped) != 0 { c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skipped) @@ -1043,6 +1086,24 @@ func (c *Client) setupDrivers() error { return nil } +// updateNodeFromFingerprint updates the node with the result of +// fingerprinting the node from the diff that was created +func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintResponse) { + c.configLock.Lock() + defer c.configLock.Unlock() + for name, val := range response.Attributes { + c.config.Node.Attributes[name] = val + } + + // update node links and resources from the diff created from + // fingerprinting + for name, val := range response.Links { + c.config.Node.Links[name] = val + } + + c.config.Node.Resources.Merge(response.Resources) +} + // retryIntv calculates a retry interval value given the base func (c *Client) retryIntv(base time.Duration) time.Duration { if c.config.DevMode { diff --git a/client/driver/docker.go b/client/driver/docker.go index fb05c646d..b2ef18550 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -26,7 +26,6 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-plugin" "github.com/hashicorp/nomad/client/allocdir" - "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" "github.com/hashicorp/nomad/client/driver/executor" dstructs "github.com/hashicorp/nomad/client/driver/structs" @@ -476,16 +475,14 @@ func NewDockerDriver(ctx *DriverContext) Driver { return &DockerDriver{DriverContext: *ctx} } -func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - // Initialize docker API clients +func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { client, _, err := d.dockerClients() if err != nil { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[INFO] driver.docker: failed to initialize client: %s", err) } - delete(node.Attributes, dockerDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } // This is the first operation taken on the client so we'll try to @@ -493,25 +490,24 @@ func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool // Docker isn't available so we'll simply disable the docker driver. env, err := client.Version() if err != nil { - delete(node.Attributes, dockerDriverAttr) if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon at %s: %s", client.Endpoint(), err) } d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } - node.Attributes[dockerDriverAttr] = "1" - node.Attributes["driver.docker.version"] = env.Get("Version") + resp.Attributes[dockerDriverAttr] = "1" + resp.Attributes["driver.docker.version"] = env.Get("Version") privileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) if privileged { - node.Attributes[dockerPrivilegedConfigOption] = "1" + resp.Attributes[dockerPrivilegedConfigOption] = "1" } // Advertise if this node supports Docker volumes if d.config.ReadBoolDefault(dockerVolumesConfigOption, dockerVolumesConfigDefault) { - node.Attributes["driver."+dockerVolumesConfigOption] = "1" + resp.Attributes["driver."+dockerVolumesConfigOption] = "1" } // Detect bridge IP address - #2785 @@ -529,7 +525,7 @@ func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool } if n.IPAM.Config[0].Gateway != "" { - node.Attributes["driver.docker.bridge_ip"] = n.IPAM.Config[0].Gateway + resp.Attributes["driver.docker.bridge_ip"] = n.IPAM.Config[0].Gateway } else if d.fingerprintSuccess == nil { // Docker 17.09.0-ce dropped the Gateway IP from the bridge network // See https://github.com/moby/moby/issues/32648 @@ -540,7 +536,7 @@ func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool } d.fingerprintSuccess = helper.BoolToPtr(true) - return true, nil + return nil } // Validate is used to validate the driver configuration diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index a0fd49209..aea0a8987 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -171,17 +171,28 @@ func TestDockerDriver_Fingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - apply, err := d.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if apply != testutil.DockerIsConnected(t) { + + if testutil.DockerIsConnected(t) && response.Attributes["driver.docker"] == "" { t.Fatalf("Fingerprinter should detect when docker is available") } - if node.Attributes["driver.docker"] != "1" { + + if response.Attributes["driver.docker"] != "1" { t.Log("Docker daemon not available. The remainder of the docker tests will be skipped.") } - t.Logf("Found docker version %s", node.Attributes["driver.docker.version"]) + + t.Logf("Found docker version %s", response.Attributes["driver.docker.version"]) } // TestDockerDriver_Fingerprint_Bridge asserts that if Docker is running we set @@ -210,18 +221,26 @@ func TestDockerDriver_Fingerprint_Bridge(t *testing.T) { conf := testConfig(t) conf.Node = mock.Node() dd := NewDockerDriver(NewDriverContext("", "", conf, conf.Node, testLogger(), nil)) - ok, err := dd.Fingerprint(conf, conf.Node) + + request := &cstructs.FingerprintRequest{Config: conf, Node: conf.Node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err = dd.Fingerprint(request, response) if err != nil { t.Fatalf("error fingerprinting docker: %v", err) } - if !ok { + if response.Attributes["driver.docker"] == "" { t.Fatalf("expected Docker to be enabled but false was returned") } - if found := conf.Node.Attributes["driver.docker.bridge_ip"]; found != expectedAddr { + if found := response.Attributes["driver.docker.bridge_ip"]; found != expectedAddr { t.Fatalf("expected bridge ip %q but found: %q", expectedAddr, found) } - t.Logf("docker bridge ip: %q", conf.Node.Attributes["driver.docker.bridge_ip"]) + t.Logf("docker bridge ip: %q", response.Attributes["driver.docker.bridge_ip"]) } func TestDockerDriver_StartOpen_Wait(t *testing.T) { diff --git a/client/driver/exec_default.go b/client/driver/exec_default.go index 2f1e26787..65011c3dd 100644 --- a/client/driver/exec_default.go +++ b/client/driver/exec_default.go @@ -3,12 +3,11 @@ package driver import ( - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/helper" - "github.com/hashicorp/nomad/nomad/structs" ) -func (d *ExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index ab3203a49..19ac61756 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -1,9 +1,8 @@ package driver import ( - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/helper" - "github.com/hashicorp/nomad/nomad/structs" "golang.org/x/sys/unix" ) @@ -13,28 +12,26 @@ const ( execDriverAttr = "driver.exec" ) -func (d *ExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // Only enable if cgroups are available and we are root - if !cgroupsMounted(node) { + if !cgroupsMounted(req.Node) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) - delete(node.Attributes, execDriverAttr) - return false, nil + return nil } else if unix.Geteuid() != 0 { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.exec: must run as root user, disabling") } - delete(node.Attributes, execDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } if d.fingerprintSuccess == nil || !*d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.exec: exec driver is enabled") } - node.Attributes[execDriverAttr] = "1" + resp.Attributes[execDriverAttr] = "1" d.fingerprintSuccess = helper.BoolToPtr(true) - return true, nil + return nil } diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index cd6365b8f..92b1bebe6 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" @@ -37,14 +38,19 @@ func TestExecDriver_Fingerprint(t *testing.T) { "unique.cgroup.mountpoint": "/sys/fs/cgroup", }, } - apply, err := d.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !apply { - t.Fatalf("should apply") - } - if node.Attributes["driver.exec"] == "" { + if response.Attributes["driver.exec"] == "" { t.Fatalf("missing driver") } } diff --git a/client/driver/java.go b/client/driver/java.go index 8c162e0cd..5736a563d 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -18,7 +18,6 @@ import ( "github.com/hashicorp/go-plugin" "github.com/mitchellh/mapstructure" - "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" "github.com/hashicorp/nomad/client/driver/executor" dstructs "github.com/hashicorp/nomad/client/driver/structs" @@ -112,15 +111,15 @@ func (d *JavaDriver) Abilities() DriverAbilities { } } -func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // 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 runtime.GOOS == "linux" && (syscall.Geteuid() != 0 || !cgroupsMounted(req.Node)) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling") } - delete(node.Attributes, "driver.java") + resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } // Find java version @@ -132,9 +131,9 @@ func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, err := cmd.Run() if err != nil { // assume Java wasn't found - delete(node.Attributes, javaDriverAttr) + resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } // 'java -version' returns output on Stderr typically. @@ -152,9 +151,9 @@ func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Println("[WARN] driver.java: error parsing Java version information, aborting") } - delete(node.Attributes, javaDriverAttr) + resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } // Assume 'java -version' returns 3 lines: @@ -166,13 +165,13 @@ func (d *JavaDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, versionString := info[0] versionString = strings.TrimPrefix(versionString, "java version ") versionString = strings.Trim(versionString, "\"") - node.Attributes[javaDriverAttr] = "1" - node.Attributes["driver.java.version"] = versionString - node.Attributes["driver.java.runtime"] = info[1] - node.Attributes["driver.java.vm"] = info[2] + resp.Attributes[javaDriverAttr] = "1" + resp.Attributes["driver.java.version"] = versionString + resp.Attributes["driver.java.runtime"] = info[1] + resp.Attributes["driver.java.vm"] = info[2] d.fingerprintSuccess = helper.BoolToPtr(true) - return true, nil + return nil } func (d *JavaDriver) Prestart(*ExecContext, *structs.Task) (*PrestartResponse, error) { diff --git a/client/driver/java_test.go b/client/driver/java_test.go index b1f6dc3f1..cd6e56447 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/assert" @@ -49,14 +50,20 @@ func TestJavaDriver_Fingerprint(t *testing.T) { "unique.cgroup.mountpoint": "/sys/fs/cgroups", }, } - apply, err := d.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if apply != javaLocated() { - t.Fatalf("Fingerprinter should detect Java when it is installed") - } - if node.Attributes["driver.java"] != "1" { + + if response.Attributes["driver.java"] != "1" && javaLocated() { if v, ok := osJavaDriverSupport[runtime.GOOS]; v && ok { t.Fatalf("missing java driver") } else { @@ -64,7 +71,7 @@ func TestJavaDriver_Fingerprint(t *testing.T) { } } for _, key := range []string{"driver.java.version", "driver.java.runtime", "driver.java.vm"} { - if node.Attributes[key] == "" { + if response.Attributes[key] == "" { t.Fatalf("missing driver key (%s)", key) } } diff --git a/client/driver/lxc.go b/client/driver/lxc.go index fefb6f2fb..77cf45240 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -14,7 +14,6 @@ import ( "syscall" "time" - "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/fingerprint" "github.com/hashicorp/nomad/client/stats" "github.com/hashicorp/nomad/helper/fields" @@ -184,24 +183,26 @@ func (d *LxcDriver) FSIsolation() cstructs.FSIsolation { } // Fingerprint fingerprints the lxc driver configuration -func (d *LxcDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *LxcDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + cfg := req.Config + enabled := cfg.ReadBoolDefault(lxcConfigOption, true) if !enabled && !cfg.DevMode { - return false, nil + return nil } version := lxc.Version() if version == "" { - return false, nil + return nil } - node.Attributes["driver.lxc.version"] = version - node.Attributes["driver.lxc"] = "1" + resp.Attributes["driver.lxc.version"] = version + resp.Attributes["driver.lxc"] = "1" // Advertise if this node supports lxc volumes if d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) { - node.Attributes["driver."+lxcVolumesConfigOption] = "1" + resp.Attributes["driver."+lxcVolumesConfigOption] = "1" } - return true, nil + return nil } func (d *LxcDriver) Prestart(*ExecContext, *structs.Task) (*PrestartResponse, error) { diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index ddc78193c..2da65d86c 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -13,6 +13,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" ctestutil "github.com/hashicorp/nomad/client/testutil" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" @@ -38,23 +39,39 @@ func TestLxcDriver_Fingerprint(t *testing.T) { node := &structs.Node{ Attributes: map[string]string{}, } - apply, err := d.Fingerprint(&config.Config{}, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !apply { - t.Fatalf("should apply by default") + + // test with an empty config + { + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } } - apply, err = d.Fingerprint(&config.Config{Options: map[string]string{lxcConfigOption: "0"}}, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if apply { - t.Fatalf("should not apply with config") - } - if node.Attributes["driver.lxc"] == "" { - t.Fatalf("missing driver") + // test when lxc is enable din the config + { + conf := &config.Config{Options: map[string]string{lxcConfigOption: "1"}} + request := &cstructs.FingerprintRequest{Config: conf, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } + if response.Attributes["driver.lxc"] == "" { + t.Fatalf("missing driver") + } } } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index 492794854..a4ccc637a 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -15,7 +15,6 @@ import ( "github.com/mitchellh/mapstructure" - "github.com/hashicorp/nomad/client/config" dstructs "github.com/hashicorp/nomad/client/driver/structs" "github.com/hashicorp/nomad/client/fingerprint" cstructs "github.com/hashicorp/nomad/client/structs" @@ -194,9 +193,9 @@ func (m *MockDriver) Validate(map[string]interface{}) error { } // Fingerprint fingerprints a node and returns if MockDriver is enabled -func (m *MockDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - node.Attributes["driver.mock_driver"] = "1" - return true, nil +func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + resp.Attributes["driver.mock_driver"] = "1" + return nil } // MockDriverHandle is a driver handler which supervises a mock task diff --git a/client/driver/qemu.go b/client/driver/qemu.go index f256c829c..ba3f96d32 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -17,7 +17,6 @@ import ( "github.com/coreos/go-semver/semver" plugin "github.com/hashicorp/go-plugin" - "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/executor" dstructs "github.com/hashicorp/nomad/client/driver/structs" "github.com/hashicorp/nomad/client/fingerprint" @@ -155,7 +154,7 @@ func (d *QemuDriver) FSIsolation() cstructs.FSIsolation { return cstructs.FSIsolationImage } -func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { bin := "qemu-system-x86_64" if runtime.GOOS == "windows" { // On windows, the "qemu-system-x86_64" command does not respond to the @@ -164,22 +163,20 @@ func (d *QemuDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, } outBytes, err := exec.Command(bin, "--version").Output() if err != nil { - delete(node.Attributes, qemuDriverAttr) - return false, nil + return nil } out := strings.TrimSpace(string(outBytes)) matches := reQemuVersion.FindStringSubmatch(out) if len(matches) != 2 { - delete(node.Attributes, qemuDriverAttr) - return false, fmt.Errorf("Unable to parse Qemu version string: %#v", matches) + return fmt.Errorf("Unable to parse Qemu version string: %#v", matches) } currentQemuVersion := matches[1] - node.Attributes[qemuDriverAttr] = "1" - node.Attributes[qemuDriverVersionAttr] = currentQemuVersion + resp.Attributes[qemuDriverAttr] = "1" + resp.Attributes[qemuDriverVersionAttr] = currentQemuVersion - return true, nil + return nil } func (d *QemuDriver) Prestart(_ *ExecContext, task *structs.Task) (*PrestartResponse, error) { diff --git a/client/driver/qemu_test.go b/client/driver/qemu_test.go index ebab5dae6..523fbf415 100644 --- a/client/driver/qemu_test.go +++ b/client/driver/qemu_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" @@ -34,17 +35,24 @@ func TestQemuDriver_Fingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - apply, err := d.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !apply { - t.Fatalf("should apply") - } - if node.Attributes[qemuDriverAttr] == "" { + + if response.Attributes[qemuDriverAttr] == "" { t.Fatalf("Missing Qemu driver") } - if node.Attributes[qemuDriverVersionAttr] == "" { + + if response.Attributes[qemuDriverVersionAttr] == "" { t.Fatalf("Missing Qemu driver version") } } @@ -164,12 +172,19 @@ func TestQemuDriver_GracefulShutdown(t *testing.T) { defer ctx.AllocDir.Destroy() d := NewQemuDriver(ctx.DriverCtx) - apply, err := d.Fingerprint(&config.Config{}, ctx.DriverCtx.node) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: ctx.DriverCtx.node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !apply { - t.Fatalf("should apply") + + for name, value := range response.Attributes { + ctx.DriverCtx.node.Attributes[name] = value } dst := ctx.ExecCtx.TaskDir.Dir diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index 86b4406e9..0d1eedf35 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/go-plugin" "github.com/hashicorp/nomad/client/allocdir" - "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" "github.com/hashicorp/nomad/client/driver/executor" dstructs "github.com/hashicorp/nomad/client/driver/structs" @@ -92,18 +91,18 @@ func (d *RawExecDriver) FSIsolation() cstructs.FSIsolation { return cstructs.FSIsolationNone } -func (d *RawExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // Check that the user has explicitly enabled this executor. - enabled := cfg.ReadBoolDefault(rawExecConfigOption, false) + enabled := req.Config.ReadBoolDefault(rawExecConfigOption, false) - if enabled || cfg.DevMode { + if enabled || req.Config.DevMode { d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") - node.Attributes[rawExecDriverAttr] = "1" - return true, nil + resp.Attributes[rawExecDriverAttr] = "1" + return nil } - delete(node.Attributes, rawExecDriverAttr) - return false, nil + resp.Attributes[rawExecDriverAttr] = "" + return nil } func (d *RawExecDriver) Prestart(*ExecContext, *structs.Task) (*PrestartResponse, error) { diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index dadaf0738..fa0fd7413 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/env" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/helper/testtask" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" @@ -34,27 +35,30 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { // Disable raw exec. cfg := &config.Config{Options: map[string]string{rawExecConfigOption: "false"}} - apply, err := d.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if apply { - t.Fatalf("should not apply") - } - if node.Attributes["driver.raw_exec"] != "" { + + if response.Attributes["driver.raw_exec"] != "" { t.Fatalf("driver incorrectly enabled") } // Enable raw exec. - cfg.Options[rawExecConfigOption] = "true" - apply, err = d.Fingerprint(cfg, node) + request.Config.Options[rawExecConfigOption] = "true" + err = d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !apply { - t.Fatalf("should apply") - } - if node.Attributes["driver.raw_exec"] != "1" { + + if response.Attributes["driver.raw_exec"] != "1" { t.Fatalf("driver not enabled") } } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 52c7c91f0..8f8d55e7f 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -311,31 +311,28 @@ func (d *RktDriver) Abilities() DriverAbilities { } } -func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // Only enable if we are root when running on non-windows systems. if runtime.GOOS != "windows" && syscall.Geteuid() != 0 { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.rkt: must run as root user, disabling") } - delete(node.Attributes, rktDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } outBytes, err := exec.Command(rktCmd, "version").Output() if err != nil { - delete(node.Attributes, rktDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } out := strings.TrimSpace(string(outBytes)) rktMatches := reRktVersion.FindStringSubmatch(out) appcMatches := reAppcVersion.FindStringSubmatch(out) if len(rktMatches) != 2 || len(appcMatches) != 2 { - delete(node.Attributes, rktDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, fmt.Errorf("Unable to parse Rkt version string: %#v", rktMatches) + return fmt.Errorf("Unable to parse Rkt version string: %#v", rktMatches) } minVersion, _ := version.NewVersion(minRktVersion) @@ -347,21 +344,20 @@ func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, e d.logger.Printf("[WARN] driver.rkt: unsupported rkt version %s; please upgrade to >= %s", currentVersion, minVersion) } - delete(node.Attributes, rktDriverAttr) d.fingerprintSuccess = helper.BoolToPtr(false) - return false, nil + return nil } - node.Attributes[rktDriverAttr] = "1" - node.Attributes["driver.rkt.version"] = rktMatches[1] - node.Attributes["driver.rkt.appc.version"] = appcMatches[1] + resp.Attributes[rktDriverAttr] = "1" + resp.Attributes["driver.rkt.version"] = rktMatches[1] + resp.Attributes["driver.rkt.appc.version"] = appcMatches[1] // Advertise if this node supports rkt volumes if d.config.ReadBoolDefault(rktVolumesConfigOption, rktVolumesConfigDefault) { - node.Attributes["driver."+rktVolumesConfigOption] = "1" + resp.Attributes["driver."+rktVolumesConfigOption] = "1" } d.fingerprintSuccess = helper.BoolToPtr(true) - return true, nil + return nil } func (d *RktDriver) Periodic() (bool, time.Duration) { diff --git a/client/driver/rkt_nonlinux.go b/client/driver/rkt_nonlinux.go index 49ae7268c..13c474def 100644 --- a/client/driver/rkt_nonlinux.go +++ b/client/driver/rkt_nonlinux.go @@ -5,7 +5,6 @@ package driver import ( "time" - "github.com/hashicorp/nomad/client/config" cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -46,8 +45,8 @@ func (RktDriver) FSIsolation() cstructs.FSIsolation { panic("not implemented") } -func (RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - return false, nil +func (RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + return nil } func (RktDriver) Periodic() (bool, time.Duration) { diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index 3b4646dbf..bba0037a7 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -17,6 +17,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/assert" @@ -57,20 +58,26 @@ func TestRktDriver_Fingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - apply, err := d.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := d.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !apply { - t.Fatalf("should apply") - } - if node.Attributes["driver.rkt"] != "1" { + + if response.Attributes["driver.rkt"] != "1" { t.Fatalf("Missing Rkt driver") } - if node.Attributes["driver.rkt.version"] == "" { + if response.Attributes["driver.rkt.version"] == "" { t.Fatalf("Missing Rkt driver version") } - if node.Attributes["driver.rkt.appc.version"] == "" { + if response.Attributes["driver.rkt.appc.version"] == "" { t.Fatalf("Missing appc version for the Rkt driver") } } diff --git a/client/fingerprint/arch.go b/client/fingerprint/arch.go index 71b5352a8..59d498b73 100644 --- a/client/fingerprint/arch.go +++ b/client/fingerprint/arch.go @@ -4,8 +4,7 @@ import ( "log" "runtime" - client "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" ) // ArchFingerprint is used to fingerprint the architecture @@ -20,7 +19,7 @@ func NewArchFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *ArchFingerprint) Fingerprint(config *client.Config, node *structs.Node) (bool, error) { - node.Attributes["cpu.arch"] = runtime.GOARCH - return true, nil +func (f *ArchFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + resp.Attributes["cpu.arch"] = runtime.GOARCH + return nil } diff --git a/client/fingerprint/arch_test.go b/client/fingerprint/arch_test.go index 4e4b94a67..269b141db 100644 --- a/client/fingerprint/arch_test.go +++ b/client/fingerprint/arch_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -12,14 +13,20 @@ func TestArchFingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") - } - if node.Attributes["cpu.arch"] == "" { + + if response.Attributes["cpu.arch"] == "" { t.Fatalf("missing arch") } } diff --git a/client/fingerprint/cgroup.go b/client/fingerprint/cgroup.go index 1ec8d8793..5994e7e04 100644 --- a/client/fingerprint/cgroup.go +++ b/client/fingerprint/cgroup.go @@ -5,8 +5,6 @@ package fingerprint import ( "log" "time" - - "github.com/hashicorp/nomad/nomad/structs" ) const ( @@ -49,8 +47,9 @@ func NewCGroupFingerprint(logger *log.Logger) Fingerprint { // clearCGroupAttributes clears any node attributes related to cgroups that might // have been set in a previous fingerprint run. -func (f *CGroupFingerprint) clearCGroupAttributes(n *structs.Node) { - delete(n.Attributes, "unique.cgroup.mountpoint") +func (f *CGroupFingerprint) clearCGroupAttributes(n map[string]string) map[string]string { + n["unique.cgroup.mountpoint"] = "" + return n } // Periodic determines the interval at which the periodic fingerprinter will run. diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go index 9abb959b5..fd10cf382 100644 --- a/client/fingerprint/cgroup_linux.go +++ b/client/fingerprint/cgroup_linux.go @@ -5,8 +5,7 @@ package fingerprint import ( "fmt" - client "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/opencontainers/runc/libcontainer/cgroups" ) @@ -28,30 +27,30 @@ func FindCgroupMountpointDir() (string, error) { } // Fingerprint tries to find a valid cgroup moint point -func (f *CGroupFingerprint) Fingerprint(cfg *client.Config, node *structs.Node) (bool, error) { +func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { mount, err := f.mountPointDetector.MountPoint() if err != nil { - f.clearCGroupAttributes(node) - return false, fmt.Errorf("Failed to discover cgroup mount point: %s", err) + resp.Attributes = f.clearCGroupAttributes(resp.Attributes) + return fmt.Errorf("Failed to discover cgroup mount point: %s", err) } // Check if a cgroup mount point was found if mount == "" { // Clear any attributes from the previous fingerprint. - f.clearCGroupAttributes(node) + resp.Attributes = f.clearCGroupAttributes(resp.Attributes) if f.lastState == cgroupAvailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are unavailable") } f.lastState = cgroupUnavailable - return true, nil + return nil } - node.Attributes["unique.cgroup.mountpoint"] = mount + resp.Attributes["unique.cgroup.mountpoint"] = mount if f.lastState == cgroupUnavailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are available") } f.lastState = cgroupAvailable - return true, nil + return nil } diff --git a/client/fingerprint/cgroup_test.go b/client/fingerprint/cgroup_test.go index f12379405..c7cf2f775 100644 --- a/client/fingerprint/cgroup_test.go +++ b/client/fingerprint/cgroup_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -39,64 +40,85 @@ func (m *MountPointDetectorEmptyMountPoint) MountPoint() (string, error) { } func TestCGroupFingerprint(t *testing.T) { - f := &CGroupFingerprint{ - logger: testLogger(), - lastState: cgroupUnavailable, - mountPointDetector: &MountPointDetectorMountPointFail{}, + { + f := &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorMountPointFail{}, + } + + node := &structs.Node{ + Attributes: make(map[string]string), + } + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) + if err == nil { + t.Fatalf("expected an error") + } + + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { + t.Fatalf("unexpected attribute found, %s", a) + } } - node := &structs.Node{ - Attributes: make(map[string]string), + { + f := &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorValidMountPoint{}, + } + + node := &structs.Node{ + Attributes: make(map[string]string), + } + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("unexpected error, %s", err) + } + if a, ok := response.Attributes["unique.cgroup.mountpoint"]; !ok { + t.Fatalf("unable to find attribute: %s", a) + } } - ok, err := f.Fingerprint(&config.Config{}, node) - if err == nil { - t.Fatalf("expected an error") - } - if ok { - t.Fatalf("should not apply") - } - if a, ok := node.Attributes["unique.cgroup.mountpoint"]; ok { - t.Fatalf("unexpected attribute found, %s", a) - } + { + f := &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupUnavailable, + mountPointDetector: &MountPointDetectorEmptyMountPoint{}, + } - f = &CGroupFingerprint{ - logger: testLogger(), - lastState: cgroupUnavailable, - mountPointDetector: &MountPointDetectorValidMountPoint{}, - } + node := &structs.Node{ + Attributes: make(map[string]string), + } - node = &structs.Node{ - Attributes: make(map[string]string), - } + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } - ok, err = f.Fingerprint(&config.Config{}, node) - if err != nil { - t.Fatalf("unexpected error, %s", err) - } - if !ok { - t.Fatalf("should apply") - } - assertNodeAttributeContains(t, node, "unique.cgroup.mountpoint") - - f = &CGroupFingerprint{ - logger: testLogger(), - lastState: cgroupUnavailable, - mountPointDetector: &MountPointDetectorEmptyMountPoint{}, - } - - node = &structs.Node{ - Attributes: make(map[string]string), - } - - ok, err = f.Fingerprint(&config.Config{}, node) - if err != nil { - t.Fatalf("unexpected error, %s", err) - } - if !ok { - t.Fatalf("should apply") - } - if a, ok := node.Attributes["unique.cgroup.mountpoint"]; ok { - t.Fatalf("unexpected attribute found, %s", a) + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("unexpected error, %s", err) + } + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { + t.Fatalf("unexpected attribute found, %s", a) + } } } diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index b3790a417..6566cefd9 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -8,7 +8,7 @@ import ( consul "github.com/hashicorp/consul/api" - client "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -29,23 +29,18 @@ func NewConsulFingerprint(logger *log.Logger) Fingerprint { return &ConsulFingerprint{logger: logger, lastState: consulUnavailable} } -func (f *ConsulFingerprint) Fingerprint(config *client.Config, node *structs.Node) (bool, error) { - // Guard against uninitialized Links - if node.Links == nil { - node.Links = map[string]string{} - } - +func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // Only create the client once to avoid creating too many connections to // Consul. if f.client == nil { - consulConfig, err := config.ConsulConfig.ApiConfig() + consulConfig, err := req.Config.ConsulConfig.ApiConfig() if err != nil { - return false, fmt.Errorf("Failed to initialize the Consul client config: %v", err) + return fmt.Errorf("Failed to initialize the Consul client config: %v", err) } f.client, err = consul.NewClient(consulConfig) if err != nil { - return false, fmt.Errorf("Failed to initialize consul client: %s", err) + return fmt.Errorf("Failed to initialize consul client: %s", err) } } @@ -53,8 +48,7 @@ func (f *ConsulFingerprint) Fingerprint(config *client.Config, node *structs.Nod // If we can't hit this URL consul is probably not running on this machine. info, err := f.client.Agent().Self() if err != nil { - // Clear any attributes set by a previous fingerprint. - f.clearConsulAttributes(node) + // TODO this should set consul in the response if the error is not nil // Print a message indicating that the Consul Agent is not available // anymore @@ -62,39 +56,39 @@ func (f *ConsulFingerprint) Fingerprint(config *client.Config, node *structs.Nod f.logger.Printf("[INFO] fingerprint.consul: consul agent is unavailable") } f.lastState = consulUnavailable - return false, nil + return nil } if s, ok := info["Config"]["Server"].(bool); ok { - node.Attributes["consul.server"] = strconv.FormatBool(s) + resp.Attributes["consul.server"] = strconv.FormatBool(s) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.server") } if v, ok := info["Config"]["Version"].(string); ok { - node.Attributes["consul.version"] = v + resp.Attributes["consul.version"] = v } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.version") } if r, ok := info["Config"]["Revision"].(string); ok { - node.Attributes["consul.revision"] = r + resp.Attributes["consul.revision"] = r } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.revision") } if n, ok := info["Config"]["NodeName"].(string); ok { - node.Attributes["unique.consul.name"] = n + resp.Attributes["unique.consul.name"] = n } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint unique.consul.name") } if d, ok := info["Config"]["Datacenter"].(string); ok { - node.Attributes["consul.datacenter"] = d + resp.Attributes["consul.datacenter"] = d } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.datacenter") } - if node.Attributes["consul.datacenter"] != "" || node.Attributes["unique.consul.name"] != "" { - node.Links["consul"] = fmt.Sprintf("%s.%s", - node.Attributes["consul.datacenter"], - node.Attributes["unique.consul.name"]) + if resp.Attributes["consul.datacenter"] != "" || resp.Attributes["unique.consul.name"] != "" { + resp.Links["consul"] = fmt.Sprintf("%s.%s", + resp.Attributes["consul.datacenter"], + resp.Attributes["unique.consul.name"]) } else { f.logger.Printf("[WARN] fingerprint.consul: malformed Consul response prevented linking") } @@ -105,17 +99,12 @@ func (f *ConsulFingerprint) Fingerprint(config *client.Config, node *structs.Nod f.logger.Printf("[INFO] fingerprint.consul: consul agent is available") } f.lastState = consulAvailable - return true, nil + return nil } // clearConsulAttributes removes consul attributes and links from the passed // Node. func (f *ConsulFingerprint) clearConsulAttributes(n *structs.Node) { - delete(n.Attributes, "consul.server") - delete(n.Attributes, "consul.version") - delete(n.Attributes, "consul.revision") - delete(n.Attributes, "unique.consul.name") - delete(n.Attributes, "consul.datacenter") delete(n.Links, "consul") } diff --git a/client/fingerprint/consul_test.go b/client/fingerprint/consul_test.go index e2eecf438..5240b782b 100644 --- a/client/fingerprint/consul_test.go +++ b/client/fingerprint/consul_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/stretchr/testify/assert" ) @@ -24,24 +25,28 @@ func TestConsulFingerprint(t *testing.T) { })) defer ts.Close() - config := config.DefaultConfig() - config.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") + conf := config.DefaultConfig() + conf.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") - ok, err := fp.Fingerprint(config, node) + request := &cstructs.FingerprintRequest{Config: conf, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := fp.Fingerprint(request, response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - if !ok { - t.Fatalf("Failed to apply node attributes") - } - assertNodeAttributeContains(t, node, "consul.server") - assertNodeAttributeContains(t, node, "consul.version") - assertNodeAttributeContains(t, node, "consul.revision") - assertNodeAttributeContains(t, node, "unique.consul.name") - assertNodeAttributeContains(t, node, "consul.datacenter") + assertNodeAttributeContains(t, response.Attributes, "consul.server") + assertNodeAttributeContains(t, response.Attributes, "consul.version") + assertNodeAttributeContains(t, response.Attributes, "consul.revision") + assertNodeAttributeContains(t, response.Attributes, "unique.consul.name") + assertNodeAttributeContains(t, response.Attributes, "consul.datacenter") - if _, ok := node.Links["consul"]; !ok { + if _, ok := response.Links["consul"]; !ok { t.Errorf("Expected a link to consul, none found") } } @@ -177,12 +182,18 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { })) defer ts.Close() - config := config.DefaultConfig() - config.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") + conf := config.DefaultConfig() + conf.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") - ok, err := fp.Fingerprint(config, node) + request := &cstructs.FingerprintRequest{Config: conf, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := fp.Fingerprint(request, response) assert.Nil(err) - assert.True(ok) attrs := []string{ "consul.server", @@ -192,7 +203,7 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { "consul.datacenter", } for _, attr := range attrs { - if v, ok := node.Attributes[attr]; ok { + if v, ok := response.Attributes[attr]; ok { t.Errorf("unexpected node attribute %q with vlaue %q", attr, v) } } diff --git a/client/fingerprint/cpu.go b/client/fingerprint/cpu.go index 0c9b4c25d..69dad8956 100644 --- a/client/fingerprint/cpu.go +++ b/client/fingerprint/cpu.go @@ -4,9 +4,8 @@ import ( "fmt" "log" - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/helper/stats" - "github.com/hashicorp/nomad/nomad/structs" ) // CPUFingerprint is used to fingerprint the CPU @@ -21,13 +20,10 @@ func NewCPUFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *CPUFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + cfg := req.Config setResources := func(totalCompute int) { - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - - node.Resources.CPU = totalCompute + resp.Resources.CPU = totalCompute } if err := stats.Init(); err != nil { @@ -36,20 +32,20 @@ func (f *CPUFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bo if cfg.CpuCompute != 0 { setResources(cfg.CpuCompute) - return true, nil + return nil } if modelName := stats.CPUModelName(); modelName != "" { - node.Attributes["cpu.modelname"] = modelName + resp.Attributes["cpu.modelname"] = modelName } if mhz := stats.CPUMHzPerCore(); mhz > 0 { - node.Attributes["cpu.frequency"] = fmt.Sprintf("%.0f", mhz) + resp.Attributes["cpu.frequency"] = fmt.Sprintf("%.0f", mhz) f.logger.Printf("[DEBUG] fingerprint.cpu: frequency: %.0f MHz", mhz) } if numCores := stats.CPUNumCores(); numCores > 0 { - node.Attributes["cpu.numcores"] = fmt.Sprintf("%d", numCores) + resp.Attributes["cpu.numcores"] = fmt.Sprintf("%d", numCores) f.logger.Printf("[DEBUG] fingerprint.cpu: core count: %d", numCores) } @@ -62,17 +58,13 @@ func (f *CPUFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bo // Return an error if no cpu was detected or explicitly set as this // node would be unable to receive any allocations. if tt == 0 { - return false, fmt.Errorf("cannot detect cpu total compute. "+ + return fmt.Errorf("cannot detect cpu total compute. "+ "CPU compute must be set manually using the client config option %q", "cpu_total_compute") } - node.Attributes["cpu.totalcompute"] = fmt.Sprintf("%d", tt) + resp.Attributes["cpu.totalcompute"] = fmt.Sprintf("%d", tt) + resp.Resources.CPU = tt - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - - node.Resources.CPU = tt - return true, nil + return nil } diff --git a/client/fingerprint/cpu_test.go b/client/fingerprint/cpu_test.go index 238d55770..e227d3c90 100644 --- a/client/fingerprint/cpu_test.go +++ b/client/fingerprint/cpu_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -12,30 +13,35 @@ func TestCPUFingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") - } // CPU info - if node.Attributes["cpu.numcores"] == "" { + if response.Attributes["cpu.numcores"] == "" { t.Fatalf("Missing Num Cores") } - if node.Attributes["cpu.modelname"] == "" { + if response.Attributes["cpu.modelname"] == "" { t.Fatalf("Missing Model Name") } - if node.Attributes["cpu.frequency"] == "" { + if response.Attributes["cpu.frequency"] == "" { t.Fatalf("Missing CPU Frequency") } - if node.Attributes["cpu.totalcompute"] == "" { + if response.Attributes["cpu.totalcompute"] == "" { t.Fatalf("Missing CPU Total Compute") } - if node.Resources == nil || node.Resources.CPU == 0 { + if response.Resources == nil || response.Resources.CPU == 0 { t.Fatalf("Expected to find CPU Resources") } @@ -49,30 +55,44 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { Attributes: make(map[string]string), } cfg := &config.Config{} - ok, err := f.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !ok { - t.Fatalf("should apply") + var originalCPU int + + { + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } + if response.Resources.CPU == 0 { + t.Fatalf("expected fingerprint of cpu of but found 0") + } + + originalCPU = response.Resources.CPU } - // Get actual system CPU - origCPU := node.Resources.CPU + { + // Override it with a setting + cfg.CpuCompute = originalCPU + 123 - // Override it with a setting - cfg.CpuCompute = origCPU + 123 + // Make sure the Fingerprinter applies the override to the node resources + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } - // Make sure the Fingerprinter applies the override - ok, err = f.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !ok { - t.Fatalf("should apply") - } - - if node.Resources.CPU != cfg.CpuCompute { - t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, node.Resources.CPU) + if response.Resources.CPU != cfg.CpuCompute { + t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, response.Resources.CPU) + } } } diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index b442f49ff..5d94c9998 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -12,7 +12,7 @@ import ( "time" "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -63,14 +63,16 @@ func NewEnvAWSFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, response *cstructs.FingerprintResponse) error { + cfg := request.Config + // Check if we should tighten the timeout if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) { f.timeout = 1 * time.Millisecond } if !f.isAWS() { - return false, nil + return nil } // newNetwork is populated and addded to the Nodes resources @@ -78,9 +80,6 @@ func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) Device: "eth0", } - if node.Links == nil { - node.Links = make(map[string]string) - } metadataURL := os.Getenv("AWS_ENV_URL") if metadataURL == "" { metadataURL = DEFAULT_AWS_URL @@ -115,10 +114,10 @@ func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) // if it's a URL error, assume we're not in an AWS environment // TODO: better way to detect AWS? Check xen virtualization? if _, ok := err.(*url.Error); ok { - return false, nil + return nil } // not sure what other errors it would return - return false, err + return err } resp, err := ioutil.ReadAll(res.Body) res.Body.Close() @@ -132,12 +131,12 @@ func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) key = structs.UniqueNamespace(key) } - node.Attributes[key] = strings.Trim(string(resp), "\n") + response.Attributes[key] = strings.Trim(string(resp), "\n") } // copy over network specific information - if val := node.Attributes["unique.platform.aws.local-ipv4"]; val != "" { - node.Attributes["unique.network.ip-address"] = val + if val := response.Attributes["unique.platform.aws.local-ipv4"]; val != "" { + response.Attributes["unique.network.ip-address"] = val newNetwork.IP = val newNetwork.CIDR = newNetwork.IP + "/32" } @@ -149,8 +148,8 @@ func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } else if throughput == 0 { // Failed to determine speed. Check if the network fingerprint got it found := false - if node.Resources != nil && len(node.Resources.Networks) > 0 { - for _, n := range node.Resources.Networks { + if request.Node.Resources != nil && len(request.Node.Resources.Networks) > 0 { + for _, n := range request.Node.Resources.Networks { if n.IP == newNetwork.IP { throughput = n.MBits found = true @@ -165,19 +164,15 @@ func (f *EnvAWSFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } } - // populate Node Network Resources - if node.Resources == nil { - node.Resources = &structs.Resources{} - } newNetwork.MBits = throughput - node.Resources.Networks = []*structs.NetworkResource{newNetwork} + response.Resources.Networks = []*structs.NetworkResource{newNetwork} // populate Links - node.Links["aws.ec2"] = fmt.Sprintf("%s.%s", - node.Attributes["platform.aws.placement.availability-zone"], - node.Attributes["unique.platform.aws.instance-id"]) + response.Links["aws.ec2"] = fmt.Sprintf("%s.%s", + response.Attributes["platform.aws.placement.availability-zone"], + response.Attributes["unique.platform.aws.instance-id"]) - return true, nil + return nil } func (f *EnvAWSFingerprint) isAWS() bool { diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index 77f7c1dc9..1e60516b5 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -19,13 +20,20 @@ func TestEnvAWSFingerprint_nonAws(t *testing.T) { Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if ok { - t.Fatalf("Should be false without test server") + if len(response.Attributes) > 0 { + t.Fatalf("Should not apply") } } @@ -51,13 +59,16 @@ func TestEnvAWSFingerprint_aws(t *testing.T) { defer ts.Close() os.Setenv("AWS_ENV_URL", ts.URL+"/latest/meta-data/") - ok, err := f.Fingerprint(&config.Config{}, node) - if err != nil { - t.Fatalf("err: %v", err) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - if !ok { - t.Fatalf("Expected AWS attributes and Links") + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) } keys := []string{ @@ -74,16 +85,16 @@ func TestEnvAWSFingerprint_aws(t *testing.T) { } for _, k := range keys { - assertNodeAttributeContains(t, node, k) + assertNodeAttributeContains(t, response.Attributes, k) } - if len(node.Links) == 0 { + if len(response.Links) == 0 { t.Fatalf("Empty links for Node in AWS Fingerprint test") } // confirm we have at least instance-id and ami-id for _, k := range []string{"aws.ec2"} { - assertNodeLinksContains(t, node, k) + assertNodeLinksContains(t, response.Links, k) } } @@ -171,22 +182,26 @@ func TestNetworkFingerprint_AWS(t *testing.T) { Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") - } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - if node.Resources == nil || len(node.Resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := node.Resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -217,73 +232,86 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { os.Setenv("AWS_ENV_URL", ts.URL+"/latest/meta-data/") f := NewEnvAWSFingerprint(testLogger()) - node := &structs.Node{ - Attributes: make(map[string]string), - } + { + node := &structs.Node{ + Attributes: make(map[string]string), + } - cfg := &config.Config{} - ok, err := f.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !ok { - t.Fatalf("should apply") - } + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } - if node.Resources == nil || len(node.Resources.Networks) == 0 { - t.Fatal("Expected to find Network Resources") - } + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - // Test at least the first Network Resource - net := node.Resources.Networks[0] - if net.IP == "" { - t.Fatal("Expected Network Resource to have an IP") - } - if net.CIDR == "" { - t.Fatal("Expected Network Resource to have a CIDR") - } - if net.Device == "" { - t.Fatal("Expected Network Resource to have a Device Name") - } - if net.MBits != 1000 { - t.Fatalf("Expected Network Resource to have speed %d; got %d", 1000, net.MBits) + if response.Resources == nil || len(response.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := response.Resources.Networks[0] + if net.IP == "" { + t.Fatal("Expected Network Resource to have an IP") + } + if net.CIDR == "" { + t.Fatal("Expected Network Resource to have a CIDR") + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + if net.MBits != 1000 { + t.Fatalf("Expected Network Resource to have speed %d; got %d", 1000, net.MBits) + } } // Try again this time setting a network speed in the config - node = &structs.Node{ - Attributes: make(map[string]string), - } + { + node := &structs.Node{ + Attributes: make(map[string]string), + } - cfg.NetworkSpeed = 10 - ok, err = f.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !ok { - t.Fatalf("should apply") - } + cfg := &config.Config{ + NetworkSpeed: 10, + } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) + } - if node.Resources == nil || len(node.Resources.Networks) == 0 { - t.Fatal("Expected to find Network Resources") - } + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - // Test at least the first Network Resource - net = node.Resources.Networks[0] - if net.IP == "" { - t.Fatal("Expected Network Resource to have an IP") - } - if net.CIDR == "" { - t.Fatal("Expected Network Resource to have a CIDR") - } - if net.Device == "" { - t.Fatal("Expected Network Resource to have a Device Name") - } - if net.MBits != 10 { - t.Fatalf("Expected Network Resource to have speed %d; got %d", 10, net.MBits) + if response.Resources == nil || len(response.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := response.Resources.Networks[0] + if net.IP == "" { + t.Fatal("Expected Network Resource to have an IP") + } + if net.CIDR == "" { + t.Fatal("Expected Network Resource to have a CIDR") + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + if net.MBits != 10 { + t.Fatalf("Expected Network Resource to have speed %d; got %d", 10, net.MBits) + } } } @@ -294,11 +322,19 @@ func TestNetworkFingerprint_notAWS(t *testing.T) { Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if ok { + + if len(response.Attributes) > 0 { t.Fatalf("Should not apply") } } diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 83da63486..132ddeba0 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -14,7 +14,7 @@ import ( "time" "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -131,18 +131,16 @@ func checkError(err error, logger *log.Logger, desc string) error { return err } -func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + cfg := req.Config + // Check if we should tighten the timeout if cfg.ReadBoolDefault(TightenNetworkTimeoutsConfig, false) { f.client.Timeout = 1 * time.Millisecond } if !f.isGCE() { - return false, nil - } - - if node.Links == nil { - node.Links = make(map[string]string) + return nil } // Keys and whether they should be namespaced as unique. Any key whose value @@ -159,7 +157,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) for k, unique := range keys { value, err := f.Get(k, false) if err != nil { - return false, checkError(err, f.logger, k) + return checkError(err, f.logger, k) } // assume we want blank entries @@ -167,7 +165,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) if unique { key = structs.UniqueNamespace(key) } - node.Attributes[key] = strings.Trim(value, "\n") + resp.Attributes[key] = strings.Trim(value, "\n") } // These keys need everything before the final slash removed to be usable. @@ -178,14 +176,14 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) for k, unique := range keys { value, err := f.Get(k, false) if err != nil { - return false, checkError(err, f.logger, k) + return checkError(err, f.logger, k) } key := "platform.gce." + k if unique { key = structs.UniqueNamespace(key) } - node.Attributes[key] = strings.Trim(lastToken(value), "\n") + resp.Attributes[key] = strings.Trim(lastToken(value), "\n") } // Get internal and external IPs (if they exist) @@ -202,10 +200,10 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) for _, intf := range interfaces { prefix := "platform.gce.network." + lastToken(intf.Network) uniquePrefix := "unique." + prefix - node.Attributes[prefix] = "true" - node.Attributes[uniquePrefix+".ip"] = strings.Trim(intf.Ip, "\n") + resp.Attributes[prefix] = "true" + resp.Attributes[uniquePrefix+".ip"] = strings.Trim(intf.Ip, "\n") for index, accessConfig := range intf.AccessConfigs { - node.Attributes[uniquePrefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp + resp.Attributes[uniquePrefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp } } } @@ -213,7 +211,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) var tagList []string value, err = f.Get("tags", false) if err != nil { - return false, checkError(err, f.logger, "tags") + return checkError(err, f.logger, "tags") } if err := json.Unmarshal([]byte(value), &tagList); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) @@ -231,13 +229,13 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) key = fmt.Sprintf("%s%s", attr, tag) } - node.Attributes[key] = "true" + resp.Attributes[key] = "true" } var attrDict map[string]string value, err = f.Get("attributes/", true) if err != nil { - return false, checkError(err, f.logger, "attributes/") + return checkError(err, f.logger, "attributes/") } if err := json.Unmarshal([]byte(value), &attrDict); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) @@ -255,13 +253,13 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) key = fmt.Sprintf("%s%s", attr, k) } - node.Attributes[key] = strings.Trim(v, "\n") + resp.Attributes[key] = strings.Trim(v, "\n") } // populate Links - node.Links["gce"] = node.Attributes["unique.platform.gce.id"] + resp.Links["gce"] = resp.Attributes["unique.platform.gce.id"] - return true, nil + return nil } func (f *EnvGCEFingerprint) isGCE() bool { diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 1e339789a..4c1febd95 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -19,13 +20,20 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if ok { - t.Fatalf("Should be false without test server") + if len(response.Attributes) > 0 { + t.Fatalf("Should have zero attributes without test server") } } @@ -76,13 +84,16 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { os.Setenv("GCE_ENV_URL", ts.URL+"/computeMetadata/v1/instance/") f := NewEnvGCEFingerprint(testLogger()) - ok, err := f.Fingerprint(&config.Config{}, node) - if err != nil { - t.Fatalf("err: %v", err) + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, } - if !ok { - t.Fatalf("should apply") + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("err: %v", err) } keys := []string{ @@ -100,40 +111,40 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { } for _, k := range keys { - assertNodeAttributeContains(t, node, k) + assertNodeAttributeContains(t, response.Attributes, k) } - if len(node.Links) == 0 { + if len(response.Links) == 0 { t.Fatalf("Empty links for Node in GCE Fingerprint test") } // Make sure Links contains the GCE ID. for _, k := range []string{"gce"} { - assertNodeLinksContains(t, node, k) + assertNodeLinksContains(t, response.Links, k) } - assertNodeAttributeEquals(t, node, "unique.platform.gce.id", "12345") - assertNodeAttributeEquals(t, node, "unique.platform.gce.hostname", "instance-1.c.project.internal") - assertNodeAttributeEquals(t, node, "platform.gce.zone", "us-central1-f") - assertNodeAttributeEquals(t, node, "platform.gce.machine-type", "n1-standard-1") - assertNodeAttributeEquals(t, node, "platform.gce.network.default", "true") - assertNodeAttributeEquals(t, node, "unique.platform.gce.network.default.ip", "10.240.0.5") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.id", "12345") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.hostname", "instance-1.c.project.internal") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.zone", "us-central1-f") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.machine-type", "n1-standard-1") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.network.default", "true") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { - assertNodeAttributeEquals(t, node, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") - assertNodeAttributeEquals(t, node, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") - } else if _, ok := node.Attributes["unique.platform.gce.network.default.external-ip.0"]; ok { + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") + } else if _, ok := response.Attributes["unique.platform.gce.network.default.external-ip.0"]; ok { t.Fatal("unique.platform.gce.network.default.external-ip is set without an external IP") } - assertNodeAttributeEquals(t, node, "platform.gce.scheduling.automatic-restart", "TRUE") - assertNodeAttributeEquals(t, node, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") - assertNodeAttributeEquals(t, node, "platform.gce.cpu-platform", "Intel Ivy Bridge") - assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") - assertNodeAttributeEquals(t, node, "platform.gce.tag.def", "true") - assertNodeAttributeEquals(t, node, "unique.platform.gce.tag.foo", "true") - assertNodeAttributeEquals(t, node, "platform.gce.attr.ghi", "111") - assertNodeAttributeEquals(t, node, "platform.gce.attr.jkl", "222") - assertNodeAttributeEquals(t, node, "unique.platform.gce.attr.bar", "333") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.automatic-restart", "TRUE") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.cpu-platform", "Intel Ivy Bridge") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.abc", "true") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.def", "true") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.tag.foo", "true") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.ghi", "111") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.jkl", "222") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.attr.bar", "333") } const GCE_routes = ` diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index 2d6d483b6..8a3477f51 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -6,8 +6,7 @@ import ( "sort" "time" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" ) // EmptyDuration is to be used by fingerprinters that are not periodic. @@ -92,8 +91,8 @@ type Factory func(*log.Logger) Fingerprint // many of them can be applied on a particular host. type Fingerprint interface { // Fingerprint is used to update properties of the Node, - // and returns if the fingerprint was applicable and a potential error. - Fingerprint(*config.Config, *structs.Node) (bool, error) + // and returns a diff of updated node attributes and a potential error. + Fingerprint(*cstructs.FingerprintRequest, *cstructs.FingerprintResponse) error // Periodic is a mechanism for the fingerprinter to indicate that it should // be run periodically. The return value is a boolean indicating if it diff --git a/client/fingerprint/fingerprint_test.go b/client/fingerprint/fingerprint_test.go index 7e62440ea..ba6dae096 100644 --- a/client/fingerprint/fingerprint_test.go +++ b/client/fingerprint/fingerprint_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -15,45 +16,55 @@ func testLogger() *log.Logger { return log.New(os.Stderr, "", log.LstdFlags) } -func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) { - ok, err := fp.Fingerprint(new(config.Config), node) +func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) *cstructs.FingerprintResponse { + request := &cstructs.FingerprintRequest{Config: new(config.Config), Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := fp.Fingerprint(request, response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - if !ok { + + if len(response.Attributes) == 0 { t.Fatalf("Failed to apply node attributes") } + + return response } -func assertNodeAttributeContains(t *testing.T, node *structs.Node, attribute string) { - actual, found := node.Attributes[attribute] +func assertNodeAttributeContains(t *testing.T, nodeAttributes map[string]string, attribute string) { + actual, found := nodeAttributes[attribute] if !found { - t.Errorf("Expected to find Attribute `%s`\n\n[DEBUG] %#v", attribute, node) + t.Errorf("Expected to find Attribute `%s`\n\n[DEBUG] %#v", attribute, nodeAttributes) return } if actual == "" { - t.Errorf("Expected non-empty Attribute value for `%s`\n\n[DEBUG] %#v", attribute, node) + t.Errorf("Expected non-empty Attribute value for `%s`\n\n[DEBUG] %#v", attribute, nodeAttributes) } } -func assertNodeAttributeEquals(t *testing.T, node *structs.Node, attribute string, expected string) { - actual, found := node.Attributes[attribute] +func assertNodeAttributeEquals(t *testing.T, nodeAttributes map[string]string, attribute string, expected string) { + actual, found := nodeAttributes[attribute] if !found { - t.Errorf("Expected to find Attribute `%s`; unable to check value\n\n[DEBUG] %#v", attribute, node) + t.Errorf("Expected to find Attribute `%s`; unable to check value\n\n[DEBUG] %#v", attribute, nodeAttributes) return } if expected != actual { - t.Errorf("Expected `%s` Attribute to be `%s`, found `%s`\n\n[DEBUG] %#v", attribute, expected, actual, node) + t.Errorf("Expected `%s` Attribute to be `%s`, found `%s`\n\n[DEBUG] %#v", attribute, expected, actual, nodeAttributes) } } -func assertNodeLinksContains(t *testing.T, node *structs.Node, link string) { - actual, found := node.Links[link] +func assertNodeLinksContains(t *testing.T, nodeLinks map[string]string, link string) { + actual, found := nodeLinks[link] if !found { - t.Errorf("Expected to find Link `%s`\n\n[DEBUG] %#v", link, node) + t.Errorf("Expected to find Link `%s`\n\n[DEBUG]", link) return } if actual == "" { - t.Errorf("Expected non-empty Link value for `%s`\n\n[DEBUG] %#v", link, node) + t.Errorf("Expected non-empty Link value for `%s`\n\n[DEBUG]", link) } } diff --git a/client/fingerprint/host.go b/client/fingerprint/host.go index a7b8ed6c8..348846b3b 100644 --- a/client/fingerprint/host.go +++ b/client/fingerprint/host.go @@ -4,8 +4,7 @@ import ( "log" "runtime" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/shirou/gopsutil/host" ) @@ -21,20 +20,20 @@ func NewHostFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *HostFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *HostFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { hostInfo, err := host.Info() if err != nil { f.logger.Println("[WARN] Error retrieving host information: ", err) - return false, err + return err } - node.Attributes["os.name"] = hostInfo.Platform - node.Attributes["os.version"] = hostInfo.PlatformVersion + resp.Attributes["os.name"] = hostInfo.Platform + resp.Attributes["os.version"] = hostInfo.PlatformVersion - node.Attributes["kernel.name"] = runtime.GOOS - node.Attributes["kernel.version"] = hostInfo.KernelVersion + resp.Attributes["kernel.name"] = runtime.GOOS + resp.Attributes["kernel.version"] = hostInfo.KernelVersion - node.Attributes["unique.hostname"] = hostInfo.Hostname + resp.Attributes["unique.hostname"] = hostInfo.Hostname - return true, nil + return nil } diff --git a/client/fingerprint/host_test.go b/client/fingerprint/host_test.go index 5a5f0cfbe..e0ef313ab 100644 --- a/client/fingerprint/host_test.go +++ b/client/fingerprint/host_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -12,16 +13,24 @@ func TestHostFingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") + + if len(response.Attributes) == 0 { + t.Fatalf("should generate a diff of node attributes") } // Host info for _, key := range []string{"os.name", "os.version", "unique.hostname", "kernel.name"} { - assertNodeAttributeContains(t, node, key) + assertNodeAttributeContains(t, response.Attributes, key) } } diff --git a/client/fingerprint/memory.go b/client/fingerprint/memory.go index b249bebf5..b95a84d7d 100644 --- a/client/fingerprint/memory.go +++ b/client/fingerprint/memory.go @@ -4,8 +4,7 @@ import ( "fmt" "log" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/shirou/gopsutil/mem" ) @@ -23,21 +22,18 @@ func NewMemoryFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *MemoryFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *MemoryFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { memInfo, err := mem.VirtualMemory() if err != nil { f.logger.Printf("[WARN] Error reading memory information: %s", err) - return false, err + return err } if memInfo.Total > 0 { - node.Attributes["memory.totalbytes"] = fmt.Sprintf("%d", memInfo.Total) + resp.Attributes["memory.totalbytes"] = fmt.Sprintf("%d", memInfo.Total) - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - node.Resources.MemoryMB = int(memInfo.Total / 1024 / 1024) + resp.Resources.MemoryMB = int(memInfo.Total / 1024 / 1024) } - return true, nil + return nil } diff --git a/client/fingerprint/memory_test.go b/client/fingerprint/memory_test.go index 44c79c0cb..3d6b3eaed 100644 --- a/client/fingerprint/memory_test.go +++ b/client/fingerprint/memory_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -12,20 +13,24 @@ func TestMemoryFingerprint(t *testing.T) { node := &structs.Node{ Attributes: make(map[string]string), } - ok, err := f.Fingerprint(&config.Config{}, node) + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") - } - assertNodeAttributeContains(t, node, "memory.totalbytes") + assertNodeAttributeContains(t, response.Attributes, "memory.totalbytes") - if node.Resources == nil { + if response.Resources == nil { t.Fatalf("Node Resources was nil") } - if node.Resources.MemoryMB == 0 { + if response.Resources.MemoryMB == 0 { t.Errorf("Expected node.Resources.MemoryMB to be non-zero") } diff --git a/client/fingerprint/network.go b/client/fingerprint/network.go index 287fb7359..58cfb1b4c 100644 --- a/client/fingerprint/network.go +++ b/client/fingerprint/network.go @@ -6,7 +6,7 @@ import ( "net" sockaddr "github.com/hashicorp/go-sockaddr" - "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -61,19 +61,17 @@ func NewNetworkFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - if node.Resources == nil { - node.Resources = &structs.Resources{} - } +func (f *NetworkFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + cfg := req.Config // Find the named interface intf, err := f.findInterface(cfg.NetworkInterface) switch { case err != nil: - return false, fmt.Errorf("Error while detecting network interface during fingerprinting: %v", err) + return fmt.Errorf("Error while detecting network interface during fingerprinting: %v", err) case intf == nil: // No interface could be found - return false, nil + return nil } // Record the throughput of the interface @@ -94,22 +92,20 @@ func (f *NetworkFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) disallowLinkLocal := cfg.ReadBoolDefault(networkDisallowLinkLocalOption, networkDisallowLinkLocalDefault) nwResources, err := f.createNetworkResources(mbits, intf, disallowLinkLocal) if err != nil { - return false, err + return err } - // Add the network resources to the node - node.Resources.Networks = nwResources + resp.Resources.Networks = nwResources for _, nwResource := range nwResources { f.logger.Printf("[DEBUG] fingerprint.network: Detected interface %v with IP: %v", intf.Name, nwResource.IP) } // Deprecated, setting the first IP as unique IP for the node if len(nwResources) > 0 { - node.Attributes["unique.network.ip-address"] = nwResources[0].IP + resp.Attributes["unique.network.ip-address"] = nwResources[0].IP } - // return true, because we have a network connection - return true, nil + return nil } // createNetworkResources creates network resources for every IP diff --git a/client/fingerprint/network_test.go b/client/fingerprint/network_test.go index 78dca0df7..8d556fcb2 100644 --- a/client/fingerprint/network_test.go +++ b/client/fingerprint/network_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) @@ -189,28 +190,35 @@ func TestNetworkFingerprint_basic(t *testing.T) { } cfg := &config.Config{NetworkSpeed: 101} - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { + if len(response.Attributes) == 0 { t.Fatalf("should apply (HINT: working offline? Set env %q=y", skipOnlineTestsEnvVar) } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - ip := node.Attributes["unique.network.ip-address"] + ip := response.Attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if node.Resources == nil || len(node.Resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := node.Resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -232,13 +240,20 @@ func TestNetworkFingerprint_default_device_absent(t *testing.T) { } cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth0"} - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err == nil { t.Fatalf("err: %v", err) } - if ok { - t.Fatalf("ok: %v", ok) + if len(response.Attributes) != 0 { + t.Fatalf("attributes should be zero but instead are: %v", response.Attributes) } } @@ -249,28 +264,34 @@ func TestNetworkFingerPrint_default_device(t *testing.T) { } cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "lo"} - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { + if len(response.Attributes) == 0 { t.Fatalf("should apply") } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - ip := node.Attributes["unique.network.ip-address"] + ip := response.Attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if node.Resources == nil || len(node.Resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := node.Resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -292,28 +313,31 @@ func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { } cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth3"} - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { - t.Fatalf("should apply") - } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - ip := node.Attributes["unique.network.ip-address"] + ip := response.Attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if node.Resources == nil || len(node.Resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := node.Resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -335,28 +359,34 @@ func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { } cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth4"} - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { + if len(response.Attributes) == 0 { t.Fatalf("should apply") } - assertNodeAttributeContains(t, node, "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - ip := node.Attributes["unique.network.ip-address"] + ip := response.Attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if node.Resources == nil || len(node.Resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := node.Resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -387,11 +417,17 @@ func TestNetworkFingerPrint_LinkLocal_Disallowed(t *testing.T) { }, } - ok, err := f.Fingerprint(cfg, node) + request := &cstructs.FingerprintRequest{Config: cfg, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { + if len(response.Attributes) != 0 { t.Fatalf("should not apply") } } diff --git a/client/fingerprint/nomad.go b/client/fingerprint/nomad.go index 0db894196..21849dc98 100644 --- a/client/fingerprint/nomad.go +++ b/client/fingerprint/nomad.go @@ -3,8 +3,7 @@ package fingerprint import ( "log" - client "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" ) // NomadFingerprint is used to fingerprint the Nomad version @@ -19,8 +18,8 @@ func NewNomadFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *NomadFingerprint) Fingerprint(config *client.Config, node *structs.Node) (bool, error) { - node.Attributes["nomad.version"] = config.Version.VersionNumber() - node.Attributes["nomad.revision"] = config.Version.Revision - return true, nil +func (f *NomadFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + resp.Attributes["nomad.version"] = req.Config.Version.VersionNumber() + resp.Attributes["nomad.revision"] = req.Config.Version.Revision + return nil } diff --git a/client/fingerprint/nomad_test.go b/client/fingerprint/nomad_test.go index 730fc3c5d..aac47a944 100644 --- a/client/fingerprint/nomad_test.go +++ b/client/fingerprint/nomad_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/version" ) @@ -21,17 +22,28 @@ func TestNomadFingerprint(t *testing.T) { Version: v, }, } - ok, err := f.Fingerprint(c, node) + + request := &cstructs.FingerprintRequest{Config: c, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) if err != nil { t.Fatalf("err: %v", err) } - if !ok { + + if len(response.Attributes) == 0 { t.Fatalf("should apply") } - if node.Attributes["nomad.version"] != v { + + if response.Attributes["nomad.version"] != v { t.Fatalf("incorrect version") } - if node.Attributes["nomad.revision"] != r { + + if response.Attributes["nomad.revision"] != r { t.Fatalf("incorrect revision") } } diff --git a/client/fingerprint/signal.go b/client/fingerprint/signal.go index d20cec07b..d87e91447 100644 --- a/client/fingerprint/signal.go +++ b/client/fingerprint/signal.go @@ -5,8 +5,7 @@ import ( "strings" "github.com/hashicorp/consul-template/signals" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" ) // SignalFingerprint is used to fingerprint the available signals @@ -21,13 +20,13 @@ func NewSignalFingerprint(logger *log.Logger) Fingerprint { return f } -func (f *SignalFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *SignalFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { // Build the list of available signals sigs := make([]string, 0, len(signals.SignalLookup)) for signal := range signals.SignalLookup { sigs = append(sigs, signal) } - node.Attributes["os.signals"] = strings.Join(sigs, ",") - return true, nil + resp.Attributes["os.signals"] = strings.Join(sigs, ",") + return nil } diff --git a/client/fingerprint/signal_test.go b/client/fingerprint/signal_test.go index 2157cf0c5..bf61f7544 100644 --- a/client/fingerprint/signal_test.go +++ b/client/fingerprint/signal_test.go @@ -12,6 +12,6 @@ func TestSignalFingerprint(t *testing.T) { Attributes: make(map[string]string), } - assertFingerprintOK(t, fp, node) - assertNodeAttributeContains(t, node, "os.signals") + response := assertFingerprintOK(t, fp, node) + assertNodeAttributeContains(t, response.Attributes, "os.signals") } diff --git a/client/fingerprint/storage.go b/client/fingerprint/storage.go index c60f13154..34d9e1202 100644 --- a/client/fingerprint/storage.go +++ b/client/fingerprint/storage.go @@ -6,8 +6,7 @@ import ( "os" "strconv" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" ) const bytesPerMegabyte = 1024 * 1024 @@ -24,15 +23,13 @@ func NewStorageFingerprint(logger *log.Logger) Fingerprint { return fp } -func (f *StorageFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { +func (f *StorageFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + cfg := req.Config // Initialize these to empty defaults - node.Attributes["unique.storage.volume"] = "" - node.Attributes["unique.storage.bytestotal"] = "" - node.Attributes["unique.storage.bytesfree"] = "" - if node.Resources == nil { - node.Resources = &structs.Resources{} - } + resp.Attributes["unique.storage.volume"] = "" + resp.Attributes["unique.storage.bytestotal"] = "" + resp.Attributes["unique.storage.bytesfree"] = "" // Guard against unset AllocDir storageDir := cfg.AllocDir @@ -40,20 +37,20 @@ func (f *StorageFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) var err error storageDir, err = os.Getwd() if err != nil { - return false, fmt.Errorf("unable to get CWD from filesystem: %s", err) + return fmt.Errorf("unable to get CWD from filesystem: %s", err) } } volume, total, free, err := f.diskFree(storageDir) if err != nil { - return false, fmt.Errorf("failed to determine disk space for %s: %v", storageDir, err) + return fmt.Errorf("failed to determine disk space for %s: %v", storageDir, err) } - node.Attributes["unique.storage.volume"] = volume - node.Attributes["unique.storage.bytestotal"] = strconv.FormatUint(total, 10) - node.Attributes["unique.storage.bytesfree"] = strconv.FormatUint(free, 10) + resp.Attributes["unique.storage.volume"] = volume + resp.Attributes["unique.storage.bytestotal"] = strconv.FormatUint(total, 10) + resp.Attributes["unique.storage.bytesfree"] = strconv.FormatUint(free, 10) - node.Resources.DiskMB = int(free / bytesPerMegabyte) + resp.Resources.DiskMB = int(free / bytesPerMegabyte) - return true, nil + return nil } diff --git a/client/fingerprint/storage_test.go b/client/fingerprint/storage_test.go index f975aec6d..b244e92e8 100644 --- a/client/fingerprint/storage_test.go +++ b/client/fingerprint/storage_test.go @@ -13,17 +13,17 @@ func TestStorageFingerprint(t *testing.T) { Attributes: make(map[string]string), } - assertFingerprintOK(t, fp, node) + response := assertFingerprintOK(t, fp, node) - assertNodeAttributeContains(t, node, "unique.storage.volume") - assertNodeAttributeContains(t, node, "unique.storage.bytestotal") - assertNodeAttributeContains(t, node, "unique.storage.bytesfree") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.volume") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytestotal") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytesfree") - total, err := strconv.ParseInt(node.Attributes["unique.storage.bytestotal"], 10, 64) + total, err := strconv.ParseInt(response.Attributes["unique.storage.bytestotal"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytestotal: %s", err) } - free, err := strconv.ParseInt(node.Attributes["unique.storage.bytesfree"], 10, 64) + free, err := strconv.ParseInt(response.Attributes["unique.storage.bytesfree"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytesfree: %s", err) } @@ -32,10 +32,10 @@ func TestStorageFingerprint(t *testing.T) { t.Fatalf("unique.storage.bytesfree %d is larger than unique.storage.bytestotal %d", free, total) } - if node.Resources == nil { + if response.Resources == nil { t.Fatalf("Node Resources was nil") } - if node.Resources.DiskMB == 0 { + if response.Resources.DiskMB == 0 { t.Errorf("Expected node.Resources.DiskMB to be non-zero") } } diff --git a/client/fingerprint/vault.go b/client/fingerprint/vault.go index a8728fc98..d1ec1a50f 100644 --- a/client/fingerprint/vault.go +++ b/client/fingerprint/vault.go @@ -7,8 +7,7 @@ import ( "strings" "time" - client "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/nomad/structs" + cstructs "github.com/hashicorp/nomad/client/structs" vapi "github.com/hashicorp/vault/api" ) @@ -29,9 +28,11 @@ func NewVaultFingerprint(logger *log.Logger) Fingerprint { return &VaultFingerprint{logger: logger, lastState: vaultUnavailable} } -func (f *VaultFingerprint) Fingerprint(config *client.Config, node *structs.Node) (bool, error) { +func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + config := req.Config + if config.VaultConfig == nil || !config.VaultConfig.IsEnabled() { - return false, nil + return nil } // Only create the client once to avoid creating too many connections to @@ -39,35 +40,32 @@ func (f *VaultFingerprint) Fingerprint(config *client.Config, node *structs.Node if f.client == nil { vaultConfig, err := config.VaultConfig.ApiConfig() if err != nil { - return false, fmt.Errorf("Failed to initialize the Vault client config: %v", err) + return fmt.Errorf("Failed to initialize the Vault client config: %v", err) } f.client, err = vapi.NewClient(vaultConfig) if err != nil { - return false, fmt.Errorf("Failed to initialize Vault client: %s", err) + return fmt.Errorf("Failed to initialize Vault client: %s", err) } } // Connect to vault and parse its information status, err := f.client.Sys().SealStatus() if err != nil { - // Clear any attributes set by a previous fingerprint. - f.clearVaultAttributes(node) - // Print a message indicating that Vault is not available anymore if f.lastState == vaultAvailable { f.logger.Printf("[INFO] fingerprint.vault: Vault is unavailable") } f.lastState = vaultUnavailable - return false, nil + return nil } - node.Attributes["vault.accessible"] = strconv.FormatBool(true) + resp.Attributes["vault.accessible"] = strconv.FormatBool(true) // We strip the Vault prefix because < 0.6.2 the version looks like: // status.Version = "Vault v0.6.1" - node.Attributes["vault.version"] = strings.TrimPrefix(status.Version, "Vault ") - node.Attributes["vault.cluster_id"] = status.ClusterID - node.Attributes["vault.cluster_name"] = status.ClusterName + resp.Attributes["vault.version"] = strings.TrimPrefix(status.Version, "Vault ") + resp.Attributes["vault.cluster_id"] = status.ClusterID + resp.Attributes["vault.cluster_name"] = status.ClusterName // If Vault was previously unavailable print a message to indicate the Agent // is available now @@ -75,14 +73,7 @@ func (f *VaultFingerprint) Fingerprint(config *client.Config, node *structs.Node f.logger.Printf("[INFO] fingerprint.vault: Vault is available") } f.lastState = vaultAvailable - return true, nil -} - -func (f *VaultFingerprint) clearVaultAttributes(n *structs.Node) { - delete(n.Attributes, "vault.accessible") - delete(n.Attributes, "vault.version") - delete(n.Attributes, "vault.cluster_id") - delete(n.Attributes, "vault.cluster_name") + return nil } func (f *VaultFingerprint) Periodic() (bool, time.Duration) { diff --git a/client/fingerprint/vault_test.go b/client/fingerprint/vault_test.go index a6835b937..70948a0e0 100644 --- a/client/fingerprint/vault_test.go +++ b/client/fingerprint/vault_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/nomad/client/config" + cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" ) @@ -17,19 +18,22 @@ func TestVaultFingerprint(t *testing.T) { Attributes: make(map[string]string), } - config := config.DefaultConfig() - config.VaultConfig = tv.Config + conf := config.DefaultConfig() + conf.VaultConfig = tv.Config - ok, err := fp.Fingerprint(config, node) + request := &cstructs.FingerprintRequest{Config: conf, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + err := fp.Fingerprint(request, response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - if !ok { - t.Fatalf("Failed to apply node attributes") - } - assertNodeAttributeContains(t, node, "vault.accessible") - assertNodeAttributeContains(t, node, "vault.version") - assertNodeAttributeContains(t, node, "vault.cluster_id") - assertNodeAttributeContains(t, node, "vault.cluster_name") + assertNodeAttributeContains(t, response.Attributes, "vault.accessible") + assertNodeAttributeContains(t, response.Attributes, "vault.version") + assertNodeAttributeContains(t, response.Attributes, "vault.cluster_id") + assertNodeAttributeContains(t, response.Attributes, "vault.cluster_name") } diff --git a/client/structs/structs.go b/client/structs/structs.go index 0673ce951..1007bfd8f 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -4,6 +4,9 @@ import ( "crypto/md5" "io" "strconv" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" ) // MemoryStats holds memory usage related stats @@ -184,3 +187,14 @@ func (d *DriverNetwork) Hash() []byte { } return h.Sum(nil) } + +type FingerprintRequest struct { + Config *config.Config + Node *structs.Node +} + +type FingerprintResponse struct { + Attributes map[string]string + Links map[string]string + Resources *structs.Resources +} From a76a404131d7b26d0b4a01d2d33155060bb15566 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 25 Jan 2018 06:02:40 -0500 Subject: [PATCH 11/79] add test case for available cgroups --- client/fingerprint/cgroup_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/client/fingerprint/cgroup_test.go b/client/fingerprint/cgroup_test.go index c7cf2f775..3f7d3ee87 100644 --- a/client/fingerprint/cgroup_test.go +++ b/client/fingerprint/cgroup_test.go @@ -121,4 +121,30 @@ func TestCGroupFingerprint(t *testing.T) { t.Fatalf("unexpected attribute found, %s", a) } } + { + f := &CGroupFingerprint{ + logger: testLogger(), + lastState: cgroupAvailable, + mountPointDetector: &MountPointDetectorValidMountPoint{}, + } + + node := &structs.Node{ + Attributes: make(map[string]string), + } + + request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} + response := &cstructs.FingerprintResponse{ + Attributes: make(map[string]string, 0), + Links: make(map[string]string, 0), + Resources: &structs.Resources{}, + } + + err := f.Fingerprint(request, response) + if err != nil { + t.Fatalf("unexpected error, %s", err) + } + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a == "" { + t.Fatalf("expected attribute to be found, %s", a) + } + } } From 2dd31f2cc7c04e0d859298e3fab41b9c4fa0e8fd Mon Sep 17 00:00:00 2001 From: Michael McCracken Date: Thu, 25 Jan 2018 13:56:14 -0800 Subject: [PATCH 12/79] fix speling in log Signed-off-by: Michael McCracken --- client/driver/lxc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/driver/lxc.go b/client/driver/lxc.go index c4a175f04..a4f34c501 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -213,7 +213,7 @@ func (d *LxcDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse, sresp, err, errCleanup := d.startWithCleanup(ctx, task) if err != nil { if cleanupErr := errCleanup(); cleanupErr != nil { - d.logger.Printf("[ERR] error occured while cleaning up from error in Start: %v", cleanupErr) + d.logger.Printf("[ERR] error occurred while cleaning up from error in Start: %v", cleanupErr) } } return sresp, err From 1223a5705e33e6946507f29764f187fb4a7c68a2 Mon Sep 17 00:00:00 2001 From: Simarpreet Singh Date: Thu, 25 Jan 2018 16:24:10 -0800 Subject: [PATCH 13/79] qemu: Fix unintentional shadowing of monitorPath variable Signed-off-by: Simarpreet Singh --- client/driver/qemu.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/driver/qemu.go b/client/driver/qemu.go index f256c829c..c8dc081ca 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -246,7 +246,7 @@ func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse } // This socket will be used to manage the virtual machine (for example, // to perform graceful shutdowns) - monitorPath, err := d.getMonitorPath(ctx.TaskDir.Dir) + monitorPath, err = d.getMonitorPath(ctx.TaskDir.Dir) if err != nil { d.logger.Printf("[ERR] driver.qemu: could not get qemu monitor path: %s", err) return nil, err @@ -464,6 +464,7 @@ func (h *qemuHandle) Kill() error { // If Nomad did not send a graceful shutdown signal, issue an interrupt to // the qemu process as a last resort if gracefulShutdownSent == false { + h.logger.Printf("[DEBUG] driver.qemu: graceful shutdown is not enabled, sending an interrupt signal to QEMU") if err := h.executor.ShutDown(); err != nil { if h.pluginClient.Exited() { return nil From 19e1a7fd61f472549ac6fd337eb8cc24d13ee968 Mon Sep 17 00:00:00 2001 From: Simarpreet Singh Date: Thu, 25 Jan 2018 16:40:16 -0800 Subject: [PATCH 14/79] qemu: Make the driver debugging output more indicative Signed-off-by: Simarpreet Singh --- client/driver/qemu.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/driver/qemu.go b/client/driver/qemu.go index c8dc081ca..3e9d6f8d9 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -464,7 +464,7 @@ func (h *qemuHandle) Kill() error { // If Nomad did not send a graceful shutdown signal, issue an interrupt to // the qemu process as a last resort if gracefulShutdownSent == false { - h.logger.Printf("[DEBUG] driver.qemu: graceful shutdown is not enabled, sending an interrupt signal to QEMU") + h.logger.Printf("[DEBUG] driver.qemu: graceful shutdown is not enabled, sending an interrupt signal to pid: %d", h.userPid) if err := h.executor.ShutDown(); err != nil { if h.pluginClient.Exited() { return nil From c21ac4633764a11fca288e765aef6ed90308123b Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 26 Jan 2018 06:51:09 -0500 Subject: [PATCH 15/79] fixups from code review --- client/client.go | 72 ++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/client/client.go b/client/client.go index 005b478f0..0c8417b3b 100644 --- a/client/client.go +++ b/client/client.go @@ -932,24 +932,28 @@ func (c *Client) fingerprint() error { c.logger.Printf("[DEBUG] client: built-in fingerprints: %v", fingerprint.BuiltinFingerprints()) - var applied []string - var skipped []string + var appliedFingerprints []string + var skippedFingerprints []string for _, name := range fingerprint.BuiltinFingerprints() { // Skip modules that are not in the whitelist if it is enabled. if _, ok := whitelist[name]; whitelistEnabled && !ok { - skipped = append(skipped, name) + skippedFingerprints = append(skippedFingerprints, name) continue } // Skip modules that are in the blacklist if _, ok := blacklist[name]; ok { - skipped = append(skipped, name) + skippedFingerprints = append(skippedFingerprints, name) continue } + f, err := fingerprint.NewFingerprint(name, c.logger) if err != nil { return err } + // Apply the finerprint to our list so that we can log it later + appliedFingerprints = append(appliedFingerprints, name) + request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} response := &cstructs.FingerprintResponse{ Attributes: make(map[string]string, 0), @@ -962,16 +966,6 @@ func (c *Client) fingerprint() error { return err } - // if an attribute should be skipped, remove it from the list which we will - // later apply to the node - for _, e := range skipped { - delete(response.Attributes, e) - } - - for name := range response.Attributes { - applied = append(applied, name) - } - // add the diff found from each fingerprinter c.updateNodeFromFingerprint(response) @@ -984,9 +978,9 @@ func (c *Client) fingerprint() error { } } - c.logger.Printf("[DEBUG] client: applied fingerprints %v", applied) - if len(skipped) != 0 { - c.logger.Printf("[DEBUG] client: fingerprint modules skipped due to white/blacklist: %v", skipped) + c.logger.Printf("[DEBUG] client: applied fingerprints %v", appliedFingerprints) + if len(skippedFingerprints) != 0 { + c.logger.Printf("[DEBUG] client: fingerprint modules skipped due to white/blacklist: %v", skippedFingerprints) } return nil } @@ -1025,22 +1019,26 @@ func (c *Client) setupDrivers() error { whitelistEnabled := len(whitelist) > 0 blacklist := c.config.ReadStringListToMap("driver.blacklist") - var avail []string - var skipped []string + var availDrivers []string + var skippedDrivers []string driverCtx := driver.NewDriverContext("", "", c.config, c.config.Node, c.logger, nil) for name := range driver.BuiltinDrivers { // Skip fingerprinting drivers that are not in the whitelist if it is // enabled. if _, ok := whitelist[name]; whitelistEnabled && !ok { - skipped = append(skipped, name) + skippedDrivers = append(skippedDrivers, name) continue } // Skip fingerprinting drivers that are in the blacklist if _, ok := blacklist[name]; ok { - skipped = append(skipped, name) + skippedDrivers = append(skippedDrivers, name) continue } + // Add this driver to our list of available drivers so that we can log it + // later + availDrivers = append(availDrivers, name) + d, err := driver.NewDriver(name, driverCtx) if err != nil { return err @@ -1053,20 +1051,10 @@ func (c *Client) setupDrivers() error { Resources: &structs.Resources{}, } - err = d.Fingerprint(request, response) - if err != nil { + if err := d.Fingerprint(request, response); err != nil { return err } - // remove attributes we are supposed to skip - for _, attr := range skipped { - delete(response.Attributes, attr) - } - - for name := range response.Attributes { - avail = append(avail, name) - } - c.updateNodeFromFingerprint(response) p, period := d.Periodic() @@ -1076,11 +1064,9 @@ func (c *Client) setupDrivers() error { } - c.logger.Printf("[DEBUG] client: available drivers %v", avail) - c.logger.Printf("[DEBUG] client: skipped attributes %v", skipped) - - if len(skipped) != 0 { - c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skipped) + c.logger.Printf("[DEBUG] client: available drivers %v", availDrivers) + if len(skippedDrivers) > 0 { + c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skippedDrivers) } return nil @@ -1092,13 +1078,21 @@ func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintRespons c.configLock.Lock() defer c.configLock.Unlock() for name, val := range response.Attributes { - c.config.Node.Attributes[name] = val + if val == "" { + delete(c.config.Node.Attributes, name) + } else { + c.config.Node.Attributes[name] = val + } } // update node links and resources from the diff created from // fingerprinting for name, val := range response.Links { - c.config.Node.Links[name] = val + if val == "" { + delete(c.config.Node.Links, name) + } else { + c.config.Node.Links[name] = val + } } c.config.Node.Resources.Merge(response.Resources) From 6c6e37364065c318706eae1419b974897ac9c266 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 26 Jan 2018 13:28:39 +0000 Subject: [PATCH 16/79] Remove `vaultError`, `failedSibling` and `messagePassthru` 1. `vaultError` and `failedSibling` are safe to remove see https://github.com/hashicorp/nomad/blob/64e9cf9/nomad/structs/structs.go#L3979-L3986 --- ui/app/models/task-event.js | 2 -- ui/mirage/factories/allocation.js | 4 ---- ui/mirage/factories/task-event.js | 7 ------- ui/mirage/factories/task-state.js | 4 +--- ui/tests/acceptance/allocation-detail-test.js | 12 +++--------- ui/tests/acceptance/task-detail-test.js | 12 +++--------- 6 files changed, 7 insertions(+), 34 deletions(-) diff --git a/ui/app/models/task-event.js b/ui/app/models/task-event.js index 10377b021..86ff9e3c7 100644 --- a/ui/app/models/task-event.js +++ b/ui/app/models/task-event.js @@ -24,7 +24,5 @@ export default Fragment.extend({ taskSignal: attr('string'), taskSignalReason: attr('string'), validationError: attr('string'), - vaultError: attr('string'), message: attr('string'), - failedSibling: attr('string'), }); diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 161f138e6..ecadc1d0e 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -18,9 +18,6 @@ export default Factory.extend({ clientStatus: faker.list.random(...CLIENT_STATUSES), desiredStatus: faker.list.random(...DESIRED_STATUSES), - // Meta property for hinting at task events - useMessagePassthru: false, - withTaskWithPorts: trait({ afterCreate(allocation, server) { const taskGroup = server.db.taskGroups.findBy({ name: allocation.taskGroup }); @@ -79,7 +76,6 @@ export default Factory.extend({ server.create('task-state', { allocation, name: server.db.tasks.find(id).name, - useMessagePassthru: allocation.useMessagePassthru, }) ); diff --git a/ui/mirage/factories/task-event.js b/ui/mirage/factories/task-event.js index e922894f4..ba64b003c 100644 --- a/ui/mirage/factories/task-event.js +++ b/ui/mirage/factories/task-event.js @@ -7,12 +7,6 @@ const STATES = provide(10, faker.system.fileExt.bind(faker.system)); export default Factory.extend({ type: faker.list.random(...STATES), - // Message is a function of type, and this type uses the vanilla - // message property. - messagePassthru: trait({ - type: 'Task Setup', - }), - signal: () => '', exitCode: () => null, time: () => faker.date.past(2 / 365, REF_TIME) * 1000000, @@ -26,6 +20,5 @@ export default Factory.extend({ setupError: () => '', taskSignalReason: () => '', validationError: () => '', - vaultError: () => '', message: () => faker.lorem.sentence(), }); diff --git a/ui/mirage/factories/task-state.js b/ui/mirage/factories/task-state.js index bbcf28a02..c0e274289 100644 --- a/ui/mirage/factories/task-state.js +++ b/ui/mirage/factories/task-state.js @@ -14,13 +14,11 @@ export default Factory.extend({ return new Date(this.startedAt + Math.random(1000 * 60 * 3) + 50); }, - useMessagePassthru: false, - afterCreate(state, server) { const props = [ 'task-event', faker.random.number({ min: 1, max: 10 }), - state.useMessagePassthru && 'messagePassthru', + false, { taskStateId: state.id, }, diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index de651ca10..a660c57a9 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -14,17 +14,13 @@ moduleForAcceptance('Acceptance | allocation detail', { node = server.create('node'); job = server.create('job', { groupCount: 0 }); - allocation = server.create('allocation', 'withTaskWithPorts', { - useMessagePassthru: true, - }); + allocation = server.create('allocation', 'withTaskWithPorts'); visit(`/allocations/${allocation.id}`); }, }); -test('/allocation/:id should name the allocation and link to the corresponding job and node', function( - assert -) { +test('/allocation/:id should name the allocation and link to the corresponding job and node', function(assert) { assert.ok( find('[data-test-title]').textContent.includes(allocation.name), 'Allocation name is in the heading' @@ -125,9 +121,7 @@ test('each task row should list high-level information for the task', function(a }); }); -test('when the allocation is not found, an error message is shown, but the URL persists', function( - assert -) { +test('when the allocation is not found, an error message is shown, but the URL persists', function(assert) { visit('/allocations/not-a-real-allocation'); andThen(() => { diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js index b0cbc65da..966ff218c 100644 --- a/ui/tests/acceptance/task-detail-test.js +++ b/ui/tests/acceptance/task-detail-test.js @@ -12,18 +12,14 @@ moduleForAcceptance('Acceptance | task detail', { server.create('agent'); server.create('node'); server.create('job', { createAllocations: false }); - allocation = server.create('allocation', 'withTaskWithPorts', { - useMessagePassthru: true, - }); + allocation = server.create('allocation', 'withTaskWithPorts'); task = server.db.taskStates.where({ allocationId: allocation.id })[0]; visit(`/allocations/${allocation.id}/${task.name}`); }, }); -test('/allocation/:id/:task_name should name the task and list high-level task information', function( - assert -) { +test('/allocation/:id/:task_name should name the task and list high-level task information', function(assert) { assert.ok(find('[data-test-title]').textContent.includes(task.name), 'Task name'); assert.ok(find('[data-test-state]').textContent.includes(task.state), 'Task state'); @@ -119,9 +115,7 @@ test('the events table lists all recent events', function(assert) { ); }); -test('each recent event should list the time, type, and description of the event', function( - assert -) { +test('each recent event should list the time, type, and description of the event', function(assert) { const event = server.db.taskEvents.where({ taskStateId: task.id })[0]; const recentEvent = findAll('[data-test-task-event]').get('lastObject'); From f5fc20a564c21396900be10bfc274c9e0333f220 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 26 Jan 2018 11:21:07 -0500 Subject: [PATCH 17/79] create safe getters and setters for fingerprint response --- client/client.go | 39 +++------ client/driver/docker.go | 10 +-- client/driver/docker_test.go | 33 +++----- client/driver/exec_linux.go | 2 +- client/driver/exec_test.go | 11 +-- client/driver/java.go | 11 +-- client/driver/java_test.go | 14 ++-- client/driver/lxc.go | 6 +- client/driver/lxc_test.go | 20 ++--- client/driver/mock_driver.go | 2 +- client/driver/qemu.go | 4 +- client/driver/qemu_test.go | 25 ++---- client/driver/raw_exec.go | 3 +- client/driver/raw_exec_test.go | 15 ++-- client/driver/rkt.go | 8 +- client/driver/rkt_test.go | 16 ++-- client/fingerprint/arch.go | 2 +- client/fingerprint/arch_test.go | 13 +-- client/fingerprint/cgroup.go | 7 -- client/fingerprint/cgroup_linux.go | 6 +- client/fingerprint/cgroup_test.go | 44 +++------- client/fingerprint/consul.go | 26 +++--- client/fingerprint/consul_test.go | 36 ++++---- client/fingerprint/cpu.go | 17 ++-- client/fingerprint/cpu_test.go | 49 +++++------ client/fingerprint/env_aws.go | 16 ++-- client/fingerprint/env_aws_test.go | 84 +++++++------------ client/fingerprint/env_gce.go | 16 ++-- client/fingerprint/env_gce_test.go | 65 +++++++-------- client/fingerprint/fingerprint_test.go | 13 +-- client/fingerprint/host.go | 10 +-- client/fingerprint/host_test.go | 13 ++- client/fingerprint/memory.go | 5 +- client/fingerprint/memory_test.go | 16 ++-- client/fingerprint/network.go | 5 +- client/fingerprint/network_test.go | 109 +++++++++++-------------- client/fingerprint/nomad.go | 4 +- client/fingerprint/nomad_test.go | 16 ++-- client/fingerprint/signal.go | 2 +- client/fingerprint/signal_test.go | 2 +- client/fingerprint/storage.go | 15 ++-- client/fingerprint/storage_test.go | 17 ++-- client/fingerprint/vault.go | 8 +- client/fingerprint/vault_test.go | 17 ++-- client/structs/structs.go | 57 ++++++++++++- 45 files changed, 388 insertions(+), 521 deletions(-) diff --git a/client/client.go b/client/client.go index 0c8417b3b..9542f734f 100644 --- a/client/client.go +++ b/client/client.go @@ -955,19 +955,14 @@ func (c *Client) fingerprint() error { appliedFingerprints = append(appliedFingerprints, name) request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err = f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err = f.Fingerprint(request, &response) if err != nil { return err } // add the diff found from each fingerprinter - c.updateNodeFromFingerprint(response) + c.updateNodeFromFingerprint(&response) p, period := f.Periodic() if p { @@ -992,18 +987,13 @@ func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d t select { case <-time.After(d): request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { c.logger.Printf("[DEBUG] client: periodic fingerprinting for %v failed: %v", name, err) } else { - c.updateNodeFromFingerprint(response) + c.updateNodeFromFingerprint(&response) } case <-c.shutdownCh: @@ -1045,17 +1035,12 @@ func (c *Client) setupDrivers() error { } request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - if err := d.Fingerprint(request, response); err != nil { + var response cstructs.FingerprintResponse + if err := d.Fingerprint(request, &response); err != nil { return err } - c.updateNodeFromFingerprint(response) + c.updateNodeFromFingerprint(&response) p, period := d.Periodic() if p { @@ -1077,7 +1062,7 @@ func (c *Client) setupDrivers() error { func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintResponse) { c.configLock.Lock() defer c.configLock.Unlock() - for name, val := range response.Attributes { + for name, val := range response.GetAttributes() { if val == "" { delete(c.config.Node.Attributes, name) } else { @@ -1087,7 +1072,7 @@ func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintRespons // update node links and resources from the diff created from // fingerprinting - for name, val := range response.Links { + for name, val := range response.GetLinks() { if val == "" { delete(c.config.Node.Links, name) } else { @@ -1095,7 +1080,7 @@ func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintRespons } } - c.config.Node.Resources.Merge(response.Resources) + c.config.Node.Resources.Merge(response.GetResources()) } // retryIntv calculates a retry interval value given the base diff --git a/client/driver/docker.go b/client/driver/docker.go index b2ef18550..be4bfd1b5 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -497,17 +497,17 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru return nil } - resp.Attributes[dockerDriverAttr] = "1" - resp.Attributes["driver.docker.version"] = env.Get("Version") + resp.AddAttribute(dockerDriverAttr, "1") + resp.AddAttribute("driver.docker.version", env.Get("Version")) privileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) if privileged { - resp.Attributes[dockerPrivilegedConfigOption] = "1" + resp.AddAttribute(dockerPrivilegedConfigOption, "1") } // Advertise if this node supports Docker volumes if d.config.ReadBoolDefault(dockerVolumesConfigOption, dockerVolumesConfigDefault) { - resp.Attributes["driver."+dockerVolumesConfigOption] = "1" + resp.AddAttribute("driver."+dockerVolumesConfigOption, "1") } // Detect bridge IP address - #2785 @@ -525,7 +525,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru } if n.IPAM.Config[0].Gateway != "" { - resp.Attributes["driver.docker.bridge_ip"] = n.IPAM.Config[0].Gateway + resp.AddAttribute("driver.docker.bridge_ip", n.IPAM.Config[0].Gateway) } else if d.fingerprintSuccess == nil { // Docker 17.09.0-ce dropped the Gateway IP from the bridge network // See https://github.com/moby/moby/issues/32648 diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index aea0a8987..844f06d79 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -173,26 +173,22 @@ func TestDockerDriver_Fingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if testutil.DockerIsConnected(t) && response.Attributes["driver.docker"] == "" { + attributes := response.GetAttributes() + if testutil.DockerIsConnected(t) && attributes["driver.docker"] == "" { t.Fatalf("Fingerprinter should detect when docker is available") } - if response.Attributes["driver.docker"] != "1" { + if attributes["driver.docker"] != "1" { t.Log("Docker daemon not available. The remainder of the docker tests will be skipped.") } - t.Logf("Found docker version %s", response.Attributes["driver.docker.version"]) + t.Logf("Found docker version %s", attributes["driver.docker.version"]) } // TestDockerDriver_Fingerprint_Bridge asserts that if Docker is running we set @@ -223,24 +219,21 @@ func TestDockerDriver_Fingerprint_Bridge(t *testing.T) { dd := NewDockerDriver(NewDriverContext("", "", conf, conf.Node, testLogger(), nil)) request := &cstructs.FingerprintRequest{Config: conf, Node: conf.Node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err = dd.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err = dd.Fingerprint(request, &response) if err != nil { t.Fatalf("error fingerprinting docker: %v", err) } - if response.Attributes["driver.docker"] == "" { + + attributes := response.GetAttributes() + if attributes["driver.docker"] == "" { t.Fatalf("expected Docker to be enabled but false was returned") } - if found := response.Attributes["driver.docker.bridge_ip"]; found != expectedAddr { + if found := attributes["driver.docker.bridge_ip"]; found != expectedAddr { t.Fatalf("expected bridge ip %q but found: %q", expectedAddr, found) } - t.Logf("docker bridge ip: %q", response.Attributes["driver.docker.bridge_ip"]) + t.Logf("docker bridge ip: %q", attributes["driver.docker.bridge_ip"]) } func TestDockerDriver_StartOpen_Wait(t *testing.T) { diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index 19ac61756..258b8a1d3 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -31,7 +31,7 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct if d.fingerprintSuccess == nil || !*d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.exec: exec driver is enabled") } - resp.Attributes[execDriverAttr] = "1" + resp.AddAttribute(execDriverAttr, "1") d.fingerprintSuccess = helper.BoolToPtr(true) return nil } diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index 92b1bebe6..ca4874dbe 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -40,17 +40,12 @@ func TestExecDriver_Fingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.exec"] == "" { + if response.GetAttributes()["driver.exec"] == "" { t.Fatalf("missing driver") } } diff --git a/client/driver/java.go b/client/driver/java.go index 5736a563d..bf125fd3c 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -117,7 +117,6 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling") } - resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) return nil } @@ -131,7 +130,6 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct err := cmd.Run() if err != nil { // assume Java wasn't found - resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) return nil } @@ -151,7 +149,6 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Println("[WARN] driver.java: error parsing Java version information, aborting") } - resp.Attributes[javaDriverAttr] = "" d.fingerprintSuccess = helper.BoolToPtr(false) return nil } @@ -165,10 +162,10 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct versionString := info[0] versionString = strings.TrimPrefix(versionString, "java version ") versionString = strings.Trim(versionString, "\"") - resp.Attributes[javaDriverAttr] = "1" - resp.Attributes["driver.java.version"] = versionString - resp.Attributes["driver.java.runtime"] = info[1] - resp.Attributes["driver.java.vm"] = info[2] + resp.AddAttribute(javaDriverAttr, "1") + resp.AddAttribute("driver.java.version", versionString) + resp.AddAttribute("driver.java.runtime", info[1]) + resp.AddAttribute("driver.java.vm", info[2]) d.fingerprintSuccess = helper.BoolToPtr(true) return nil diff --git a/client/driver/java_test.go b/client/driver/java_test.go index cd6e56447..2c3ac2b2c 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -52,18 +52,14 @@ func TestJavaDriver_Fingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.java"] != "1" && javaLocated() { + attributes := response.GetAttributes() + if attributes["driver.java"] != "1" && javaLocated() { if v, ok := osJavaDriverSupport[runtime.GOOS]; v && ok { t.Fatalf("missing java driver") } else { @@ -71,7 +67,7 @@ func TestJavaDriver_Fingerprint(t *testing.T) { } } for _, key := range []string{"driver.java.version", "driver.java.runtime", "driver.java.vm"} { - if response.Attributes[key] == "" { + if attributes[key] == "" { t.Fatalf("missing driver key (%s)", key) } } diff --git a/client/driver/lxc.go b/client/driver/lxc.go index 77cf45240..70700420c 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -194,12 +194,12 @@ func (d *LxcDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs if version == "" { return nil } - resp.Attributes["driver.lxc.version"] = version - resp.Attributes["driver.lxc"] = "1" + resp.AddAttribute("driver.lxc.version", version) + resp.AddAttribute("driver.lxc", "1") // Advertise if this node supports lxc volumes if d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) { - resp.Attributes["driver."+lxcVolumesConfigOption] = "1" + resp.AddAttribute("driver."+lxcVolumesConfigOption, "1") } return nil diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index 2da65d86c..5086097f5 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -43,13 +43,8 @@ func TestLxcDriver_Fingerprint(t *testing.T) { // test with an empty config { request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } @@ -59,17 +54,12 @@ func TestLxcDriver_Fingerprint(t *testing.T) { { conf := &config.Config{Options: map[string]string{lxcConfigOption: "1"}} request := &cstructs.FingerprintRequest{Config: conf, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.lxc"] == "" { + if response.GetAttributes()["driver.lxc"] == "" { t.Fatalf("missing driver") } } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index a4ccc637a..ad9ce1ded 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -194,7 +194,7 @@ func (m *MockDriver) Validate(map[string]interface{}) error { // Fingerprint fingerprints a node and returns if MockDriver is enabled func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { - resp.Attributes["driver.mock_driver"] = "1" + resp.AddAttribute("driver.mock_driver", "1") return nil } diff --git a/client/driver/qemu.go b/client/driver/qemu.go index ba3f96d32..a4d740ef9 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -173,8 +173,8 @@ func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } currentQemuVersion := matches[1] - resp.Attributes[qemuDriverAttr] = "1" - resp.Attributes[qemuDriverVersionAttr] = currentQemuVersion + resp.AddAttribute(qemuDriverAttr, "1") + resp.AddAttribute(qemuDriverVersionAttr, currentQemuVersion) return nil } diff --git a/client/driver/qemu_test.go b/client/driver/qemu_test.go index 523fbf415..3b0cf4e52 100644 --- a/client/driver/qemu_test.go +++ b/client/driver/qemu_test.go @@ -37,22 +37,19 @@ func TestQemuDriver_Fingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes[qemuDriverAttr] == "" { + attributes := response.GetAttributes() + + if attributes[qemuDriverAttr] == "" { t.Fatalf("Missing Qemu driver") } - if response.Attributes[qemuDriverVersionAttr] == "" { + if attributes[qemuDriverVersionAttr] == "" { t.Fatalf("Missing Qemu driver version") } } @@ -173,17 +170,13 @@ func TestQemuDriver_GracefulShutdown(t *testing.T) { d := NewQemuDriver(ctx.DriverCtx) request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: ctx.DriverCtx.node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - for name, value := range response.Attributes { + for name, value := range response.GetAttributes() { ctx.DriverCtx.node.Attributes[name] = value } diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index 0d1eedf35..9d7ba400a 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -97,11 +97,10 @@ func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstr if enabled || req.Config.DevMode { d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") - resp.Attributes[rawExecDriverAttr] = "1" + resp.AddAttribute(rawExecDriverAttr, "1") return nil } - resp.Attributes[rawExecDriverAttr] = "" return nil } diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index fa0fd7413..62021e9cc 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -36,29 +36,24 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { cfg := &config.Config{Options: map[string]string{rawExecConfigOption: "false"}} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.raw_exec"] != "" { + if response.GetAttributes()["driver.raw_exec"] != "" { t.Fatalf("driver incorrectly enabled") } // Enable raw exec. request.Config.Options[rawExecConfigOption] = "true" - err = d.Fingerprint(request, response) + err = d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.raw_exec"] != "1" { + if response.GetAttributes()["driver.raw_exec"] != "1" { t.Fatalf("driver not enabled") } } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 8f8d55e7f..789a8f6aa 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -348,13 +348,13 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs return nil } - resp.Attributes[rktDriverAttr] = "1" - resp.Attributes["driver.rkt.version"] = rktMatches[1] - resp.Attributes["driver.rkt.appc.version"] = appcMatches[1] + resp.AddAttribute(rktDriverAttr, "1") + resp.AddAttribute("driver.rkt.version", rktMatches[1]) + resp.AddAttribute("driver.rkt.appc.version", appcMatches[1]) // Advertise if this node supports rkt volumes if d.config.ReadBoolDefault(rktVolumesConfigOption, rktVolumesConfigDefault) { - resp.Attributes["driver."+rktVolumesConfigOption] = "1" + resp.AddAttribute("driver."+rktVolumesConfigOption, "1") } d.fingerprintSuccess = helper.BoolToPtr(true) return nil diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index bba0037a7..dd2f65699 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -60,24 +60,20 @@ func TestRktDriver_Fingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := d.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := d.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["driver.rkt"] != "1" { + attributes := response.GetAttributes() + if attributes["driver.rkt"] != "1" { t.Fatalf("Missing Rkt driver") } - if response.Attributes["driver.rkt.version"] == "" { + if attributes["driver.rkt.version"] == "" { t.Fatalf("Missing Rkt driver version") } - if response.Attributes["driver.rkt.appc.version"] == "" { + if attributes["driver.rkt.appc.version"] == "" { t.Fatalf("Missing appc version for the Rkt driver") } } diff --git a/client/fingerprint/arch.go b/client/fingerprint/arch.go index 59d498b73..d71c86a8d 100644 --- a/client/fingerprint/arch.go +++ b/client/fingerprint/arch.go @@ -20,6 +20,6 @@ func NewArchFingerprint(logger *log.Logger) Fingerprint { } func (f *ArchFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { - resp.Attributes["cpu.arch"] = runtime.GOARCH + resp.AddAttribute("cpu.arch", runtime.GOARCH) return nil } diff --git a/client/fingerprint/arch_test.go b/client/fingerprint/arch_test.go index 269b141db..660ff7f0c 100644 --- a/client/fingerprint/arch_test.go +++ b/client/fingerprint/arch_test.go @@ -15,18 +15,11 @@ func TestArchFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Attributes["cpu.arch"] == "" { - t.Fatalf("missing arch") - } + assertNodeAttributeContains(t, response.GetAttributes(), "cpu.arch") } diff --git a/client/fingerprint/cgroup.go b/client/fingerprint/cgroup.go index 5994e7e04..5ee5b26cc 100644 --- a/client/fingerprint/cgroup.go +++ b/client/fingerprint/cgroup.go @@ -45,13 +45,6 @@ func NewCGroupFingerprint(logger *log.Logger) Fingerprint { return f } -// clearCGroupAttributes clears any node attributes related to cgroups that might -// have been set in a previous fingerprint run. -func (f *CGroupFingerprint) clearCGroupAttributes(n map[string]string) map[string]string { - n["unique.cgroup.mountpoint"] = "" - return n -} - // Periodic determines the interval at which the periodic fingerprinter will run. func (f *CGroupFingerprint) Periodic() (bool, time.Duration) { return true, interval * time.Second diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go index fd10cf382..47beecc49 100644 --- a/client/fingerprint/cgroup_linux.go +++ b/client/fingerprint/cgroup_linux.go @@ -30,15 +30,11 @@ func FindCgroupMountpointDir() (string, error) { func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { mount, err := f.mountPointDetector.MountPoint() if err != nil { - resp.Attributes = f.clearCGroupAttributes(resp.Attributes) return fmt.Errorf("Failed to discover cgroup mount point: %s", err) } // Check if a cgroup mount point was found if mount == "" { - // Clear any attributes from the previous fingerprint. - resp.Attributes = f.clearCGroupAttributes(resp.Attributes) - if f.lastState == cgroupAvailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are unavailable") } @@ -46,7 +42,7 @@ func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * return nil } - resp.Attributes["unique.cgroup.mountpoint"] = mount + resp.AddAttribute("unique.cgroup.mountpoint", mount) if f.lastState == cgroupUnavailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are available") diff --git a/client/fingerprint/cgroup_test.go b/client/fingerprint/cgroup_test.go index 3f7d3ee87..93ee44eb9 100644 --- a/client/fingerprint/cgroup_test.go +++ b/client/fingerprint/cgroup_test.go @@ -52,18 +52,13 @@ func TestCGroupFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err == nil { t.Fatalf("expected an error") } - if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { + if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a != "" { t.Fatalf("unexpected attribute found, %s", a) } } @@ -80,17 +75,12 @@ func TestCGroupFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, ok := response.Attributes["unique.cgroup.mountpoint"]; !ok { + if a, ok := response.GetAttributes()["unique.cgroup.mountpoint"]; !ok { t.Fatalf("unable to find attribute: %s", a) } } @@ -107,17 +97,12 @@ func TestCGroupFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { + if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a != "" { t.Fatalf("unexpected attribute found, %s", a) } } @@ -133,17 +118,12 @@ func TestCGroupFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a == "" { + if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a == "" { t.Fatalf("expected attribute to be found, %s", a) } } diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index 6566cefd9..79ed813e5 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -9,7 +9,6 @@ import ( consul "github.com/hashicorp/consul/api" cstructs "github.com/hashicorp/nomad/client/structs" - "github.com/hashicorp/nomad/nomad/structs" ) const ( @@ -60,35 +59,36 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } if s, ok := info["Config"]["Server"].(bool); ok { - resp.Attributes["consul.server"] = strconv.FormatBool(s) + resp.AddAttribute("consul.server", strconv.FormatBool(s)) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.server") } if v, ok := info["Config"]["Version"].(string); ok { - resp.Attributes["consul.version"] = v + resp.AddAttribute("consul.version", v) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.version") } if r, ok := info["Config"]["Revision"].(string); ok { - resp.Attributes["consul.revision"] = r + resp.AddAttribute("consul.revision", r) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.revision") } if n, ok := info["Config"]["NodeName"].(string); ok { - resp.Attributes["unique.consul.name"] = n + resp.AddAttribute("unique.consul.name", n) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint unique.consul.name") } if d, ok := info["Config"]["Datacenter"].(string); ok { - resp.Attributes["consul.datacenter"] = d + resp.AddAttribute("consul.datacenter", d) } else { f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.datacenter") } - if resp.Attributes["consul.datacenter"] != "" || resp.Attributes["unique.consul.name"] != "" { - resp.Links["consul"] = fmt.Sprintf("%s.%s", - resp.Attributes["consul.datacenter"], - resp.Attributes["unique.consul.name"]) + attributes := resp.GetAttributes() + if attributes["consul.datacenter"] != "" || attributes["unique.consul.name"] != "" { + resp.AddLink("consul", fmt.Sprintf("%s.%s", + attributes["consul.datacenter"], + attributes["unique.consul.name"])) } else { f.logger.Printf("[WARN] fingerprint.consul: malformed Consul response prevented linking") } @@ -102,12 +102,6 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * return nil } -// clearConsulAttributes removes consul attributes and links from the passed -// Node. -func (f *ConsulFingerprint) clearConsulAttributes(n *structs.Node) { - delete(n.Links, "consul") -} - func (f *ConsulFingerprint) Periodic() (bool, time.Duration) { return true, 15 * time.Second } diff --git a/client/fingerprint/consul_test.go b/client/fingerprint/consul_test.go index 5240b782b..51df918b6 100644 --- a/client/fingerprint/consul_test.go +++ b/client/fingerprint/consul_test.go @@ -29,24 +29,21 @@ func TestConsulFingerprint(t *testing.T) { conf.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") request := &cstructs.FingerprintRequest{Config: conf, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := fp.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := fp.Fingerprint(request, &response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - assertNodeAttributeContains(t, response.Attributes, "consul.server") - assertNodeAttributeContains(t, response.Attributes, "consul.version") - assertNodeAttributeContains(t, response.Attributes, "consul.revision") - assertNodeAttributeContains(t, response.Attributes, "unique.consul.name") - assertNodeAttributeContains(t, response.Attributes, "consul.datacenter") + attributes := response.GetAttributes() + assertNodeAttributeContains(t, attributes, "consul.server") + assertNodeAttributeContains(t, attributes, "consul.version") + assertNodeAttributeContains(t, attributes, "consul.revision") + assertNodeAttributeContains(t, attributes, "unique.consul.name") + assertNodeAttributeContains(t, attributes, "consul.datacenter") - if _, ok := response.Links["consul"]; !ok { + links := response.GetLinks() + if _, ok := links["consul"]; !ok { t.Errorf("Expected a link to consul, none found") } } @@ -186,13 +183,8 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { conf.ConsulConfig.Addr = strings.TrimPrefix(ts.URL, "http://") request := &cstructs.FingerprintRequest{Config: conf, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := fp.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := fp.Fingerprint(request, &response) assert.Nil(err) attrs := []string{ @@ -202,8 +194,10 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { "unique.consul.name", "consul.datacenter", } + + returnedAttrs := response.GetAttributes() for _, attr := range attrs { - if v, ok := response.Attributes[attr]; ok { + if v, ok := returnedAttrs[attr]; ok { t.Errorf("unexpected node attribute %q with vlaue %q", attr, v) } } diff --git a/client/fingerprint/cpu.go b/client/fingerprint/cpu.go index 69dad8956..23809fc9e 100644 --- a/client/fingerprint/cpu.go +++ b/client/fingerprint/cpu.go @@ -22,8 +22,9 @@ func NewCPUFingerprint(logger *log.Logger) Fingerprint { func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { cfg := req.Config - setResources := func(totalCompute int) { - resp.Resources.CPU = totalCompute + setResourcesCPU := func(totalCompute int) { + resources := resp.GetResources() + resources.CPU = totalCompute } if err := stats.Init(); err != nil { @@ -31,21 +32,21 @@ func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cst } if cfg.CpuCompute != 0 { - setResources(cfg.CpuCompute) + setResourcesCPU(cfg.CpuCompute) return nil } if modelName := stats.CPUModelName(); modelName != "" { - resp.Attributes["cpu.modelname"] = modelName + resp.AddAttribute("cpu.modelname", modelName) } if mhz := stats.CPUMHzPerCore(); mhz > 0 { - resp.Attributes["cpu.frequency"] = fmt.Sprintf("%.0f", mhz) + resp.AddAttribute("cpu.frequency", fmt.Sprintf("%.0f", mhz)) f.logger.Printf("[DEBUG] fingerprint.cpu: frequency: %.0f MHz", mhz) } if numCores := stats.CPUNumCores(); numCores > 0 { - resp.Attributes["cpu.numcores"] = fmt.Sprintf("%d", numCores) + resp.AddAttribute("cpu.numcores", fmt.Sprintf("%d", numCores)) f.logger.Printf("[DEBUG] fingerprint.cpu: core count: %d", numCores) } @@ -63,8 +64,8 @@ func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cst "cpu_total_compute") } - resp.Attributes["cpu.totalcompute"] = fmt.Sprintf("%d", tt) - resp.Resources.CPU = tt + resp.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", tt)) + setResourcesCPU(tt) return nil } diff --git a/client/fingerprint/cpu_test.go b/client/fingerprint/cpu_test.go index e227d3c90..1081bb987 100644 --- a/client/fingerprint/cpu_test.go +++ b/client/fingerprint/cpu_test.go @@ -15,36 +15,32 @@ func TestCPUFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } // CPU info - if response.Attributes["cpu.numcores"] == "" { + attributes := response.GetAttributes() + if attributes["cpu.numcores"] == "" { t.Fatalf("Missing Num Cores") } - if response.Attributes["cpu.modelname"] == "" { + if attributes["cpu.modelname"] == "" { t.Fatalf("Missing Model Name") } - if response.Attributes["cpu.frequency"] == "" { + if attributes["cpu.frequency"] == "" { t.Fatalf("Missing CPU Frequency") } - if response.Attributes["cpu.totalcompute"] == "" { + if attributes["cpu.totalcompute"] == "" { t.Fatalf("Missing CPU Total Compute") } - if response.Resources == nil || response.Resources.CPU == 0 { + resources := response.GetResources() + if resources.CPU == 0 { t.Fatalf("Expected to find CPU Resources") } - } // TestCPUFingerprint_OverrideCompute asserts that setting cpu_total_compute in @@ -59,20 +55,18 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { { request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Resources.CPU == 0 { + + resources := response.GetResources() + if resources.CPU == 0 { t.Fatalf("expected fingerprint of cpu of but found 0") } - originalCPU = response.Resources.CPU + originalCPU = resources.CPU } { @@ -81,18 +75,15 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { // Make sure the Fingerprinter applies the override to the node resources request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if response.Resources.CPU != cfg.CpuCompute { - t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, response.Resources.CPU) + resources := response.GetResources() + if resources.CPU != cfg.CpuCompute { + t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, resources.CPU) } } } diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index 5d94c9998..b994d67eb 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -131,12 +131,12 @@ func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, re key = structs.UniqueNamespace(key) } - response.Attributes[key] = strings.Trim(string(resp), "\n") + response.AddAttribute(key, strings.Trim(string(resp), "\n")) } // copy over network specific information - if val := response.Attributes["unique.platform.aws.local-ipv4"]; val != "" { - response.Attributes["unique.network.ip-address"] = val + if val := response.GetAttributes()["unique.platform.aws.local-ipv4"]; val != "" { + response.AddAttribute("unique.network.ip-address", val) newNetwork.IP = val newNetwork.CIDR = newNetwork.IP + "/32" } @@ -165,12 +165,14 @@ func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, re } newNetwork.MBits = throughput - response.Resources.Networks = []*structs.NetworkResource{newNetwork} + res := response.GetResources() + res.Networks = []*structs.NetworkResource{newNetwork} // populate Links - response.Links["aws.ec2"] = fmt.Sprintf("%s.%s", - response.Attributes["platform.aws.placement.availability-zone"], - response.Attributes["unique.platform.aws.instance-id"]) + attributes := response.GetAttributes() + response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", + attributes["platform.aws.placement.availability-zone"], + attributes["unique.platform.aws.instance-id"])) return nil } diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index 1e60516b5..1bf5d28e8 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -21,18 +21,13 @@ func TestEnvAWSFingerprint_nonAws(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) > 0 { + if len(response.GetAttributes()) > 0 { t.Fatalf("Should not apply") } } @@ -60,13 +55,8 @@ func TestEnvAWSFingerprint_aws(t *testing.T) { os.Setenv("AWS_ENV_URL", ts.URL+"/latest/meta-data/") request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } @@ -85,16 +75,16 @@ func TestEnvAWSFingerprint_aws(t *testing.T) { } for _, k := range keys { - assertNodeAttributeContains(t, response.Attributes, k) + assertNodeAttributeContains(t, response.GetAttributes(), k) } - if len(response.Links) == 0 { + if len(response.GetLinks()) == 0 { t.Fatalf("Empty links for Node in AWS Fingerprint test") } // confirm we have at least instance-id and ami-id for _, k := range []string{"aws.ec2"} { - assertNodeLinksContains(t, response.Links, k) + assertNodeLinksContains(t, response.GetLinks(), k) } } @@ -183,25 +173,21 @@ func TestNetworkFingerprint_AWS(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") - if response.Resources == nil || len(response.Resources.Networks) == 0 { + res := response.GetResources() + if len(res.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := res.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -238,25 +224,21 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") - if response.Resources == nil || len(response.Resources.Networks) == 0 { + res := response.GetResources() + if len(res.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := res.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -282,24 +264,21 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") - if response.Resources == nil || len(response.Resources.Networks) == 0 { + res := response.GetResources() + if len(res.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := res.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -323,18 +302,13 @@ func TestNetworkFingerprint_notAWS(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) > 0 { + if len(response.GetAttributes()) > 0 { t.Fatalf("Should not apply") } } diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 132ddeba0..8352a63d4 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -165,7 +165,7 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * if unique { key = structs.UniqueNamespace(key) } - resp.Attributes[key] = strings.Trim(value, "\n") + resp.AddAttribute(key, strings.Trim(value, "\n")) } // These keys need everything before the final slash removed to be usable. @@ -183,7 +183,7 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * if unique { key = structs.UniqueNamespace(key) } - resp.Attributes[key] = strings.Trim(lastToken(value), "\n") + resp.AddAttribute(key, strings.Trim(lastToken(value), "\n")) } // Get internal and external IPs (if they exist) @@ -200,10 +200,10 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * for _, intf := range interfaces { prefix := "platform.gce.network." + lastToken(intf.Network) uniquePrefix := "unique." + prefix - resp.Attributes[prefix] = "true" - resp.Attributes[uniquePrefix+".ip"] = strings.Trim(intf.Ip, "\n") + resp.AddAttribute(prefix, "true") + resp.AddAttribute(uniquePrefix+".ip", strings.Trim(intf.Ip, "\n")) for index, accessConfig := range intf.AccessConfigs { - resp.Attributes[uniquePrefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp + resp.AddAttribute(uniquePrefix+".external-ip."+strconv.Itoa(index), accessConfig.ExternalIp) } } } @@ -229,7 +229,7 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * key = fmt.Sprintf("%s%s", attr, tag) } - resp.Attributes[key] = "true" + resp.AddAttribute(key, "true") } var attrDict map[string]string @@ -253,11 +253,11 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * key = fmt.Sprintf("%s%s", attr, k) } - resp.Attributes[key] = strings.Trim(v, "\n") + resp.AddAttribute(key, strings.Trim(v, "\n")) } // populate Links - resp.Links["gce"] = resp.Attributes["unique.platform.gce.id"] + resp.AddLink("gce", resp.GetAttributes()["unique.platform.gce.id"]) return nil } diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 4c1febd95..49d693b72 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -21,18 +21,13 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) > 0 { + if len(response.GetAttributes()) > 0 { t.Fatalf("Should have zero attributes without test server") } } @@ -85,13 +80,8 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { f := NewEnvGCEFingerprint(testLogger()) request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } @@ -110,41 +100,44 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { "unique.platform.gce.attr.bar", } + attributes := response.GetAttributes() + links := response.GetLinks() + for _, k := range keys { - assertNodeAttributeContains(t, response.Attributes, k) + assertNodeAttributeContains(t, attributes, k) } - if len(response.Links) == 0 { + if len(links) == 0 { t.Fatalf("Empty links for Node in GCE Fingerprint test") } // Make sure Links contains the GCE ID. for _, k := range []string{"gce"} { - assertNodeLinksContains(t, response.Links, k) + assertNodeLinksContains(t, links, k) } - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.id", "12345") - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.hostname", "instance-1.c.project.internal") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.zone", "us-central1-f") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.machine-type", "n1-standard-1") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.network.default", "true") - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.ip", "10.240.0.5") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.id", "12345") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.hostname", "instance-1.c.project.internal") + assertNodeAttributeEquals(t, attributes, "platform.gce.zone", "us-central1-f") + assertNodeAttributeEquals(t, attributes, "platform.gce.machine-type", "n1-standard-1") + assertNodeAttributeEquals(t, attributes, "platform.gce.network.default", "true") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") - } else if _, ok := response.Attributes["unique.platform.gce.network.default.external-ip.0"]; ok { + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") + } else if _, ok := attributes["unique.platform.gce.network.default.external-ip.0"]; ok { t.Fatal("unique.platform.gce.network.default.external-ip is set without an external IP") } - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.automatic-restart", "TRUE") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.cpu-platform", "Intel Ivy Bridge") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.abc", "true") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.def", "true") - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.tag.foo", "true") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.ghi", "111") - assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.jkl", "222") - assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.attr.bar", "333") + assertNodeAttributeEquals(t, attributes, "platform.gce.scheduling.automatic-restart", "TRUE") + assertNodeAttributeEquals(t, attributes, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") + assertNodeAttributeEquals(t, attributes, "platform.gce.cpu-platform", "Intel Ivy Bridge") + assertNodeAttributeEquals(t, attributes, "platform.gce.tag.abc", "true") + assertNodeAttributeEquals(t, attributes, "platform.gce.tag.def", "true") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.tag.foo", "true") + assertNodeAttributeEquals(t, attributes, "platform.gce.attr.ghi", "111") + assertNodeAttributeEquals(t, attributes, "platform.gce.attr.jkl", "222") + assertNodeAttributeEquals(t, attributes, "unique.platform.gce.attr.bar", "333") } const GCE_routes = ` diff --git a/client/fingerprint/fingerprint_test.go b/client/fingerprint/fingerprint_test.go index ba6dae096..9e3dab544 100644 --- a/client/fingerprint/fingerprint_test.go +++ b/client/fingerprint/fingerprint_test.go @@ -18,22 +18,17 @@ func testLogger() *log.Logger { func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) *cstructs.FingerprintResponse { request := &cstructs.FingerprintRequest{Config: new(config.Config), Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := fp.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := fp.Fingerprint(request, &response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - if len(response.Attributes) == 0 { + if len(response.GetAttributes()) == 0 { t.Fatalf("Failed to apply node attributes") } - return response + return &response } func assertNodeAttributeContains(t *testing.T, nodeAttributes map[string]string, attribute string) { diff --git a/client/fingerprint/host.go b/client/fingerprint/host.go index 348846b3b..1273e89f8 100644 --- a/client/fingerprint/host.go +++ b/client/fingerprint/host.go @@ -27,13 +27,13 @@ func (f *HostFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cs return err } - resp.Attributes["os.name"] = hostInfo.Platform - resp.Attributes["os.version"] = hostInfo.PlatformVersion + resp.AddAttribute("os.name", hostInfo.Platform) + resp.AddAttribute("os.version", hostInfo.PlatformVersion) - resp.Attributes["kernel.name"] = runtime.GOOS - resp.Attributes["kernel.version"] = hostInfo.KernelVersion + resp.AddAttribute("kernel.name", runtime.GOOS) + resp.AddAttribute("kernel.version", hostInfo.KernelVersion) - resp.Attributes["unique.hostname"] = hostInfo.Hostname + resp.AddAttribute("unique.hostname", hostInfo.Hostname) return nil } diff --git a/client/fingerprint/host_test.go b/client/fingerprint/host_test.go index e0ef313ab..433f40c6a 100644 --- a/client/fingerprint/host_test.go +++ b/client/fingerprint/host_test.go @@ -15,22 +15,19 @@ func TestHostFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) == 0 { + attributes := response.GetAttributes() + if len(attributes) == 0 { t.Fatalf("should generate a diff of node attributes") } // Host info for _, key := range []string{"os.name", "os.version", "unique.hostname", "kernel.name"} { - assertNodeAttributeContains(t, response.Attributes, key) + assertNodeAttributeContains(t, attributes, key) } } diff --git a/client/fingerprint/memory.go b/client/fingerprint/memory.go index b95a84d7d..473b624f4 100644 --- a/client/fingerprint/memory.go +++ b/client/fingerprint/memory.go @@ -30,9 +30,10 @@ func (f *MemoryFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } if memInfo.Total > 0 { - resp.Attributes["memory.totalbytes"] = fmt.Sprintf("%d", memInfo.Total) + resp.AddAttribute("memory.totalbytes", fmt.Sprintf("%d", memInfo.Total)) - resp.Resources.MemoryMB = int(memInfo.Total / 1024 / 1024) + res := resp.GetResources() + res.MemoryMB = int(memInfo.Total / 1024 / 1024) } return nil diff --git a/client/fingerprint/memory_test.go b/client/fingerprint/memory_test.go index 3d6b3eaed..190c6f126 100644 --- a/client/fingerprint/memory_test.go +++ b/client/fingerprint/memory_test.go @@ -15,22 +15,16 @@ func TestMemoryFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: &config.Config{}, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.Attributes, "memory.totalbytes") + assertNodeAttributeContains(t, response.GetAttributes(), "memory.totalbytes") - if response.Resources == nil { - t.Fatalf("Node Resources was nil") - } - if response.Resources.MemoryMB == 0 { + res := response.GetResources() + if res.MemoryMB == 0 { t.Errorf("Expected node.Resources.MemoryMB to be non-zero") } diff --git a/client/fingerprint/network.go b/client/fingerprint/network.go index 58cfb1b4c..d9c592256 100644 --- a/client/fingerprint/network.go +++ b/client/fingerprint/network.go @@ -95,14 +95,15 @@ func (f *NetworkFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp return err } - resp.Resources.Networks = nwResources + res := resp.GetResources() + res.Networks = nwResources for _, nwResource := range nwResources { f.logger.Printf("[DEBUG] fingerprint.network: Detected interface %v with IP: %v", intf.Name, nwResource.IP) } // Deprecated, setting the first IP as unique IP for the node if len(nwResources) > 0 { - resp.Attributes["unique.network.ip-address"] = nwResources[0].IP + resp.AddAttribute("unique.network.ip-address", nwResources[0].IP) } return nil diff --git a/client/fingerprint/network_test.go b/client/fingerprint/network_test.go index 8d556fcb2..eff2e119a 100644 --- a/client/fingerprint/network_test.go +++ b/client/fingerprint/network_test.go @@ -191,34 +191,32 @@ func TestNetworkFingerprint_basic(t *testing.T) { cfg := &config.Config{NetworkSpeed: 101} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) == 0 { + + attributes := response.GetAttributes() + if len(attributes) == 0 { t.Fatalf("should apply (HINT: working offline? Set env %q=y", skipOnlineTestsEnvVar) } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, attributes, "unique.network.ip-address") - ip := response.Attributes["unique.network.ip-address"] + ip := attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if response.Resources == nil || len(response.Resources.Networks) == 0 { + resources := response.GetResources() + if resources == nil || len(resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -241,19 +239,14 @@ func TestNetworkFingerprint_default_device_absent(t *testing.T) { cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth0"} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err == nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) != 0 { - t.Fatalf("attributes should be zero but instead are: %v", response.Attributes) + if len(response.GetAttributes()) != 0 { + t.Fatalf("attributes should be zero but instead are: %v", response.GetAttributes()) } } @@ -265,33 +258,32 @@ func TestNetworkFingerPrint_default_device(t *testing.T) { cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "lo"} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) == 0 { + + attributes := response.GetAttributes() + if len(attributes) == 0 { t.Fatalf("should apply") } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, attributes, "unique.network.ip-address") - ip := response.Attributes["unique.network.ip-address"] + ip := attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if response.Resources == nil || len(response.Resources.Networks) == 0 { + resources := response.GetResources() + if len(resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -314,30 +306,28 @@ func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth3"} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + attributes := response.GetAttributes() + assertNodeAttributeContains(t, attributes, "unique.network.ip-address") - ip := response.Attributes["unique.network.ip-address"] + ip := attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if response.Resources == nil || len(response.Resources.Networks) == 0 { + resources := response.GetResources() + if resources == nil || len(resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -360,33 +350,32 @@ func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { cfg := &config.Config{NetworkSpeed: 100, NetworkInterface: "eth4"} request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) == 0 { - t.Fatalf("should apply") + + attributes := response.GetAttributes() + if len(attributes) == 0 { + t.Fatalf("should apply attributes") } - assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + assertNodeAttributeContains(t, attributes, "unique.network.ip-address") - ip := response.Attributes["unique.network.ip-address"] + ip := attributes["unique.network.ip-address"] match := net.ParseIP(ip) if match == nil { t.Fatalf("Bad IP match: %s", ip) } - if response.Resources == nil || len(response.Resources.Networks) == 0 { + resources := response.GetResources() + if resources == nil || len(resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := response.Resources.Networks[0] + net := resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -418,16 +407,12 @@ func TestNetworkFingerPrint_LinkLocal_Disallowed(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: cfg, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) != 0 { - t.Fatalf("should not apply") + if len(response.GetAttributes()) != 0 { + t.Fatalf("should not apply attributes") } } diff --git a/client/fingerprint/nomad.go b/client/fingerprint/nomad.go index 21849dc98..32c5913bf 100644 --- a/client/fingerprint/nomad.go +++ b/client/fingerprint/nomad.go @@ -19,7 +19,7 @@ func NewNomadFingerprint(logger *log.Logger) Fingerprint { } func (f *NomadFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { - resp.Attributes["nomad.version"] = req.Config.Version.VersionNumber() - resp.Attributes["nomad.revision"] = req.Config.Version.Revision + resp.AddAttribute("nomad.version", req.Config.Version.VersionNumber()) + resp.AddAttribute("nomad.revision", req.Config.Version.Revision) return nil } diff --git a/client/fingerprint/nomad_test.go b/client/fingerprint/nomad_test.go index aac47a944..3daa97d47 100644 --- a/client/fingerprint/nomad_test.go +++ b/client/fingerprint/nomad_test.go @@ -24,26 +24,22 @@ func TestNomadFingerprint(t *testing.T) { } request := &cstructs.FingerprintRequest{Config: c, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - - err := f.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := f.Fingerprint(request, &response) if err != nil { t.Fatalf("err: %v", err) } - if len(response.Attributes) == 0 { + attributes := response.GetAttributes() + if len(attributes) == 0 { t.Fatalf("should apply") } - if response.Attributes["nomad.version"] != v { + if attributes["nomad.version"] != v { t.Fatalf("incorrect version") } - if response.Attributes["nomad.revision"] != r { + if attributes["nomad.revision"] != r { t.Fatalf("incorrect revision") } } diff --git a/client/fingerprint/signal.go b/client/fingerprint/signal.go index d87e91447..dbf0669f2 100644 --- a/client/fingerprint/signal.go +++ b/client/fingerprint/signal.go @@ -27,6 +27,6 @@ func (f *SignalFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * sigs = append(sigs, signal) } - resp.Attributes["os.signals"] = strings.Join(sigs, ",") + resp.AddAttribute("os.signals", strings.Join(sigs, ",")) return nil } diff --git a/client/fingerprint/signal_test.go b/client/fingerprint/signal_test.go index bf61f7544..6d05456f7 100644 --- a/client/fingerprint/signal_test.go +++ b/client/fingerprint/signal_test.go @@ -13,5 +13,5 @@ func TestSignalFingerprint(t *testing.T) { } response := assertFingerprintOK(t, fp, node) - assertNodeAttributeContains(t, response.Attributes, "os.signals") + assertNodeAttributeContains(t, response.GetAttributes(), "os.signals") } diff --git a/client/fingerprint/storage.go b/client/fingerprint/storage.go index 34d9e1202..963325316 100644 --- a/client/fingerprint/storage.go +++ b/client/fingerprint/storage.go @@ -26,11 +26,6 @@ func NewStorageFingerprint(logger *log.Logger) Fingerprint { func (f *StorageFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { cfg := req.Config - // Initialize these to empty defaults - resp.Attributes["unique.storage.volume"] = "" - resp.Attributes["unique.storage.bytestotal"] = "" - resp.Attributes["unique.storage.bytesfree"] = "" - // Guard against unset AllocDir storageDir := cfg.AllocDir if storageDir == "" { @@ -46,11 +41,13 @@ func (f *StorageFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp return fmt.Errorf("failed to determine disk space for %s: %v", storageDir, err) } - resp.Attributes["unique.storage.volume"] = volume - resp.Attributes["unique.storage.bytestotal"] = strconv.FormatUint(total, 10) - resp.Attributes["unique.storage.bytesfree"] = strconv.FormatUint(free, 10) + resp.AddAttribute("unique.storage.volume", volume) + resp.AddAttribute("unique.storage.bytestotal", strconv.FormatUint(total, 10)) + resp.AddAttribute("unique.storage.bytesfree", strconv.FormatUint(free, 10)) - resp.Resources.DiskMB = int(free / bytesPerMegabyte) + // set the disk size for the response + res := resp.GetResources() + res.DiskMB = int(free / bytesPerMegabyte) return nil } diff --git a/client/fingerprint/storage_test.go b/client/fingerprint/storage_test.go index b244e92e8..8d26a6bcb 100644 --- a/client/fingerprint/storage_test.go +++ b/client/fingerprint/storage_test.go @@ -15,15 +15,17 @@ func TestStorageFingerprint(t *testing.T) { response := assertFingerprintOK(t, fp, node) - assertNodeAttributeContains(t, response.Attributes, "unique.storage.volume") - assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytestotal") - assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytesfree") + attributes := response.GetAttributes() - total, err := strconv.ParseInt(response.Attributes["unique.storage.bytestotal"], 10, 64) + assertNodeAttributeContains(t, attributes, "unique.storage.volume") + assertNodeAttributeContains(t, attributes, "unique.storage.bytestotal") + assertNodeAttributeContains(t, attributes, "unique.storage.bytesfree") + + total, err := strconv.ParseInt(attributes["unique.storage.bytestotal"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytestotal: %s", err) } - free, err := strconv.ParseInt(response.Attributes["unique.storage.bytesfree"], 10, 64) + free, err := strconv.ParseInt(attributes["unique.storage.bytesfree"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytesfree: %s", err) } @@ -32,10 +34,11 @@ func TestStorageFingerprint(t *testing.T) { t.Fatalf("unique.storage.bytesfree %d is larger than unique.storage.bytestotal %d", free, total) } - if response.Resources == nil { + resources := response.GetResources() + if resources == nil { t.Fatalf("Node Resources was nil") } - if response.Resources.DiskMB == 0 { + if resources.DiskMB == 0 { t.Errorf("Expected node.Resources.DiskMB to be non-zero") } } diff --git a/client/fingerprint/vault.go b/client/fingerprint/vault.go index d1ec1a50f..612302365 100644 --- a/client/fingerprint/vault.go +++ b/client/fingerprint/vault.go @@ -60,12 +60,12 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c return nil } - resp.Attributes["vault.accessible"] = strconv.FormatBool(true) + resp.AddAttribute("vault.accessible", strconv.FormatBool(true)) // We strip the Vault prefix because < 0.6.2 the version looks like: // status.Version = "Vault v0.6.1" - resp.Attributes["vault.version"] = strings.TrimPrefix(status.Version, "Vault ") - resp.Attributes["vault.cluster_id"] = status.ClusterID - resp.Attributes["vault.cluster_name"] = status.ClusterName + resp.AddAttribute("vault.version", strings.TrimPrefix(status.Version, "Vault ")) + resp.AddAttribute("vault.cluster_id", status.ClusterID) + resp.AddAttribute("vault.cluster_name", status.ClusterName) // If Vault was previously unavailable print a message to indicate the Agent // is available now diff --git a/client/fingerprint/vault_test.go b/client/fingerprint/vault_test.go index 70948a0e0..f036e280e 100644 --- a/client/fingerprint/vault_test.go +++ b/client/fingerprint/vault_test.go @@ -22,18 +22,15 @@ func TestVaultFingerprint(t *testing.T) { conf.VaultConfig = tv.Config request := &cstructs.FingerprintRequest{Config: conf, Node: node} - response := &cstructs.FingerprintResponse{ - Attributes: make(map[string]string, 0), - Links: make(map[string]string, 0), - Resources: &structs.Resources{}, - } - err := fp.Fingerprint(request, response) + var response cstructs.FingerprintResponse + err := fp.Fingerprint(request, &response) if err != nil { t.Fatalf("Failed to fingerprint: %s", err) } - assertNodeAttributeContains(t, response.Attributes, "vault.accessible") - assertNodeAttributeContains(t, response.Attributes, "vault.version") - assertNodeAttributeContains(t, response.Attributes, "vault.cluster_id") - assertNodeAttributeContains(t, response.Attributes, "vault.cluster_name") + attributes := response.GetAttributes() + assertNodeAttributeContains(t, attributes, "vault.accessible") + assertNodeAttributeContains(t, attributes, "vault.version") + assertNodeAttributeContains(t, attributes, "vault.cluster_id") + assertNodeAttributeContains(t, attributes, "vault.cluster_name") } diff --git a/client/structs/structs.go b/client/structs/structs.go index 1007bfd8f..7c9ea1479 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -194,7 +194,58 @@ type FingerprintRequest struct { } type FingerprintResponse struct { - Attributes map[string]string - Links map[string]string - Resources *structs.Resources + attributes map[string]string + links map[string]string + resources *structs.Resources +} + +// AddAttribute adds the name and value for a node attribute to the fingerprint +// response +func (f *FingerprintResponse) AddAttribute(name, value string) { + // initialize attributes if it has not been already + if f.attributes == nil { + f.attributes = make(map[string]string, 0) + } + + f.attributes[name] = value +} + +// GetAttributes fetches the attributes for the fingerprint response +func (f *FingerprintResponse) GetAttributes() map[string]string { + // initialize attributes if it has not been already + if f.attributes == nil { + f.attributes = make(map[string]string, 0) + } + + return f.attributes +} + +// AddLink adds a link entry to the fingerprint response +func (f *FingerprintResponse) AddLink(name, value string) { + // initialize links if it has not been already + if f.links == nil { + f.links = make(map[string]string, 0) + } + + f.links[name] = value +} + +// GetLinks returns the links for the fingerprint response +func (f *FingerprintResponse) GetLinks() map[string]string { + // initialize links if it has not been already + if f.links == nil { + f.links = make(map[string]string, 0) + } + + return f.links +} + +// GetResources returns the resources for a fingerprint response +func (f *FingerprintResponse) GetResources() *structs.Resources { + // initialize resourcesif it has not been already + if f.resources == nil { + f.resources = &structs.Resources{} + } + + return f.resources } From ae889b4fbab01a5696d547926ea46332a3a48834 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Fri, 26 Jan 2018 14:31:37 -0500 Subject: [PATCH 18/79] remove attributes from periodic fingerprints when state changes write test for client periodic fingerprinters --- client/client_test.go | 41 ++++++++++++++++++++++++++ client/driver/docker.go | 2 ++ client/driver/exec_linux.go | 2 ++ client/driver/java.go | 3 ++ client/driver/mock_driver.go | 46 ++++++++++++++++++++++++++++-- client/driver/qemu.go | 3 ++ client/driver/raw_exec.go | 1 + client/driver/rkt.go | 3 ++ client/fingerprint/cgroup.go | 8 ++++++ client/fingerprint/cgroup_linux.go | 4 +++ client/fingerprint/consul.go | 13 ++++++++- client/fingerprint/fingerprint.go | 10 +++++++ client/fingerprint/vault.go | 8 ++++++ client/structs/structs.go | 22 ++++++++++++++ 14 files changed, 162 insertions(+), 4 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 95ff480d3..02226ac4f 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/consul/lib/freeport" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver" "github.com/hashicorp/nomad/client/fingerprint" "github.com/hashicorp/nomad/command/agent/consul" "github.com/hashicorp/nomad/helper" @@ -252,6 +253,46 @@ func TestClient_HasNodeChanged(t *testing.T) { } } +func TestClient_Fingerprint_Periodic(t *testing.T) { + if _, ok := driver.BuiltinDrivers["mock_driver"]; !ok { + t.Skip(`test requires mock_driver; run with "-tags nomad_test"`) + } + t.Parallel() + + c1 := testClient(t, func(c *config.Config) { + c.Options = map[string]string{ + "test.shutdown_periodic_after": "true", + "test.shutdown_periodic_duration": "3", + } + }) + defer c1.Shutdown() + + node := c1.config.Node + mockDriverName := "driver.mock_driver" + + // Ensure the mock driver is registered on the client + testutil.WaitForResult(func() (bool, error) { + mockDriverStatus := node.Attributes[mockDriverName] + if mockDriverStatus == "" { + return false, fmt.Errorf("mock driver attribute should be set on the client") + } + return true, nil + }, func(err error) { + t.Fatalf("err: %v", err) + }) + + // Ensure that the client fingerprinter eventually removes this attribute + testutil.WaitForResult(func() (bool, error) { + mockDriverStatus := node.Attributes[mockDriverName] + if mockDriverStatus != "" { + return false, fmt.Errorf("mock driver attribute should not be set on the client") + } + return true, nil + }, func(err error) { + t.Fatalf("err: %v", err) + }) +} + func TestClient_Fingerprint_InWhitelist(t *testing.T) { t.Parallel() c := testClient(t, func(c *config.Config) { diff --git a/client/driver/docker.go b/client/driver/docker.go index be4bfd1b5..7fa709d2e 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -482,6 +482,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru d.logger.Printf("[INFO] driver.docker: failed to initialize client: %s", err) } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(dockerDriverAttr) return nil } @@ -494,6 +495,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon at %s: %s", client.Endpoint(), err) } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(dockerDriverAttr) return nil } diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index 258b8a1d3..e35c81683 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -19,12 +19,14 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(execDriverAttr) return nil } else if unix.Geteuid() != 0 { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { d.logger.Printf("[DEBUG] driver.exec: must run as root user, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(execDriverAttr) return nil } diff --git a/client/driver/java.go b/client/driver/java.go index bf125fd3c..d3ee3df17 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -118,6 +118,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(javaDriverAttr) return nil } @@ -131,6 +132,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct if err != nil { // assume Java wasn't found d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(javaDriverAttr) return nil } @@ -150,6 +152,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct d.logger.Println("[WARN] driver.java: error parsing Java version information, aborting") } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(javaDriverAttr) return nil } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index ad9ce1ded..065c38d9d 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -77,14 +77,30 @@ type MockDriverConfig struct { // MockDriver is a driver which is used for testing purposes type MockDriver struct { DriverContext - fingerprint.StaticFingerprinter + + // isShutdown is an internal concept to use to track whether the driver + // should be shut down + isShutdown bool cleanupFailNum int } // NewMockDriver is a factory method which returns a new Mock Driver func NewMockDriver(ctx *DriverContext) Driver { - return &MockDriver{DriverContext: *ctx} + md := &MockDriver{DriverContext: *ctx} + + // if the shutdown configuration options are set, start the timer here. + // This config option defaults to false + if ctx.config != nil && ctx.config.ReadBoolDefault(fingerprint.ShutdownPeriodicAfter, false) { + duration, err := ctx.config.ReadInt(fingerprint.ShutdownPeriodicDuration) + if err != nil { + errMsg := fmt.Sprintf("unable to read config option for shutdown_periodic_duration %s, got err %s", duration, err.Error()) + panic(errMsg) + } + go md.startShutdownTimer(duration) + } + + return md } func (d *MockDriver) Abilities() DriverAbilities { @@ -165,6 +181,20 @@ func (m *MockDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse return &StartResponse{Handle: &h, Network: net}, nil } +// startShutdownTimer sets a timer, after which the mock driver will no loger be +// responsive. This is used for testing periodic fingerprinting functionality +func (m *MockDriver) startShutdownTimer(duration int) { + timer := time.NewTimer(time.Duration(duration) * time.Second) + for { + select { + case <-timer.C: + m.isShutdown = true + default: + time.Sleep(100 * time.Millisecond) + } + } +} + // Cleanup deletes all keys except for Config.Options["cleanup_fail_on"] for // Config.Options["cleanup_fail_num"] times. For failures it will return a // recoverable error. @@ -194,7 +224,12 @@ func (m *MockDriver) Validate(map[string]interface{}) error { // Fingerprint fingerprints a node and returns if MockDriver is enabled func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { - resp.AddAttribute("driver.mock_driver", "1") + switch { + case m.isShutdown: + resp.RemoveAttribute("driver.mock_driver") + default: + resp.AddAttribute("driver.mock_driver", "1") + } return nil } @@ -338,3 +373,8 @@ func (h *mockDriverHandle) run() { } } } + +// When testing, poll for updates +func (m *MockDriver) Periodic() (bool, time.Duration) { + return true, 500 * time.Millisecond +} diff --git a/client/driver/qemu.go b/client/driver/qemu.go index a4d740ef9..b03c85842 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -163,12 +163,15 @@ func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } outBytes, err := exec.Command(bin, "--version").Output() if err != nil { + // return no error, as it isn't an error to not find qemu, it just means we + // can't use it. return nil } out := strings.TrimSpace(string(outBytes)) matches := reQemuVersion.FindStringSubmatch(out) if len(matches) != 2 { + resp.RemoveAttribute(qemuDriverAttr) return fmt.Errorf("Unable to parse Qemu version string: %#v", matches) } currentQemuVersion := matches[1] diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index 9d7ba400a..bb11387e1 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -101,6 +101,7 @@ func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstr return nil } + resp.RemoveAttribute(rawExecDriverAttr) return nil } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 789a8f6aa..1ace66bd7 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -318,6 +318,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs d.logger.Printf("[DEBUG] driver.rkt: must run as root user, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(rktDriverAttr) return nil } @@ -332,6 +333,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs appcMatches := reAppcVersion.FindStringSubmatch(out) if len(rktMatches) != 2 || len(appcMatches) != 2 { d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(rktDriverAttr) return fmt.Errorf("Unable to parse Rkt version string: %#v", rktMatches) } @@ -345,6 +347,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs currentVersion, minVersion) } d.fingerprintSuccess = helper.BoolToPtr(false) + resp.RemoveAttribute(rktDriverAttr) return nil } diff --git a/client/fingerprint/cgroup.go b/client/fingerprint/cgroup.go index 5ee5b26cc..2e6c44637 100644 --- a/client/fingerprint/cgroup.go +++ b/client/fingerprint/cgroup.go @@ -5,6 +5,8 @@ package fingerprint import ( "log" "time" + + cstructs "github.com/hashicorp/nomad/client/structs" ) const ( @@ -45,6 +47,12 @@ func NewCGroupFingerprint(logger *log.Logger) Fingerprint { return f } +// clearCGroupAttributes clears any node attributes related to cgroups that might +// have been set in a previous fingerprint run. +func (f *CGroupFingerprint) clearCGroupAttributes(r *cstructs.FingerprintResponse) { + r.RemoveAttribute("unique.cgroup.mountpoint") +} + // Periodic determines the interval at which the periodic fingerprinter will run. func (f *CGroupFingerprint) Periodic() (bool, time.Duration) { return true, interval * time.Second diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go index 47beecc49..a13e17d3b 100644 --- a/client/fingerprint/cgroup_linux.go +++ b/client/fingerprint/cgroup_linux.go @@ -30,11 +30,15 @@ func FindCgroupMountpointDir() (string, error) { func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { mount, err := f.mountPointDetector.MountPoint() if err != nil { + f.clearCGroupAttributes(resp) return fmt.Errorf("Failed to discover cgroup mount point: %s", err) } // Check if a cgroup mount point was found if mount == "" { + + f.clearCGroupAttributes(resp) + if f.lastState == cgroupAvailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are unavailable") } diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index 79ed813e5..d4088614c 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -47,7 +47,7 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * // If we can't hit this URL consul is probably not running on this machine. info, err := f.client.Agent().Self() if err != nil { - // TODO this should set consul in the response if the error is not nil + f.clearConsulAttributes(resp) // Print a message indicating that the Consul Agent is not available // anymore @@ -102,6 +102,17 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * return nil } +// clearConsulAttributes removes consul attributes and links from the passed +// Node. +func (f *ConsulFingerprint) clearConsulAttributes(r *cstructs.FingerprintResponse) { + r.RemoveAttribute("consul.server") + r.RemoveAttribute("consul.version") + r.RemoveAttribute("consul.revision") + r.RemoveAttribute("unique.consul.name") + r.RemoveAttribute("consul.datacenter") + r.RemoveLink("consul") +} + func (f *ConsulFingerprint) Periodic() (bool, time.Duration) { return true, 15 * time.Second } diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index 8a3477f51..ed50b344e 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -16,6 +16,16 @@ const ( // TightenNetworkTimeoutsConfig is a config key that can be used during // tests to tighten the timeouts for fingerprinters that make network calls. TightenNetworkTimeoutsConfig = "test.tighten_network_timeouts" + + // ShutdownPeriodicAfter is a config key that can be used during tests to + // "stop" a previously-functioning driver, allowing for testing of periodic + // drivers and fingerprinters + ShutdownPeriodicAfter = "test.shutdown_periodic_after" + + // ShutdownPeriodicDuration is a config option that can be used during tests + // to "stop" a previously functioning driver after the specified duration + // (specified in seconds) for testing of periodic drivers and fingerprinters. + ShutdownPeriodicDuration = "test.shutdown_periodic_duration" ) func init() { diff --git a/client/fingerprint/vault.go b/client/fingerprint/vault.go index 612302365..04d8e1eba 100644 --- a/client/fingerprint/vault.go +++ b/client/fingerprint/vault.go @@ -52,6 +52,7 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c // Connect to vault and parse its information status, err := f.client.Sys().SealStatus() if err != nil { + f.clearVaultAttributes(resp) // Print a message indicating that Vault is not available anymore if f.lastState == vaultAvailable { f.logger.Printf("[INFO] fingerprint.vault: Vault is unavailable") @@ -79,3 +80,10 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c func (f *VaultFingerprint) Periodic() (bool, time.Duration) { return true, 15 * time.Second } + +func (f *VaultFingerprint) clearVaultAttributes(r *cstructs.FingerprintResponse) { + r.RemoveAttribute("vault.accessible") + r.RemoveAttribute("vault.version") + r.RemoveAttribute("vault.cluster_id") + r.RemoveAttribute("vault.cluster_name") +} diff --git a/client/structs/structs.go b/client/structs/structs.go index 7c9ea1479..9d21b4362 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -210,6 +210,17 @@ func (f *FingerprintResponse) AddAttribute(name, value string) { f.attributes[name] = value } +// RemoveAttribute sets the given attribute to empty, which will later remove +// it entirely from the node +func (f *FingerprintResponse) RemoveAttribute(name string) { + // initialize attributes if it has not been already + if f.attributes == nil { + f.attributes = make(map[string]string, 0) + } + + f.attributes[name] = "" +} + // GetAttributes fetches the attributes for the fingerprint response func (f *FingerprintResponse) GetAttributes() map[string]string { // initialize attributes if it has not been already @@ -230,6 +241,17 @@ func (f *FingerprintResponse) AddLink(name, value string) { f.links[name] = value } +// RemoveLink removes a link entry from the fingerprint response. This will +// later remove it entirely from the node +func (f *FingerprintResponse) RemoveLink(name string) { + // initialize links if it has not been already + if f.links == nil { + f.links = make(map[string]string, 0) + } + + f.links[name] = "" +} + // GetLinks returns the links for the fingerprint response func (f *FingerprintResponse) GetLinks() map[string]string { // initialize links if it has not been already From 2c873adba4c8a1495133f094dea1472ea5b11a38 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Mon, 29 Jan 2018 19:53:34 -0800 Subject: [PATCH 19/79] Refactor redundancy_zone/upgrade_version out of client meta --- api/operator_autopilot.go | 128 +++++++++++------- api/operator_autopilot_test.go | 10 +- command/agent/agent.go | 14 +- command/agent/config-test-fixtures/basic.hcl | 6 +- command/agent/config.go | 17 ++- command/agent/config_parse.go | 13 +- command/agent/config_parse_test.go | 6 +- command/agent/config_test.go | 12 +- command/agent/operator_endpoint.go | 78 ++--------- command/agent/operator_endpoint_test.go | 52 +------ command/operator_autopilot_get.go | 4 +- command/operator_autopilot_set.go | 28 ++-- command/operator_autopilot_set_test.go | 4 +- nomad/autopilot.go | 29 +++- nomad/autopilot_test.go | 7 +- nomad/config.go | 12 +- nomad/fsm_test.go | 3 +- nomad/leader.go | 3 +- nomad/operator_endpoint.go | 2 +- nomad/server.go | 6 + nomad/state/autopilot.go | 16 +-- nomad/state/autopilot_test.go | 14 +- nomad/structs/config/autopilot.go | 26 ++-- nomad/structs/config/autopilot_test.go | 12 +- nomad/structs/operator.go | 41 +++++- nomad/util.go | 3 - website/source/api/operator.html.md | 23 ++-- .../agent/configuration/autopilot.html.md | 25 ++-- .../docs/agent/configuration/server.html.md | 13 +- .../source/guides/cluster/autopilot.html.md | 42 +++--- 30 files changed, 350 insertions(+), 299 deletions(-) diff --git a/api/operator_autopilot.go b/api/operator_autopilot.go index a61ad21d6..2dbde9dd2 100644 --- a/api/operator_autopilot.go +++ b/api/operator_autopilot.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "encoding/json" "fmt" "io" "strconv" @@ -19,7 +20,7 @@ type AutopilotConfiguration struct { // LastContactThreshold is the limit on the amount of time a server can go // without leader contact before being considered unhealthy. - LastContactThreshold *ReadableDuration + LastContactThreshold time.Duration // MaxTrailingLogs is the amount of entries in the Raft Log that a server can // be behind before being considered unhealthy. @@ -28,20 +29,19 @@ type AutopilotConfiguration struct { // ServerStabilizationTime is the minimum amount of time a server must be // in a stable, healthy state before it can be added to the cluster. Only // applicable with Raft protocol version 3 or higher. - ServerStabilizationTime *ReadableDuration + ServerStabilizationTime time.Duration - // (Enterprise-only) RedundancyZoneTag is the node tag to use for separating - // servers into zones for redundancy. If left blank, this feature will be disabled. - RedundancyZoneTag string + // (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones. + EnableRedundancyZones bool // (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration // strategy of waiting until enough newer-versioned servers have been added to the // cluster before promoting them to voters. DisableUpgradeMigration bool - // (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when - // performing upgrade migrations. If left blank, the Nomad version will be used. - UpgradeVersionTag string + // (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom + // upgrade versions when performing migrations. + EnableCustomUpgrades bool // CreateIndex holds the index corresponding the creation of this configuration. // This is a read-only field. @@ -54,6 +54,45 @@ type AutopilotConfiguration struct { ModifyIndex uint64 } +func (u *AutopilotConfiguration) MarshalJSON() ([]byte, error) { + type Alias AutopilotConfiguration + return json.Marshal(&struct { + LastContactThreshold string + ServerStabilizationTime string + *Alias + }{ + LastContactThreshold: u.LastContactThreshold.String(), + ServerStabilizationTime: u.ServerStabilizationTime.String(), + Alias: (*Alias)(u), + }) +} + +func (u *AutopilotConfiguration) UnmarshalJSON(data []byte) error { + type Alias AutopilotConfiguration + aux := &struct { + LastContactThreshold string + ServerStabilizationTime string + *Alias + }{ + Alias: (*Alias)(u), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + var err error + if aux.LastContactThreshold != "" { + if u.LastContactThreshold, err = time.ParseDuration(aux.LastContactThreshold); err != nil { + return err + } + } + if aux.ServerStabilizationTime != "" { + if u.ServerStabilizationTime, err = time.ParseDuration(aux.ServerStabilizationTime); err != nil { + return err + } + } + return nil +} + // ServerHealth is the health (from the leader's point of view) of a server. type ServerHealth struct { // ID is the raft ID of the server. @@ -75,7 +114,7 @@ type ServerHealth struct { Leader bool // LastContact is the time since this node's last contact with the leader. - LastContact *ReadableDuration + LastContact time.Duration // LastTerm is the highest leader term this server has a record of in its Raft log. LastTerm uint64 @@ -94,6 +133,37 @@ type ServerHealth struct { StableSince time.Time } +func (u *ServerHealth) MarshalJSON() ([]byte, error) { + type Alias ServerHealth + return json.Marshal(&struct { + LastContact string + *Alias + }{ + LastContact: u.LastContact.String(), + Alias: (*Alias)(u), + }) +} + +func (u *ServerHealth) UnmarshalJSON(data []byte) error { + type Alias ServerHealth + aux := &struct { + LastContact string + *Alias + }{ + Alias: (*Alias)(u), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + var err error + if aux.LastContact != "" { + if u.LastContact, err = time.ParseDuration(aux.LastContact); err != nil { + return err + } + } + return nil +} + // OperatorHealthReply is a representation of the overall health of the cluster type OperatorHealthReply struct { // Healthy is true if all the servers in the cluster are healthy. @@ -107,46 +177,6 @@ type OperatorHealthReply struct { Servers []ServerHealth } -// ReadableDuration is a duration type that is serialized to JSON in human readable format. -type ReadableDuration time.Duration - -func NewReadableDuration(dur time.Duration) *ReadableDuration { - d := ReadableDuration(dur) - return &d -} - -func (d *ReadableDuration) String() string { - return d.Duration().String() -} - -func (d *ReadableDuration) Duration() time.Duration { - if d == nil { - return time.Duration(0) - } - return time.Duration(*d) -} - -func (d *ReadableDuration) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"%s"`, d.Duration().String())), nil -} - -func (d *ReadableDuration) UnmarshalJSON(raw []byte) error { - if d == nil { - return fmt.Errorf("cannot unmarshal to nil pointer") - } - - str := string(raw) - if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' { - return fmt.Errorf("must be enclosed with quotes: %s", str) - } - dur, err := time.ParseDuration(str[1 : len(str)-1]) - if err != nil { - return err - } - *d = ReadableDuration(dur) - return nil -} - // AutopilotGetConfiguration is used to query the current Autopilot configuration. func (op *Operator) AutopilotGetConfiguration(q *QueryOptions) (*AutopilotConfiguration, error) { r, err := op.c.newRequest("GET", "/v1/operator/autopilot/configuration") diff --git a/api/operator_autopilot_test.go b/api/operator_autopilot_test.go index 1c18e8e0f..c491d855c 100644 --- a/api/operator_autopilot_test.go +++ b/api/operator_autopilot_test.go @@ -17,13 +17,17 @@ func TestAPI_OperatorAutopilotGetSetConfiguration(t *testing.T) { defer s.Stop() operator := c.Operator() - config, err := operator.AutopilotGetConfiguration(nil) - assert.Nil(err) + var config *AutopilotConfiguration + retry.Run(t, func(r *retry.R) { + var err error + config, err = operator.AutopilotGetConfiguration(nil) + r.Check(err) + }) assert.True(config.CleanupDeadServers) // Change a config setting newConf := &AutopilotConfiguration{CleanupDeadServers: false} - err = operator.AutopilotSetConfiguration(newConf, nil) + err := operator.AutopilotSetConfiguration(newConf, nil) assert.Nil(err) config, err = operator.AutopilotGetConfiguration(nil) diff --git a/command/agent/agent.go b/command/agent/agent.go index 43e42c1bb..eff225a3b 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -163,6 +163,12 @@ func convertServerConfig(agentConfig *Config, logOutput io.Writer) (*nomad.Confi if agentConfig.Server.NonVotingServer { conf.NonVoter = true } + if agentConfig.Server.RedundancyZone != "" { + conf.RedundancyZone = agentConfig.Server.RedundancyZone + } + if agentConfig.Server.UpgradeVersion != "" { + conf.UpgradeVersion = agentConfig.Server.UpgradeVersion + } if agentConfig.Autopilot != nil { if agentConfig.Autopilot.CleanupDeadServers != nil { conf.AutopilotConfig.CleanupDeadServers = *agentConfig.Autopilot.CleanupDeadServers @@ -176,14 +182,14 @@ func convertServerConfig(agentConfig *Config, logOutput io.Writer) (*nomad.Confi if agentConfig.Autopilot.MaxTrailingLogs != 0 { conf.AutopilotConfig.MaxTrailingLogs = uint64(agentConfig.Autopilot.MaxTrailingLogs) } - if agentConfig.Autopilot.RedundancyZoneTag != "" { - conf.AutopilotConfig.RedundancyZoneTag = agentConfig.Autopilot.RedundancyZoneTag + if agentConfig.Autopilot.EnableRedundancyZones != nil { + conf.AutopilotConfig.EnableRedundancyZones = *agentConfig.Autopilot.EnableRedundancyZones } if agentConfig.Autopilot.DisableUpgradeMigration != nil { conf.AutopilotConfig.DisableUpgradeMigration = *agentConfig.Autopilot.DisableUpgradeMigration } - if agentConfig.Autopilot.UpgradeVersionTag != "" { - conf.AutopilotConfig.UpgradeVersionTag = agentConfig.Autopilot.UpgradeVersionTag + if agentConfig.Autopilot.EnableCustomUpgrades != nil { + conf.AutopilotConfig.EnableCustomUpgrades = *agentConfig.Autopilot.EnableCustomUpgrades } } diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl index 5cf8603e7..4ddc65771 100644 --- a/command/agent/config-test-fixtures/basic.hcl +++ b/command/agent/config-test-fixtures/basic.hcl @@ -83,6 +83,8 @@ server { retry_interval = "15s" rejoin_after_leave = true non_voting_server = true + redundancy_zone = "foo" + upgrade_version = "0.8.0" encrypt = "abc" } acl { @@ -166,7 +168,7 @@ autopilot { disable_upgrade_migration = true last_contact_threshold = "12705s" max_trailing_logs = 17849 - redundancy_zone_tag = "foo" + enable_redundancy_zones = true server_stabilization_time = "23057s" - upgrade_version_tag = "bar" + enable_custom_upgrades = true } diff --git a/command/agent/config.go b/command/agent/config.go index 6cff6c378..6eb1fad06 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -330,10 +330,17 @@ type ServerConfig struct { // true, we ignore the leave, and rejoin the cluster on start. RejoinAfterLeave bool `mapstructure:"rejoin_after_leave"` - // NonVotingServer is whether this server will act as a non-voting member - // of the cluster to help provide read scalability. (Enterprise-only) + // (Enterprise-only) NonVotingServer is whether this server will act as a + // non-voting member of the cluster to help provide read scalability. NonVotingServer bool `mapstructure:"non_voting_server"` + // (Enterprise-only) RedundancyZone is the redundancy zone to use for this server. + RedundancyZone string `mapstructure:"redundancy_zone"` + + // (Enterprise-only) UpgradeVersion is the custom upgrade version to use when + // performing upgrade migrations. + UpgradeVersion string `mapstructure:"upgrade_version"` + // Encryption key to use for the Serf communication EncryptKey string `mapstructure:"encrypt" json:"-"` } @@ -1034,6 +1041,12 @@ func (a *ServerConfig) Merge(b *ServerConfig) *ServerConfig { if b.NonVotingServer { result.NonVotingServer = true } + if b.RedundancyZone != "" { + result.RedundancyZone = b.RedundancyZone + } + if b.UpgradeVersion != "" { + result.UpgradeVersion = b.UpgradeVersion + } if b.EncryptKey != "" { result.EncryptKey = b.EncryptKey } diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index e860a68af..6feaa6d04 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -9,6 +9,7 @@ import ( "time" multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-version" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/nomad/helper" @@ -536,6 +537,8 @@ func parseServer(result **ServerConfig, list *ast.ObjectList) error { "encrypt", "authoritative_region", "non_voting_server", + "redundancy_zone", + "upgrade_version", } if err := helper.CheckHCLKeys(listVal, valid); err != nil { return err @@ -559,6 +562,12 @@ func parseServer(result **ServerConfig, list *ast.ObjectList) error { return err } + if config.UpgradeVersion != "" { + if _, err := version.NewVersion(config.UpgradeVersion); err != nil { + return fmt.Errorf("error parsing upgrade_version: %v", err) + } + } + *result = &config return nil } @@ -865,9 +874,9 @@ func parseAutopilot(result **config.AutopilotConfig, list *ast.ObjectList) error "server_stabilization_time", "last_contact_threshold", "max_trailing_logs", - "redundancy_zone_tag", + "enable_redundancy_zones", "disable_upgrade_migration", - "upgrade_version_tag", + "enable_custom_upgrades", } if err := helper.CheckHCLKeys(listVal, valid); err != nil { diff --git a/command/agent/config_parse_test.go b/command/agent/config_parse_test.go index c28989d9f..826bceb5f 100644 --- a/command/agent/config_parse_test.go +++ b/command/agent/config_parse_test.go @@ -104,6 +104,8 @@ func TestConfig_Parse(t *testing.T) { RejoinAfterLeave: true, RetryMaxAttempts: 3, NonVotingServer: true, + RedundancyZone: "foo", + UpgradeVersion: "0.8.0", EncryptKey: "abc", }, ACL: &ACLConfig{ @@ -193,9 +195,9 @@ func TestConfig_Parse(t *testing.T) { ServerStabilizationTime: 23057 * time.Second, LastContactThreshold: 12705 * time.Second, MaxTrailingLogs: 17849, - RedundancyZoneTag: "foo", + EnableRedundancyZones: &trueValue, DisableUpgradeMigration: &trueValue, - UpgradeVersionTag: "bar", + EnableCustomUpgrades: &trueValue, }, }, false, diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 400b57615..3ed05bd46 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -107,6 +107,8 @@ func TestConfig_Merge(t *testing.T) { HeartbeatGrace: 30 * time.Second, MinHeartbeatTTL: 30 * time.Second, MaxHeartbeatsPerSecond: 30.0, + RedundancyZone: "foo", + UpgradeVersion: "foo", }, ACL: &ACLConfig{ Enabled: true, @@ -165,9 +167,9 @@ func TestConfig_Merge(t *testing.T) { ServerStabilizationTime: 1 * time.Second, LastContactThreshold: 1 * time.Second, MaxTrailingLogs: 1, - RedundancyZoneTag: "1", + EnableRedundancyZones: &falseValue, DisableUpgradeMigration: &falseValue, - UpgradeVersionTag: "1", + EnableCustomUpgrades: &falseValue, }, } @@ -260,6 +262,8 @@ func TestConfig_Merge(t *testing.T) { RetryInterval: "10s", retryInterval: time.Second * 10, NonVotingServer: true, + RedundancyZone: "bar", + UpgradeVersion: "bar", }, ACL: &ACLConfig{ Enabled: true, @@ -328,9 +332,9 @@ func TestConfig_Merge(t *testing.T) { ServerStabilizationTime: 2 * time.Second, LastContactThreshold: 2 * time.Second, MaxTrailingLogs: 2, - RedundancyZoneTag: "2", + EnableRedundancyZones: &trueValue, DisableUpgradeMigration: &trueValue, - UpgradeVersionTag: "2", + EnableCustomUpgrades: &trueValue, }, } diff --git a/command/agent/operator_endpoint.go b/command/agent/operator_endpoint.go index 93db317a2..58f293375 100644 --- a/command/agent/operator_endpoint.go +++ b/command/agent/operator_endpoint.go @@ -104,19 +104,19 @@ func (s *HTTPServer) OperatorAutopilotConfiguration(resp http.ResponseWriter, re return nil, nil } - var reply autopilot.Config + var reply structs.AutopilotConfig if err := s.agent.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { return nil, err } out := api.AutopilotConfiguration{ CleanupDeadServers: reply.CleanupDeadServers, - LastContactThreshold: api.NewReadableDuration(reply.LastContactThreshold), + LastContactThreshold: reply.LastContactThreshold, MaxTrailingLogs: reply.MaxTrailingLogs, - ServerStabilizationTime: api.NewReadableDuration(reply.ServerStabilizationTime), - RedundancyZoneTag: reply.RedundancyZoneTag, + ServerStabilizationTime: reply.ServerStabilizationTime, + EnableRedundancyZones: reply.EnableRedundancyZones, DisableUpgradeMigration: reply.DisableUpgradeMigration, - UpgradeVersionTag: reply.UpgradeVersionTag, + EnableCustomUpgrades: reply.EnableCustomUpgrades, CreateIndex: reply.CreateIndex, ModifyIndex: reply.ModifyIndex, } @@ -129,21 +129,20 @@ func (s *HTTPServer) OperatorAutopilotConfiguration(resp http.ResponseWriter, re s.parseToken(req, &args.AuthToken) var conf api.AutopilotConfiguration - durations := NewDurationFixer("lastcontactthreshold", "serverstabilizationtime") - if err := decodeBodyFunc(req, &conf, durations.FixupDurations); err != nil { + if err := decodeBody(req, &conf); err != nil { resp.WriteHeader(http.StatusBadRequest) fmt.Fprintf(resp, "Error parsing autopilot config: %v", err) return nil, nil } - args.Config = autopilot.Config{ + args.Config = structs.AutopilotConfig{ CleanupDeadServers: conf.CleanupDeadServers, - LastContactThreshold: conf.LastContactThreshold.Duration(), + LastContactThreshold: conf.LastContactThreshold, MaxTrailingLogs: conf.MaxTrailingLogs, - ServerStabilizationTime: conf.ServerStabilizationTime.Duration(), - RedundancyZoneTag: conf.RedundancyZoneTag, + ServerStabilizationTime: conf.ServerStabilizationTime, + EnableRedundancyZones: conf.EnableRedundancyZones, DisableUpgradeMigration: conf.DisableUpgradeMigration, - UpgradeVersionTag: conf.UpgradeVersionTag, + EnableCustomUpgrades: conf.EnableCustomUpgrades, } // Check for cas value @@ -210,7 +209,7 @@ func (s *HTTPServer) OperatorServerHealth(resp http.ResponseWriter, req *http.Re Version: server.Version, Leader: server.Leader, SerfStatus: server.SerfStatus.String(), - LastContact: api.NewReadableDuration(server.LastContact), + LastContact: server.LastContact, LastTerm: server.LastTerm, LastIndex: server.LastIndex, Healthy: server.Healthy, @@ -221,56 +220,3 @@ func (s *HTTPServer) OperatorServerHealth(resp http.ResponseWriter, req *http.Re return out, nil } - -type durationFixer map[string]bool - -func NewDurationFixer(fields ...string) durationFixer { - d := make(map[string]bool) - for _, field := range fields { - d[field] = true - } - return d -} - -// FixupDurations is used to handle parsing any field names in the map to time.Durations -func (d durationFixer) FixupDurations(raw interface{}) error { - rawMap, ok := raw.(map[string]interface{}) - if !ok { - return nil - } - for key, val := range rawMap { - switch val.(type) { - case map[string]interface{}: - if err := d.FixupDurations(val); err != nil { - return err - } - - case []interface{}: - for _, v := range val.([]interface{}) { - if err := d.FixupDurations(v); err != nil { - return err - } - } - - case []map[string]interface{}: - for _, v := range val.([]map[string]interface{}) { - if err := d.FixupDurations(v); err != nil { - return err - } - } - - default: - if d[strings.ToLower(key)] { - // Convert a string value into an integer - if vStr, ok := val.(string); ok { - dur, err := time.ParseDuration(vStr) - if err != nil { - return err - } - rawMap[key] = dur - } - } - } - } - return nil -} diff --git a/command/agent/operator_endpoint_test.go b/command/agent/operator_endpoint_test.go index 10ee36821..2d8486765 100644 --- a/command/agent/operator_endpoint_test.go +++ b/command/agent/operator_endpoint_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/nomad/structs" @@ -112,7 +111,7 @@ func TestOperator_AutopilotSetConfiguration(t *testing.T) { t.Fatalf("err: %v", err) } if resp.Code != 200 { - t.Fatalf("bad code: %d", resp.Code) + t.Fatalf("bad code: %d, %q", resp.Code, resp.Body.String()) } args := structs.GenericRequest{ @@ -121,7 +120,7 @@ func TestOperator_AutopilotSetConfiguration(t *testing.T) { }, } - var reply autopilot.Config + var reply structs.AutopilotConfig if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { t.Fatalf("err: %v", err) } @@ -150,7 +149,7 @@ func TestOperator_AutopilotCASConfiguration(t *testing.T) { }, } - var reply autopilot.Config + var reply structs.AutopilotConfig if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { t.Fatalf("err: %v", err) } @@ -200,7 +199,6 @@ func TestOperator_AutopilotCASConfiguration(t *testing.T) { } func TestOperator_ServerHealth(t *testing.T) { - t.Parallel() httpTest(t, func(c *Config) { c.Server.RaftProtocol = 3 }, func(s *TestAgent) { @@ -259,47 +257,3 @@ func TestOperator_ServerHealth_Unhealthy(t *testing.T) { }) }) } - -func TestDurationFixer(t *testing.T) { - assert := assert.New(t) - obj := map[string]interface{}{ - "key1": []map[string]interface{}{ - { - "subkey1": "10s", - }, - { - "subkey2": "5d", - }, - }, - "key2": map[string]interface{}{ - "subkey3": "30s", - "subkey4": "20m", - }, - "key3": "11s", - "key4": "49h", - } - expected := map[string]interface{}{ - "key1": []map[string]interface{}{ - { - "subkey1": 10 * time.Second, - }, - { - "subkey2": "5d", - }, - }, - "key2": map[string]interface{}{ - "subkey3": "30s", - "subkey4": 20 * time.Minute, - }, - "key3": "11s", - "key4": 49 * time.Hour, - } - - fixer := NewDurationFixer("key4", "subkey1", "subkey4") - if err := fixer.FixupDurations(obj); err != nil { - t.Fatal(err) - } - - // Ensure we only processed the intended fieldnames - assert.Equal(obj, expected) -} diff --git a/command/operator_autopilot_get.go b/command/operator_autopilot_get.go index b533b4749..c85c4e5c4 100644 --- a/command/operator_autopilot_get.go +++ b/command/operator_autopilot_get.go @@ -45,9 +45,9 @@ func (c *OperatorAutopilotGetCommand) Run(args []string) int { c.Ui.Output(fmt.Sprintf("LastContactThreshold = %v", config.LastContactThreshold.String())) c.Ui.Output(fmt.Sprintf("MaxTrailingLogs = %v", config.MaxTrailingLogs)) c.Ui.Output(fmt.Sprintf("ServerStabilizationTime = %v", config.ServerStabilizationTime.String())) - c.Ui.Output(fmt.Sprintf("RedundancyZoneTag = %q", config.RedundancyZoneTag)) + c.Ui.Output(fmt.Sprintf("EnableRedundancyZones = %v", config.EnableRedundancyZones)) c.Ui.Output(fmt.Sprintf("DisableUpgradeMigration = %v", config.DisableUpgradeMigration)) - c.Ui.Output(fmt.Sprintf("UpgradeVersionTag = %q", config.UpgradeVersionTag)) + c.Ui.Output(fmt.Sprintf("EnableCustomUpgrades = %v", config.EnableCustomUpgrades)) return 0 } diff --git a/command/operator_autopilot_set.go b/command/operator_autopilot_set.go index bacefe339..280a0413e 100644 --- a/command/operator_autopilot_set.go +++ b/command/operator_autopilot_set.go @@ -3,10 +3,8 @@ package command import ( "fmt" "strings" - "time" "github.com/hashicorp/consul/command/flags" - "github.com/hashicorp/nomad/api" "github.com/posener/complete" ) @@ -21,9 +19,9 @@ func (c *OperatorAutopilotSetCommand) AutocompleteFlags() complete.Flags { "-max-trailing-logs": complete.PredictAnything, "-last-contact-threshold": complete.PredictAnything, "-server-stabilization-time": complete.PredictAnything, - "-redundancy-zone-tag": complete.PredictAnything, + "-enable-redundancy-zones": complete.PredictAnything, "-disable-upgrade-migration": complete.PredictAnything, - "-upgrade-version-tag": complete.PredictAnything, + "-enable-custom-upgrades": complete.PredictAnything, }) } @@ -36,9 +34,9 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int { var maxTrailingLogs flags.UintValue var lastContactThreshold flags.DurationValue var serverStabilizationTime flags.DurationValue - var redundancyZoneTag flags.StringValue + var enableRedundancyZones flags.BoolValue var disableUpgradeMigration flags.BoolValue - var upgradeVersionTag flags.StringValue + var enableCustomUpgrades flags.BoolValue f := c.Meta.FlagSet("autopilot", FlagSetClient) f.Usage = func() { c.Ui.Output(c.Help()) } @@ -47,9 +45,9 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int { f.Var(&maxTrailingLogs, "max-trailing-logs", "") f.Var(&lastContactThreshold, "last-contact-threshold", "") f.Var(&serverStabilizationTime, "server-stabilization-time", "") - f.Var(&redundancyZoneTag, "redundancy-zone-tag", "") + f.Var(&enableRedundancyZones, "enable-redundancy-zones", "") f.Var(&disableUpgradeMigration, "disable-upgrade-migration", "") - f.Var(&upgradeVersionTag, "upgrade-version-tag", "") + f.Var(&enableCustomUpgrades, "enable-custom-upgrades", "") if err := f.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Failed to parse args: %v", err)) @@ -73,21 +71,15 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int { // Update the config values based on the set flags. cleanupDeadServers.Merge(&conf.CleanupDeadServers) - redundancyZoneTag.Merge(&conf.RedundancyZoneTag) + enableRedundancyZones.Merge(&conf.EnableRedundancyZones) disableUpgradeMigration.Merge(&conf.DisableUpgradeMigration) - upgradeVersionTag.Merge(&conf.UpgradeVersionTag) + enableRedundancyZones.Merge(&conf.EnableCustomUpgrades) trailing := uint(conf.MaxTrailingLogs) maxTrailingLogs.Merge(&trailing) conf.MaxTrailingLogs = uint64(trailing) - - last := time.Duration(*conf.LastContactThreshold) - lastContactThreshold.Merge(&last) - conf.LastContactThreshold = api.NewReadableDuration(last) - - stablization := time.Duration(*conf.ServerStabilizationTime) - serverStabilizationTime.Merge(&stablization) - conf.ServerStabilizationTime = api.NewReadableDuration(stablization) + lastContactThreshold.Merge(&conf.LastContactThreshold) + serverStabilizationTime.Merge(&conf.ServerStabilizationTime) // Check-and-set the new configuration. result, err := operator.AutopilotCASConfiguration(conf, nil) diff --git a/command/operator_autopilot_set_test.go b/command/operator_autopilot_set_test.go index 8991ce51e..f76f75b5d 100644 --- a/command/operator_autopilot_set_test.go +++ b/command/operator_autopilot_set_test.go @@ -53,10 +53,10 @@ func TestOperatorAutopilotSetConfigCommmand(t *testing.T) { if conf.MaxTrailingLogs != 99 { t.Fatalf("bad: %#v", conf) } - if conf.LastContactThreshold.Duration() != 123*time.Millisecond { + if conf.LastContactThreshold != 123*time.Millisecond { t.Fatalf("bad: %#v", conf) } - if conf.ServerStabilizationTime.Duration() != 123*time.Millisecond { + if conf.ServerStabilizationTime != 123*time.Millisecond { t.Fatalf("bad: %#v", conf) } } diff --git a/nomad/autopilot.go b/nomad/autopilot.go index 5fd2a9b37..4c5ea180b 100644 --- a/nomad/autopilot.go +++ b/nomad/autopilot.go @@ -10,13 +10,40 @@ import ( "github.com/hashicorp/serf/serf" ) +const ( + AutopilotRZTag = "ap_zone" + AutopilotVersionTag = "ap_version" +) + // AutopilotDelegate is a Nomad delegate for autopilot operations. type AutopilotDelegate struct { server *Server } func (d *AutopilotDelegate) AutopilotConfig() *autopilot.Config { - return d.server.getOrCreateAutopilotConfig() + c := d.server.getOrCreateAutopilotConfig() + if c == nil { + return nil + } + + conf := &autopilot.Config{ + CleanupDeadServers: c.CleanupDeadServers, + LastContactThreshold: c.LastContactThreshold, + MaxTrailingLogs: c.MaxTrailingLogs, + ServerStabilizationTime: c.ServerStabilizationTime, + DisableUpgradeMigration: c.DisableUpgradeMigration, + ModifyIndex: c.ModifyIndex, + CreateIndex: c.CreateIndex, + } + + if c.EnableRedundancyZones { + conf.RedundancyZoneTag = AutopilotRZTag + } + if c.EnableCustomUpgrades { + conf.UpgradeVersionTag = AutopilotVersionTag + } + + return conf } func (d *AutopilotDelegate) FetchStats(ctx context.Context, servers []serf.Member) map[string]*autopilot.ServerStats { diff --git a/nomad/autopilot_test.go b/nomad/autopilot_test.go index 6511c8be9..13cee5044 100644 --- a/nomad/autopilot_test.go +++ b/nomad/autopilot_test.go @@ -270,8 +270,11 @@ func TestAutopilot_CleanupStaleRaftServer(t *testing.T) { testutil.WaitForLeader(t, s1.RPC) // Add s4 to peers directly - addr := fmt.Sprintf("127.0.0.1:%d", s4.config.SerfConfig.MemberlistConfig.BindPort) - s1.raft.AddVoter(raft.ServerID(s4.config.NodeID), raft.ServerAddress(addr), 0, 0) + addr := fmt.Sprintf("127.0.0.1:%d", s4.config.RPCAddr.Port) + future := s1.raft.AddVoter(raft.ServerID(s4.config.NodeID), raft.ServerAddress(addr), 0, 0) + if err := future.Error(); err != nil { + t.Fatal(err) + } // Verify we have 4 peers peers, err := s1.numPeers() diff --git a/nomad/config.go b/nomad/config.go index 5fd8dad8e..a2c553eb2 100644 --- a/nomad/config.go +++ b/nomad/config.go @@ -8,7 +8,6 @@ import ( "runtime" "time" - "github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/memberlist" "github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/helper/uuid" @@ -98,6 +97,13 @@ type Config struct { // as a voting member of the Raft cluster. NonVoter bool + // (Enterprise-only) RedundancyZone is the redundancy zone to use for this server. + RedundancyZone string + + // (Enterprise-only) UpgradeVersion is the custom upgrade version to use when + // performing upgrade migrations. + UpgradeVersion string + // SerfConfig is the configuration for the serf cluster SerfConfig *serf.Config @@ -269,7 +275,7 @@ type Config struct { // AutopilotConfig is used to apply the initial autopilot config when // bootstrapping. - AutopilotConfig *autopilot.Config + AutopilotConfig *structs.AutopilotConfig // ServerHealthInterval is the frequency with which the health of the // servers in the cluster will be updated. @@ -339,7 +345,7 @@ func DefaultConfig() *Config { TLSConfig: &config.TLSConfig{}, ReplicationBackoff: 30 * time.Second, SentinelGCInterval: 30 * time.Second, - AutopilotConfig: &autopilot.Config{ + AutopilotConfig: &structs.AutopilotConfig{ CleanupDeadServers: true, LastContactThreshold: 200 * time.Millisecond, MaxTrailingLogs: 250, diff --git a/nomad/fsm_test.go b/nomad/fsm_test.go index 90c0b6c12..aa16be373 100644 --- a/nomad/fsm_test.go +++ b/nomad/fsm_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/consul/agent/consul/autopilot" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/nomad/mock" @@ -2319,7 +2318,7 @@ func TestFSM_Autopilot(t *testing.T) { // Set the autopilot config using a request. req := structs.AutopilotSetConfigRequest{ Datacenter: "dc1", - Config: autopilot.Config{ + Config: structs.AutopilotConfig{ CleanupDeadServers: true, LastContactThreshold: 10 * time.Second, MaxTrailingLogs: 300, diff --git a/nomad/leader.go b/nomad/leader.go index 712fa75b9..4d1a571cc 100644 --- a/nomad/leader.go +++ b/nomad/leader.go @@ -13,7 +13,6 @@ import ( "golang.org/x/time/rate" "github.com/armon/go-metrics" - "github.com/hashicorp/consul/agent/consul/autopilot" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-version" "github.com/hashicorp/nomad/helper/uuid" @@ -1174,7 +1173,7 @@ func diffACLTokens(state *state.StateStore, minIndex uint64, remoteList []*struc } // getOrCreateAutopilotConfig is used to get the autopilot config, initializing it if necessary -func (s *Server) getOrCreateAutopilotConfig() *autopilot.Config { +func (s *Server) getOrCreateAutopilotConfig() *structs.AutopilotConfig { state := s.fsm.State() _, config, err := state.AutopilotConfig() if err != nil { diff --git a/nomad/operator_endpoint.go b/nomad/operator_endpoint.go index b0a54d700..e6c992132 100644 --- a/nomad/operator_endpoint.go +++ b/nomad/operator_endpoint.go @@ -192,7 +192,7 @@ REMOVE: } // AutopilotGetConfiguration is used to retrieve the current Autopilot configuration. -func (op *Operator) AutopilotGetConfiguration(args *structs.GenericRequest, reply *autopilot.Config) error { +func (op *Operator) AutopilotGetConfiguration(args *structs.GenericRequest, reply *structs.AutopilotConfig) error { if done, err := op.srv.forward("Operator.AutopilotGetConfiguration", args, args, reply); done { return err } diff --git a/nomad/server.go b/nomad/server.go index d3faa60f5..09b7964c7 100644 --- a/nomad/server.go +++ b/nomad/server.go @@ -1105,6 +1105,12 @@ func (s *Server) setupSerf(conf *serf.Config, ch chan serf.Event, path string) ( if s.config.NonVoter { conf.Tags["nonvoter"] = "1" } + if s.config.RedundancyZone != "" { + conf.Tags[AutopilotRZTag] = s.config.RedundancyZone + } + if s.config.UpgradeVersion != "" { + conf.Tags[AutopilotVersionTag] = s.config.UpgradeVersion + } conf.MemberlistConfig.LogOutput = s.config.LogOutput conf.LogOutput = s.config.LogOutput conf.EventCh = ch diff --git a/nomad/state/autopilot.go b/nomad/state/autopilot.go index 65654ca79..83613817d 100644 --- a/nomad/state/autopilot.go +++ b/nomad/state/autopilot.go @@ -3,8 +3,8 @@ package state import ( "fmt" - "github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/nomad/structs" ) // autopilotConfigTableSchema returns a new table schema used for storing @@ -26,7 +26,7 @@ func autopilotConfigTableSchema() *memdb.TableSchema { } // AutopilotConfig is used to get the current Autopilot configuration. -func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) { +func (s *StateStore) AutopilotConfig() (uint64, *structs.AutopilotConfig, error) { tx := s.db.Txn(false) defer tx.Abort() @@ -36,7 +36,7 @@ func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) { return 0, nil, fmt.Errorf("failed autopilot config lookup: %s", err) } - config, ok := c.(*autopilot.Config) + config, ok := c.(*structs.AutopilotConfig) if !ok { return 0, nil, nil } @@ -45,7 +45,7 @@ func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) { } // AutopilotSetConfig is used to set the current Autopilot configuration. -func (s *StateStore) AutopilotSetConfig(idx uint64, config *autopilot.Config) error { +func (s *StateStore) AutopilotSetConfig(idx uint64, config *structs.AutopilotConfig) error { tx := s.db.Txn(true) defer tx.Abort() @@ -58,7 +58,7 @@ func (s *StateStore) AutopilotSetConfig(idx uint64, config *autopilot.Config) er // AutopilotCASConfig is used to try updating the Autopilot configuration with a // given Raft index. If the CAS index specified is not equal to the last observed index // for the config, then the call is a noop, -func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Config) (bool, error) { +func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *structs.AutopilotConfig) (bool, error) { tx := s.db.Txn(true) defer tx.Abort() @@ -71,7 +71,7 @@ func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Conf // If the existing index does not match the provided CAS // index arg, then we shouldn't update anything and can safely // return early here. - e, ok := existing.(*autopilot.Config) + e, ok := existing.(*structs.AutopilotConfig) if !ok || e.ModifyIndex != cidx { return false, nil } @@ -82,7 +82,7 @@ func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Conf return true, nil } -func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *autopilot.Config) error { +func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *structs.AutopilotConfig) error { // Check for an existing config existing, err := tx.First("autopilot-config", "id") if err != nil { @@ -91,7 +91,7 @@ func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *au // Set the indexes. if existing != nil { - config.CreateIndex = existing.(*autopilot.Config).CreateIndex + config.CreateIndex = existing.(*structs.AutopilotConfig).CreateIndex } else { config.CreateIndex = idx } diff --git a/nomad/state/autopilot_test.go b/nomad/state/autopilot_test.go index 59bf7b417..a43ddebe8 100644 --- a/nomad/state/autopilot_test.go +++ b/nomad/state/autopilot_test.go @@ -5,20 +5,20 @@ import ( "testing" "time" - "github.com/hashicorp/consul/agent/consul/autopilot" + "github.com/hashicorp/nomad/nomad/structs" ) func TestStateStore_Autopilot(t *testing.T) { s := testStateStore(t) - expected := &autopilot.Config{ + expected := &structs.AutopilotConfig{ CleanupDeadServers: true, LastContactThreshold: 5 * time.Second, MaxTrailingLogs: 500, ServerStabilizationTime: 100 * time.Second, - RedundancyZoneTag: "az", + EnableRedundancyZones: true, DisableUpgradeMigration: true, - UpgradeVersionTag: "build", + EnableCustomUpgrades: true, } if err := s.AutopilotSetConfig(0, expected); err != nil { @@ -40,7 +40,7 @@ func TestStateStore_Autopilot(t *testing.T) { func TestStateStore_AutopilotCAS(t *testing.T) { s := testStateStore(t) - expected := &autopilot.Config{ + expected := &structs.AutopilotConfig{ CleanupDeadServers: true, } @@ -52,7 +52,7 @@ func TestStateStore_AutopilotCAS(t *testing.T) { } // Do a CAS with an index lower than the entry - ok, err := s.AutopilotCASConfig(2, 0, &autopilot.Config{ + ok, err := s.AutopilotCASConfig(2, 0, &structs.AutopilotConfig{ CleanupDeadServers: false, }) if ok || err != nil { @@ -73,7 +73,7 @@ func TestStateStore_AutopilotCAS(t *testing.T) { } // Do another CAS, this time with the correct index - ok, err = s.AutopilotCASConfig(2, 1, &autopilot.Config{ + ok, err = s.AutopilotCASConfig(2, 1, &structs.AutopilotConfig{ CleanupDeadServers: false, }) if !ok || err != nil { diff --git a/nomad/structs/config/autopilot.go b/nomad/structs/config/autopilot.go index b1501b82f..ffa52bc09 100644 --- a/nomad/structs/config/autopilot.go +++ b/nomad/structs/config/autopilot.go @@ -24,25 +24,23 @@ type AutopilotConfig struct { // be behind before being considered unhealthy. MaxTrailingLogs int `mapstructure:"max_trailing_logs"` - // (Enterprise-only) RedundancyZoneTag is the node tag to use for separating - // servers into zones for redundancy. If left blank, this feature will be disabled. - RedundancyZoneTag string `mapstructure:"redundancy_zone_tag"` + // (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones. + EnableRedundancyZones *bool `mapstructure:"enable_redundancy_zones"` // (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration // strategy of waiting until enough newer-versioned servers have been added to the // cluster before promoting them to voters. DisableUpgradeMigration *bool `mapstructure:"disable_upgrade_migration"` - // (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when - // performing upgrade migrations. If left blank, the Nomad version will be used. - UpgradeVersionTag string `mapstructure:"upgrade_version_tag"` + // (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom + // upgrade versions when performing migrations. + EnableCustomUpgrades *bool `mapstructure:"enable_custom_upgrades"` } // DefaultAutopilotConfig() returns the canonical defaults for the Nomad // `autopilot` configuration. func DefaultAutopilotConfig() *AutopilotConfig { return &AutopilotConfig{ - CleanupDeadServers: helper.BoolToPtr(true), LastContactThreshold: 200 * time.Millisecond, MaxTrailingLogs: 250, ServerStabilizationTime: 10 * time.Second, @@ -64,14 +62,14 @@ func (a *AutopilotConfig) Merge(b *AutopilotConfig) *AutopilotConfig { if b.MaxTrailingLogs != 0 { result.MaxTrailingLogs = b.MaxTrailingLogs } - if b.RedundancyZoneTag != "" { - result.RedundancyZoneTag = b.RedundancyZoneTag + if b.EnableRedundancyZones != nil { + result.EnableRedundancyZones = b.EnableRedundancyZones } if b.DisableUpgradeMigration != nil { result.DisableUpgradeMigration = helper.BoolToPtr(*b.DisableUpgradeMigration) } - if b.UpgradeVersionTag != "" { - result.UpgradeVersionTag = b.UpgradeVersionTag + if b.EnableCustomUpgrades != nil { + result.EnableCustomUpgrades = b.EnableCustomUpgrades } return result @@ -90,9 +88,15 @@ func (a *AutopilotConfig) Copy() *AutopilotConfig { if a.CleanupDeadServers != nil { nc.CleanupDeadServers = helper.BoolToPtr(*a.CleanupDeadServers) } + if a.EnableRedundancyZones != nil { + nc.EnableRedundancyZones = helper.BoolToPtr(*a.EnableRedundancyZones) + } if a.DisableUpgradeMigration != nil { nc.DisableUpgradeMigration = helper.BoolToPtr(*a.DisableUpgradeMigration) } + if a.EnableCustomUpgrades != nil { + nc.EnableCustomUpgrades = helper.BoolToPtr(*a.EnableCustomUpgrades) + } return nc } diff --git a/nomad/structs/config/autopilot_test.go b/nomad/structs/config/autopilot_test.go index 1dcb725a0..644541c0a 100644 --- a/nomad/structs/config/autopilot_test.go +++ b/nomad/structs/config/autopilot_test.go @@ -14,9 +14,9 @@ func TestAutopilotConfig_Merge(t *testing.T) { ServerStabilizationTime: 1 * time.Second, LastContactThreshold: 1 * time.Second, MaxTrailingLogs: 1, - RedundancyZoneTag: "1", + EnableRedundancyZones: &trueValue, DisableUpgradeMigration: &falseValue, - UpgradeVersionTag: "1", + EnableCustomUpgrades: &trueValue, } c2 := &AutopilotConfig{ @@ -24,9 +24,9 @@ func TestAutopilotConfig_Merge(t *testing.T) { ServerStabilizationTime: 2 * time.Second, LastContactThreshold: 2 * time.Second, MaxTrailingLogs: 2, - RedundancyZoneTag: "2", + EnableRedundancyZones: nil, DisableUpgradeMigration: nil, - UpgradeVersionTag: "2", + EnableCustomUpgrades: nil, } e := &AutopilotConfig{ @@ -34,9 +34,9 @@ func TestAutopilotConfig_Merge(t *testing.T) { ServerStabilizationTime: 2 * time.Second, LastContactThreshold: 2 * time.Second, MaxTrailingLogs: 2, - RedundancyZoneTag: "2", + EnableRedundancyZones: &trueValue, DisableUpgradeMigration: &falseValue, - UpgradeVersionTag: "2", + EnableCustomUpgrades: &trueValue, } result := c1.Merge(c2) diff --git a/nomad/structs/operator.go b/nomad/structs/operator.go index fe83ec86f..43bd6a420 100644 --- a/nomad/structs/operator.go +++ b/nomad/structs/operator.go @@ -1,7 +1,8 @@ package structs import ( - "github.com/hashicorp/consul/agent/consul/autopilot" + "time" + "github.com/hashicorp/raft" ) @@ -69,7 +70,7 @@ type AutopilotSetConfigRequest struct { Datacenter string // Config is the new Autopilot configuration to use. - Config autopilot.Config + Config AutopilotConfig // CAS controls whether to use check-and-set semantics for this request. CAS bool @@ -82,3 +83,39 @@ type AutopilotSetConfigRequest struct { func (op *AutopilotSetConfigRequest) RequestDatacenter() string { return op.Datacenter } + +// AutopilotConfig is the internal config for the Autopilot mechanism. +type AutopilotConfig struct { + // CleanupDeadServers controls whether to remove dead servers when a new + // server is added to the Raft peers. + CleanupDeadServers bool + + // ServerStabilizationTime is the minimum amount of time a server must be + // in a stable, healthy state before it can be added to the cluster. Only + // applicable with Raft protocol version 3 or higher. + ServerStabilizationTime time.Duration + + // LastContactThreshold is the limit on the amount of time a server can go + // without leader contact before being considered unhealthy. + LastContactThreshold time.Duration + + // MaxTrailingLogs is the amount of entries in the Raft Log that a server can + // be behind before being considered unhealthy. + MaxTrailingLogs uint64 + + // (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones. + EnableRedundancyZones bool + + // (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration + // strategy of waiting until enough newer-versioned servers have been added to the + // cluster before promoting them to voters. + DisableUpgradeMigration bool + + // (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom + // upgrade versions when performing migrations. + EnableCustomUpgrades bool + + // CreateIndex/ModifyIndex store the create/modify indexes of this configuration. + CreateIndex uint64 + ModifyIndex uint64 +} diff --git a/nomad/util.go b/nomad/util.go index 8b02a585f..be01dc418 100644 --- a/nomad/util.go +++ b/nomad/util.go @@ -46,7 +46,6 @@ type serverParts struct { MinorVersion int Build version.Version RaftVersion int - NonVoter bool Addr net.Addr RPCAddr net.Addr Status serf.MemberStatus @@ -71,7 +70,6 @@ func isNomadServer(m serf.Member) (bool, *serverParts) { region := m.Tags["region"] datacenter := m.Tags["dc"] _, bootstrap := m.Tags["bootstrap"] - _, nonVoter := m.Tags["nonvoter"] expect := 0 expectStr, ok := m.Tags["expect"] @@ -140,7 +138,6 @@ func isNomadServer(m serf.Member) (bool, *serverParts) { MinorVersion: minorVersion, Build: *buildVersion, RaftVersion: raftVsn, - NonVoter: nonVoter, Status: m.Status, } return true, parts diff --git a/website/source/api/operator.html.md b/website/source/api/operator.html.md index 98226bfe9..edc6115de 100644 --- a/website/source/api/operator.html.md +++ b/website/source/api/operator.html.md @@ -168,9 +168,9 @@ $ curl \ "LastContactThreshold": "200ms", "MaxTrailingLogs": 250, "ServerStabilizationTime": "10s", - "RedundancyZoneTag": "", + "EnableRedundancyZones": false, "DisableUpgradeMigration": false, - "UpgradeVersionTag": "", + "EnableCustomUpgrades": false, "CreateIndex": 4, "ModifyIndex": 4 } @@ -221,19 +221,16 @@ The table below shows this endpoint's support for cluster. Only takes effect if all servers are running Raft protocol version 3 or higher. Must be a duration value such as `30s`. -- `RedundancyZoneTag` `(string: "")` - Controls the node-meta key to use when - Autopilot is separating servers into zones for redundancy. Only one server in - each zone can be a voting member at one time. If left blank, this feature will - be disabled. +- `EnableRedundancyZones` `(bool: false)` - (Enterprise-only) Specifies whether + to enable redundancy zones. -- `DisableUpgradeMigration` `(bool: false)` - Disables Autopilot's upgrade - migration strategy in Nomad Enterprise of waiting until enough +- `DisableUpgradeMigration` `(bool: false)` - (Enterprise-only) Disables Autopilot's + upgrade migration strategy in Nomad Enterprise of waiting until enough newer-versioned servers have been added to the cluster before promoting any of them to voters. -- `UpgradeVersionTag` `(string: "")` - Controls the node-meta key to use for - version info when performing upgrade migrations. If left blank, the Nomad - version will be used. +- `EnableCustomUpgrades` `(bool: false)` - (Enterprise-only) Specifies whether to + enable using custom upgrade versions when performing migrations. ### Sample Payload @@ -243,9 +240,9 @@ The table below shows this endpoint's support for "LastContactThreshold": "200ms", "MaxTrailingLogs": 250, "ServerStabilizationTime": "10s", - "RedundancyZoneTag": "", + "EnableRedundancyZones": false, "DisableUpgradeMigration": false, - "UpgradeVersionTag": "", + "EnableCustomUpgrades": false, "CreateIndex": 4, "ModifyIndex": 4 } diff --git a/website/source/docs/agent/configuration/autopilot.html.md b/website/source/docs/agent/configuration/autopilot.html.md index ed13f37aa..5328c0174 100644 --- a/website/source/docs/agent/configuration/autopilot.html.md +++ b/website/source/docs/agent/configuration/autopilot.html.md @@ -18,6 +18,7 @@ description: |- The `autopilot` stanza configures the Nomad agent to configure Autopilot behavior. +For more information about Autopilot, see the [Autopilot Guide](/guides/cluster/autopilot.html). ```hcl autopilot { @@ -25,9 +26,9 @@ autopilot { last_contact_threshold = "200ms" max_trailing_logs = 250 server_stabilization_time = "10s" - redundancy_zone_tag = "" - disable_upgrade_migration = true - upgrade_version_tag = "" + enable_redundancy_zones = false + disable_upgrade_migration = false + enable_custom_upgrades = false } ``` @@ -48,17 +49,17 @@ autopilot { cluster. Only takes effect if all servers are running Raft protocol version 3 or higher. Must be a duration value such as `30s`. -- `redundancy_zone_tag` `(string: "")` - Controls the node-meta key to use when - Autopilot is separating servers into zones for redundancy. Only one server in - each zone can be a voting member at one time. If left blank, this feature will - be disabled. +- `enable_redundancy_zones` `(bool: false)` - (Enterprise-only) Controls whether + Autopilot separates servers into zones for redundancy, in conjunction with the + [redundancy_zone](/docs/agent/configuration/server.html#redundancy_zone) parameter. + Only one server in each zone can be a voting member at one time. -- `disable_upgrade_migration` `(bool: false)` - Disables Autopilot's upgrade - migration strategy in Nomad Enterprise of waiting until enough +- `disable_upgrade_migration` `(bool: false)` - (Enterprise-only) Disables Autopilot's + upgrade migration strategy in Nomad Enterprise of waiting until enough newer-versioned servers have been added to the cluster before promoting any of them to voters. -- `upgrade_version_tag` `(string: "")` - Controls the node-meta key to use for - version info when performing upgrade migrations. If left blank, the Nomad - version will be used. +- `enable_custom_upgrades` `(bool: false)` - (Enterprise-only) Specifies whether to + enable using custom upgrade versions when performing migrations, in conjunction with + the [upgrade_version](/docs/agent/configuration/server.html#upgrade_version) parameter. diff --git a/website/source/docs/agent/configuration/server.html.md b/website/source/docs/agent/configuration/server.html.md index 92854ae18..56b5e4abc 100644 --- a/website/source/docs/agent/configuration/server.html.md +++ b/website/source/docs/agent/configuration/server.html.md @@ -102,8 +102,9 @@ server { second is a tradeoff as it lowers failure detection time of nodes at the tradeoff of false positives and increased load on the leader. -- `non_voting_server` `(bool: false)` - is whether this server will act as - a non-voting member of the cluster to help provide read scalability. (Enterprise-only) +- `non_voting_server` `(bool: false)` - (Enterprise-only) Specifies whether + this server will act as a non-voting member of the cluster to help provide + read scalability. - `num_schedulers` `(int: [num-cores])` - Specifies the number of parallel scheduler threads to run. This can be as many as one per core, or `0` to @@ -120,6 +121,10 @@ server { features and is typically not required as the agent internally knows the latest version, but may be useful in some upgrade scenarios. +- `redundancy_zone` `(string: "")` - (Enterprise-only) Specifies the redundancy + zone that this server will be a part of for Autopilot management. For more + information, see the [Autopilot Guide](/guides/cluster/autopilot.html). + - `rejoin_after_leave` `(bool: false)` - Specifies if Nomad will ignore a previous leave and attempt to rejoin the cluster when starting. By default, Nomad treats leave as a permanent intent and does not attempt to join the @@ -149,6 +154,10 @@ server { [server address format](#server-address-format) section for more information on the format of the string. +- `upgrade_version` `(string: "")` - A custom version of the format X.Y.Z to use + in place of the Nomad version when custom upgrades are enabled in Autopilot. + For more information, see the [Autopilot Guide](/guides/cluster/autopilot.html). + ### Server Address Format This section describes the acceptable syntax and format for describing the diff --git a/website/source/guides/cluster/autopilot.html.md b/website/source/guides/cluster/autopilot.html.md index 24e390cb0..a3af1d0fa 100644 --- a/website/source/guides/cluster/autopilot.html.md +++ b/website/source/guides/cluster/autopilot.html.md @@ -32,9 +32,9 @@ autopilot { last_contact_threshold = 200ms max_trailing_logs = 250 server_stabilization_time = "10s" - redundancy_zone_tag = "az" + enable_redundancy_zones = false disable_upgrade_migration = false - upgrade_version_tag = "" + enable_custom_upgrades = false } ``` @@ -49,9 +49,9 @@ CleanupDeadServers = true LastContactThreshold = 200ms MaxTrailingLogs = 250 ServerStabilizationTime = 10s -RedundancyZoneTag = "" +EnableRedundancyZones = false DisableUpgradeMigration = false -UpgradeVersionTag = "" +EnableCustomUpgrades = false $ Nomad operator autopilot set-config -cleanup-dead-servers=false Configuration updated! @@ -61,9 +61,9 @@ CleanupDeadServers = false LastContactThreshold = 200ms MaxTrailingLogs = 250 ServerStabilizationTime = 10s -RedundancyZoneTag = "" +EnableRedundancyZones = false DisableUpgradeMigration = false -UpgradeVersionTag = "" +EnableCustomUpgrades = false ``` ## Dead Server Cleanup @@ -164,15 +164,21 @@ isolated failure domains such as AWS Availability Zones; users would be forced t have an overly-large quorum (2-3 nodes per AZ) or give up redundancy within an AZ by deploying just one server in each. -If the `RedundancyZoneTag` setting is set, Nomad will use its value to look for a -zone in each server's specified [`-meta`](/docs/agent/configuration/client.html#meta) -tag. For example, if `RedundancyZoneTag` is set to `zone`, and `-meta zone=east1a` -is used when starting a server, that server's redundancy zone will be `east1a`. +If the `EnableRedundancyZones` setting is set, Nomad will use its value to look for a +zone in each server's specified [`redundancy_zone`] +(/docs/agent/configuration/server.html#redundancy_zone) field. Here's an example showing how to configure this: +```hcl +/* config.hcl */ +server { + redundancy_zone = "west-1" +} ``` -$ nomad operator autopilot set-config -redundancy-zone-tag=zone + +``` +$ nomad operator autopilot set-config -enable-redundancy-zones=true Configuration updated! ``` @@ -193,11 +199,11 @@ to voters and demoting the old servers. After this is finished, the old servers safely removed from the cluster. To check the Nomad version of the servers, either the [autopilot health] -(/api/operator.html#read-health) endpoint or the `Nomad members` +(/api/operator.html#read-health) endpoint or the `nomad members` command can be used: ``` -$ Nomad members +$ nomad members Node Address Status Type Build Protocol DC node1 127.0.0.1:8301 alive server 0.7.5 2 dc1 node2 127.0.0.1:8703 alive server 0.7.5 2 dc1 @@ -207,13 +213,11 @@ node4 127.0.0.1:8203 alive server 0.8.0 2 dc1 ### Migrations Without a Nomad Version Change -The `UpgradeVersionTag` can be used to override the version information used during +The `EnableCustomUpgrades` field can be used to override the version information used during a migration, so that the migration logic can be used for updating the cluster when changing configuration. -If the `UpgradeVersionTag` setting is set, Nomad will use its value to look for a -version in each server's specified [`-meta`](/docs/agent/configuration/client.html#meta) -tag. For example, if `UpgradeVersionTag` is set to `build`, and `-meta build:0.0.2` -is used when starting a server, that server's version will be `0.0.2` when considered in -a migration. The upgrade logic will follow semantic versioning and the version string +If the `EnableCustomUpgrades` setting is set to `true`, Nomad will use its value to look for a +version in each server's specified [`upgrade_version`](/docs/agent/configuration/server.html#upgrade_version) +tag. The upgrade logic will follow semantic versioning and the `upgrade_version` must be in the form of either `X`, `X.Y`, or `X.Y.Z`. From 0a13199471d2b9fc87aa1504e06b3874f03ad2d8 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Tue, 30 Jan 2018 08:57:07 +0000 Subject: [PATCH 20/79] Update CLI to use ISO_8601 time format. This change updates the formatTime CLI helper function to return an ISO_8601 time format which will make CLI time usage more consistent and easier. Previosuly the time format was in US style format which was somewhat confusing to non US users. Closes #3806 --- command/helpers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/helpers.go b/command/helpers.go index 690220597..20a341252 100644 --- a/command/helpers.go +++ b/command/helpers.go @@ -59,7 +59,8 @@ func formatTime(t time.Time) string { // It's more confusing to display the UNIX epoch or a zero value than nothing return "" } - return t.Format("01/02/06 15:04:05 MST") + // Return ISO_8601 time format GH-3806 + return t.Format("2006-01-02T15:04:05Z07:00") } // formatUnixNanoTime is a helper for formatting time for output. From 795d84e08f0131d40eb7a0e724f8bb1841ce2d26 Mon Sep 17 00:00:00 2001 From: Wim Date: Wed, 24 Jan 2018 14:39:50 +0100 Subject: [PATCH 21/79] Service registration for IPv6 docker addresses --- client/driver/docker.go | 7 ++ website/source/docs/drivers/docker.html.md | 5 ++ .../docs/job-specification/service.html.md | 70 +++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/client/driver/docker.go b/client/driver/docker.go index 5f2dedc29..34d2f5c5e 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -217,6 +217,7 @@ type DockerDriverConfig struct { CapAdd []string `mapstructure:"cap_add"` // Flags to pass directly to cap-add CapDrop []string `mapstructure:"cap_drop"` // Flags to pass directly to cap-drop ReadonlyRootfs bool `mapstructure:"readonly_rootfs"` // Mount the container’s root filesystem as read only + UseIPv6Address bool `mapstructure:"use_ipv6_address"` // Flag to use the GlobalIPv6Address from the container as the detected IP } func sliceMergeUlimit(ulimitsRaw map[string]string) ([]docker.ULimit, error) { @@ -674,6 +675,9 @@ func (d *DockerDriver) Validate(config map[string]interface{}) error { "readonly_rootfs": { Type: fields.TypeBool, }, + "use_ipv6_address": { + Type: fields.TypeBool, + }, }, } @@ -884,6 +888,9 @@ func (d *DockerDriver) detectIP(c *docker.Container) (string, bool) { } ip = net.IPAddress + if d.driverConfig.UseIPv6Address { + ip = net.GlobalIPv6Address + } ipName = name // Don't auto-advertise IPs for default networks (bridge on diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index c75238dfd..0eca3537d 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -355,6 +355,11 @@ The `docker` driver supports the following configuration in the job spec. Only ] } ``` +* `use_ipv6_address` - (Optional) `true` or `false` (default). Use IPv6 Address + will use the containers IPv6 address (GlobalIPv6Address) when registering service checks and using + `address_mode = driver`. + See [service](/docs/job-specification/service.html) for details. + * `readonly_rootfs` - (Optional) `true` or `false` (default). Mount the container's filesystem as read only. diff --git a/website/source/docs/job-specification/service.html.md b/website/source/docs/job-specification/service.html.md index 54f429075..21d485352 100644 --- a/website/source/docs/job-specification/service.html.md +++ b/website/source/docs/job-specification/service.html.md @@ -104,6 +104,9 @@ does not automatically enable service discovery. `address_mode="driver"`. Numeric ports may be used when in driver addressing mode. + Docker and IPv6 containers: This setting is required if you want to register + the port of the (IPv6) service. See [below for examples.](#IPv6 docker containers) + - `tags` `(array: [])` - Specifies the list of tags to associate with this service. If this is not supplied, no tags will be assigned to the service when it is registered. @@ -124,6 +127,10 @@ does not automatically enable service discovery. addresses. Task will fail if driver network cannot be determined. Only implemented for Docker and rkt. + Docker and IPv6 containers: If you want to register the IPv6 address + of the container you'll have to enable this and specify `use_ipv6_address` + in the docker driver configuration. See [below for examples.](#IPv6 docker containers) + - `host` - Use the host IP and port. ### `check` Parameters @@ -140,6 +147,10 @@ scripts. [below for details.](#using-driver-address-mode) Unlike `port`, this setting is *not* inherited from the `service`. + Docker and IPv6 containers: If you want to check the IPv6 address + of the container you'll have to enable this and specify `use_ipv6_address` + in the docker driver configuration. See [below for examples.](#IPv6 docker containers) + - `args` `(array: [])` - Specifies additional arguments to the `command`. This only applies to script-based health checks. @@ -186,6 +197,9 @@ scripts. default. In Nomad 0.7.1 or later numeric ports may be used if `address_mode="driver"` is set on the check. + Docker and IPv6 containers: Using a numeric port is required if you want to + check the port of (IPv6) service. See [below for examples.](#IPv6 docker containers) + - `protocol` `(string: "http")` - Specifies the protocol for the http-based health checks. Valid options are `http` and `https`. @@ -463,6 +477,62 @@ In this case Nomad doesn't need to assign Redis any host ports. The `service` and `check` stanzas can both specify the port number to advertise and check directly since Nomad isn't managing any port assignments. +### IPv6 docker containers + +The [Docker](/docs/drivers/docker.html#use_ipv6_address) driver support the +`use_ipv6_address` parameter in it's configuration. + +Besides enabling this parameter you have to set `address_mode` parameter in +both `service` and `check` stanzas to `driver`. + +You also have explicily specify the `port` that will be registered and checked. + +For example + +```hcl +job "example" { + datacenters = ["dc1"] + group "cache" { + + task "redis" { + driver = "docker" + + config { + image = "redis:3.2" + use_ipv6_address = true + # No port map required! + } + + resources { + cpu = 500 # 500 MHz + memory = 256 # 256MB + network { + mbits = 10 + } + } + + service { + name = "ipv6-redis" + port = 6379 + address_mode = "driver" + check { + name = "ipv6-redis-check" + type = "tcp" + interval = "10s" + timeout = "2s" + port = 6379 + address_mode = "driver" + } + } + } + } +} +``` + +With IPv6 Nomad doesn't need to assign Redis any host ports. The `service` +and `check` stanzas can both specify the port number to advertise and check +directly since Nomad isn't managing any port assignments. + - - - From e8aaa934593637bd4ec0a56ef8261b2dc6da3762 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 30 Jan 2018 11:27:26 -0500 Subject: [PATCH 22/79] locks for fingerprint reads/writes --- client/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/client.go b/client/client.go index 9542f734f..d8d6516e3 100644 --- a/client/client.go +++ b/client/client.go @@ -956,7 +956,9 @@ func (c *Client) fingerprint() error { request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse + c.configLock.Lock() err = f.Fingerprint(request, &response) + c.configLock.Unlock() if err != nil { return err } @@ -988,7 +990,9 @@ func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d t case <-time.After(d): request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse + c.configLock.Lock() err := f.Fingerprint(request, &response) + c.configLock.Unlock() if err != nil { c.logger.Printf("[DEBUG] client: periodic fingerprinting for %v failed: %v", name, err) @@ -1036,9 +1040,11 @@ func (c *Client) setupDrivers() error { request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse + c.configLock.Lock() if err := d.Fingerprint(request, &response); err != nil { return err } + c.configLock.Unlock() c.updateNodeFromFingerprint(&response) From a6ecce59f35e5ec556cdd0245b27e347a85507c3 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 30 Jan 2018 11:14:05 -0800 Subject: [PATCH 23/79] Minor formatting/style updates --- website/source/guides/acl.html.markdown | 27 +++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index 09c5cb3b4..9015313c4 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -371,13 +371,15 @@ This is because the reset file is in place, but with the incorrect index. The reset file can be deleted, but Nomad will not reset the bootstrap until the index is corrected. ## Vault Integration -Hashicorp Vault has a secret backend for generating short lived Nomad tokens. As Vault has a number of +Hashicorp Vault has a secret backend for generating short-lived Nomad tokens. As Vault has a number of authentication backends, it could provide a workflow where a user or orchestration system authenticates -using an pre-existing identity service (LDAP, Okta, Amazon IAM, etc. ...) in order to obtain a short-lived +using an pre-existing identity service (LDAP, Okta, Amazon IAM, etc.) in order to obtain a short-lived Nomad token. -~> Hashicorp Vault is a standalone product with it's own set of deployment and configuration best -practices. Please review [Vault's documentation](https://www.vaultproject.io/docs/index.html) before deploying it in production. +~> Hashicorp Vault is a standalone product with it's own set of deployment and + configuration best practices. Please review [Vault's + documentation](https://www.vaultproject.io/docs/index.html) before deploying it + in production. For evaluation purposes, a Vault server in "dev" mode can be used. @@ -412,25 +414,25 @@ want to seal/unseal the Vault or play with authentication. Unseal Key: YzFfPgnLl9R1f6bLU7tGqi/PIDhDaAV/tlNDMV5Rrq0= Root Token: f84b587e-5882-bba1-a3f0-d1a3d90ca105 - ``` ### Pre-requisites - Nomad ACL system bootstrapped. -- A management token (the bootstrap token can be used, but for production systems it's recommended to -have a separate token) +- A management token (the bootstrap token can be used, but for production + systems it's recommended to have a separate token) - A set of policies created in Nomad -- An unsealed Vault server (Vault running in `dev` mode is unsealed automatically upon startup) +- An unsealed Vault server (Vault running in `dev` mode is unsealed + automatically upon startup) ### Configuration -Mount the "nomad" secret backend in Vault: +Mount the [`nomad`][nomad_backend] secret backend in Vault: ``` $ vault mount nomad Successfully mounted 'nomad' at 'nomad'! ``` -Configure access with the right address and management token: +Configure access with Nomad's address and management token: ``` $ vault write nomad/config/access \ @@ -440,7 +442,7 @@ Success! Data written to: nomad/config/access ``` Vault secret backends have the concept of roles, which are configuration units that group one or more -Vault policies to a potential identity attribute, (Like an LDAP Group membership). The name of the role +Vault policies to a potential identity attribute, (e.g. LDAP Group membership). The name of the role is specified on the path, while the mapping to policies is done by naming them in a comma separated list, for example: @@ -511,3 +513,6 @@ Modify Index = 228 Any user or process with access to Vault can now obtain short lived Nomad Tokens in order to carry out operations, thus centralising the access to Nomad tokens. + + +[nomad_backend]: https://www.vaultproject.io/docs/secrets/nomad/index.html From 47d6651c3af44dccf592edafbdb2cba8d9c31f86 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 30 Jan 2018 11:31:10 -0800 Subject: [PATCH 24/79] Mention minimum Vault version --- website/source/guides/acl.html.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/website/source/guides/acl.html.markdown b/website/source/guides/acl.html.markdown index 9015313c4..bd517c120 100644 --- a/website/source/guides/acl.html.markdown +++ b/website/source/guides/acl.html.markdown @@ -423,6 +423,7 @@ Root Token: f84b587e-5882-bba1-a3f0-d1a3d90ca105 - A set of policies created in Nomad - An unsealed Vault server (Vault running in `dev` mode is unsealed automatically upon startup) + - Vault must be version 0.9.3 or later to have the Nomad plugin ### Configuration Mount the [`nomad`][nomad_backend] secret backend in Vault: From 149494da1d7ed36a189da9090ba3761423b1fbdd Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 30 Jan 2018 17:43:51 -0500 Subject: [PATCH 25/79] changelog for PR 3814 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a2ca22a..ede7b6b2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ __BACKWARDS INCOMPATIBILITIES:__ IMPROVEMENTS: * core: A set of features (Autopilot) has been added to allow for automatic operator-friendly management of Nomad servers. For more information about Autopilot, see the [Autopilot Guide](https://www.nomadproject.io/guides/cluster/autopilot.html). [[GH-3670](https://github.com/hashicorp/nomad/pull/3670)] + * cli: Use ISO_8601 time format for cli output + [[GH-3814](https://github.com/hashicorp/nomad/pull/3814)] * client: Allow '.' in environment variable names [[GH-3760](https://github.com/hashicorp/nomad/issues/3760)] * discovery: Allow `check_restart` to be specified in the `service` stanza. [[GH-3718](https://github.com/hashicorp/nomad/issues/3718)] From c294323dd051dd86fc25ce411449881beecd0ef0 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 30 Jan 2018 14:59:00 -0800 Subject: [PATCH 26/79] Add changelog entry for #3773 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede7b6b2d..b04c6b7e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ BUG FIXES: * client: Always advertise driver IP when in driver address mode [[GH-3682](https://github.com/hashicorp/nomad/issues/3682)] * 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. + * driver/lxc: Cleanup LXC containers after errors on container startup. [[GH-3773](https://github.com/hashicorp/nomad/issues/3773)] * 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)] From 078c7d9aa9f3d23ecc68d57890961cc752743c92 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 30 Jan 2018 15:14:03 -0800 Subject: [PATCH 27/79] Fix remaining issues with autopilot change --- command/agent/config-test-fixtures/basic.hcl | 6 +++--- command/agent/http.go | 19 ------------------- command/operator_autopilot_set.go | 6 +++--- nomad/autopilot.go | 7 ++++++- nomad/util_test.go | 4 ---- .../source/guides/cluster/autopilot.html.md | 6 +++--- 6 files changed, 15 insertions(+), 33 deletions(-) diff --git a/command/agent/config-test-fixtures/basic.hcl b/command/agent/config-test-fixtures/basic.hcl index 4ddc65771..d7731efc6 100644 --- a/command/agent/config-test-fixtures/basic.hcl +++ b/command/agent/config-test-fixtures/basic.hcl @@ -83,9 +83,9 @@ server { retry_interval = "15s" rejoin_after_leave = true non_voting_server = true - redundancy_zone = "foo" - upgrade_version = "0.8.0" - encrypt = "abc" + redundancy_zone = "foo" + upgrade_version = "0.8.0" + encrypt = "abc" } acl { enabled = true diff --git a/command/agent/http.go b/command/agent/http.go index dddb4baf8..dc147b142 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -18,7 +18,6 @@ import ( assetfs "github.com/elazarl/go-bindata-assetfs" "github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/nomad/structs" - "github.com/mitchellh/mapstructure" "github.com/rs/cors" "github.com/ugorji/go/codec" ) @@ -346,24 +345,6 @@ func decodeBody(req *http.Request, out interface{}) error { return dec.Decode(&out) } -// decodeBodyFunc is used to decode a JSON request body invoking -// a given callback function -func decodeBodyFunc(req *http.Request, out interface{}, cb func(interface{}) error) error { - var raw interface{} - dec := json.NewDecoder(req.Body) - if err := dec.Decode(&raw); err != nil { - return err - } - - // Invoke the callback prior to decode - if cb != nil { - if err := cb(raw); err != nil { - return err - } - } - return mapstructure.Decode(raw, out) -} - // setIndex is used to set the index response header func setIndex(resp http.ResponseWriter, index uint64) { resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) diff --git a/command/operator_autopilot_set.go b/command/operator_autopilot_set.go index 280a0413e..3e8873279 100644 --- a/command/operator_autopilot_set.go +++ b/command/operator_autopilot_set.go @@ -19,9 +19,9 @@ func (c *OperatorAutopilotSetCommand) AutocompleteFlags() complete.Flags { "-max-trailing-logs": complete.PredictAnything, "-last-contact-threshold": complete.PredictAnything, "-server-stabilization-time": complete.PredictAnything, - "-enable-redundancy-zones": complete.PredictAnything, - "-disable-upgrade-migration": complete.PredictAnything, - "-enable-custom-upgrades": complete.PredictAnything, + "-enable-redundancy-zones": complete.PredictNothing, + "-disable-upgrade-migration": complete.PredictNothing, + "-enable-custom-upgrades": complete.PredictNothing, }) } diff --git a/nomad/autopilot.go b/nomad/autopilot.go index 4c5ea180b..399b3458f 100644 --- a/nomad/autopilot.go +++ b/nomad/autopilot.go @@ -11,7 +11,12 @@ import ( ) const ( - AutopilotRZTag = "ap_zone" + // AutopilotRZTag is the Serf tag to use for the redundancy zone value + // when passing the server metadata to Autopilot. + AutopilotRZTag = "ap_zone" + + // AutopilotRZTag is the Serf tag to use for the custom version value + // when passing the server metadata to Autopilot. AutopilotVersionTag = "ap_version" ) diff --git a/nomad/util_test.go b/nomad/util_test.go index 12db5100c..3e1db6296 100644 --- a/nomad/util_test.go +++ b/nomad/util_test.go @@ -24,7 +24,6 @@ func TestIsNomadServer(t *testing.T) { "port": "10000", "vsn": "1", "raft_vsn": "2", - "nonvoter": "1", "build": "0.7.0+ent", }, } @@ -51,9 +50,6 @@ func TestIsNomadServer(t *testing.T) { if parts.RPCAddr.String() != "1.1.1.1:10000" { t.Fatalf("bad: %v", parts.RPCAddr.String()) } - if !parts.NonVoter { - t.Fatalf("bad: %v", parts.NonVoter) - } if seg := parts.Build.Segments(); len(seg) != 3 { t.Fatalf("bad: %v", parts.Build) } else if seg[0] != 0 && seg[1] != 7 && seg[2] != 0 { diff --git a/website/source/guides/cluster/autopilot.html.md b/website/source/guides/cluster/autopilot.html.md index a3af1d0fa..6e7d53422 100644 --- a/website/source/guides/cluster/autopilot.html.md +++ b/website/source/guides/cluster/autopilot.html.md @@ -53,10 +53,10 @@ EnableRedundancyZones = false DisableUpgradeMigration = false EnableCustomUpgrades = false -$ Nomad operator autopilot set-config -cleanup-dead-servers=false +$ nomad operator autopilot set-config -cleanup-dead-servers=false Configuration updated! -$ Nomad operator autopilot get-config +$ nomad operator autopilot get-config CleanupDeadServers = false LastContactThreshold = 200ms MaxTrailingLogs = 250 @@ -203,7 +203,7 @@ To check the Nomad version of the servers, either the [autopilot health] command can be used: ``` -$ nomad members +$ nomad server-members Node Address Status Type Build Protocol DC node1 127.0.0.1:8301 alive server 0.7.5 2 dc1 node2 127.0.0.1:8703 alive server 0.7.5 2 dc1 From f72a94b6e64551f73df4ee70b92a3a584a4aa633 Mon Sep 17 00:00:00 2001 From: Kyle Havlovitz Date: Tue, 30 Jan 2018 16:10:38 -0800 Subject: [PATCH 28/79] Fix autopilot guide example --- website/source/guides/cluster/autopilot.html.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/source/guides/cluster/autopilot.html.md b/website/source/guides/cluster/autopilot.html.md index 6e7d53422..15f650732 100644 --- a/website/source/guides/cluster/autopilot.html.md +++ b/website/source/guides/cluster/autopilot.html.md @@ -204,11 +204,11 @@ command can be used: ``` $ nomad server-members -Node Address Status Type Build Protocol DC -node1 127.0.0.1:8301 alive server 0.7.5 2 dc1 -node2 127.0.0.1:8703 alive server 0.7.5 2 dc1 -node3 127.0.0.1:8803 alive server 0.7.5 2 dc1 -node4 127.0.0.1:8203 alive server 0.8.0 2 dc1 +Name Address Port Status Leader Protocol Build Datacenter Region +node1 127.0.0.1 4648 alive true 3 0.7.1 dc1 global +node2 127.0.0.1 4748 alive false 3 0.7.1 dc1 global +node3 127.0.0.1 4848 alive false 3 0.7.1 dc1 global +node4 127.0.0.1 4948 alive false 3 0.8.0 dc1 global ``` ### Migrations Without a Nomad Version Change From 51201fb7cc8d5b001782125eb0865e32788de4eb Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 31 Jan 2018 10:11:30 -0800 Subject: [PATCH 29/79] fix changelog entry --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b04c6b7e8..ae608a781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -166,8 +166,7 @@ BUG FIXES: change [[GH-3214](https://github.com/hashicorp/nomad/issues/3214)] * api: Fix search handling of jobs with more than four hyphens and case were length could cause lookup error [[GH-3203](https://github.com/hashicorp/nomad/issues/3203)] - * client: Improve the speed at which clients detect garbage collection events - [GH_-3452] + * client: Improve the speed at which clients detect garbage collection events [[GH-3452](https://github.com/hashicorp/nomad/issues/3452)] * client: Fix lock contention that could cause a node to miss a heartbeat and be marked as down [[GH-3195](https://github.com/hashicorp/nomad/issues/3195)] * client: Fix data race that could lead to concurrent map read/writes during From a9447addd374109c1ac2fb47f3798898452e5e1b Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Tue, 30 Jan 2018 12:57:37 -0500 Subject: [PATCH 30/79] add applicable boolean to fingerprint response public fields and remove getter functions --- client/client.go | 25 +++++---- client/driver/docker.go | 1 + client/driver/docker_test.go | 19 ++++++- client/driver/exec_default.go | 1 + client/driver/exec_linux.go | 1 + client/driver/exec_test.go | 7 ++- client/driver/java.go | 1 + client/driver/java_test.go | 9 ++-- client/driver/lxc.go | 1 + client/driver/lxc_test.go | 7 ++- client/driver/mock_driver.go | 1 + client/driver/qemu.go | 1 + client/driver/qemu_test.go | 11 +++- client/driver/raw_exec.go | 1 + client/driver/raw_exec_test.go | 8 ++- client/driver/rkt.go | 1 + client/driver/rkt_test.go | 9 +++- client/fingerprint/arch.go | 1 + client/fingerprint/arch_test.go | 6 ++- client/fingerprint/cgroup_linux.go | 1 + client/fingerprint/cgroup_test.go | 8 +-- client/fingerprint/consul.go | 10 ++-- client/fingerprint/consul_test.go | 27 ++++++---- client/fingerprint/cpu.go | 7 ++- client/fingerprint/cpu_test.go | 26 ++++++---- client/fingerprint/env_aws.go | 13 ++--- client/fingerprint/env_aws_test.go | 35 +++++++------ client/fingerprint/env_gce.go | 6 ++- client/fingerprint/env_gce_test.go | 55 +++++++++++--------- client/fingerprint/fingerprint_test.go | 15 +++++- client/fingerprint/host.go | 1 + client/fingerprint/host_test.go | 9 ++-- client/fingerprint/memory.go | 6 ++- client/fingerprint/memory_test.go | 11 ++-- client/fingerprint/network.go | 6 ++- client/fingerprint/network_test.go | 59 ++++++++++++++------- client/fingerprint/nomad.go | 1 + client/fingerprint/nomad_test.go | 11 ++-- client/fingerprint/signal.go | 1 + client/fingerprint/signal_test.go | 2 +- client/fingerprint/storage.go | 7 ++- client/fingerprint/storage_test.go | 19 +++---- client/fingerprint/vault.go | 1 + client/fingerprint/vault_test.go | 13 +++-- client/structs/structs.go | 72 ++++++++------------------ 45 files changed, 328 insertions(+), 205 deletions(-) diff --git a/client/client.go b/client/client.go index d8d6516e3..09e69dc36 100644 --- a/client/client.go +++ b/client/client.go @@ -951,9 +951,6 @@ func (c *Client) fingerprint() error { return err } - // Apply the finerprint to our list so that we can log it later - appliedFingerprints = append(appliedFingerprints, name) - request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse c.configLock.Lock() @@ -963,6 +960,11 @@ func (c *Client) fingerprint() error { return err } + // log the fingerprinters which have been applied + if response.Applicable { + appliedFingerprints = append(appliedFingerprints, name) + } + // add the diff found from each fingerprinter c.updateNodeFromFingerprint(&response) @@ -1029,10 +1031,6 @@ func (c *Client) setupDrivers() error { continue } - // Add this driver to our list of available drivers so that we can log it - // later - availDrivers = append(availDrivers, name) - d, err := driver.NewDriver(name, driverCtx) if err != nil { return err @@ -1046,6 +1044,11 @@ func (c *Client) setupDrivers() error { } c.configLock.Unlock() + // log the fingerprinters which have been applied + if response.Applicable { + availDrivers = append(availDrivers, name) + } + c.updateNodeFromFingerprint(&response) p, period := d.Periodic() @@ -1068,7 +1071,7 @@ func (c *Client) setupDrivers() error { func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintResponse) { c.configLock.Lock() defer c.configLock.Unlock() - for name, val := range response.GetAttributes() { + for name, val := range response.Attributes { if val == "" { delete(c.config.Node.Attributes, name) } else { @@ -1078,7 +1081,7 @@ func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintRespons // update node links and resources from the diff created from // fingerprinting - for name, val := range response.GetLinks() { + for name, val := range response.Links { if val == "" { delete(c.config.Node.Links, name) } else { @@ -1086,7 +1089,9 @@ func (c *Client) updateNodeFromFingerprint(response *cstructs.FingerprintRespons } } - c.config.Node.Resources.Merge(response.GetResources()) + if response.Resources != nil { + c.config.Node.Resources.Merge(response.Resources) + } } // retryIntv calculates a retry interval value given the base diff --git a/client/driver/docker.go b/client/driver/docker.go index 7fa709d2e..887e68aae 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -501,6 +501,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru resp.AddAttribute(dockerDriverAttr, "1") resp.AddAttribute("driver.docker.version", env.Get("Version")) + resp.Applicable = true privileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) if privileged { diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index 844f06d79..bfb3d9554 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -179,13 +179,20 @@ func TestDockerDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + attributes := response.Attributes if testutil.DockerIsConnected(t) && attributes["driver.docker"] == "" { t.Fatalf("Fingerprinter should detect when docker is available") } if attributes["driver.docker"] != "1" { t.Log("Docker daemon not available. The remainder of the docker tests will be skipped.") + } else { + + // if docker is available, make sure that the response is tagged as + // applicable + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } } t.Logf("Found docker version %s", attributes["driver.docker.version"]) @@ -225,7 +232,15 @@ func TestDockerDriver_Fingerprint_Bridge(t *testing.T) { t.Fatalf("error fingerprinting docker: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes + if attributes == nil { + t.Fatalf("expected attributes to be set") + } + if attributes["driver.docker"] == "" { t.Fatalf("expected Docker to be enabled but false was returned") } diff --git a/client/driver/exec_default.go b/client/driver/exec_default.go index 65011c3dd..a0e84e50f 100644 --- a/client/driver/exec_default.go +++ b/client/driver/exec_default.go @@ -9,5 +9,6 @@ import ( func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { d.fingerprintSuccess = helper.BoolToPtr(false) + resp.Applicable = true return nil } diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index e35c81683..cca4eb11f 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -35,5 +35,6 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } resp.AddAttribute(execDriverAttr, "1") d.fingerprintSuccess = helper.BoolToPtr(true) + resp.Applicable = true return nil } diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index ca4874dbe..1e3414d2b 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -45,7 +45,12 @@ func TestExecDriver_Fingerprint(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if response.GetAttributes()["driver.exec"] == "" { + + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if response.Attributes == nil || response.Attributes["driver.exec"] == "" { t.Fatalf("missing driver") } } diff --git a/client/driver/java.go b/client/driver/java.go index d3ee3df17..296d26990 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -170,6 +170,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct resp.AddAttribute("driver.java.runtime", info[1]) resp.AddAttribute("driver.java.vm", info[2]) d.fingerprintSuccess = helper.BoolToPtr(true) + resp.Applicable = true return nil } diff --git a/client/driver/java_test.go b/client/driver/java_test.go index 2c3ac2b2c..6c952310d 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -58,8 +58,11 @@ func TestJavaDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() - if attributes["driver.java"] != "1" && javaLocated() { + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if response.Attributes["driver.java"] != "1" && javaLocated() { if v, ok := osJavaDriverSupport[runtime.GOOS]; v && ok { t.Fatalf("missing java driver") } else { @@ -67,7 +70,7 @@ func TestJavaDriver_Fingerprint(t *testing.T) { } } for _, key := range []string{"driver.java.version", "driver.java.runtime", "driver.java.vm"} { - if attributes[key] == "" { + if response.Attributes[key] == "" { t.Fatalf("missing driver key (%s)", key) } } diff --git a/client/driver/lxc.go b/client/driver/lxc.go index 70700420c..763807a61 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -196,6 +196,7 @@ func (d *LxcDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs } resp.AddAttribute("driver.lxc.version", version) resp.AddAttribute("driver.lxc", "1") + resp.Applicable = true // Advertise if this node supports lxc volumes if d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) { diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index 5086097f5..e1ba19f6e 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -59,7 +59,12 @@ func TestLxcDriver_Fingerprint(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if response.GetAttributes()["driver.lxc"] == "" { + + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if response.Attributes["driver.lxc"] == "" { t.Fatalf("missing driver") } } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index 065c38d9d..406157e7c 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -229,6 +229,7 @@ func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct resp.RemoveAttribute("driver.mock_driver") default: resp.AddAttribute("driver.mock_driver", "1") + resp.Applicable = true } return nil } diff --git a/client/driver/qemu.go b/client/driver/qemu.go index b03c85842..9d2b025bb 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -178,6 +178,7 @@ func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct resp.AddAttribute(qemuDriverAttr, "1") resp.AddAttribute(qemuDriverVersionAttr, currentQemuVersion) + resp.Applicable = true return nil } diff --git a/client/driver/qemu_test.go b/client/driver/qemu_test.go index 3b0cf4e52..52fe05aca 100644 --- a/client/driver/qemu_test.go +++ b/client/driver/qemu_test.go @@ -43,7 +43,14 @@ func TestQemuDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes + if attributes == nil { + t.Fatalf("attributes should not be nil") + } if attributes[qemuDriverAttr] == "" { t.Fatalf("Missing Qemu driver") @@ -176,7 +183,7 @@ func TestQemuDriver_GracefulShutdown(t *testing.T) { t.Fatalf("err: %v", err) } - for name, value := range response.GetAttributes() { + for name, value := range response.Attributes { ctx.DriverCtx.node.Attributes[name] = value } diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index bb11387e1..f39a25fb4 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -98,6 +98,7 @@ func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstr if enabled || req.Config.DevMode { d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") resp.AddAttribute(rawExecDriverAttr, "1") + resp.Applicable = true return nil } diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index 62021e9cc..21d3bccbd 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -42,7 +42,7 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if response.GetAttributes()["driver.raw_exec"] != "" { + if response.Attributes["driver.raw_exec"] != "" { t.Fatalf("driver incorrectly enabled") } @@ -53,7 +53,11 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if response.GetAttributes()["driver.raw_exec"] != "1" { + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if response.Attributes["driver.raw_exec"] != "1" { t.Fatalf("driver not enabled") } } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 1ace66bd7..8c5447922 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -354,6 +354,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs resp.AddAttribute(rktDriverAttr, "1") resp.AddAttribute("driver.rkt.version", rktMatches[1]) resp.AddAttribute("driver.rkt.appc.version", appcMatches[1]) + resp.Applicable = true // Advertise if this node supports rkt volumes if d.config.ReadBoolDefault(rktVolumesConfigOption, rktVolumesConfigDefault) { diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index dd2f65699..0de3e130f 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -66,7 +66,14 @@ func TestRktDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes + if attributes == nil { + t.Fatalf("expected attributes to not equal nil") + } if attributes["driver.rkt"] != "1" { t.Fatalf("Missing Rkt driver") } diff --git a/client/fingerprint/arch.go b/client/fingerprint/arch.go index d71c86a8d..0c6c95ed2 100644 --- a/client/fingerprint/arch.go +++ b/client/fingerprint/arch.go @@ -21,5 +21,6 @@ func NewArchFingerprint(logger *log.Logger) Fingerprint { func (f *ArchFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { resp.AddAttribute("cpu.arch", runtime.GOARCH) + resp.Applicable = true return nil } diff --git a/client/fingerprint/arch_test.go b/client/fingerprint/arch_test.go index 660ff7f0c..8463f803c 100644 --- a/client/fingerprint/arch_test.go +++ b/client/fingerprint/arch_test.go @@ -21,5 +21,9 @@ func TestArchFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.GetAttributes(), "cpu.arch") + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + assertNodeAttributeContains(t, response.Attributes, "cpu.arch") } diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go index a13e17d3b..19c69b4f8 100644 --- a/client/fingerprint/cgroup_linux.go +++ b/client/fingerprint/cgroup_linux.go @@ -47,6 +47,7 @@ func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } resp.AddAttribute("unique.cgroup.mountpoint", mount) + resp.Applicable = true if f.lastState == cgroupUnavailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are available") diff --git a/client/fingerprint/cgroup_test.go b/client/fingerprint/cgroup_test.go index 93ee44eb9..2dc1d51ec 100644 --- a/client/fingerprint/cgroup_test.go +++ b/client/fingerprint/cgroup_test.go @@ -58,7 +58,7 @@ func TestCGroupFingerprint(t *testing.T) { t.Fatalf("expected an error") } - if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a != "" { + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { t.Fatalf("unexpected attribute found, %s", a) } } @@ -80,7 +80,7 @@ func TestCGroupFingerprint(t *testing.T) { if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, ok := response.GetAttributes()["unique.cgroup.mountpoint"]; !ok { + if a, ok := response.Attributes["unique.cgroup.mountpoint"]; !ok { t.Fatalf("unable to find attribute: %s", a) } } @@ -102,7 +102,7 @@ func TestCGroupFingerprint(t *testing.T) { if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a != "" { + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a != "" { t.Fatalf("unexpected attribute found, %s", a) } } @@ -123,7 +123,7 @@ func TestCGroupFingerprint(t *testing.T) { if err != nil { t.Fatalf("unexpected error, %s", err) } - if a, _ := response.GetAttributes()["unique.cgroup.mountpoint"]; a == "" { + if a, _ := response.Attributes["unique.cgroup.mountpoint"]; a == "" { t.Fatalf("expected attribute to be found, %s", a) } } diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index d4088614c..356917167 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -84,11 +84,10 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * f.logger.Printf("[WARN] fingerprint.consul: unable to fingerprint consul.datacenter") } - attributes := resp.GetAttributes() - if attributes["consul.datacenter"] != "" || attributes["unique.consul.name"] != "" { - resp.AddLink("consul", fmt.Sprintf("%s.%s", - attributes["consul.datacenter"], - attributes["unique.consul.name"])) + if dc, ok := resp.Attributes["consul.datacenter"]; ok { + if name, ok2 := resp.Attributes["unique.consul.name"]; ok2 { + resp.AddLink("consul", fmt.Sprintf("%s.%s", dc, name)) + } } else { f.logger.Printf("[WARN] fingerprint.consul: malformed Consul response prevented linking") } @@ -99,6 +98,7 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * f.logger.Printf("[INFO] fingerprint.consul: consul agent is available") } f.lastState = consulAvailable + resp.Applicable = true return nil } diff --git a/client/fingerprint/consul_test.go b/client/fingerprint/consul_test.go index 51df918b6..d97e0d6fa 100644 --- a/client/fingerprint/consul_test.go +++ b/client/fingerprint/consul_test.go @@ -35,15 +35,17 @@ func TestConsulFingerprint(t *testing.T) { t.Fatalf("Failed to fingerprint: %s", err) } - attributes := response.GetAttributes() - assertNodeAttributeContains(t, attributes, "consul.server") - assertNodeAttributeContains(t, attributes, "consul.version") - assertNodeAttributeContains(t, attributes, "consul.revision") - assertNodeAttributeContains(t, attributes, "unique.consul.name") - assertNodeAttributeContains(t, attributes, "consul.datacenter") + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } - links := response.GetLinks() - if _, ok := links["consul"]; !ok { + assertNodeAttributeContains(t, response.Attributes, "consul.server") + assertNodeAttributeContains(t, response.Attributes, "consul.version") + assertNodeAttributeContains(t, response.Attributes, "consul.revision") + assertNodeAttributeContains(t, response.Attributes, "unique.consul.name") + assertNodeAttributeContains(t, response.Attributes, "consul.datacenter") + + if _, ok := response.Links["consul"]; !ok { t.Errorf("Expected a link to consul, none found") } } @@ -187,6 +189,10 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { err := fp.Fingerprint(request, &response) assert.Nil(err) + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + attrs := []string{ "consul.server", "consul.version", @@ -195,14 +201,13 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { "consul.datacenter", } - returnedAttrs := response.GetAttributes() for _, attr := range attrs { - if v, ok := returnedAttrs[attr]; ok { + if v, ok := response.Attributes[attr]; ok { t.Errorf("unexpected node attribute %q with vlaue %q", attr, v) } } - if v, ok := node.Links["consul"]; ok { + if v, ok := response.Links["consul"]; ok { t.Errorf("Unexpected link to consul: %v", v) } } diff --git a/client/fingerprint/cpu.go b/client/fingerprint/cpu.go index 23809fc9e..8b032e0fd 100644 --- a/client/fingerprint/cpu.go +++ b/client/fingerprint/cpu.go @@ -6,6 +6,7 @@ import ( cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/helper/stats" + "github.com/hashicorp/nomad/nomad/structs" ) // CPUFingerprint is used to fingerprint the CPU @@ -23,8 +24,9 @@ func NewCPUFingerprint(logger *log.Logger) Fingerprint { func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { cfg := req.Config setResourcesCPU := func(totalCompute int) { - resources := resp.GetResources() - resources.CPU = totalCompute + resp.Resources = &structs.Resources{ + CPU: totalCompute, + } } if err := stats.Init(); err != nil { @@ -66,6 +68,7 @@ func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cst resp.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", tt)) setResourcesCPU(tt) + resp.Applicable = true return nil } diff --git a/client/fingerprint/cpu_test.go b/client/fingerprint/cpu_test.go index 1081bb987..fc7b77571 100644 --- a/client/fingerprint/cpu_test.go +++ b/client/fingerprint/cpu_test.go @@ -21,8 +21,15 @@ func TestCPUFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + // CPU info - attributes := response.GetAttributes() + attributes := response.Attributes + if attributes == nil { + t.Fatalf("expected attributes to be initialized") + } if attributes["cpu.numcores"] == "" { t.Fatalf("Missing Num Cores") } @@ -37,8 +44,7 @@ func TestCPUFingerprint(t *testing.T) { t.Fatalf("Missing CPU Total Compute") } - resources := response.GetResources() - if resources.CPU == 0 { + if response.Resources == nil || response.Resources.CPU == 0 { t.Fatalf("Expected to find CPU Resources") } } @@ -61,12 +67,15 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { t.Fatalf("err: %v", err) } - resources := response.GetResources() - if resources.CPU == 0 { + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if response.Resources.CPU == 0 { t.Fatalf("expected fingerprint of cpu of but found 0") } - originalCPU = resources.CPU + originalCPU = response.Resources.CPU } { @@ -81,9 +90,8 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { t.Fatalf("err: %v", err) } - resources := response.GetResources() - if resources.CPU != cfg.CpuCompute { - t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, resources.CPU) + if response.Resources.CPU != cfg.CpuCompute { + t.Fatalf("expected override cpu of %d but found %d", cfg.CpuCompute, response.Resources.CPU) } } } diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index b994d67eb..a8b572d0f 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -135,7 +135,7 @@ func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, re } // copy over network specific information - if val := response.GetAttributes()["unique.platform.aws.local-ipv4"]; val != "" { + if val, ok := response.Attributes["unique.platform.aws.local-ipv4"]; ok && val != "" { response.AddAttribute("unique.network.ip-address", val) newNetwork.IP = val newNetwork.CIDR = newNetwork.IP + "/32" @@ -165,14 +165,15 @@ func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, re } newNetwork.MBits = throughput - res := response.GetResources() - res.Networks = []*structs.NetworkResource{newNetwork} + response.Resources = &structs.Resources{ + Networks: []*structs.NetworkResource{newNetwork}, + } // populate Links - attributes := response.GetAttributes() response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", - attributes["platform.aws.placement.availability-zone"], - attributes["unique.platform.aws.instance-id"])) + response.Attributes["platform.aws.placement.availability-zone"], + response.Attributes["unique.platform.aws.instance-id"])) + response.Applicable = true return nil } diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index 1bf5d28e8..df924590f 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -27,7 +27,7 @@ func TestEnvAWSFingerprint_nonAws(t *testing.T) { t.Fatalf("err: %v", err) } - if len(response.GetAttributes()) > 0 { + if len(response.Attributes) > 0 { t.Fatalf("Should not apply") } } @@ -75,16 +75,16 @@ func TestEnvAWSFingerprint_aws(t *testing.T) { } for _, k := range keys { - assertNodeAttributeContains(t, response.GetAttributes(), k) + assertNodeAttributeContains(t, response.Attributes, k) } - if len(response.GetLinks()) == 0 { + if len(response.Links) == 0 { t.Fatalf("Empty links for Node in AWS Fingerprint test") } // confirm we have at least instance-id and ami-id for _, k := range []string{"aws.ec2"} { - assertNodeLinksContains(t, response.GetLinks(), k) + assertNodeLinksContains(t, response.Links, k) } } @@ -179,15 +179,14 @@ func TestNetworkFingerprint_AWS(t *testing.T) { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - res := response.GetResources() - if len(res.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := res.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -230,15 +229,18 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } - res := response.GetResources() - if len(res.Networks) == 0 { + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") + + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := res.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -270,15 +272,14 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.GetAttributes(), "unique.network.ip-address") + assertNodeAttributeContains(t, response.Attributes, "unique.network.ip-address") - res := response.GetResources() - if len(res.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := res.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to have an IP") } @@ -308,7 +309,7 @@ func TestNetworkFingerprint_notAWS(t *testing.T) { t.Fatalf("err: %v", err) } - if len(response.GetAttributes()) > 0 { + if len(response.Attributes) > 0 { t.Fatalf("Should not apply") } } diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 8352a63d4..3e717d13e 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -257,7 +257,11 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } // populate Links - resp.AddLink("gce", resp.GetAttributes()["unique.platform.gce.id"]) + if id, ok := resp.Attributes["unique.platform.gce.id"]; ok { + resp.AddLink("gce", id) + } + + resp.Applicable = true return nil } diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 49d693b72..9ccd736cd 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -27,7 +27,11 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { t.Fatalf("err: %v", err) } - if len(response.GetAttributes()) > 0 { + if response.Applicable { + t.Fatalf("expected response to not be applicable") + } + + if len(response.Attributes) > 0 { t.Fatalf("Should have zero attributes without test server") } } @@ -86,6 +90,10 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { t.Fatalf("err: %v", err) } + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + keys := []string{ "unique.platform.gce.id", "unique.platform.gce.hostname", @@ -100,44 +108,41 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { "unique.platform.gce.attr.bar", } - attributes := response.GetAttributes() - links := response.GetLinks() - for _, k := range keys { - assertNodeAttributeContains(t, attributes, k) + assertNodeAttributeContains(t, response.Attributes, k) } - if len(links) == 0 { + if len(response.Links) == 0 { t.Fatalf("Empty links for Node in GCE Fingerprint test") } // Make sure Links contains the GCE ID. for _, k := range []string{"gce"} { - assertNodeLinksContains(t, links, k) + assertNodeLinksContains(t, response.Links, k) } - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.id", "12345") - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.hostname", "instance-1.c.project.internal") - assertNodeAttributeEquals(t, attributes, "platform.gce.zone", "us-central1-f") - assertNodeAttributeEquals(t, attributes, "platform.gce.machine-type", "n1-standard-1") - assertNodeAttributeEquals(t, attributes, "platform.gce.network.default", "true") - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.ip", "10.240.0.5") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.id", "12345") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.hostname", "instance-1.c.project.internal") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.zone", "us-central1-f") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.machine-type", "n1-standard-1") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.network.default", "true") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") - } else if _, ok := attributes["unique.platform.gce.network.default.external-ip.0"]; ok { + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.0", "104.44.55.66") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.network.default.external-ip.1", "104.44.55.67") + } else if _, ok := response.Attributes["unique.platform.gce.network.default.external-ip.0"]; ok { t.Fatal("unique.platform.gce.network.default.external-ip is set without an external IP") } - assertNodeAttributeEquals(t, attributes, "platform.gce.scheduling.automatic-restart", "TRUE") - assertNodeAttributeEquals(t, attributes, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") - assertNodeAttributeEquals(t, attributes, "platform.gce.cpu-platform", "Intel Ivy Bridge") - assertNodeAttributeEquals(t, attributes, "platform.gce.tag.abc", "true") - assertNodeAttributeEquals(t, attributes, "platform.gce.tag.def", "true") - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.tag.foo", "true") - assertNodeAttributeEquals(t, attributes, "platform.gce.attr.ghi", "111") - assertNodeAttributeEquals(t, attributes, "platform.gce.attr.jkl", "222") - assertNodeAttributeEquals(t, attributes, "unique.platform.gce.attr.bar", "333") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.automatic-restart", "TRUE") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.scheduling.on-host-maintenance", "MIGRATE") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.cpu-platform", "Intel Ivy Bridge") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.abc", "true") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.tag.def", "true") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.tag.foo", "true") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.ghi", "111") + assertNodeAttributeEquals(t, response.Attributes, "platform.gce.attr.jkl", "222") + assertNodeAttributeEquals(t, response.Attributes, "unique.platform.gce.attr.bar", "333") } const GCE_routes = ` diff --git a/client/fingerprint/fingerprint_test.go b/client/fingerprint/fingerprint_test.go index 9e3dab544..41a7e6549 100644 --- a/client/fingerprint/fingerprint_test.go +++ b/client/fingerprint/fingerprint_test.go @@ -24,7 +24,7 @@ func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) *cstr t.Fatalf("Failed to fingerprint: %s", err) } - if len(response.GetAttributes()) == 0 { + if len(response.Attributes) == 0 { t.Fatalf("Failed to apply node attributes") } @@ -32,6 +32,11 @@ func assertFingerprintOK(t *testing.T, fp Fingerprint, node *structs.Node) *cstr } func assertNodeAttributeContains(t *testing.T, nodeAttributes map[string]string, attribute string) { + if nodeAttributes == nil { + t.Errorf("expected an initialized map for node attributes") + return + } + actual, found := nodeAttributes[attribute] if !found { t.Errorf("Expected to find Attribute `%s`\n\n[DEBUG] %#v", attribute, nodeAttributes) @@ -43,6 +48,10 @@ func assertNodeAttributeContains(t *testing.T, nodeAttributes map[string]string, } func assertNodeAttributeEquals(t *testing.T, nodeAttributes map[string]string, attribute string, expected string) { + if nodeAttributes == nil { + t.Errorf("expected an initialized map for node attributes") + return + } actual, found := nodeAttributes[attribute] if !found { t.Errorf("Expected to find Attribute `%s`; unable to check value\n\n[DEBUG] %#v", attribute, nodeAttributes) @@ -54,6 +63,10 @@ func assertNodeAttributeEquals(t *testing.T, nodeAttributes map[string]string, a } func assertNodeLinksContains(t *testing.T, nodeLinks map[string]string, link string) { + if nodeLinks == nil { + t.Errorf("expected an initialized map for node links") + return + } actual, found := nodeLinks[link] if !found { t.Errorf("Expected to find Link `%s`\n\n[DEBUG]", link) diff --git a/client/fingerprint/host.go b/client/fingerprint/host.go index 1273e89f8..0bc34a474 100644 --- a/client/fingerprint/host.go +++ b/client/fingerprint/host.go @@ -34,6 +34,7 @@ func (f *HostFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cs resp.AddAttribute("kernel.version", hostInfo.KernelVersion) resp.AddAttribute("unique.hostname", hostInfo.Hostname) + resp.Applicable = true return nil } diff --git a/client/fingerprint/host_test.go b/client/fingerprint/host_test.go index 433f40c6a..5dba118c5 100644 --- a/client/fingerprint/host_test.go +++ b/client/fingerprint/host_test.go @@ -21,13 +21,16 @@ func TestHostFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() - if len(attributes) == 0 { + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if len(response.Attributes) == 0 { t.Fatalf("should generate a diff of node attributes") } // Host info for _, key := range []string{"os.name", "os.version", "unique.hostname", "kernel.name"} { - assertNodeAttributeContains(t, attributes, key) + assertNodeAttributeContains(t, response.Attributes, key) } } diff --git a/client/fingerprint/memory.go b/client/fingerprint/memory.go index 473b624f4..c24937a43 100644 --- a/client/fingerprint/memory.go +++ b/client/fingerprint/memory.go @@ -5,6 +5,7 @@ import ( "log" cstructs "github.com/hashicorp/nomad/client/structs" + "github.com/hashicorp/nomad/nomad/structs" "github.com/shirou/gopsutil/mem" ) @@ -32,8 +33,9 @@ func (f *MemoryFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * if memInfo.Total > 0 { resp.AddAttribute("memory.totalbytes", fmt.Sprintf("%d", memInfo.Total)) - res := resp.GetResources() - res.MemoryMB = int(memInfo.Total / 1024 / 1024) + resp.Resources = &structs.Resources{ + MemoryMB: int(memInfo.Total / 1024 / 1024), + } } return nil diff --git a/client/fingerprint/memory_test.go b/client/fingerprint/memory_test.go index 190c6f126..1b2cebb5b 100644 --- a/client/fingerprint/memory_test.go +++ b/client/fingerprint/memory_test.go @@ -21,11 +21,12 @@ func TestMemoryFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - assertNodeAttributeContains(t, response.GetAttributes(), "memory.totalbytes") + assertNodeAttributeContains(t, response.Attributes, "memory.totalbytes") - res := response.GetResources() - if res.MemoryMB == 0 { - t.Errorf("Expected node.Resources.MemoryMB to be non-zero") + if response.Resources == nil { + t.Fatalf("response resources should not be nil") + } + if response.Resources.MemoryMB == 0 { + t.Fatalf("Expected node.Resources.MemoryMB to be non-zero") } - } diff --git a/client/fingerprint/network.go b/client/fingerprint/network.go index d9c592256..22ea9bcb5 100644 --- a/client/fingerprint/network.go +++ b/client/fingerprint/network.go @@ -95,8 +95,9 @@ func (f *NetworkFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp return err } - res := resp.GetResources() - res.Networks = nwResources + resp.Resources = &structs.Resources{ + Networks: nwResources, + } for _, nwResource := range nwResources { f.logger.Printf("[DEBUG] fingerprint.network: Detected interface %v with IP: %v", intf.Name, nwResource.IP) } @@ -105,6 +106,7 @@ func (f *NetworkFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp if len(nwResources) > 0 { resp.AddAttribute("unique.network.ip-address", nwResources[0].IP) } + resp.Applicable = true return nil } diff --git a/client/fingerprint/network_test.go b/client/fingerprint/network_test.go index eff2e119a..606cfd741 100644 --- a/client/fingerprint/network_test.go +++ b/client/fingerprint/network_test.go @@ -197,7 +197,11 @@ func TestNetworkFingerprint_basic(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes if len(attributes) == 0 { t.Fatalf("should apply (HINT: working offline? Set env %q=y", skipOnlineTestsEnvVar) } @@ -210,13 +214,12 @@ func TestNetworkFingerprint_basic(t *testing.T) { t.Fatalf("Bad IP match: %s", ip) } - resources := response.GetResources() - if resources == nil || len(resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -245,8 +248,12 @@ func TestNetworkFingerprint_default_device_absent(t *testing.T) { t.Fatalf("err: %v", err) } - if len(response.GetAttributes()) != 0 { - t.Fatalf("attributes should be zero but instead are: %v", response.GetAttributes()) + if response.Applicable { + t.Fatalf("expected response to not be applicable") + } + + if len(response.Attributes) != 0 { + t.Fatalf("attributes should be zero but instead are: %v", response.Attributes) } } @@ -264,7 +271,11 @@ func TestNetworkFingerPrint_default_device(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes if len(attributes) == 0 { t.Fatalf("should apply") } @@ -277,13 +288,12 @@ func TestNetworkFingerPrint_default_device(t *testing.T) { t.Fatalf("Bad IP match: %s", ip) } - resources := response.GetResources() - if len(resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -312,7 +322,11 @@ func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes assertNodeAttributeContains(t, attributes, "unique.network.ip-address") ip := attributes["unique.network.ip-address"] @@ -321,13 +335,12 @@ func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { t.Fatalf("Bad IP match: %s", ip) } - resources := response.GetResources() - if resources == nil || len(resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -356,7 +369,11 @@ func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + attributes := response.Attributes if len(attributes) == 0 { t.Fatalf("should apply attributes") } @@ -369,13 +386,12 @@ func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { t.Fatalf("Bad IP match: %s", ip) } - resources := response.GetResources() - if resources == nil || len(resources.Networks) == 0 { + if response.Resources == nil || len(response.Resources.Networks) == 0 { t.Fatal("Expected to find Network Resources") } // Test at least the first Network Resource - net := resources.Networks[0] + net := response.Resources.Networks[0] if net.IP == "" { t.Fatal("Expected Network Resource to not be empty") } @@ -412,7 +428,12 @@ func TestNetworkFingerPrint_LinkLocal_Disallowed(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if len(response.GetAttributes()) != 0 { + + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if len(response.Attributes) != 0 { t.Fatalf("should not apply attributes") } } diff --git a/client/fingerprint/nomad.go b/client/fingerprint/nomad.go index 32c5913bf..3b30f9e75 100644 --- a/client/fingerprint/nomad.go +++ b/client/fingerprint/nomad.go @@ -21,5 +21,6 @@ func NewNomadFingerprint(logger *log.Logger) Fingerprint { func (f *NomadFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { resp.AddAttribute("nomad.version", req.Config.Version.VersionNumber()) resp.AddAttribute("nomad.revision", req.Config.Version.Revision) + resp.Applicable = true return nil } diff --git a/client/fingerprint/nomad_test.go b/client/fingerprint/nomad_test.go index 3daa97d47..bdab1b9e9 100644 --- a/client/fingerprint/nomad_test.go +++ b/client/fingerprint/nomad_test.go @@ -30,16 +30,19 @@ func TestNomadFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - attributes := response.GetAttributes() - if len(attributes) == 0 { + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + if len(response.Attributes) == 0 { t.Fatalf("should apply") } - if attributes["nomad.version"] != v { + if response.Attributes["nomad.version"] != v { t.Fatalf("incorrect version") } - if attributes["nomad.revision"] != r { + if response.Attributes["nomad.revision"] != r { t.Fatalf("incorrect revision") } } diff --git a/client/fingerprint/signal.go b/client/fingerprint/signal.go index dbf0669f2..139f4c3fb 100644 --- a/client/fingerprint/signal.go +++ b/client/fingerprint/signal.go @@ -28,5 +28,6 @@ func (f *SignalFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } resp.AddAttribute("os.signals", strings.Join(sigs, ",")) + resp.Applicable = true return nil } diff --git a/client/fingerprint/signal_test.go b/client/fingerprint/signal_test.go index 6d05456f7..bf61f7544 100644 --- a/client/fingerprint/signal_test.go +++ b/client/fingerprint/signal_test.go @@ -13,5 +13,5 @@ func TestSignalFingerprint(t *testing.T) { } response := assertFingerprintOK(t, fp, node) - assertNodeAttributeContains(t, response.GetAttributes(), "os.signals") + assertNodeAttributeContains(t, response.Attributes, "os.signals") } diff --git a/client/fingerprint/storage.go b/client/fingerprint/storage.go index 963325316..7ec43104d 100644 --- a/client/fingerprint/storage.go +++ b/client/fingerprint/storage.go @@ -7,6 +7,7 @@ import ( "strconv" cstructs "github.com/hashicorp/nomad/client/structs" + "github.com/hashicorp/nomad/nomad/structs" ) const bytesPerMegabyte = 1024 * 1024 @@ -46,8 +47,10 @@ func (f *StorageFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp resp.AddAttribute("unique.storage.bytesfree", strconv.FormatUint(free, 10)) // set the disk size for the response - res := resp.GetResources() - res.DiskMB = int(free / bytesPerMegabyte) + resp.Resources = &structs.Resources{ + DiskMB: int(free / bytesPerMegabyte), + } + resp.Applicable = true return nil } diff --git a/client/fingerprint/storage_test.go b/client/fingerprint/storage_test.go index 8d26a6bcb..86b15cecb 100644 --- a/client/fingerprint/storage_test.go +++ b/client/fingerprint/storage_test.go @@ -15,17 +15,19 @@ func TestStorageFingerprint(t *testing.T) { response := assertFingerprintOK(t, fp, node) - attributes := response.GetAttributes() + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } - assertNodeAttributeContains(t, attributes, "unique.storage.volume") - assertNodeAttributeContains(t, attributes, "unique.storage.bytestotal") - assertNodeAttributeContains(t, attributes, "unique.storage.bytesfree") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.volume") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytestotal") + assertNodeAttributeContains(t, response.Attributes, "unique.storage.bytesfree") - total, err := strconv.ParseInt(attributes["unique.storage.bytestotal"], 10, 64) + total, err := strconv.ParseInt(response.Attributes["unique.storage.bytestotal"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytestotal: %s", err) } - free, err := strconv.ParseInt(attributes["unique.storage.bytesfree"], 10, 64) + free, err := strconv.ParseInt(response.Attributes["unique.storage.bytesfree"], 10, 64) if err != nil { t.Fatalf("Failed to parse unique.storage.bytesfree: %s", err) } @@ -34,11 +36,10 @@ func TestStorageFingerprint(t *testing.T) { t.Fatalf("unique.storage.bytesfree %d is larger than unique.storage.bytestotal %d", free, total) } - resources := response.GetResources() - if resources == nil { + if response.Resources == nil { t.Fatalf("Node Resources was nil") } - if resources.DiskMB == 0 { + if response.Resources.DiskMB == 0 { t.Errorf("Expected node.Resources.DiskMB to be non-zero") } } diff --git a/client/fingerprint/vault.go b/client/fingerprint/vault.go index 04d8e1eba..b7cee695f 100644 --- a/client/fingerprint/vault.go +++ b/client/fingerprint/vault.go @@ -74,6 +74,7 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c f.logger.Printf("[INFO] fingerprint.vault: Vault is available") } f.lastState = vaultAvailable + resp.Applicable = true return nil } diff --git a/client/fingerprint/vault_test.go b/client/fingerprint/vault_test.go index f036e280e..76eb347e1 100644 --- a/client/fingerprint/vault_test.go +++ b/client/fingerprint/vault_test.go @@ -28,9 +28,12 @@ func TestVaultFingerprint(t *testing.T) { t.Fatalf("Failed to fingerprint: %s", err) } - attributes := response.GetAttributes() - assertNodeAttributeContains(t, attributes, "vault.accessible") - assertNodeAttributeContains(t, attributes, "vault.version") - assertNodeAttributeContains(t, attributes, "vault.cluster_id") - assertNodeAttributeContains(t, attributes, "vault.cluster_name") + if !response.Applicable { + t.Fatalf("expected response to be applicable") + } + + assertNodeAttributeContains(t, response.Attributes, "vault.accessible") + assertNodeAttributeContains(t, response.Attributes, "vault.version") + assertNodeAttributeContains(t, response.Attributes, "vault.cluster_id") + assertNodeAttributeContains(t, response.Attributes, "vault.cluster_name") } diff --git a/client/structs/structs.go b/client/structs/structs.go index 9d21b4362..98980da79 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -194,80 +194,54 @@ type FingerprintRequest struct { } type FingerprintResponse struct { - attributes map[string]string - links map[string]string - resources *structs.Resources + Attributes map[string]string + Links map[string]string + Resources *structs.Resources + + // Applicable is a boolean indicating whether the fingerprint should be + // applied + Applicable bool } // AddAttribute adds the name and value for a node attribute to the fingerprint // response func (f *FingerprintResponse) AddAttribute(name, value string) { - // initialize attributes if it has not been already - if f.attributes == nil { - f.attributes = make(map[string]string, 0) + // initialize Attributes if it has not been already + if f.Attributes == nil { + f.Attributes = make(map[string]string, 0) } - f.attributes[name] = value + f.Attributes[name] = value } // RemoveAttribute sets the given attribute to empty, which will later remove // it entirely from the node func (f *FingerprintResponse) RemoveAttribute(name string) { - // initialize attributes if it has not been already - if f.attributes == nil { - f.attributes = make(map[string]string, 0) + // initialize Attributes if it has not been already + if f.Attributes == nil { + f.Attributes = make(map[string]string, 0) } - f.attributes[name] = "" -} - -// GetAttributes fetches the attributes for the fingerprint response -func (f *FingerprintResponse) GetAttributes() map[string]string { - // initialize attributes if it has not been already - if f.attributes == nil { - f.attributes = make(map[string]string, 0) - } - - return f.attributes + f.Attributes[name] = "" } // AddLink adds a link entry to the fingerprint response func (f *FingerprintResponse) AddLink(name, value string) { - // initialize links if it has not been already - if f.links == nil { - f.links = make(map[string]string, 0) + // initialize Links if it has not been already + if f.Links == nil { + f.Links = make(map[string]string, 0) } - f.links[name] = value + f.Links[name] = value } // RemoveLink removes a link entry from the fingerprint response. This will // later remove it entirely from the node func (f *FingerprintResponse) RemoveLink(name string) { - // initialize links if it has not been already - if f.links == nil { - f.links = make(map[string]string, 0) + // initialize Links if it has not been already + if f.Links == nil { + f.Links = make(map[string]string, 0) } - f.links[name] = "" -} - -// GetLinks returns the links for the fingerprint response -func (f *FingerprintResponse) GetLinks() map[string]string { - // initialize links if it has not been already - if f.links == nil { - f.links = make(map[string]string, 0) - } - - return f.links -} - -// GetResources returns the resources for a fingerprint response -func (f *FingerprintResponse) GetResources() *structs.Resources { - // initialize resourcesif it has not been already - if f.resources == nil { - f.resources = &structs.Resources{} - } - - return f.resources + f.Links[name] = "" } From 32174fd7298ae66fec4afcdd94042a1207d6d0c4 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 31 Jan 2018 13:33:51 -0500 Subject: [PATCH 31/79] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae608a781..5602c4fae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ __BACKWARDS INCOMPATIBILITIES:__ in HTTP check paths will now fail to validate. [[GH-3685](https://github.com/hashicorp/nomad/issues/3685)] IMPROVEMENTS: + * core: Allow upgrading/downgrading TLS via SIGHUP on both servers and clients [[GH-3492](https://github.com/hashicorp/nomad/issues/3492)] * core: A set of features (Autopilot) has been added to allow for automatic operator-friendly management of Nomad servers. For more information about Autopilot, see the [Autopilot Guide](https://www.nomadproject.io/guides/cluster/autopilot.html). [[GH-3670](https://github.com/hashicorp/nomad/pull/3670)] * cli: Use ISO_8601 time format for cli output [[GH-3814](https://github.com/hashicorp/nomad/pull/3814)] @@ -19,7 +20,6 @@ IMPROVEMENTS: BUG FIXES: * core: Fix search endpoint forwarding for multi-region clusters [[GH-3680](https://github.com/hashicorp/nomad/issues/3680)] - * core: Allow upgrading/downgrading TLS via SIGHUP on both servers and clients [[GH-3492](https://github.com/hashicorp/nomad/issues/3492)] * 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)] From dea460bb5aaeb31f16c59fefd08da91292a4f811 Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 1 Feb 2018 00:01:25 +0100 Subject: [PATCH 32/79] * Change use_ipv6_address to advertise_ipv6_address. * Set autoadvertise to true. * Update documentation. --- client/driver/docker.go | 95 ++++++++++--------- website/source/docs/drivers/docker.html.md | 8 +- .../docs/job-specification/service.html.md | 83 +++++++++++----- 3 files changed, 109 insertions(+), 77 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 34d2f5c5e..70511e81d 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -173,51 +173,51 @@ type DockerVolumeDriverConfig struct { // DockerDriverConfig defines the user specified config block in a jobspec type DockerDriverConfig struct { - ImageName string `mapstructure:"image"` // Container's Image Name - LoadImage string `mapstructure:"load"` // LoadImage is a path to an image archive file - Command string `mapstructure:"command"` // The Command to run when the container starts up - Args []string `mapstructure:"args"` // The arguments to the Command - Entrypoint []string `mapstructure:"entrypoint"` // Override the containers entrypoint - IpcMode string `mapstructure:"ipc_mode"` // The IPC mode of the container - host and none - NetworkMode string `mapstructure:"network_mode"` // The network mode of the container - host, nat and none - NetworkAliases []string `mapstructure:"network_aliases"` // The network-scoped alias for the container - IPv4Address string `mapstructure:"ipv4_address"` // The container ipv4 address - IPv6Address string `mapstructure:"ipv6_address"` // the container ipv6 address - PidMode string `mapstructure:"pid_mode"` // The PID mode of the container - host and none - UTSMode string `mapstructure:"uts_mode"` // The UTS mode of the container - host and none - UsernsMode string `mapstructure:"userns_mode"` // The User namespace mode of the container - host and none - PortMapRaw []map[string]string `mapstructure:"port_map"` // - PortMap map[string]int `mapstructure:"-"` // A map of host port labels and the ports exposed on the container - Privileged bool `mapstructure:"privileged"` // Flag to run the container in privileged mode - SysctlRaw []map[string]string `mapstructure:"sysctl"` // - Sysctl map[string]string `mapstructure:"-"` // The sysctl custom configurations - UlimitRaw []map[string]string `mapstructure:"ulimit"` // - Ulimit []docker.ULimit `mapstructure:"-"` // The ulimit custom configurations - DNSServers []string `mapstructure:"dns_servers"` // DNS Server for containers - DNSSearchDomains []string `mapstructure:"dns_search_domains"` // DNS Search domains for containers - DNSOptions []string `mapstructure:"dns_options"` // DNS Options - ExtraHosts []string `mapstructure:"extra_hosts"` // Add host to /etc/hosts (host:IP) - Hostname string `mapstructure:"hostname"` // Hostname for containers - LabelsRaw []map[string]string `mapstructure:"labels"` // - Labels map[string]string `mapstructure:"-"` // Labels to set when the container starts up - Auth []DockerDriverAuth `mapstructure:"auth"` // Authentication credentials for a private Docker registry - AuthSoftFail bool `mapstructure:"auth_soft_fail"` // Soft-fail if auth creds are provided but fail - TTY bool `mapstructure:"tty"` // Allocate a Pseudo-TTY - Interactive bool `mapstructure:"interactive"` // Keep STDIN open even if not attached - ShmSize int64 `mapstructure:"shm_size"` // Size of /dev/shm of the container in bytes - WorkDir string `mapstructure:"work_dir"` // Working directory inside the container - Logging []DockerLoggingOpts `mapstructure:"logging"` // Logging options for syslog server - Volumes []string `mapstructure:"volumes"` // Host-Volumes to mount in, syntax: /path/to/host/directory:/destination/path/in/container - Mounts []DockerMount `mapstructure:"mounts"` // Docker volumes to mount - VolumeDriver string `mapstructure:"volume_driver"` // Docker volume driver used for the container's volumes - ForcePull bool `mapstructure:"force_pull"` // Always force pull before running image, useful if your tags are mutable - MacAddress string `mapstructure:"mac_address"` // Pin mac address to container - SecurityOpt []string `mapstructure:"security_opt"` // Flags to pass directly to security-opt - Devices []DockerDevice `mapstructure:"devices"` // To allow mounting USB or other serial control devices - CapAdd []string `mapstructure:"cap_add"` // Flags to pass directly to cap-add - CapDrop []string `mapstructure:"cap_drop"` // Flags to pass directly to cap-drop - ReadonlyRootfs bool `mapstructure:"readonly_rootfs"` // Mount the container’s root filesystem as read only - UseIPv6Address bool `mapstructure:"use_ipv6_address"` // Flag to use the GlobalIPv6Address from the container as the detected IP + ImageName string `mapstructure:"image"` // Container's Image Name + LoadImage string `mapstructure:"load"` // LoadImage is a path to an image archive file + Command string `mapstructure:"command"` // The Command to run when the container starts up + Args []string `mapstructure:"args"` // The arguments to the Command + Entrypoint []string `mapstructure:"entrypoint"` // Override the containers entrypoint + IpcMode string `mapstructure:"ipc_mode"` // The IPC mode of the container - host and none + NetworkMode string `mapstructure:"network_mode"` // The network mode of the container - host, nat and none + NetworkAliases []string `mapstructure:"network_aliases"` // The network-scoped alias for the container + IPv4Address string `mapstructure:"ipv4_address"` // The container ipv4 address + IPv6Address string `mapstructure:"ipv6_address"` // the container ipv6 address + PidMode string `mapstructure:"pid_mode"` // The PID mode of the container - host and none + UTSMode string `mapstructure:"uts_mode"` // The UTS mode of the container - host and none + UsernsMode string `mapstructure:"userns_mode"` // The User namespace mode of the container - host and none + PortMapRaw []map[string]string `mapstructure:"port_map"` // + PortMap map[string]int `mapstructure:"-"` // A map of host port labels and the ports exposed on the container + Privileged bool `mapstructure:"privileged"` // Flag to run the container in privileged mode + SysctlRaw []map[string]string `mapstructure:"sysctl"` // + Sysctl map[string]string `mapstructure:"-"` // The sysctl custom configurations + UlimitRaw []map[string]string `mapstructure:"ulimit"` // + Ulimit []docker.ULimit `mapstructure:"-"` // The ulimit custom configurations + DNSServers []string `mapstructure:"dns_servers"` // DNS Server for containers + DNSSearchDomains []string `mapstructure:"dns_search_domains"` // DNS Search domains for containers + DNSOptions []string `mapstructure:"dns_options"` // DNS Options + ExtraHosts []string `mapstructure:"extra_hosts"` // Add host to /etc/hosts (host:IP) + Hostname string `mapstructure:"hostname"` // Hostname for containers + LabelsRaw []map[string]string `mapstructure:"labels"` // + Labels map[string]string `mapstructure:"-"` // Labels to set when the container starts up + Auth []DockerDriverAuth `mapstructure:"auth"` // Authentication credentials for a private Docker registry + AuthSoftFail bool `mapstructure:"auth_soft_fail"` // Soft-fail if auth creds are provided but fail + TTY bool `mapstructure:"tty"` // Allocate a Pseudo-TTY + Interactive bool `mapstructure:"interactive"` // Keep STDIN open even if not attached + ShmSize int64 `mapstructure:"shm_size"` // Size of /dev/shm of the container in bytes + WorkDir string `mapstructure:"work_dir"` // Working directory inside the container + Logging []DockerLoggingOpts `mapstructure:"logging"` // Logging options for syslog server + Volumes []string `mapstructure:"volumes"` // Host-Volumes to mount in, syntax: /path/to/host/directory:/destination/path/in/container + Mounts []DockerMount `mapstructure:"mounts"` // Docker volumes to mount + VolumeDriver string `mapstructure:"volume_driver"` // Docker volume driver used for the container's volumes + ForcePull bool `mapstructure:"force_pull"` // Always force pull before running image, useful if your tags are mutable + MacAddress string `mapstructure:"mac_address"` // Pin mac address to container + SecurityOpt []string `mapstructure:"security_opt"` // Flags to pass directly to security-opt + Devices []DockerDevice `mapstructure:"devices"` // To allow mounting USB or other serial control devices + CapAdd []string `mapstructure:"cap_add"` // Flags to pass directly to cap-add + CapDrop []string `mapstructure:"cap_drop"` // Flags to pass directly to cap-drop + ReadonlyRootfs bool `mapstructure:"readonly_rootfs"` // Mount the container’s root filesystem as read only + AdvertiseIPv6Address bool `mapstructure:"advertise_ipv6_address"` // Flag to use the GlobalIPv6Address from the container as the detected IP } func sliceMergeUlimit(ulimitsRaw map[string]string) ([]docker.ULimit, error) { @@ -675,7 +675,7 @@ func (d *DockerDriver) Validate(config map[string]interface{}) error { "readonly_rootfs": { Type: fields.TypeBool, }, - "use_ipv6_address": { + "advertise_ipv6_address": { Type: fields.TypeBool, }, }, @@ -888,8 +888,9 @@ func (d *DockerDriver) detectIP(c *docker.Container) (string, bool) { } ip = net.IPAddress - if d.driverConfig.UseIPv6Address { + if d.driverConfig.AdvertiseIPv6Address { ip = net.GlobalIPv6Address + auto = true } ipName = name diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index 0eca3537d..99ef6de58 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -355,11 +355,9 @@ The `docker` driver supports the following configuration in the job spec. Only ] } ``` -* `use_ipv6_address` - (Optional) `true` or `false` (default). Use IPv6 Address - will use the containers IPv6 address (GlobalIPv6Address) when registering service checks and using - `address_mode = driver`. - See [service](/docs/job-specification/service.html) for details. - +* `advertise_ipv6_address` - (Optional) `true` or `false` (default). Use the container's + IPv6 address (GlobalIPv6Address in Docker) when registering services and checks. + See [IPv6 Docker containers](/docs/job-specification/service.html#IPv6 Docker containers) for details. * `readonly_rootfs` - (Optional) `true` or `false` (default). Mount the container's filesystem as read only. diff --git a/website/source/docs/job-specification/service.html.md b/website/source/docs/job-specification/service.html.md index 21d485352..ff5fcdba5 100644 --- a/website/source/docs/job-specification/service.html.md +++ b/website/source/docs/job-specification/service.html.md @@ -104,9 +104,6 @@ does not automatically enable service discovery. `address_mode="driver"`. Numeric ports may be used when in driver addressing mode. - Docker and IPv6 containers: This setting is required if you want to register - the port of the (IPv6) service. See [below for examples.](#IPv6 docker containers) - - `tags` `(array: [])` - Specifies the list of tags to associate with this service. If this is not supplied, no tags will be assigned to the service when it is registered. @@ -127,10 +124,6 @@ does not automatically enable service discovery. addresses. Task will fail if driver network cannot be determined. Only implemented for Docker and rkt. - Docker and IPv6 containers: If you want to register the IPv6 address - of the container you'll have to enable this and specify `use_ipv6_address` - in the docker driver configuration. See [below for examples.](#IPv6 docker containers) - - `host` - Use the host IP and port. ### `check` Parameters @@ -147,10 +140,6 @@ scripts. [below for details.](#using-driver-address-mode) Unlike `port`, this setting is *not* inherited from the `service`. - Docker and IPv6 containers: If you want to check the IPv6 address - of the container you'll have to enable this and specify `use_ipv6_address` - in the docker driver configuration. See [below for examples.](#IPv6 docker containers) - - `args` `(array: [])` - Specifies additional arguments to the `command`. This only applies to script-based health checks. @@ -197,9 +186,6 @@ scripts. default. In Nomad 0.7.1 or later numeric ports may be used if `address_mode="driver"` is set on the check. - Docker and IPv6 containers: Using a numeric port is required if you want to - check the port of (IPv6) service. See [below for examples.](#IPv6 docker containers) - - `protocol` `(string: "http")` - Specifies the protocol for the http-based health checks. Valid options are `http` and `https`. @@ -477,17 +463,21 @@ In this case Nomad doesn't need to assign Redis any host ports. The `service` and `check` stanzas can both specify the port number to advertise and check directly since Nomad isn't managing any port assignments. -### IPv6 docker containers +### IPv6 Docker containers -The [Docker](/docs/drivers/docker.html#use_ipv6_address) driver support the -`use_ipv6_address` parameter in it's configuration. +The [Docker](/docs/drivers/docker.html#advertise_ipv6_address) driver supports the +`advertise_ipv6_address` parameter in it's configuration. -Besides enabling this parameter you have to set `address_mode` parameter in -both `service` and `check` stanzas to `driver`. +For the `service`stanza is no explicit `address_mode` required. +Services default to the `auto` address mode. -You also have explicily specify the `port` that will be registered and checked. +Unlike services, checks do not have an `auto` address mode as there's no way +for Nomad to know which is the best address to use for checks. Consul needs +access to the address for any HTTP or TCP checks. -For example +So you have to set `address_mode` parameter in the `check` stanza to `driver`. + +For example using `auto` address mode: ```hcl job "example" { @@ -499,7 +489,51 @@ job "example" { config { image = "redis:3.2" - use_ipv6_address = true + advertise_ipv6_address = true + port_map { + db = 6379 + } + } + + resources { + cpu = 500 # 500 MHz + memory = 256 # 256MB + network { + mbits = 10 + port "db" {} + } + } + + service { + name = "ipv6-redis" + port = db + check { + name = "ipv6-redis-check" + type = "tcp" + interval = "10s" + timeout = "2s" + port = db + address_mode = "driver" + } + } + } + } +} +``` + +Or using `address_mode=driver` for `service` and `check` with numeric ports: + +```hcl +job "example" { + datacenters = ["dc1"] + group "cache" { + + task "redis" { + driver = "docker" + + config { + image = "redis:3.2" + advertise_ipv6_address = true # No port map required! } @@ -529,9 +563,8 @@ job "example" { } ``` -With IPv6 Nomad doesn't need to assign Redis any host ports. The `service` -and `check` stanzas can both specify the port number to advertise and check -directly since Nomad isn't managing any port assignments. +The `service` and `check` stanzas can both specify the port number to +advertise and check directly since Nomad isn't managing any port assignments. - - - From ba2ebbc7f93ad926a84e0c43af74eea7f999baf7 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Wed, 31 Jan 2018 17:03:55 -0500 Subject: [PATCH 33/79] code review fixup --- client/client.go | 9 +++--- client/client_test.go | 4 +-- client/driver/docker.go | 2 +- client/driver/docker_test.go | 4 +-- client/driver/exec_default.go | 2 +- client/driver/exec_linux.go | 4 +-- client/driver/exec_test.go | 2 +- client/driver/java.go | 4 +-- client/driver/java_test.go | 2 +- client/driver/lxc.go | 2 +- client/driver/lxc_test.go | 2 +- client/driver/mock_driver.go | 48 +++++++++++++++--------------- client/driver/qemu.go | 2 +- client/driver/qemu_test.go | 2 +- client/driver/raw_exec.go | 2 +- client/driver/raw_exec_test.go | 2 +- client/driver/rkt.go | 2 +- client/driver/rkt_test.go | 2 +- client/fingerprint/arch.go | 2 +- client/fingerprint/arch_test.go | 2 +- client/fingerprint/cgroup_linux.go | 2 +- client/fingerprint/consul.go | 2 +- client/fingerprint/consul_test.go | 4 +-- client/fingerprint/cpu.go | 2 +- client/fingerprint/cpu_test.go | 4 +-- client/fingerprint/env_aws.go | 2 +- client/fingerprint/env_aws_test.go | 2 +- client/fingerprint/env_gce.go | 2 +- client/fingerprint/env_gce_test.go | 4 +-- client/fingerprint/fingerprint.go | 10 ------- client/fingerprint/host.go | 2 +- client/fingerprint/host_test.go | 2 +- client/fingerprint/network.go | 2 +- client/fingerprint/network_test.go | 12 ++++---- client/fingerprint/nomad.go | 2 +- client/fingerprint/nomad_test.go | 2 +- client/fingerprint/signal.go | 2 +- client/fingerprint/storage.go | 2 +- client/fingerprint/storage_test.go | 2 +- client/fingerprint/vault.go | 2 +- client/fingerprint/vault_test.go | 2 +- client/structs/structs.go | 6 ++-- 42 files changed, 82 insertions(+), 91 deletions(-) diff --git a/client/client.go b/client/client.go index 09e69dc36..0d749ffe1 100644 --- a/client/client.go +++ b/client/client.go @@ -961,7 +961,7 @@ func (c *Client) fingerprint() error { } // log the fingerprinters which have been applied - if response.Applicable { + if response.Detected { appliedFingerprints = append(appliedFingerprints, name) } @@ -1039,13 +1039,14 @@ func (c *Client) setupDrivers() error { request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse c.configLock.Lock() - if err := d.Fingerprint(request, &response); err != nil { + err = d.Fingerprint(request, &response) + c.configLock.Unlock() + if err != nil { return err } - c.configLock.Unlock() // log the fingerprinters which have been applied - if response.Applicable { + if response.Detected { availDrivers = append(availDrivers, name) } diff --git a/client/client_test.go b/client/client_test.go index 02226ac4f..fc749edf6 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -261,8 +261,8 @@ func TestClient_Fingerprint_Periodic(t *testing.T) { c1 := testClient(t, func(c *config.Config) { c.Options = map[string]string{ - "test.shutdown_periodic_after": "true", - "test.shutdown_periodic_duration": "3", + driver.ShutdownPeriodicAfter: "true", + driver.ShutdownPeriodicDuration: "3", } }) defer c1.Shutdown() diff --git a/client/driver/docker.go b/client/driver/docker.go index 887e68aae..c95c6ed11 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -501,7 +501,7 @@ func (d *DockerDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstru resp.AddAttribute(dockerDriverAttr, "1") resp.AddAttribute("driver.docker.version", env.Get("Version")) - resp.Applicable = true + resp.Detected = true privileged := d.config.ReadBoolDefault(dockerPrivilegedConfigOption, false) if privileged { diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index bfb3d9554..17fce4014 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -190,7 +190,7 @@ func TestDockerDriver_Fingerprint(t *testing.T) { // if docker is available, make sure that the response is tagged as // applicable - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } } @@ -232,7 +232,7 @@ func TestDockerDriver_Fingerprint_Bridge(t *testing.T) { t.Fatalf("error fingerprinting docker: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/exec_default.go b/client/driver/exec_default.go index a0e84e50f..05a609e9d 100644 --- a/client/driver/exec_default.go +++ b/client/driver/exec_default.go @@ -9,6 +9,6 @@ import ( func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { d.fingerprintSuccess = helper.BoolToPtr(false) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index cca4eb11f..b88226ffb 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -16,7 +16,7 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct // Only enable if cgroups are available and we are root if !cgroupsMounted(req.Node) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { - d.logger.Printf("[DEBUG] driver.exec: cgroups unavailable, disabling") + d.logger.Printf("[INFO] driver.exec: cgroups unavailable, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) resp.RemoveAttribute(execDriverAttr) @@ -35,6 +35,6 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } resp.AddAttribute(execDriverAttr, "1") d.fingerprintSuccess = helper.BoolToPtr(true) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index 1e3414d2b..d854c56a0 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -46,7 +46,7 @@ func TestExecDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/java.go b/client/driver/java.go index 296d26990..3e0a831ec 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -115,7 +115,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct // Only enable if we are root and cgroups are mounted when running on linux systems. if runtime.GOOS == "linux" && (syscall.Geteuid() != 0 || !cgroupsMounted(req.Node)) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { - d.logger.Printf("[DEBUG] driver.java: root privileges and mounted cgroups required on linux, disabling") + d.logger.Printf("[INFO] driver.java: root privileges and mounted cgroups required on linux, disabling") } d.fingerprintSuccess = helper.BoolToPtr(false) resp.RemoveAttribute(javaDriverAttr) @@ -170,7 +170,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct resp.AddAttribute("driver.java.runtime", info[1]) resp.AddAttribute("driver.java.vm", info[2]) d.fingerprintSuccess = helper.BoolToPtr(true) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/driver/java_test.go b/client/driver/java_test.go index 6c952310d..f273869a7 100644 --- a/client/driver/java_test.go +++ b/client/driver/java_test.go @@ -58,7 +58,7 @@ func TestJavaDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/lxc.go b/client/driver/lxc.go index 763807a61..c13a3c4cf 100644 --- a/client/driver/lxc.go +++ b/client/driver/lxc.go @@ -196,7 +196,7 @@ func (d *LxcDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs } resp.AddAttribute("driver.lxc.version", version) resp.AddAttribute("driver.lxc", "1") - resp.Applicable = true + resp.Detected = true // Advertise if this node supports lxc volumes if d.config.ReadBoolDefault(lxcVolumesConfigOption, lxcVolumesConfigDefault) { diff --git a/client/driver/lxc_test.go b/client/driver/lxc_test.go index e1ba19f6e..2ee416a32 100644 --- a/client/driver/lxc_test.go +++ b/client/driver/lxc_test.go @@ -60,7 +60,7 @@ func TestLxcDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index 406157e7c..4c7085533 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -16,11 +16,23 @@ import ( "github.com/mitchellh/mapstructure" dstructs "github.com/hashicorp/nomad/client/driver/structs" - "github.com/hashicorp/nomad/client/fingerprint" cstructs "github.com/hashicorp/nomad/client/structs" "github.com/hashicorp/nomad/nomad/structs" ) +const ( + + // ShutdownPeriodicAfter is a config key that can be used during tests to + // "stop" a previously-functioning driver, allowing for testing of periodic + // drivers and fingerprinters + ShutdownPeriodicAfter = "test.shutdown_periodic_after" + + // ShutdownPeriodicDuration is a config option that can be used during tests + // to "stop" a previously functioning driver after the specified duration + // (specified in seconds) for testing of periodic drivers and fingerprinters. + ShutdownPeriodicDuration = "test.shutdown_periodic_duration" +) + // Add the mock driver to the list of builtin drivers func init() { BuiltinDrivers["mock_driver"] = NewMockDriver @@ -78,11 +90,10 @@ type MockDriverConfig struct { type MockDriver struct { DriverContext - // isShutdown is an internal concept to use to track whether the driver - // should be shut down - isShutdown bool - cleanupFailNum int + + // shutdownFingerprintTime is the time up to which the driver will be up + shutdownFingerprintTime time.Time } // NewMockDriver is a factory method which returns a new Mock Driver @@ -91,13 +102,13 @@ func NewMockDriver(ctx *DriverContext) Driver { // if the shutdown configuration options are set, start the timer here. // This config option defaults to false - if ctx.config != nil && ctx.config.ReadBoolDefault(fingerprint.ShutdownPeriodicAfter, false) { - duration, err := ctx.config.ReadInt(fingerprint.ShutdownPeriodicDuration) + if ctx.config != nil && ctx.config.ReadBoolDefault(ShutdownPeriodicAfter, false) { + duration, err := ctx.config.ReadInt(ShutdownPeriodicDuration) if err != nil { errMsg := fmt.Sprintf("unable to read config option for shutdown_periodic_duration %s, got err %s", duration, err.Error()) panic(errMsg) } - go md.startShutdownTimer(duration) + md.shutdownFingerprintTime = time.Now().Add(time.Second * time.Duration(duration)) } return md @@ -181,20 +192,6 @@ func (m *MockDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse return &StartResponse{Handle: &h, Network: net}, nil } -// startShutdownTimer sets a timer, after which the mock driver will no loger be -// responsive. This is used for testing periodic fingerprinting functionality -func (m *MockDriver) startShutdownTimer(duration int) { - timer := time.NewTimer(time.Duration(duration) * time.Second) - for { - select { - case <-timer.C: - m.isShutdown = true - default: - time.Sleep(100 * time.Millisecond) - } - } -} - // Cleanup deletes all keys except for Config.Options["cleanup_fail_on"] for // Config.Options["cleanup_fail_num"] times. For failures it will return a // recoverable error. @@ -225,11 +222,14 @@ func (m *MockDriver) Validate(map[string]interface{}) error { // Fingerprint fingerprints a node and returns if MockDriver is enabled func (m *MockDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { switch { - case m.isShutdown: + // If the driver is configured to shut down after a period of time, and the + // current time is after the time which the node should shut down, simulate + // driver failure + case !m.shutdownFingerprintTime.IsZero() && time.Now().After(m.shutdownFingerprintTime): resp.RemoveAttribute("driver.mock_driver") default: resp.AddAttribute("driver.mock_driver", "1") - resp.Applicable = true + resp.Detected = true } return nil } diff --git a/client/driver/qemu.go b/client/driver/qemu.go index 9d2b025bb..046477ac2 100644 --- a/client/driver/qemu.go +++ b/client/driver/qemu.go @@ -178,7 +178,7 @@ func (d *QemuDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct resp.AddAttribute(qemuDriverAttr, "1") resp.AddAttribute(qemuDriverVersionAttr, currentQemuVersion) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/driver/qemu_test.go b/client/driver/qemu_test.go index 52fe05aca..9dbfde76a 100644 --- a/client/driver/qemu_test.go +++ b/client/driver/qemu_test.go @@ -43,7 +43,7 @@ func TestQemuDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index f39a25fb4..0cea2ea55 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -98,7 +98,7 @@ func (d *RawExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstr if enabled || req.Config.DevMode { d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") resp.AddAttribute(rawExecDriverAttr, "1") - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index 21d3bccbd..1cec71a29 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -53,7 +53,7 @@ func TestRawExecDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 8c5447922..446a48eae 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -354,7 +354,7 @@ func (d *RktDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs resp.AddAttribute(rktDriverAttr, "1") resp.AddAttribute("driver.rkt.version", rktMatches[1]) resp.AddAttribute("driver.rkt.appc.version", appcMatches[1]) - resp.Applicable = true + resp.Detected = true // Advertise if this node supports rkt volumes if d.config.ReadBoolDefault(rktVolumesConfigOption, rktVolumesConfigDefault) { diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index 0de3e130f..978757e72 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -66,7 +66,7 @@ func TestRktDriver_Fingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/arch.go b/client/fingerprint/arch.go index 0c6c95ed2..3277822bc 100644 --- a/client/fingerprint/arch.go +++ b/client/fingerprint/arch.go @@ -21,6 +21,6 @@ func NewArchFingerprint(logger *log.Logger) Fingerprint { func (f *ArchFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { resp.AddAttribute("cpu.arch", runtime.GOARCH) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/arch_test.go b/client/fingerprint/arch_test.go index 8463f803c..320ccc321 100644 --- a/client/fingerprint/arch_test.go +++ b/client/fingerprint/arch_test.go @@ -21,7 +21,7 @@ func TestArchFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/cgroup_linux.go b/client/fingerprint/cgroup_linux.go index 19c69b4f8..c6f78cd35 100644 --- a/client/fingerprint/cgroup_linux.go +++ b/client/fingerprint/cgroup_linux.go @@ -47,7 +47,7 @@ func (f *CGroupFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } resp.AddAttribute("unique.cgroup.mountpoint", mount) - resp.Applicable = true + resp.Detected = true if f.lastState == cgroupUnavailable { f.logger.Printf("[INFO] fingerprint.cgroups: cgroups are available") diff --git a/client/fingerprint/consul.go b/client/fingerprint/consul.go index 356917167..84c6a97d6 100644 --- a/client/fingerprint/consul.go +++ b/client/fingerprint/consul.go @@ -98,7 +98,7 @@ func (f *ConsulFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * f.logger.Printf("[INFO] fingerprint.consul: consul agent is available") } f.lastState = consulAvailable - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/consul_test.go b/client/fingerprint/consul_test.go index d97e0d6fa..400ab01f4 100644 --- a/client/fingerprint/consul_test.go +++ b/client/fingerprint/consul_test.go @@ -35,7 +35,7 @@ func TestConsulFingerprint(t *testing.T) { t.Fatalf("Failed to fingerprint: %s", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -189,7 +189,7 @@ func TestConsulFingerprint_UnexpectedResponse(t *testing.T) { err := fp.Fingerprint(request, &response) assert.Nil(err) - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/cpu.go b/client/fingerprint/cpu.go index 8b032e0fd..434eb5844 100644 --- a/client/fingerprint/cpu.go +++ b/client/fingerprint/cpu.go @@ -68,7 +68,7 @@ func (f *CPUFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cst resp.AddAttribute("cpu.totalcompute", fmt.Sprintf("%d", tt)) setResourcesCPU(tt) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/cpu_test.go b/client/fingerprint/cpu_test.go index fc7b77571..5bb761970 100644 --- a/client/fingerprint/cpu_test.go +++ b/client/fingerprint/cpu_test.go @@ -21,7 +21,7 @@ func TestCPUFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -67,7 +67,7 @@ func TestCPUFingerprint_OverrideCompute(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index a8b572d0f..79bd6730f 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -173,7 +173,7 @@ func (f *EnvAWSFingerprint) Fingerprint(request *cstructs.FingerprintRequest, re response.AddLink("aws.ec2", fmt.Sprintf("%s.%s", response.Attributes["platform.aws.placement.availability-zone"], response.Attributes["unique.platform.aws.instance-id"])) - response.Applicable = true + response.Detected = true return nil } diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index df924590f..2a94561df 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -229,7 +229,7 @@ func TestNetworkFingerprint_AWS_network(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 3e717d13e..280914f3d 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -261,7 +261,7 @@ func (f *EnvGCEFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * resp.AddLink("gce", id) } - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 9ccd736cd..14837dca8 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -27,7 +27,7 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { t.Fatalf("err: %v", err) } - if response.Applicable { + if response.Detected { t.Fatalf("expected response to not be applicable") } @@ -90,7 +90,7 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index ed50b344e..8a3477f51 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -16,16 +16,6 @@ const ( // TightenNetworkTimeoutsConfig is a config key that can be used during // tests to tighten the timeouts for fingerprinters that make network calls. TightenNetworkTimeoutsConfig = "test.tighten_network_timeouts" - - // ShutdownPeriodicAfter is a config key that can be used during tests to - // "stop" a previously-functioning driver, allowing for testing of periodic - // drivers and fingerprinters - ShutdownPeriodicAfter = "test.shutdown_periodic_after" - - // ShutdownPeriodicDuration is a config option that can be used during tests - // to "stop" a previously functioning driver after the specified duration - // (specified in seconds) for testing of periodic drivers and fingerprinters. - ShutdownPeriodicDuration = "test.shutdown_periodic_duration" ) func init() { diff --git a/client/fingerprint/host.go b/client/fingerprint/host.go index 0bc34a474..cfeabd4ac 100644 --- a/client/fingerprint/host.go +++ b/client/fingerprint/host.go @@ -34,7 +34,7 @@ func (f *HostFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cs resp.AddAttribute("kernel.version", hostInfo.KernelVersion) resp.AddAttribute("unique.hostname", hostInfo.Hostname) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/host_test.go b/client/fingerprint/host_test.go index 5dba118c5..79ef870c5 100644 --- a/client/fingerprint/host_test.go +++ b/client/fingerprint/host_test.go @@ -21,7 +21,7 @@ func TestHostFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/network.go b/client/fingerprint/network.go index 22ea9bcb5..9634a7969 100644 --- a/client/fingerprint/network.go +++ b/client/fingerprint/network.go @@ -106,7 +106,7 @@ func (f *NetworkFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp if len(nwResources) > 0 { resp.AddAttribute("unique.network.ip-address", nwResources[0].IP) } - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/network_test.go b/client/fingerprint/network_test.go index 606cfd741..87b57bd4f 100644 --- a/client/fingerprint/network_test.go +++ b/client/fingerprint/network_test.go @@ -197,7 +197,7 @@ func TestNetworkFingerprint_basic(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -248,7 +248,7 @@ func TestNetworkFingerprint_default_device_absent(t *testing.T) { t.Fatalf("err: %v", err) } - if response.Applicable { + if response.Detected { t.Fatalf("expected response to not be applicable") } @@ -271,7 +271,7 @@ func TestNetworkFingerPrint_default_device(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -322,7 +322,7 @@ func TestNetworkFingerPrint_LinkLocal_Allowed(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -369,7 +369,7 @@ func TestNetworkFingerPrint_LinkLocal_Allowed_MixedIntf(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } @@ -429,7 +429,7 @@ func TestNetworkFingerPrint_LinkLocal_Disallowed(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/nomad.go b/client/fingerprint/nomad.go index 3b30f9e75..0a00cc026 100644 --- a/client/fingerprint/nomad.go +++ b/client/fingerprint/nomad.go @@ -21,6 +21,6 @@ func NewNomadFingerprint(logger *log.Logger) Fingerprint { func (f *NomadFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { resp.AddAttribute("nomad.version", req.Config.Version.VersionNumber()) resp.AddAttribute("nomad.revision", req.Config.Version.Revision) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/nomad_test.go b/client/fingerprint/nomad_test.go index bdab1b9e9..2060fd8e2 100644 --- a/client/fingerprint/nomad_test.go +++ b/client/fingerprint/nomad_test.go @@ -30,7 +30,7 @@ func TestNomadFingerprint(t *testing.T) { t.Fatalf("err: %v", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/signal.go b/client/fingerprint/signal.go index 139f4c3fb..9aac819e1 100644 --- a/client/fingerprint/signal.go +++ b/client/fingerprint/signal.go @@ -28,6 +28,6 @@ func (f *SignalFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp * } resp.AddAttribute("os.signals", strings.Join(sigs, ",")) - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/storage.go b/client/fingerprint/storage.go index 7ec43104d..6dc72fb6d 100644 --- a/client/fingerprint/storage.go +++ b/client/fingerprint/storage.go @@ -50,7 +50,7 @@ func (f *StorageFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp resp.Resources = &structs.Resources{ DiskMB: int(free / bytesPerMegabyte), } - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/storage_test.go b/client/fingerprint/storage_test.go index 86b15cecb..c4388905f 100644 --- a/client/fingerprint/storage_test.go +++ b/client/fingerprint/storage_test.go @@ -15,7 +15,7 @@ func TestStorageFingerprint(t *testing.T) { response := assertFingerprintOK(t, fp, node) - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/fingerprint/vault.go b/client/fingerprint/vault.go index b7cee695f..0613e98a0 100644 --- a/client/fingerprint/vault.go +++ b/client/fingerprint/vault.go @@ -74,7 +74,7 @@ func (f *VaultFingerprint) Fingerprint(req *cstructs.FingerprintRequest, resp *c f.logger.Printf("[INFO] fingerprint.vault: Vault is available") } f.lastState = vaultAvailable - resp.Applicable = true + resp.Detected = true return nil } diff --git a/client/fingerprint/vault_test.go b/client/fingerprint/vault_test.go index 76eb347e1..25e7f1386 100644 --- a/client/fingerprint/vault_test.go +++ b/client/fingerprint/vault_test.go @@ -28,7 +28,7 @@ func TestVaultFingerprint(t *testing.T) { t.Fatalf("Failed to fingerprint: %s", err) } - if !response.Applicable { + if !response.Detected { t.Fatalf("expected response to be applicable") } diff --git a/client/structs/structs.go b/client/structs/structs.go index 98980da79..15b70e2ce 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -198,9 +198,9 @@ type FingerprintResponse struct { Links map[string]string Resources *structs.Resources - // Applicable is a boolean indicating whether the fingerprint should be - // applied - Applicable bool + // Detected is a boolean indicating whether the fingerprinter detected + // if the resource was avaialble + Detected bool } // AddAttribute adds the name and value for a node attribute to the fingerprint From cdd55e45919adaca5703f5f010629d8e3a26ce48 Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 1 Feb 2018 14:25:14 +0100 Subject: [PATCH 34/79] Update documentation --- website/source/docs/job-specification/service.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/job-specification/service.html.md b/website/source/docs/job-specification/service.html.md index ff5fcdba5..6d04e8132 100644 --- a/website/source/docs/job-specification/service.html.md +++ b/website/source/docs/job-specification/service.html.md @@ -468,8 +468,8 @@ directly since Nomad isn't managing any port assignments. The [Docker](/docs/drivers/docker.html#advertise_ipv6_address) driver supports the `advertise_ipv6_address` parameter in it's configuration. -For the `service`stanza is no explicit `address_mode` required. -Services default to the `auto` address mode. +Services will automatically advertise the IPv6 address when `advertise_ipv6_address` +is used. Unlike services, checks do not have an `auto` address mode as there's no way for Nomad to know which is the best address to use for checks. Consul needs From e5ccc551dcd66cf5ca23cc5c5dc8b7643e02d584 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 1 Feb 2018 11:28:17 -0500 Subject: [PATCH 35/79] add detected to more drivers where the driver is found but unusable --- client/driver/exec_linux.go | 4 +++- client/driver/java.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/driver/exec_linux.go b/client/driver/exec_linux.go index b88226ffb..138a92d75 100644 --- a/client/driver/exec_linux.go +++ b/client/driver/exec_linux.go @@ -13,6 +13,9 @@ const ( ) func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstructs.FingerprintResponse) error { + // The exec driver will be detected in every case + resp.Detected = true + // Only enable if cgroups are available and we are root if !cgroupsMounted(req.Node) { if d.fingerprintSuccess == nil || *d.fingerprintSuccess { @@ -35,6 +38,5 @@ func (d *ExecDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } resp.AddAttribute(execDriverAttr, "1") d.fingerprintSuccess = helper.BoolToPtr(true) - resp.Detected = true return nil } diff --git a/client/driver/java.go b/client/driver/java.go index 3e0a831ec..3c4b31958 100644 --- a/client/driver/java.go +++ b/client/driver/java.go @@ -119,6 +119,7 @@ func (d *JavaDriver) Fingerprint(req *cstructs.FingerprintRequest, resp *cstruct } d.fingerprintSuccess = helper.BoolToPtr(false) resp.RemoveAttribute(javaDriverAttr) + resp.Detected = true return nil } From 15cb7683f8c798ef6003c76e32a05239bd33e703 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 1 Feb 2018 12:26:38 -0500 Subject: [PATCH 36/79] fix up linting --- client/client_test.go | 6 ++++-- client/structs/structs.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index fc749edf6..9b16f93e7 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -259,10 +259,12 @@ func TestClient_Fingerprint_Periodic(t *testing.T) { } t.Parallel() + // these constants are only defined when nomad_test is enabled, so these fail + // our linter without explicit disabling. c1 := testClient(t, func(c *config.Config) { c.Options = map[string]string{ - driver.ShutdownPeriodicAfter: "true", - driver.ShutdownPeriodicDuration: "3", + driver.ShutdownPeriodicAfter: "true", // nolint: varcheck + driver.ShutdownPeriodicDuration: "3", // nolint: varcheck } }) defer c1.Shutdown() diff --git a/client/structs/structs.go b/client/structs/structs.go index 15b70e2ce..9348acfb6 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -199,7 +199,7 @@ type FingerprintResponse struct { Resources *structs.Resources // Detected is a boolean indicating whether the fingerprinter detected - // if the resource was avaialble + // if the resource was available Detected bool } From d0dd6ef22f1ecd436a2dbe4eb32af262e24c00b5 Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 1 Feb 2018 23:21:28 +0100 Subject: [PATCH 37/79] Add IPv6 support to travis docker --- scripts/travis-linux.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/travis-linux.sh b/scripts/travis-linux.sh index 4c402b797..4d5986788 100644 --- a/scripts/travis-linux.sh +++ b/scripts/travis-linux.sh @@ -2,6 +2,10 @@ set -o errexit +#enable ipv6 +echo '{"ipv6":true, "fixed-cidr-v6":"2001:db8:1::/64"}' | sudo tee /etc/docker/daemon.json +sudo service docker restart + apt-get update apt-get install -y liblxc1 lxc-dev lxc shellcheck apt-get install -y qemu From 51bf06f468197175dcf705588e65913423a1f093 Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 1 Feb 2018 23:21:47 +0100 Subject: [PATCH 38/79] Add AdvertiseIPv6Address test --- client/driver/docker_test.go | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index 6dc5d97c5..922083ce2 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -2269,3 +2269,74 @@ func TestDockerDriver_ReadonlyRootfs(t *testing.T) { assert.True(t, container.HostConfig.ReadonlyRootfs, "ReadonlyRootfs option not set") } + +func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { + if !tu.IsTravis() { + t.Parallel() + } + if !testutil.DockerIsConnected(t) { + t.Skip("Docker not connected") + } + expectedPrefix := "2001:db8:1::242:ac11" + expectedAdvertise := true + task := &structs.Task{ + Name: "nc-demo", + Driver: "docker", + Config: map[string]interface{}{ + "image": "busybox", + "load": "busybox.tar", + "command": "/bin/nc", + "args": []string{"-l", "127.0.0.1", "-p", "0"}, + "advertise_ipv6_address": expectedAdvertise, + }, + Resources: &structs.Resources{ + MemoryMB: 256, + CPU: 512, + }, + LogConfig: &structs.LogConfig{ + MaxFiles: 10, + MaxFileSizeMB: 10, + }, + } + + client := newTestDockerClient(t) + tctx := testDockerDriverContexts(t, task) + driver := NewDockerDriver(tctx.DriverCtx) + copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") + defer tctx.AllocDir.Destroy() + + presp, err := driver.Prestart(tctx.ExecCtx, task) + defer driver.Cleanup(tctx.ExecCtx, presp.CreatedResources) + if err != nil { + t.Fatalf("Error in prestart: %v", err) + } + + sresp, err := driver.Start(tctx.ExecCtx, task) + if err != nil { + t.Fatalf("Error in start: %v", err) + } + + if sresp.Handle == nil { + t.Fatalf("handle is nil\nStack\n%s", debug.Stack()) + } + + assert.Equal(t, expectedAdvertise, sresp.Network.AutoAdvertise, "Wrong autoadvertise. Expect: %s, got: %s", expectedAdvertise, sresp.Network.AutoAdvertise) + + if !strings.HasPrefix(sresp.Network.IP, expectedPrefix) { + t.Fatalf("Got IP address %s want ip address with prefix %s", sresp.Network.IP, expectedPrefix) + } + + defer sresp.Handle.Kill() + handle := sresp.Handle.(*DockerHandle) + + waitForExist(t, client, handle) + + container, err := client.InspectContainer(handle.ContainerID()) + if err != nil { + t.Fatalf("Error inspecting container: %v", err) + } + + if !strings.HasPrefix(container.NetworkSettings.GlobalIPv6Address, expectedPrefix) { + t.Fatalf("Got GlobalIPv6address %s want GlobalIPv6address with prefix %s", expectedPrefix, container.NetworkSettings.GlobalIPv6Address) + } +} From 3202200ccb1bfc5cfee29b359df7c92fda1d3ed8 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 1 Feb 2018 18:38:23 -0500 Subject: [PATCH 39/79] req/resp should be within config locks; rename for detected fingerprints changelog --- CHANGELOG.md | 2 ++ client/client.go | 18 +++++++++--------- client/driver/mock_driver.go | 1 - client/structs/structs.go | 4 ++++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f17f868b1..3849a7737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ __BACKWARDS INCOMPATIBILITIES:__ IMPROVEMENTS: * core: A set of features (Autopilot) has been added to allow for automatic operator-friendly management of Nomad servers. For more information about Autopilot, see the [Autopilot Guide](https://www.nomadproject.io/guides/cluster/autopilot.html). [[GH-3670](https://github.com/hashicorp/nomad/pull/3670)] * client: Allow '.' in environment variable names [[GH-3760](https://github.com/hashicorp/nomad/issues/3760)] + * client: Refactor client fingerprint methods to a request/response format + [[GH-3781](https://github.com/hashicorp/nomad/issues/3781)] * discovery: Allow `check_restart` to be specified in the `service` stanza. [[GH-3718](https://github.com/hashicorp/nomad/issues/3718)] * driver/docker; Support overriding image entrypoint [[GH-3788](https://github.com/hashicorp/nomad/issues/3788)] diff --git a/client/client.go b/client/client.go index 0d749ffe1..17737d39f 100644 --- a/client/client.go +++ b/client/client.go @@ -932,7 +932,7 @@ func (c *Client) fingerprint() error { c.logger.Printf("[DEBUG] client: built-in fingerprints: %v", fingerprint.BuiltinFingerprints()) - var appliedFingerprints []string + var detectedFingerprints []string var skippedFingerprints []string for _, name := range fingerprint.BuiltinFingerprints() { // Skip modules that are not in the whitelist if it is enabled. @@ -951,9 +951,9 @@ func (c *Client) fingerprint() error { return err } + c.configLock.Lock() request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse - c.configLock.Lock() err = f.Fingerprint(request, &response) c.configLock.Unlock() if err != nil { @@ -962,7 +962,7 @@ func (c *Client) fingerprint() error { // log the fingerprinters which have been applied if response.Detected { - appliedFingerprints = append(appliedFingerprints, name) + detectedFingerprints = append(detectedFingerprints, name) } // add the diff found from each fingerprinter @@ -977,7 +977,7 @@ func (c *Client) fingerprint() error { } } - c.logger.Printf("[DEBUG] client: applied fingerprints %v", appliedFingerprints) + c.logger.Printf("[DEBUG] client: detected fingerprints %v", detectedFingerprints) if len(skippedFingerprints) != 0 { c.logger.Printf("[DEBUG] client: fingerprint modules skipped due to white/blacklist: %v", skippedFingerprints) } @@ -990,9 +990,9 @@ func (c *Client) fingerprintPeriodic(name string, f fingerprint.Fingerprint, d t for { select { case <-time.After(d): + c.configLock.Lock() request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse - c.configLock.Lock() err := f.Fingerprint(request, &response) c.configLock.Unlock() @@ -1015,7 +1015,7 @@ func (c *Client) setupDrivers() error { whitelistEnabled := len(whitelist) > 0 blacklist := c.config.ReadStringListToMap("driver.blacklist") - var availDrivers []string + var detectedDrivers []string var skippedDrivers []string driverCtx := driver.NewDriverContext("", "", c.config, c.config.Node, c.logger, nil) for name := range driver.BuiltinDrivers { @@ -1036,9 +1036,9 @@ func (c *Client) setupDrivers() error { return err } + c.configLock.Lock() request := &cstructs.FingerprintRequest{Config: c.config, Node: c.config.Node} var response cstructs.FingerprintResponse - c.configLock.Lock() err = d.Fingerprint(request, &response) c.configLock.Unlock() if err != nil { @@ -1047,7 +1047,7 @@ func (c *Client) setupDrivers() error { // log the fingerprinters which have been applied if response.Detected { - availDrivers = append(availDrivers, name) + detectedDrivers = append(detectedDrivers, name) } c.updateNodeFromFingerprint(&response) @@ -1059,7 +1059,7 @@ func (c *Client) setupDrivers() error { } - c.logger.Printf("[DEBUG] client: available drivers %v", availDrivers) + c.logger.Printf("[DEBUG] client: available drivers %v", detectedDrivers) if len(skippedDrivers) > 0 { c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skippedDrivers) } diff --git a/client/driver/mock_driver.go b/client/driver/mock_driver.go index 4c7085533..15cc56b5b 100644 --- a/client/driver/mock_driver.go +++ b/client/driver/mock_driver.go @@ -21,7 +21,6 @@ import ( ) const ( - // ShutdownPeriodicAfter is a config key that can be used during tests to // "stop" a previously-functioning driver, allowing for testing of periodic // drivers and fingerprinters diff --git a/client/structs/structs.go b/client/structs/structs.go index 9348acfb6..97887232d 100644 --- a/client/structs/structs.go +++ b/client/structs/structs.go @@ -188,11 +188,15 @@ func (d *DriverNetwork) Hash() []byte { return h.Sum(nil) } +// FingerprintRequest is a request which a fingerprinter accepts to fingerprint +// the node type FingerprintRequest struct { Config *config.Config Node *structs.Node } +// FingerprintResponse is the response which a fingerprinter annotates with the +// results of the fingerprint method type FingerprintResponse struct { Attributes map[string]string Links map[string]string From 8049aa083f29a35929482ee3a8cac999d5045a10 Mon Sep 17 00:00:00 2001 From: Chelsea Holland Komlo Date: Thu, 1 Feb 2018 19:46:57 -0500 Subject: [PATCH 40/79] update log message --- client/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/client.go b/client/client.go index 17737d39f..4054fca6b 100644 --- a/client/client.go +++ b/client/client.go @@ -1059,7 +1059,7 @@ func (c *Client) setupDrivers() error { } - c.logger.Printf("[DEBUG] client: available drivers %v", detectedDrivers) + c.logger.Printf("[DEBUG] client: detected drivers %v", detectedDrivers) if len(skippedDrivers) > 0 { c.logger.Printf("[DEBUG] client: drivers skipped due to white/blacklist: %v", skippedDrivers) } From e486a27a4e65fd635519deea828112f47b80e0e8 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Mon, 5 Feb 2018 16:24:30 -0800 Subject: [PATCH 41/79] docker: Skip IPv6 test if IPv6 disabled --- client/driver/docker_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index 922083ce2..db5273d02 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -2277,6 +2277,7 @@ func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { if !testutil.DockerIsConnected(t) { t.Skip("Docker not connected") } + expectedPrefix := "2001:db8:1::242:ac11" expectedAdvertise := true task := &structs.Task{ @@ -2300,6 +2301,16 @@ func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { } client := newTestDockerClient(t) + + // Make sure IPv6 is enabled + net, err := client.NetworkInfo("bridge") + if err != nil { + t.Skip("error retrieving bridge network information, skipping") + } + if net == nil || !net.EnableIPv6 { + t.Skip("IPv6 not enabled on bridge network, skipping") + } + tctx := testDockerDriverContexts(t, task) driver := NewDockerDriver(tctx.DriverCtx) copyImage(t, tctx.ExecCtx.TaskDir, "busybox.tar") @@ -2323,7 +2334,7 @@ func TestDockerDriver_AdvertiseIPv6Address(t *testing.T) { assert.Equal(t, expectedAdvertise, sresp.Network.AutoAdvertise, "Wrong autoadvertise. Expect: %s, got: %s", expectedAdvertise, sresp.Network.AutoAdvertise) if !strings.HasPrefix(sresp.Network.IP, expectedPrefix) { - t.Fatalf("Got IP address %s want ip address with prefix %s", sresp.Network.IP, expectedPrefix) + t.Fatalf("Got IP address %q want ip address with prefix %q", sresp.Network.IP, expectedPrefix) } defer sresp.Handle.Kill() From 5ff86f0d2abfa07f2f9d6f07e4e7c4a24655aca2 Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Mon, 5 Feb 2018 16:29:29 -0800 Subject: [PATCH 42/79] Add changelog entry for #3790 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5a2ca22a..aa29452e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ IMPROVEMENTS: * client: Allow '.' in environment variable names [[GH-3760](https://github.com/hashicorp/nomad/issues/3760)] * discovery: Allow `check_restart` to be specified in the `service` stanza. [[GH-3718](https://github.com/hashicorp/nomad/issues/3718)] + * driver/docker: Support advertising IPv6 addresses [[GH-3790](https://github.com/hashicorp/nomad/issues/3790)] * driver/docker; Support overriding image entrypoint [[GH-3788](https://github.com/hashicorp/nomad/issues/3788)] * driver/docker: Support adding or dropping capabilities [[GH-3754](https://github.com/hashicorp/nomad/issues/3754)] * driver/docker: Support mounting root filesystem as read-only [[GH-3802](https://github.com/hashicorp/nomad/issues/3802)] From 37c39a9a05e31a180c833ddce6393d4415f6415a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:23:16 -0800 Subject: [PATCH 43/79] Filter child jobs out of the root jobs list --- ui/app/controllers/jobs/index.js | 22 ++++++++++++---------- ui/app/serializers/job.js | 13 +++++++++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/ui/app/controllers/jobs/index.js b/ui/app/controllers/jobs/index.js index d0d06e9ee..db1a561c0 100644 --- a/ui/app/controllers/jobs/index.js +++ b/ui/app/controllers/jobs/index.js @@ -1,5 +1,5 @@ import { inject as service } from '@ember/service'; -import { alias, filterBy } from '@ember/object/computed'; +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'; @@ -11,10 +11,6 @@ export default Controller.extend(Sortable, Searchable, { isForbidden: alias('jobsController.isForbidden'), - pendingJobs: filterBy('model', 'status', 'pending'), - runningJobs: filterBy('model', 'status', 'running'), - deadJobs: filterBy('model', 'status', 'dead'), - queryParams: { currentPage: 'page', searchTerm: 'search', @@ -30,16 +26,22 @@ export default Controller.extend(Sortable, Searchable, { searchProps: computed(() => ['id', 'name']), + /** + Filtered jobs are those that match the selected namespace and aren't children + of periodic or parameterized jobs. + */ filteredJobs: computed( 'model.[]', + 'model.@each.parent', 'system.activeNamespace', 'system.namespaces.length', function() { - if (this.get('system.namespaces.length')) { - return this.get('model').filterBy('namespace.id', this.get('system.activeNamespace.id')); - } else { - return this.get('model'); - } + const hasNamespaces = this.get('system.namespaces.length'); + const activeNamespace = this.get('system.activeNamespace.id'); + + return this.get('model') + .filter(job => !hasNamespaces || job.get('namespace.id') === activeNamespace) + .filter(job => !job.get('parent.content')); } ), diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index e09c95cd8..7e42a54af 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -15,6 +15,19 @@ export default ApplicationSerializer.extend({ hash.PlainId = hash.ID; hash.ID = JSON.stringify([hash.ID, hash.NamespaceID || 'default']); + // ParentID comes in as "" instead of null + if (!hash.ParentID) { + hash.ParentID = null; + } else { + hash.ParentID = JSON.stringify([hash.ParentID, hash.NamespaceID || 'default']); + } + + // Periodic is a boolean on list and an object on single + if (hash.Periodic instanceof Object) { + hash.PeriodicDetails = hash.Periodic; + hash.Periodic = true; + } + // Transform the map-based JobSummary object into an array-based // JobSummary fragment list hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => { From 0c50472bb57714c6e3a40d519d57d03a6290f716 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:24:13 -0800 Subject: [PATCH 44/79] Model the parent/child relationship in jobs --- ui/app/models/job.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 511f89856..89ff58108 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -1,4 +1,4 @@ -import { collect, sum, bool, equal } from '@ember/object/computed'; +import { collect, sum, bool, equal, or } from '@ember/object/computed'; import { computed } from '@ember/object'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; @@ -19,9 +19,16 @@ export default Model.extend({ createIndex: attr('number'), modifyIndex: attr('number'), + // True when the job is the parent periodic or parameterized jobs + // Instances of periodic or parameterized jobs are false for both properties periodic: attr('boolean'), parameterized: attr('boolean'), + hasChildren: or('periodic', 'parameterized'), + + parent: belongsTo('job', { inverse: 'children' }), + children: hasMany('job', { inverse: 'parent' }), + datacenters: attr(), taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), taskGroupSummaries: fragmentArray('task-group-summary'), From 552b958d67ee4b8a80ab504f4b815ae3be0ac518 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:25:45 -0800 Subject: [PATCH 45/79] Specialized children-status-bar variant of the allocation-status-bar --- ui/app/components/children-status-bar.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 ui/app/components/children-status-bar.js diff --git a/ui/app/components/children-status-bar.js b/ui/app/components/children-status-bar.js new file mode 100644 index 000000000..cb32db128 --- /dev/null +++ b/ui/app/components/children-status-bar.js @@ -0,0 +1,25 @@ +import { computed } from '@ember/object'; +import DistributionBar from './distribution-bar'; + +export default DistributionBar.extend({ + layoutName: 'components/distribution-bar', + + job: null, + + data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() { + if (!this.get('job')) { + return []; + } + + const children = this.get('job').getProperties( + 'pendingChildren', + 'runningChildren', + 'deadChildren' + ); + return [ + { label: 'Pending', value: children.pendingChildren, className: 'queued' }, + { label: 'Running', value: children.runningChildren, className: 'running' }, + { label: 'Dead', value: children.deadChildren, className: 'complete' }, + ]; + }), +}); From eae275f71993cbac9ea29f24f1dd4eb07989e6ef Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:26:07 -0800 Subject: [PATCH 46/79] Use the children summary instead of alloc summary when applicable --- ui/app/templates/components/job-row.hbs | 8 +++++++- ui/app/templates/jobs/index.hbs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index 54032c664..d2c62b494 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -12,5 +12,11 @@ {{/if}} -
{{allocation-status-bar allocationContainer=job isNarrow=true}}
+
+ {{#if job.hasChildren}} + {{children-status-bar job=job isNarrow=true}} + {{else}} + {{allocation-status-bar allocationContainer=job isNarrow=true}} + {{/if}} +
diff --git a/ui/app/templates/jobs/index.hbs b/ui/app/templates/jobs/index.hbs index 9c8c93888..34e24c33a 100644 --- a/ui/app/templates/jobs/index.hbs +++ b/ui/app/templates/jobs/index.hbs @@ -28,7 +28,7 @@ {{#t.sort-by prop="type"}}Type{{/t.sort-by}} {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}} Groups - Allocation Status + Summary {{/t.head}} {{#t.body key="model.id" as |row|}} {{job-row data-test-job-row job=row.model onClick=(action "gotoJob" row.model)}} From 525312dc92547156681d7ca8eaecff0afc8317a5 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 23 Jan 2018 13:34:19 -0800 Subject: [PATCH 47/79] Fix lint-staged paths Paths now start from package.json location, not project root. --- ui/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/package.json b/ui/package.json index 914d94ad2..7866bcc6b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,11 +14,11 @@ "precommit": "lint-staged" }, "lint-staged": { - "ui/{app,tests,config,lib,mirage}/**/*.js": [ + "{app,tests,config,lib,mirage}/**/*.js": [ "prettier --write", "git add" ], - "ui/app/styles/**/*.*": [ + "app/styles/**/*.*": [ "prettier --write", "git add" ] From c6369a6ccbc315695fcdf20c76ffde5c32fb0747 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:29:47 -0800 Subject: [PATCH 48/79] Computed a template type for a job This is a composite of scheduler type, batch variations, and children v. template --- ui/app/models/job.js | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 89ff58108..c6ab54835 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -6,6 +6,8 @@ import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; import sumAggregation from '../utils/properties/sum-aggregation'; +const JOB_TYPES = ['service', 'batch', 'system']; + export default Model.extend({ region: attr('string'), name: attr('string'), @@ -24,11 +26,44 @@ export default Model.extend({ periodic: attr('boolean'), parameterized: attr('boolean'), + periodicDetails: attr(), + parameterizedDetails: attr(), + hasChildren: or('periodic', 'parameterized'), parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // A composite of type and other job attributes to determine + // type for templating rather than scheduling + templateType: computed( + 'type', + 'periodic', + 'parameterized', + 'parent.periodic', + 'parent.parameterized', + function() { + const type = this.get('type'); + + if (this.get('periodic')) { + return 'periodic'; + } else if (this.get('parameterized')) { + return 'parameterized'; + } else if (this.get('parent.periodic')) { + return 'periodic-child'; + } else if (this.get('parent.parameterized')) { + return 'parameterized-child'; + } else if (JOB_TYPES.includes(type)) { + // Guard against the API introducing a new type before the UI + // is prepared to handle it. + return this.get('type'); + } else { + // A fail-safe in the event the API introduces a new type. + return 'service'; + } + } + ), + datacenters: attr(), taskGroups: fragmentArray('task-group', { defaultValue: () => [] }), taskGroupSummaries: fragmentArray('task-group-summary'), @@ -56,6 +91,12 @@ export default Model.extend({ runningChildren: attr('number'), deadChildren: attr('number'), + childrenList: collect('pendingChildren', 'runningChildren', 'deadChildren'), + + totalChildren: sum('childrenList'), + + version: attr('number'), + versions: hasMany('job-versions'), allocations: hasMany('allocations'), deployments: hasMany('deployments'), From a47835f34f423fdf6792ae02d8925c3f6ecf7407 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:30:46 -0800 Subject: [PATCH 49/79] Handle the difference between parameterized on single and list responses --- ui/app/serializers/job.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/app/serializers/job.js b/ui/app/serializers/job.js index 7e42a54af..df77e2f38 100644 --- a/ui/app/serializers/job.js +++ b/ui/app/serializers/job.js @@ -28,6 +28,12 @@ export default ApplicationSerializer.extend({ hash.Periodic = true; } + // Parameterized behaves like Periodic + if (hash.ParameterizedJob instanceof Object) { + hash.ParameterizedDetails = hash.ParameterizedJob; + hash.ParameterizedJob = true; + } + // Transform the map-based JobSummary object into an array-based // JobSummary fragment list hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => { From abfae77545e085e85ed401b80d62e5de43dc1880 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Tue, 6 Feb 2018 11:21:15 -0500 Subject: [PATCH 50/79] Add tags option to datadog telemetry Expose an global tags option in telemetry config for dogstatsd, for purposes of distinguishing between multiple nomad cluster metrics. --- command/agent/command.go | 1 + command/agent/config.go | 4 ++++ command/agent/config_parse.go | 1 + command/agent/config_test.go | 2 ++ website/source/api/agent.html.md | 1 + website/source/docs/agent/configuration/telemetry.html.md | 6 ++++++ 6 files changed, 15 insertions(+) diff --git a/command/agent/command.go b/command/agent/command.go index 74bbf9614..89ecc58ea 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -748,6 +748,7 @@ func (c *Command) setupTelemetry(config *Config) (*metrics.InmemSink, error) { if err != nil { return inm, err } + sink.SetTags(telConfig.DataDogTags) fanout = append(fanout, sink) } diff --git a/command/agent/config.go b/command/agent/config.go index 6eb1fad06..5cd1ebf17 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -355,6 +355,7 @@ type Telemetry struct { StatsiteAddr string `mapstructure:"statsite_address"` StatsdAddr string `mapstructure:"statsd_address"` DataDogAddr string `mapstructure:"datadog_address"` + DataDogTags []string `mapstructure:"datadog_tags"` PrometheusMetrics bool `mapstructure:"prometheus_metrics"` DisableHostname bool `mapstructure:"disable_hostname"` UseNodeName bool `mapstructure:"use_node_name"` @@ -1170,6 +1171,9 @@ func (a *Telemetry) Merge(b *Telemetry) *Telemetry { if b.DataDogAddr != "" { result.DataDogAddr = b.DataDogAddr } + if b.DataDogTags != nil { + result.DataDogTags = b.DataDogTags + } if b.PrometheusMetrics { result.PrometheusMetrics = b.PrometheusMetrics } diff --git a/command/agent/config_parse.go b/command/agent/config_parse.go index 6feaa6d04..774ec793c 100644 --- a/command/agent/config_parse.go +++ b/command/agent/config_parse.go @@ -641,6 +641,7 @@ func parseTelemetry(result **Telemetry, list *ast.ObjectList) error { "publish_allocation_metrics", "publish_node_metrics", "datadog_address", + "datadog_tags", "prometheus_metrics", "circonus_api_token", "circonus_api_app", diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 3ed05bd46..35a6b59e2 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -56,6 +56,7 @@ func TestConfig_Merge(t *testing.T) { StatsiteAddr: "127.0.0.1:8125", StatsdAddr: "127.0.0.1:8125", DataDogAddr: "127.0.0.1:8125", + DataDogTags: []string{"cat1:tag1", "cat2:tag2"}, PrometheusMetrics: true, DisableHostname: false, DisableTaggedMetrics: true, @@ -191,6 +192,7 @@ func TestConfig_Merge(t *testing.T) { StatsiteAddr: "127.0.0.2:8125", StatsdAddr: "127.0.0.2:8125", DataDogAddr: "127.0.0.1:8125", + DataDogTags: []string{"cat1:tag1", "cat2:tag2"}, PrometheusMetrics: true, DisableHostname: true, PublishNodeMetrics: true, diff --git a/website/source/api/agent.html.md b/website/source/api/agent.html.md index 6172d2e02..b8b390fa6 100644 --- a/website/source/api/agent.html.md +++ b/website/source/api/agent.html.md @@ -279,6 +279,7 @@ $ curl \ "CirconusSubmissionInterval": "", "CollectionInterval": "1s", "DataDogAddr": "", + "DataDogTags": [], "DisableHostname": false, "PublishAllocationMetrics": false, "PublishNodeMetrics": false, diff --git a/website/source/docs/agent/configuration/telemetry.html.md b/website/source/docs/agent/configuration/telemetry.html.md index b32af1e81..bf5b486e3 100644 --- a/website/source/docs/agent/configuration/telemetry.html.md +++ b/website/source/docs/agent/configuration/telemetry.html.md @@ -108,9 +108,15 @@ These `telemetry` parameters apply to - `datadog_address` `(string: "")` - Specifies the address of a DataDog statsd server to forward metrics to. +- `datadog_tags` `(list: [])` - Specifies a list of global tags that will be + added to all telemetry packets sent to DogStatsD. It is a list of strings, + where each string looks like "my_tag_name:my_tag_value". + + ```hcl telemetry { datadog_address = "dogstatsd.company.local:8125" + datadog_tags = ["my_tag_name:my_tag_value"] } ``` From ec35b5a02f33be92b9d02e7d2bc36563c27087ef Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:31:48 -0800 Subject: [PATCH 51/79] Deconstruct the existing job detail page into common parts This is to later compose job detail page variations --- .../components/job-page/parts/evaluations.js | 12 ++++++ .../job-page/parts/placement-failures.js | 6 +++ .../job-page/parts/running-deployment.js | 6 +++ ui/app/components/job-page/parts/summary.js | 7 ++++ .../components/job-page/parts/task-groups.js | 23 +++++++++++ .../components/job-page/parts/body.hbs | 6 +++ .../components/job-page/parts/evaluations.hbs | 38 +++++++++++++++++++ .../job-page/parts/placement-failures.hbs | 12 ++++++ .../job-page/parts/running-deployment.hbs | 33 ++++++++++++++++ .../components/job-page/parts/summary.hbs | 27 +++++++++++++ .../components/job-page/parts/task-groups.hbs | 25 ++++++++++++ 11 files changed, 195 insertions(+) create mode 100644 ui/app/components/job-page/parts/evaluations.js create mode 100644 ui/app/components/job-page/parts/placement-failures.js create mode 100644 ui/app/components/job-page/parts/running-deployment.js create mode 100644 ui/app/components/job-page/parts/summary.js create mode 100644 ui/app/components/job-page/parts/task-groups.js create mode 100644 ui/app/templates/components/job-page/parts/body.hbs create mode 100644 ui/app/templates/components/job-page/parts/evaluations.hbs create mode 100644 ui/app/templates/components/job-page/parts/placement-failures.hbs create mode 100644 ui/app/templates/components/job-page/parts/running-deployment.hbs create mode 100644 ui/app/templates/components/job-page/parts/summary.hbs create mode 100644 ui/app/templates/components/job-page/parts/task-groups.hbs diff --git a/ui/app/components/job-page/parts/evaluations.js b/ui/app/components/job-page/parts/evaluations.js new file mode 100644 index 000000000..33f6054a7 --- /dev/null +++ b/ui/app/components/job-page/parts/evaluations.js @@ -0,0 +1,12 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + job: null, + + classNames: ['boxed-section'], + + sortedEvaluations: computed('job.evaluations.@each.modifyIndex', function() { + return (this.get('job.evaluations') || []).sortBy('modifyIndex').reverse(); + }), +}); diff --git a/ui/app/components/job-page/parts/placement-failures.js b/ui/app/components/job-page/parts/placement-failures.js new file mode 100644 index 000000000..7df4236d8 --- /dev/null +++ b/ui/app/components/job-page/parts/placement-failures.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + tagName: '', +}); diff --git a/ui/app/components/job-page/parts/running-deployment.js b/ui/app/components/job-page/parts/running-deployment.js new file mode 100644 index 000000000..7df4236d8 --- /dev/null +++ b/ui/app/components/job-page/parts/running-deployment.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + tagName: '', +}); diff --git a/ui/app/components/job-page/parts/summary.js b/ui/app/components/job-page/parts/summary.js new file mode 100644 index 000000000..0ff44fc5a --- /dev/null +++ b/ui/app/components/job-page/parts/summary.js @@ -0,0 +1,7 @@ +import Component from '@ember/component'; + +export default Component.extend({ + job: null, + + classNames: ['boxed-section'], +}); diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js new file mode 100644 index 000000000..a1829c33b --- /dev/null +++ b/ui/app/components/job-page/parts/task-groups.js @@ -0,0 +1,23 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import Sortable from 'nomad-ui/mixins/sortable'; + +export default Component.extend(Sortable, { + job: null, + + classNames: ['boxed-section'], + + // Provide a value that is bound to a query param + sortProperty: null, + sortDescending: null, + + gotoTaskGroup() {}, + + taskGroups: computed('job.taskGroups.[]', function() { + return this.get('job.taskGroups') || []; + }), + + listToSort: alias('taskGroups'), + sortedTaskGroups: alias('listSorted'), +}); diff --git a/ui/app/templates/components/job-page/parts/body.hbs b/ui/app/templates/components/job-page/parts/body.hbs new file mode 100644 index 000000000..12c339ce4 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/body.hbs @@ -0,0 +1,6 @@ +{{#gutter-menu class="page-body" onNamespaceChange=onNamespaceChange}} + {{partial "jobs/job/subnav"}} +
+ {{yield}} +
+{{/gutter-menu}} diff --git a/ui/app/templates/components/job-page/parts/evaluations.hbs b/ui/app/templates/components/job-page/parts/evaluations.hbs new file mode 100644 index 000000000..f49a6f10b --- /dev/null +++ b/ui/app/templates/components/job-page/parts/evaluations.hbs @@ -0,0 +1,38 @@ +
+ Evaluations +
+
+ {{#if sortedEvaluations.length}} + {{#list-table source=sortedEvaluations as |t|}} + {{#t.head}} + ID + Priority + Triggered By + Status + Placement Failures + {{/t.head}} + {{#t.body as |row|}} + + {{row.model.shortId}} + {{row.model.priority}} + {{row.model.triggeredBy}} + {{row.model.status}} + + {{#if (eq row.model.status "blocked")}} + N/A - In Progress + {{else if row.model.hasPlacementFailures}} + True + {{else}} + False + {{/if}} + + + {{/t.body}} + {{/list-table}} + {{else}} +
+

No Evaluations

+

This is most likely due to garbage collection.

+
+ {{/if}} +
diff --git a/ui/app/templates/components/job-page/parts/placement-failures.hbs b/ui/app/templates/components/job-page/parts/placement-failures.hbs new file mode 100644 index 000000000..f8bf078d0 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/placement-failures.hbs @@ -0,0 +1,12 @@ +{{#if job.hasPlacementFailures}} +
+
+ Placement Failures +
+
+ {{#each job.taskGroups as |taskGroup|}} + {{placement-failure taskGroup=taskGroup}} + {{/each}} +
+
+{{/if}} diff --git a/ui/app/templates/components/job-page/parts/running-deployment.hbs b/ui/app/templates/components/job-page/parts/running-deployment.hbs new file mode 100644 index 000000000..c42bb866d --- /dev/null +++ b/ui/app/templates/components/job-page/parts/running-deployment.hbs @@ -0,0 +1,33 @@ +{{#if job.runningDeployment}} +
+
+
+ Active Deployment + {{job.runningDeployment.shortId}} + {{#if job.runningDeployment.version.submitTime}} + {{moment-from-now job.runningDeployment.version.submitTime}} + {{/if}} +
+
+ Running + {{#if job.runningDeployment.requiresPromotion}} + Deployment is running but requires promotion + {{/if}} +
+
+
+ {{#job-deployment-details deployment=job.runningDeployment as |d|}} + {{d.metrics}} + {{#if isShowingDeploymentDetails}} + {{d.taskGroups}} + {{d.allocations}} + {{/if}} + {{/job-deployment-details}} +
+ +
+{{/if}} diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs new file mode 100644 index 000000000..f00984304 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -0,0 +1,27 @@ +
+
+ {{#if job.hasChildren}} + Children Status {{job.totalChildren}} + {{else}} + Allocation Status {{job.totalAllocs}} + {{/if}} +
+
+
+ {{#component (if job.hasChildren "children-status-bar" "allocation-status-bar") + allocationContainer=job + job=job + class="split-view" as |chart|}} +
    + {{#each chart.data as |datum index|}} +
  1. + + {{datum.value}} + + {{datum.label}} + +
  2. + {{/each}} +
+ {{/component}} +
diff --git a/ui/app/templates/components/job-page/parts/task-groups.hbs b/ui/app/templates/components/job-page/parts/task-groups.hbs new file mode 100644 index 000000000..d6fbf5942 --- /dev/null +++ b/ui/app/templates/components/job-page/parts/task-groups.hbs @@ -0,0 +1,25 @@ +
+
+ Task Groups +
+
+ {{#list-table + source=sortedTaskGroups + sortProperty=sortProperty + sortDescending=sortDescending as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="count"}}Count{{/t.sort-by}} + {{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}} + {{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}} + {{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}} + {{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}} + {{/t.head}} + {{#t.body as |row|}} + {{task-group-row data-test-task-group + taskGroup=row.model + onClick=(action gotoTaskGroup row.model)}} + {{/t.body}} + {{/list-table}} +
+
From bb1e0d66b69373b10954677b0dcdc737fb21ff7b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 09:32:34 -0800 Subject: [PATCH 52/79] Recreate the service job detail page using job part components --- ui/app/components/job-page/abstract.js | 25 +++++++++++++ ui/app/components/job-page/service.js | 3 ++ .../templates/components/job-page/service.hbs | 37 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 ui/app/components/job-page/abstract.js create mode 100644 ui/app/components/job-page/service.js create mode 100644 ui/app/templates/components/job-page/service.hbs diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js new file mode 100644 index 000000000..ecd0cc2b2 --- /dev/null +++ b/ui/app/components/job-page/abstract.js @@ -0,0 +1,25 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; + +export default Component.extend({ + job: null, + + // Provide a value that is bound to a query param + sortProperty: null, + sortDescending: null, + + // Provide actions that require routing + onNamespaceChange() {}, + gotoTaskGroup() {}, + + breadcrumbs: computed('job.{name,id}', function() { + const job = this.get('job'); + return [ + { label: 'Jobs', args: ['jobs'] }, + { + label: job.get('name'), + args: ['jobs.job', job], + }, + ]; + }), +}); diff --git a/ui/app/components/job-page/service.js b/ui/app/components/job-page/service.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/service.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/templates/components/job-page/service.hbs b/ui/app/templates/components/job-page/service.hbs new file mode 100644 index 000000000..47343c49a --- /dev/null +++ b/ui/app/templates/components/job-page/service.hbs @@ -0,0 +1,37 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/running-deployment job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From 0ea1d0daaff017641b545ff755225b7595306a79 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:18:53 -0800 Subject: [PATCH 53/79] Fleshing out job page parts and differences --- ui/app/components/job-page/abstract.js | 1 + ui/app/components/job-page/batch.js | 3 + .../job-page/parameterized-child.js | 3 + ui/app/components/job-page/parameterized.js | 3 + ui/app/components/job-page/parts/children.js | 31 ++++ .../components/job-page/parts/task-groups.js | 1 + ui/app/components/job-page/periodic-child.js | 3 + ui/app/components/job-page/periodic.js | 3 + ui/app/controllers/jobs/job/index.js | 22 +-- ui/app/templates/jobs/job/index.hbs | 170 +----------------- 10 files changed, 63 insertions(+), 177 deletions(-) create mode 100644 ui/app/components/job-page/batch.js create mode 100644 ui/app/components/job-page/parameterized-child.js create mode 100644 ui/app/components/job-page/parameterized.js create mode 100644 ui/app/components/job-page/parts/children.js create mode 100644 ui/app/components/job-page/periodic-child.js create mode 100644 ui/app/components/job-page/periodic.js diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index ecd0cc2b2..e9ea804a4 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -11,6 +11,7 @@ export default Component.extend({ // Provide actions that require routing onNamespaceChange() {}, gotoTaskGroup() {}, + gotoJob() {}, breadcrumbs: computed('job.{name,id}', function() { const job = this.get('job'); diff --git a/ui/app/components/job-page/batch.js b/ui/app/components/job-page/batch.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/batch.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/parameterized-child.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parameterized.js b/ui/app/components/job-page/parameterized.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/parameterized.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/parts/children.js b/ui/app/components/job-page/parts/children.js new file mode 100644 index 000000000..30772cd0a --- /dev/null +++ b/ui/app/components/job-page/parts/children.js @@ -0,0 +1,31 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import Sortable from 'nomad-ui/mixins/sortable'; + +export default Component.extend(Sortable, { + job: null, + + classNames: ['boxed-section'], + + // Provide a value that is bound to a query param + sortProperty: null, + sortDescending: null, + currentPage: null, + + // Provide an action with access to the router + gotoJob() {}, + + pageSize: 10, + + taskGroups: computed('job.taskGroups.[]', function() { + return this.get('job.taskGroups') || []; + }), + + children: computed('job.children.[]', function() { + return this.get('job.children') || []; + }), + + listToSort: alias('children'), + sortedChildren: alias('listSorted'), +}); diff --git a/ui/app/components/job-page/parts/task-groups.js b/ui/app/components/job-page/parts/task-groups.js index a1829c33b..f5ce33757 100644 --- a/ui/app/components/job-page/parts/task-groups.js +++ b/ui/app/components/job-page/parts/task-groups.js @@ -12,6 +12,7 @@ export default Component.extend(Sortable, { sortProperty: null, sortDescending: null, + // Provide an action with access to the router gotoTaskGroup() {}, taskGroups: computed('job.taskGroups.[]', function() { diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/periodic-child.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js new file mode 100644 index 000000000..559b3c8b8 --- /dev/null +++ b/ui/app/components/job-page/periodic.js @@ -0,0 +1,3 @@ +import AbstractJobPage from './abstract'; + +export default AbstractJobPage.extend(); diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 97b97efb5..c5cb709a9 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,11 +1,9 @@ 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'; -export default Controller.extend(Sortable, WithNamespaceResetting, { +export default Controller.extend(WithNamespaceResetting, { system: service(), jobController: controller('jobs.job'), @@ -16,7 +14,6 @@ export default Controller.extend(Sortable, WithNamespaceResetting, { }, currentPage: 1, - pageSize: 10, sortProperty: 'name', sortDescending: false, @@ -24,20 +21,15 @@ export default Controller.extend(Sortable, WithNamespaceResetting, { breadcrumbs: alias('jobController.breadcrumbs'), job: alias('model'), - taskGroups: computed('model.taskGroups.[]', function() { - return this.get('model.taskGroups') || []; - }), - - listToSort: alias('taskGroups'), - sortedTaskGroups: alias('listSorted'), - - sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() { - return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse(); - }), - actions: { gotoTaskGroup(taskGroup) { this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup); }, + + gotoJob(job) { + this.transitionToRoute('jobs.job', job, { + queryParams: { jobNamespace: job.get('namespace.name') }, + }); + }, }, }); diff --git a/ui/app/templates/jobs/job/index.hbs b/ui/app/templates/jobs/job/index.hbs index bc2e6b7ce..7d81851b2 100644 --- a/ui/app/templates/jobs/job/index.hbs +++ b/ui/app/templates/jobs/job/index.hbs @@ -1,162 +1,8 @@ -{{#global-header class="page-header"}} - {{#each breadcrumbs as |breadcrumb index|}} - - {{/each}} -{{/global-header}} -{{#gutter-menu class="page-body" onNamespaceChange=(action "gotoJobs")}} - {{partial "jobs/job/subnav"}} -
-

- {{model.name}} - {{model.status}} - {{#if model.periodic}} - periodic - {{else if model.parameterized}} - parameterized - {{/if}} -

- -
-
- Type: {{model.type}} | - Priority: {{model.priority}} - {{#if (and model.namespace system.shouldShowNamespaces)}} - | Namespace: {{model.namespace.name}} - {{/if}} -
-
- -
-
-
Allocation Status {{taskGroups.length}}
-
-
- {{#allocation-status-bar allocationContainer=model class="split-view" as |chart|}} -
    - {{#each chart.data as |datum index|}} -
  1. - - {{datum.value}} - - {{datum.label}} - -
  2. - {{/each}} -
- {{/allocation-status-bar}} -
-
- - {{#if model.hasPlacementFailures}} -
-
- Placement Failures -
-
- {{#each model.taskGroups as |taskGroup|}} - {{placement-failure taskGroup=taskGroup}} - {{/each}} -
-
- {{/if}} - - {{#if model.runningDeployment}} -
-
-
- Active Deployment - {{model.runningDeployment.shortId}} - {{#if model.runningDeployment.version.submitTime}} - {{moment-from-now model.runningDeployment.version.submitTime}} - {{/if}} -
-
- Running - {{#if model.runningDeployment.requiresPromotion}} - Deployment is running but requires promotion - {{/if}} -
-
-
- {{#job-deployment-details deployment=model.runningDeployment as |d|}} - {{d.metrics}} - {{#if isShowingDeploymentDetails}} - {{d.taskGroups}} - {{d.allocations}} - {{/if}} - {{/job-deployment-details}} -
- -
- {{/if}} - -
-
- Task Groups -
-
- {{#list-pagination - source=sortedTaskGroups - sortProperty=sortProperty - sortDescending=sortDescending as |p|}} - {{#list-table - source=p.list - sortProperty=sortProperty - sortDescending=sortDescending as |t|}} - {{#t.head}} - {{#t.sort-by prop="name"}}Name{{/t.sort-by}} - {{#t.sort-by prop="count"}}Count{{/t.sort-by}} - {{#t.sort-by prop="queuedOrStartingAllocs" class="is-3"}}Allocation Status{{/t.sort-by}} - {{#t.sort-by prop="reservedCPU"}}Reserved CPU{{/t.sort-by}} - {{#t.sort-by prop="reservedMemory"}}Reserved Memory{{/t.sort-by}} - {{#t.sort-by prop="reservedEphemeralDisk"}}Reserved Disk{{/t.sort-by}} - {{/t.head}} - {{#t.body as |row|}} - {{task-group-row data-test-task-group taskGroup=row.model onClick=(action "gotoTaskGroup" row.model)}} - {{/t.body}} - {{/list-table}} - {{/list-pagination}} -
-
- -
-
- Evaluations -
-
- {{#list-table source=sortedEvaluations as |t|}} - {{#t.head}} - ID - Priority - Triggered By - Status - Placement Failures - {{/t.head}} - {{#t.body as |row|}} - - {{row.model.shortId}} - {{row.model.priority}} - {{row.model.triggeredBy}} - {{row.model.status}} - - {{#if (eq row.model.status "blocked")}} - N/A - In Progress - {{else if row.model.hasPlacementFailures}} - True - {{else}} - False - {{/if}} - - - {{/t.body}} - {{/list-table}} -
-
-
-{{/gutter-menu}} +{{component (concat "job-page/" model.templateType) + job=model + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + onNamespaceChange=(action "gotoJobs") + gotoJob=(action "gotoJob") + gotoTaskGroup=(action "gotoTaskGroup")}} From b4bdc61594f05f525cc5b75bf2245f5c75f29604 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:19:18 -0800 Subject: [PATCH 54/79] New job page components for parent jobs and batch jobs --- .../templates/components/job-page/batch.hbs | 35 +++++++++++++++++++ .../components/job-page/parameterized.hbs | 32 +++++++++++++++++ .../components/job-page/periodic.hbs | 33 +++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 ui/app/templates/components/job-page/batch.hbs create mode 100644 ui/app/templates/components/job-page/parameterized.hbs create mode 100644 ui/app/templates/components/job-page/periodic.hbs diff --git a/ui/app/templates/components/job-page/batch.hbs b/ui/app/templates/components/job-page/batch.hbs new file mode 100644 index 000000000..74b7f5c28 --- /dev/null +++ b/ui/app/templates/components/job-page/batch.hbs @@ -0,0 +1,35 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} diff --git a/ui/app/templates/components/job-page/parameterized.hbs b/ui/app/templates/components/job-page/parameterized.hbs new file mode 100644 index 000000000..a8e3ed47f --- /dev/null +++ b/ui/app/templates/components/job-page/parameterized.hbs @@ -0,0 +1,32 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} + Parameterized +

+ +
+
+ Version: {{job.version}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} +{{/job-page/parts/body}} diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs new file mode 100644 index 000000000..c172bc9c1 --- /dev/null +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -0,0 +1,33 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} + periodic +

+ +
+
+ Version: {{job.version}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} + | Cron: {{job.periodicDetails.Spec}} +
+
+ + {{job-page/parts/summary job=job}} + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} +{{/job-page/parts/body}} From ab22e95bbf2554f810f21393787af624c7745381 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 14:19:47 -0800 Subject: [PATCH 55/79] Paginated and sortable table for job launches/children jobs --- .../components/job-page/parts/children.hbs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 ui/app/templates/components/job-page/parts/children.hbs diff --git a/ui/app/templates/components/job-page/parts/children.hbs b/ui/app/templates/components/job-page/parts/children.hbs new file mode 100644 index 000000000..944168f1b --- /dev/null +++ b/ui/app/templates/components/job-page/parts/children.hbs @@ -0,0 +1,42 @@ +
+ Job Launches +
+
+ {{#list-pagination + source=sortedChildren + size=pageSize + page=currentPage as |p|}} + {{#list-table + source=p.list + sortProperty=sortProperty + sortDescending=sortDescending + class="with-foot" as |t|}} + {{#t.head}} + {{#t.sort-by prop="name"}}Name{{/t.sort-by}} + {{#t.sort-by prop="status"}}Status{{/t.sort-by}} + {{#t.sort-by prop="type"}}Type{{/t.sort-by}} + {{#t.sort-by prop="priority"}}Priority{{/t.sort-by}} + Groups + Summary + {{/t.head}} + {{#t.body key="model.id" as |row|}} + {{job-row data-test-job-row job=row.model onClick=(action gotoJob row.model)}} + {{/t.body}} + {{/list-table}} +
+ +
+ {{else}} +
+

No Job Launches

+

No remaining living job launches.

+
+ {{/list-pagination}} +
From 81273dca988f05a4d3ea04ace9042027589daf80 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:50:31 -0800 Subject: [PATCH 56/79] Bring payload in from the job api response --- ui/app/models/job.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index c6ab54835..3b8b98d0a 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -148,4 +148,10 @@ export default Model.extend({ return classMap[this.get('status')] || 'is-dark'; }), + + payload: attr('string'), + decodedPayload: computed('payload', function() { + // Lazily decode the base64 encoded payload + return window.atob(this.get('payload') || ''); + }), }); From fbd166bc9be2a01ad8383f270fe874e6af98befe Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:55:09 -0800 Subject: [PATCH 57/79] Breadcrumbs for the periodic child job page --- ui/app/components/job-page/periodic-child.js | 20 ++++++++- .../components/job-page/periodic-child.hbs | 41 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 ui/app/templates/components/job-page/periodic-child.hbs diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index 559b3c8b8..ea19ed4d3 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -1,3 +1,21 @@ import AbstractJobPage from './abstract'; +import { computed } from '@ember/object'; -export default AbstractJobPage.extend(); +export default AbstractJobPage.extend({ + breadcrumbs: computed('job.{name,id}', 'job.parent.{name,id}', function() { + const job = this.get('job'); + const parent = this.get('job.parent'); + + return [ + { label: 'Jobs', args: ['jobs'] }, + { + label: parent.get('name'), + args: ['jobs.job', parent], + }, + { + label: job.get('name'), + args: ['jobs.job', job], + }, + ]; + }), +}); diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs new file mode 100644 index 000000000..387f0a585 --- /dev/null +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -0,0 +1,41 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + + Parent: + {{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}} + {{job.parent.name}} + {{/link-to}} + + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From ec49a72a060350d41aabe12476f31d5a74846147 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:56:52 -0800 Subject: [PATCH 58/79] Elastic mode for cli window component --- ui/app/styles/components/cli-window.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/styles/components/cli-window.scss b/ui/app/styles/components/cli-window.scss index f8be6ff7a..0f78d1708 100644 --- a/ui/app/styles/components/cli-window.scss +++ b/ui/app/styles/components/cli-window.scss @@ -12,4 +12,8 @@ .is-light { color: $text; } + + &.is-elastic { + height: auto; + } } From 3fc0910ee7f2cf3c0b914e3db0e21182bb7017fe Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:57:58 -0800 Subject: [PATCH 59/79] Payload details for the parameterized child job detail page --- .../job-page/parameterized-child.js | 17 +++++- .../job-page/parameterized-child.hbs | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 ui/app/templates/components/job-page/parameterized-child.hbs diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index 559b3c8b8..841c6fa60 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -1,3 +1,16 @@ -import AbstractJobPage from './abstract'; +import { computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; +import PeriodicChildJobPage from './periodic-child'; -export default AbstractJobPage.extend(); +export default PeriodicChildJobPage.extend({ + payload: alias('job.decodedPayload'), + payloadJSON: computed('payload', function() { + let json; + try { + json = JSON.parse(this.get('payload')); + } catch (e) { + // Swallow error and fall back to plain text rendering + } + return json; + }), +}); diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs new file mode 100644 index 000000000..245395c9f --- /dev/null +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -0,0 +1,52 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + + Parent: + {{#link-to "jobs.job" job.parent (query-params jobNamespace=job.parent.namespace.name)}} + {{job.parent.name}} + {{/link-to}} + + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} + +
+
Payload
+
+ {{#if payloadJSON}} + {{json-viewer json=payloadJSON}} + {{else}} +
{{payload}}
+ {{/if}} +
+
+{{/job-page/parts/body}} From d7b9283488858051be337ded62154d81009eb313 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 25 Jan 2018 16:59:30 -0800 Subject: [PATCH 60/79] For now, the system job is identical to the service job --- .../templates/components/job-page/system.hbs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 ui/app/templates/components/job-page/system.hbs diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs new file mode 100644 index 000000000..47343c49a --- /dev/null +++ b/ui/app/templates/components/job-page/system.hbs @@ -0,0 +1,37 @@ +{{#global-header class="page-header"}} + {{#each breadcrumbs as |breadcrumb index|}} + + {{/each}} +{{/global-header}} +{{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +

+ {{job.name}} + {{job.status}} +

+ +
+
+ Type: {{job.type}} | + Priority: {{job.priority}} + {{#if (and job.namespace system.shouldShowNamespaces)}} + | Namespace: {{job.namespace.name}} + {{/if}} +
+
+ + {{job-page/parts/summary job=job}} + + {{job-page/parts/placement-failures job=job}} + + {{job-page/parts/running-deployment job=job}} + + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + + {{job-page/parts/evaluations job=job}} +{{/job-page/parts/body}} From 52380527b2261125b1c529f0da0d2d40d9c1436f Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:32:11 -0800 Subject: [PATCH 61/79] Add ability to force a periodic job launch --- ui/app/adapters/job.js | 13 +++++++++++++ ui/app/components/job-page/periodic.js | 14 +++++++++++++- ui/app/models/job.js | 4 ++++ ui/app/templates/components/job-page/periodic.hbs | 1 + 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/ui/app/adapters/job.js b/ui/app/adapters/job.js index 80398adac..c783c6ff2 100644 --- a/ui/app/adapters/job.js +++ b/ui/app/adapters/job.js @@ -65,4 +65,17 @@ export default ApplicationAdapter.extend({ const url = this.buildURL('job', name, job, 'findRecord'); return this.ajax(url, 'GET', { data: assign(this.buildQuery() || {}, namespaceQuery) }); }, + + forcePeriodic(job) { + if (job.get('periodic')) { + const [name, namespace] = JSON.parse(job.get('id')); + let url = `${this.buildURL('job', name, job, 'findRecord')}/periodic/force`; + + if (namespace) { + url += `?namespace=${namespace}`; + } + + return this.ajax(url, 'POST'); + } + }, }); diff --git a/ui/app/components/job-page/periodic.js b/ui/app/components/job-page/periodic.js index 559b3c8b8..705d95a2f 100644 --- a/ui/app/components/job-page/periodic.js +++ b/ui/app/components/job-page/periodic.js @@ -1,3 +1,15 @@ import AbstractJobPage from './abstract'; +import { inject as service } from '@ember/service'; -export default AbstractJobPage.extend(); +export default AbstractJobPage.extend({ + store: service(), + actions: { + forceLaunch() { + this.get('job') + .forcePeriodic() + .then(() => { + this.get('store').findAll('job'); + }); + }, + }, +}); diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 3b8b98d0a..1f9786145 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -139,6 +139,10 @@ export default Model.extend({ return this.store.adapterFor('job').fetchRawDefinition(this); }, + forcePeriodic() { + return this.store.adapterFor('job').forcePeriodic(this); + }, + statusClass: computed('status', function() { const classMap = { pending: 'is-pending', diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index c172bc9c1..6273f1375 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,6 +10,7 @@ {{job.name}} {{job.status}} periodic +
From 60cb1ac27e06649430ccfe6d4e6022e810d1d997 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:36:50 -0800 Subject: [PATCH 62/79] Differentiate between no search matches and no allocs on task group page --- ui/app/templates/jobs/job/task-group.hbs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/ui/app/templates/jobs/job/task-group.hbs b/ui/app/templates/jobs/job/task-group.hbs index 90928f830..2934d2c1a 100644 --- a/ui/app/templates/jobs/job/task-group.hbs +++ b/ui/app/templates/jobs/job/task-group.hbs @@ -100,12 +100,21 @@
{{else}} -
-
-

No Matches

-

No allocations match the term {{searchTerm}}

+ {{#if allocations.length}} +
+
+

No Matches

+

No allocations match the term {{searchTerm}}

+
-
+ {{else}} +
+
+

No Allocations

+

No allocations have been placed.

+
+
+ {{/if}} {{/list-pagination}}
From dc0fa160f95c7429ec0d6a038f9e34cf347ee2d7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 14:56:23 -0800 Subject: [PATCH 63/79] Clean up force launch button --- ui/app/styles/core/buttons.scss | 4 ++++ ui/app/templates/components/job-page/periodic.hbs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 27d44f0e9..9a360f3c9 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -24,6 +24,10 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2); background: transparent; } + &.is-inline { + vertical-align: middle; + } + &.is-compact { padding: 0.25em 0.75em; margin: -0.25em -0.25em -0.25em 0; diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index 6273f1375..065c8c96e 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,7 +10,7 @@ {{job.name}} {{job.status}} periodic - +
From d264c43eae8138793bb223676a92ef397f62ffe9 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 15:01:10 -0800 Subject: [PATCH 64/79] State periodic or parameterized as the job type when applicable --- ui/app/models/job.js | 18 +++++++++++++++--- ui/app/templates/components/job-row.hbs | 2 +- ui/tests/acceptance/jobs-list-test.js | 18 ++++++++---------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 1f9786145..2fbf90626 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -34,6 +34,18 @@ export default Model.extend({ parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // A composite of type and other job attributes to determine + // a better type descriptor for human interpretation rather + // than for scheduling. + displayType: computed('type', 'periodic', 'parameterized', function() { + if (this.get('periodic')) { + return 'periodic'; + } else if (this.get('parameterized')) { + return 'parameterized'; + } + return this.get('type'); + }), + // A composite of type and other job attributes to determine // type for templating rather than scheduling templateType: computed( @@ -57,10 +69,10 @@ export default Model.extend({ // Guard against the API introducing a new type before the UI // is prepared to handle it. return this.get('type'); - } else { - // A fail-safe in the event the API introduces a new type. - return 'service'; } + + // A fail-safe in the event the API introduces a new type. + return 'service'; } ), diff --git a/ui/app/templates/components/job-row.hbs b/ui/app/templates/components/job-row.hbs index d2c62b494..f64d05889 100644 --- a/ui/app/templates/components/job-row.hbs +++ b/ui/app/templates/components/job-row.hbs @@ -2,7 +2,7 @@ {{job.status}} -{{job.type}} +{{job.displayType}} {{job.priority}} {{#if job.isReloading}} diff --git a/ui/tests/acceptance/jobs-list-test.js b/ui/tests/acceptance/jobs-list-test.js index d7f870214..d7a30a1d1 100644 --- a/ui/tests/acceptance/jobs-list-test.js +++ b/ui/tests/acceptance/jobs-list-test.js @@ -59,7 +59,7 @@ test('each job row should contain information about the job', function(assert) { job.status, 'Status' ); - assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, job.type, 'Type'); + assert.equal(jobRow.querySelector('[data-test-job-type]').textContent, typeForJob(job), 'Type'); assert.equal( jobRow.querySelector('[data-test-job-priority]').textContent, job.priority, @@ -99,9 +99,7 @@ test('when there are no jobs, there is an empty message', function(assert) { }); }); -test('when there are jobs, but no matches for a search result, there is an empty message', function( - assert -) { +test('when there are jobs, but no matches for a search result, there is an empty message', function(assert) { server.create('job', { name: 'cat 1' }); server.create('job', { name: 'cat 2' }); @@ -117,9 +115,7 @@ test('when there are jobs, but no matches for a search result, there is an empty }); }); -test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function( - assert -) { +test('when the namespace query param is set, only matching jobs are shown and the namespace value is forwarded to app state', function(assert) { server.createList('namespace', 2); const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id }); const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id }); @@ -144,9 +140,7 @@ test('when the namespace query param is set, only matching jobs are shown and th }); }); -test('when accessing jobs is forbidden, show a message with a link to the tokens page', function( - assert -) { +test('when accessing jobs is forbidden, show a message with a link to the tokens page', function(assert) { server.pretender.get('/v1/jobs', () => [403, {}, null]); visit('/jobs'); @@ -163,3 +157,7 @@ test('when accessing jobs is forbidden, show a message with a link to the tokens assert.equal(currentURL(), '/settings/tokens'); }); }); + +function typeForJob(job) { + return job.periodic ? 'periodic' : job.parameterized ? 'parameterized' : job.type; +} From 35f388a712654fc12e76432f93c48ee13fb8c2b8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 26 Jan 2018 16:27:04 -0800 Subject: [PATCH 65/79] Trim the parent job prefix off the child job names when displaying them --- ui/app/components/job-page/periodic-child.js | 2 +- ui/app/models/job.js | 5 +++++ ui/app/templates/components/job-page/parameterized-child.hbs | 2 +- ui/app/templates/components/job-page/periodic-child.hbs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index ea19ed4d3..060627d93 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -13,7 +13,7 @@ export default AbstractJobPage.extend({ args: ['jobs.job', parent], }, { - label: job.get('name'), + label: job.get('trimmedName'), args: ['jobs.job', job], }, ]; diff --git a/ui/app/models/job.js b/ui/app/models/job.js index 2fbf90626..b77fc7a66 100644 --- a/ui/app/models/job.js +++ b/ui/app/models/job.js @@ -34,6 +34,11 @@ export default Model.extend({ parent: belongsTo('job', { inverse: 'children' }), children: hasMany('job', { inverse: 'parent' }), + // The parent job name is prepended to child launch job names + trimmedName: computed('name', 'parent', function() { + return this.get('parent.content') ? this.get('name').replace(/.+?\//, '') : this.get('name'); + }), + // A composite of type and other job attributes to determine // a better type descriptor for human interpretation rather // than for scheduling. diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 245395c9f..b01ad400f 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -7,7 +7,7 @@ {{/global-header}} {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}

- {{job.name}} + {{job.trimmedName}} {{job.status}}

diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 387f0a585..e39aed637 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -7,7 +7,7 @@ {{/global-header}} {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}}

- {{job.name}} + {{job.trimmedName}} {{job.status}}

From 98b00680d9c98ac1c2111a6fe24d2fdaf63852c1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 30 Jan 2018 13:19:24 -0800 Subject: [PATCH 66/79] Update job factory to use traits for specifying job type --- ui/mirage/factories/job-summary.js | 38 ++++++++++------ ui/mirage/factories/job.js | 69 +++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 20 deletions(-) diff --git a/ui/mirage/factories/job-summary.js b/ui/mirage/factories/job-summary.js index c766b8b22..c76d39e9f 100644 --- a/ui/mirage/factories/job-summary.js +++ b/ui/mirage/factories/job-summary.js @@ -1,4 +1,4 @@ -import { Factory, faker } from 'ember-cli-mirage'; +import { Factory, faker, trait } from 'ember-cli-mirage'; export default Factory.extend({ // Hidden property used to compute the Summary hash @@ -6,17 +6,27 @@ export default Factory.extend({ JobID: '', - Summary: function() { - return this.groupNames.reduce((summary, group) => { - summary[group] = { - Queued: faker.random.number(10), - Complete: faker.random.number(10), - Failed: faker.random.number(10), - Running: faker.random.number(10), - Starting: faker.random.number(10), - Lost: faker.random.number(10), - }; - return summary; - }, {}); - }, + withSummary: trait({ + Summary: function() { + return this.groupNames.reduce((summary, group) => { + summary[group] = { + Queued: faker.random.number(10), + Complete: faker.random.number(10), + Failed: faker.random.number(10), + Running: faker.random.number(10), + Starting: faker.random.number(10), + Lost: faker.random.number(10), + }; + return summary; + }, {}); + }, + }), + + withChildren: trait({ + Children: () => ({ + Pending: faker.random.number(10), + Running: faker.random.number(10), + Dead: faker.random.number(10), + }), + }), }); diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index b18d16d71..6a9103b78 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -1,4 +1,4 @@ -import { Factory, faker } from 'ember-cli-mirage'; +import { Factory, faker, trait } from 'ember-cli-mirage'; import { provide, provider, pickOne } from '../utils'; import { DATACENTERS } from '../common'; @@ -22,10 +22,48 @@ export default Factory.extend({ faker.list.random(...DATACENTERS) ), - periodic: () => Math.random() > 0.5, - parameterized() { - return !this.periodic; - }, + childrenCount: () => faker.random.number({ min: 1, max: 5 }), + + periodic: trait({ + type: 'batch', + periodic: true, + // periodic details object + // serializer update for bool vs details object + periodicDetails: () => ({ + Enabled: true, + ProhibitOverlap: true, + Spec: '*/5 * * * * *', + SpecType: 'cron', + TimeZone: 'UTC', + }), + }), + + parameterized: trait({ + type: 'batch', + parameterized: true, + // parameterized details object + // serializer update for bool vs details object + parameterizedDetails: () => ({ + MetaOptional: null, + MetaRequired: null, + Payload: Math.random() > 0.5 ? 'required' : null, + }), + }), + + periodicChild: trait({ + // Periodic children need a parent job, + // It is the Periodic job's responsibility to create + // periodicChild jobs and provide a parent job. + type: 'batch', + }), + + parameterizedChild: trait({ + // Parameterized children need a parent job, + // It is the Parameterized job's responsibility to create + // parameterizedChild jobs and provide a parent job. + type: 'batch', + payload: window.btoa(faker.lorem.sentence()), + }), createIndex: i => i, modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), @@ -70,7 +108,8 @@ export default Factory.extend({ }); } - const jobSummary = server.create('job-summary', { + const hasChildren = job.periodic || job.parameterized; + const jobSummary = server.create('job-summary', hasChildren ? 'withChildren' : 'withSummary', { groupNames: groups.mapBy('name'), job, }); @@ -102,5 +141,23 @@ export default Factory.extend({ modifyIndex: 4000, }); } + + if (job.periodic) { + // Create periodicChild jobs + server.createList('job', job.childrenCount, 'periodicChild', { + parentId: job.id, + namespaceId: job.namespaceId, + namespace: job.namespace, + }); + } + + if (job.parameterized) { + // Create parameterizedChild jobs + server.createList('job', job.childrenCount, 'parameterizedChild', { + parentId: job.id, + namespaceId: job.namespaceId, + namespace: job.namespace, + }); + } }, }); From 3907f39c9a155084ff25b29875f123bcbdab34e2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 09:23:41 -0800 Subject: [PATCH 67/79] Integration tests for the body job part --- ui/app/templates/components/gutter-menu.hbs | 2 +- ui/app/templates/jobs/job/subnav.hbs | 2 +- .../integration/job-page/parts/body-test.js | 137 ++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 ui/tests/integration/job-page/parts/body-test.js diff --git a/ui/app/templates/components/gutter-menu.hbs b/ui/app/templates/components/gutter-menu.hbs index 055e8e323..716a623fd 100644 --- a/ui/app/templates/components/gutter-menu.hbs +++ b/ui/app/templates/components/gutter-menu.hbs @@ -42,6 +42,6 @@
-
+
{{yield}}
diff --git a/ui/app/templates/jobs/job/subnav.hbs b/ui/app/templates/jobs/job/subnav.hbs index cfbdeb672..e4ef97f80 100644 --- a/ui/app/templates/jobs/job/subnav.hbs +++ b/ui/app/templates/jobs/job/subnav.hbs @@ -1,4 +1,4 @@ -
+
  • {{#link-to "jobs.job.index" job activeClass="is-active"}}Overview{{/link-to}}
  • {{#link-to "jobs.job.definition" job activeClass="is-active"}}Definition{{/link-to}}
  • diff --git a/ui/tests/integration/job-page/parts/body-test.js b/ui/tests/integration/job-page/parts/body-test.js new file mode 100644 index 000000000..042d1048b --- /dev/null +++ b/ui/tests/integration/job-page/parts/body-test.js @@ -0,0 +1,137 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find, findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; +import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/parts/body', 'Integration | Component | job-page/parts/body', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.server = startMirage(); + this.server.createList('namespace', 3); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, +}); + +test('includes a subnav for the job', function(assert) { + this.set('job', {}); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-subnav="job"]'), 'Job subnav is rendered'); + }); +}); + +test('the subnav includes the deployments link when the job is a service', function(assert) { + const store = getOwner(this).lookup('service:store'); + let job; + + run(() => { + job = store.createRecord('job', { + id: 'service-job', + type: 'service', + }); + }); + + this.set('job', job); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); + assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); + assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); + assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); + }); +}); + +test('the subnav does not include the deployments link when the job is not a service', function(assert) { + const store = getOwner(this).lookup('service:store'); + let job; + + run(() => { + job = store.createRecord('job', { + id: 'batch-job', + type: 'batch', + }); + }); + + this.set('job', job); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); + assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); + assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); + assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); + }); +}); + +test('body yields content to a section after the subnav', function(assert) { + this.set('job', {}); + this.set('onNamespaceChange', () => {}); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + assert.ok( + find('[data-test-page-content] .section > .inner-content'), + 'Content is rendered in a section in a gutter menu' + ); + assert.ok( + find('[data-test-subnav="job"] + .section > .inner-content'), + 'Content is rendered immediately after the subnav' + ); + }); +}); + +test('onNamespaceChange action is called when the namespace changes in the nested gutter menu', function(assert) { + const namespaceSpy = sinon.spy(); + + this.set('job', {}); + this.set('onNamespaceChange', namespaceSpy); + + this.render(hbs` + {{#job-page/parts/body job=job onNamespaceChange=onNamespaceChange}} +
    Inner content
    + {{/job-page/parts/body}} + `); + + return wait().then(() => { + clickTrigger('[data-test-namespace-switcher]'); + click(findAll('.ember-power-select-option')[1]); + + return wait().then(() => { + assert.ok(namespaceSpy.calledOnce, 'Switching namespaces calls the onNamespaceChange action'); + }); + }); +}); From e8606f7cc65bd12578e195aa8f2f64962a7212c2 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 14:01:13 -0800 Subject: [PATCH 68/79] Job part children tests --- ui/mirage/factories/job.js | 2 + .../job-page/parts/children-test.js | 202 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/children-test.js diff --git a/ui/mirage/factories/job.js b/ui/mirage/factories/job.js index 6a9103b78..fe97f3573 100644 --- a/ui/mirage/factories/job.js +++ b/ui/mirage/factories/job.js @@ -148,6 +148,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + createAllocations: job.createAllocations, }); } @@ -157,6 +158,7 @@ export default Factory.extend({ parentId: job.id, namespaceId: job.namespaceId, namespace: job.namespace, + createAllocations: job.createAllocations, }); } }, diff --git a/ui/tests/integration/job-page/parts/children-test.js b/ui/tests/integration/job-page/parts/children-test.js new file mode 100644 index 000000000..aecc32c7d --- /dev/null +++ b/ui/tests/integration/job-page/parts/children-test.js @@ -0,0 +1,202 @@ +import { getOwner } from '@ember/application'; +import { assign } from '@ember/polyfills'; +import { run } from '@ember/runloop'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { findAll, find, click } from 'ember-native-dom-helpers'; +import sinon from 'sinon'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/parts/children', 'Integration | Component | job-page/parts/children', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, +}); + +const props = (job, options = {}) => + assign( + { + job, + sortProperty: 'name', + sortDescending: true, + currentPage: 1, + gotoJob: () => {}, + }, + options + ); + +test('lists each child', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 3, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + assert.equal( + findAll('[data-test-job-name]').length, + parent.get('children.length'), + 'A row for each child' + ); + }); + }); +}); + +test('eventually paginates', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 11, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const childrenCount = parent.get('children.length'); + assert.ok(childrenCount > 10, 'Parent has more children than one page size'); + assert.equal(findAll('[data-test-job-name]').length, 10, 'Table length maxes out at 10'); + assert.ok(find('.pagination-next'), 'Next button is rendered'); + + assert.ok( + new RegExp(`1.10.+?${childrenCount}`).test(find('.pagination-numbers').textContent.trim()) + ); + }); + }); +}); + +test('is sorted based on the sortProperty and sortDescending properties', function(assert) { + let parent; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 3, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties(props(parent)); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const sortedChildren = parent.get('children').sortBy('name'); + const childRows = findAll('[data-test-job-name]'); + + sortedChildren.reverse().forEach((child, index) => { + assert.equal( + childRows[index].textContent.trim(), + child.get('name'), + `Child ${index} is ${child.get('name')}` + ); + }); + + this.set('sortDescending', false); + + sortedChildren.forEach((child, index) => { + assert.equal( + childRows[index].textContent.trim(), + child.get('name'), + `Child ${index} is ${child.get('name')}` + ); + }); + }); + }); +}); + +test('gotoJob is called when a job row is clicked', function(assert) { + let parent; + const gotoJobSpy = sinon.spy(); + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount: 1, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + parent = this.store.peekAll('job').findBy('plainId', 'parent'); + }); + + this.setProperties( + props(parent, { + gotoJob: gotoJobSpy, + }) + ); + + this.render(hbs` + {{job-page/parts/children + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + click('tr.job-row'); + assert.ok( + gotoJobSpy.withArgs(parent.get('children.firstObject')).calledOnce, + 'Clicking the job row calls the gotoJob action' + ); + }); + }); +}); From 88f1349276e0d64ee7f6bbd2a9453c4f2059523b Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 14:01:32 -0800 Subject: [PATCH 69/79] Job part evaluations test --- .../job-page/parts/evaluations-test.js | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/evaluations-test.js diff --git a/ui/tests/integration/job-page/parts/evaluations-test.js b/ui/tests/integration/job-page/parts/evaluations-test.js new file mode 100644 index 000000000..982c1d294 --- /dev/null +++ b/ui/tests/integration/job-page/parts/evaluations-test.js @@ -0,0 +1,65 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent( + 'job-page/parts/evaluations', + 'Integration | Component | job-page/parts/evaluations', + { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, + } +); + +test('lists all evaluations for the job', function(assert) { + let job; + + this.server.create('job', { noFailedPlacements: true, createAllocations: false }); + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + job = this.store.peekAll('job').get('firstObject'); + }); + + this.setProperties({ job }); + + this.render(hbs` + {{job-page/parts/evaluations job=job}} + `); + + return wait().then(() => { + const evaluationRows = findAll('[data-test-evaluation]'); + assert.equal( + evaluationRows.length, + job.get('evaluations.length'), + 'All evaluations are listed' + ); + + job + .get('evaluations') + .sortBy('modifyIndex') + .reverse() + .forEach((evaluation, index) => { + assert.equal( + evaluationRows[index].querySelector('[data-test-id]').textContent.trim(), + evaluation.get('shortId'), + `Evaluation ${index} is ${evaluation.get('shortId')}` + ); + }); + }); + }); +}); From 80c2acd218b5c8c2182fdbb270a0668fb16e3018 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 15:27:29 -0800 Subject: [PATCH 70/79] Running deployment job page part tests --- ui/tests/helpers/start-app.js | 2 +- .../job-page/parts/running-deployment-test.js | 141 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/parts/running-deployment-test.js diff --git a/ui/tests/helpers/start-app.js b/ui/tests/helpers/start-app.js index 2ff98a176..304c6e377 100644 --- a/ui/tests/helpers/start-app.js +++ b/ui/tests/helpers/start-app.js @@ -2,7 +2,7 @@ import { run } from '@ember/runloop'; import { merge } from '@ember/polyfills'; import Application from '../../app'; import config from '../../config/environment'; -import registerPowerSelectHelpers from '../../tests/helpers/ember-power-select'; +import registerPowerSelectHelpers from 'ember-power-select/test-support/helpers'; registerPowerSelectHelpers(); diff --git a/ui/tests/integration/job-page/parts/running-deployment-test.js b/ui/tests/integration/job-page/parts/running-deployment-test.js new file mode 100644 index 000000000..e6cb09bbd --- /dev/null +++ b/ui/tests/integration/job-page/parts/running-deployment-test.js @@ -0,0 +1,141 @@ +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import moment from 'moment'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/running-deployment', + 'Integration | Component | job-page/parts/running-deployment', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, + } +); + +test('there is no active deployment section when the job has no active deployment', function(assert) { + this.server.create('job', { + type: 'service', + noActiveDeployment: true, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}}) + `); + + return wait().then(() => { + assert.notOk(find('[data-test-active-deployment]'), 'No active deployment'); + }); + }); +}); + +test('the active deployment section shows up for the currently running deployment', function(assert) { + this.server.create('job', { type: 'service', createAllocations: false, activeDeployment: true }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}} + `); + + return wait().then(() => { + const deployment = this.get('job.runningDeployment'); + const version = deployment.get('version'); + + assert.ok(find('[data-test-active-deployment]'), 'Active deployment'); + assert.equal( + find('[data-test-active-deployment-stat="id"]').textContent.trim(), + deployment.get('shortId'), + 'The active deployment is the most recent running deployment' + ); + + assert.equal( + find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(), + moment(version.get('submitTime')).fromNow(), + 'Time since the job was submitted is in the active deployment header' + ); + + assert.equal( + find('[data-test-deployment-metric="canaries"]').textContent.trim(), + `${deployment.get('placedCanaries')} / ${deployment.get('desiredCanaries')}`, + 'Canaries, both places and desired, are in the metrics' + ); + + assert.equal( + find('[data-test-deployment-metric="placed"]').textContent.trim(), + deployment.get('placedAllocs'), + 'Placed allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="desired"]').textContent.trim(), + deployment.get('desiredTotal'), + 'Desired allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="healthy"]').textContent.trim(), + deployment.get('healthyAllocs'), + 'Healthy allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-metric="unhealthy"]').textContent.trim(), + deployment.get('unhealthyAllocs'), + 'Unhealthy allocs aggregates across task groups' + ); + + assert.equal( + find('[data-test-deployment-notification]').textContent.trim(), + deployment.get('statusDescription'), + 'Status description is in the metrics block' + ); + }); + }); +}); + +test('the active deployment section can be expanded to show task groups and allocations', function(assert) { + this.server.create('node'); + this.server.create('job', { type: 'service', activeDeployment: true }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + this.render(hbs` + {{job-page/parts/running-deployment job=job}} + `); + + return wait().then(() => { + assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found'); + assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found'); + + click('[data-test-deployment-toggle-details]'); + + return wait().then(() => { + assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found'); + assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found'); + }); + }); + }); +}); From 3ca0d64e0c79b3816e6c405e737e3f66dcee7425 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 16:12:50 -0800 Subject: [PATCH 71/79] Tests for the placement failures job part --- .../job-page/parts/placement-failures-test.js | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/placement-failures-test.js diff --git a/ui/tests/integration/job-page/parts/placement-failures-test.js b/ui/tests/integration/job-page/parts/placement-failures-test.js new file mode 100644 index 000000000..d77467540 --- /dev/null +++ b/ui/tests/integration/job-page/parts/placement-failures-test.js @@ -0,0 +1,86 @@ +import { getOwner } from '@ember/application'; +import { run } from '@ember/runloop'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { findAll, find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/placement-failures', + 'Integration | Component | job-page/parts/placement-failures', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + } +); + +test('when the job has placement failures, they are called out', function(assert) { + this.server.create('job', { failedPlacements: true, createAllocations: false }); + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + run(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + }); + + this.render(hbs` + {{job-page/parts/placement-failures job=job}}) + `); + + return wait().then(() => { + const failedEvaluation = this.get('job.evaluations') + .filterBy('hasPlacementFailures') + .sortBy('modifyIndex') + .reverse() + .get('firstObject'); + const failedTGAllocs = failedEvaluation.get('failedTGAllocs'); + + assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found'); + + const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title => + title.textContent.trim() + ); + + failedTGAllocs.forEach(alloc => { + const name = alloc.get('name'); + assert.ok( + taskGroupLabels.find(label => label.includes(name)), + `${name} included in placement failures list` + ); + assert.ok( + taskGroupLabels.find(label => label.includes(alloc.get('coalescedFailures') + 1)), + 'The number of unplaced allocs = CoalescedFailures + 1' + ); + }); + }); + }); +}); + +test('when the job has no placement failures, the placement failures section is gone', function(assert) { + this.server.create('job', { noFailedPlacements: true, createAllocations: false }); + this.store.findAll('job'); + + return wait().then(() => { + run(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + }); + + this.render(hbs` + {{job-page/parts/placement-failures job=job}}) + `); + + return wait().then(() => { + assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found'); + }); + }); +}); From 82de00720e9dec6ea147c0effa9adfe36f257a0c Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 31 Jan 2018 16:55:43 -0800 Subject: [PATCH 72/79] Tests for the summary job page part --- ui/app/components/allocation-status-bar.js | 2 + ui/app/components/children-status-bar.js | 2 + .../components/job-page/parts/summary.hbs | 2 +- .../job-page/parts/summary-test.js | 150 ++++++++++++++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/parts/summary-test.js diff --git a/ui/app/components/allocation-status-bar.js b/ui/app/components/allocation-status-bar.js index 7f2d1e2fc..5322ac97e 100644 --- a/ui/app/components/allocation-status-bar.js +++ b/ui/app/components/allocation-status-bar.js @@ -6,6 +6,8 @@ export default DistributionBar.extend({ allocationContainer: null, + 'data-test-allocation-status-bar': true, + data: computed( 'allocationContainer.{queuedAllocs,completeAllocs,failedAllocs,runningAllocs,startingAllocs}', function() { diff --git a/ui/app/components/children-status-bar.js b/ui/app/components/children-status-bar.js index cb32db128..b95b4f240 100644 --- a/ui/app/components/children-status-bar.js +++ b/ui/app/components/children-status-bar.js @@ -6,6 +6,8 @@ export default DistributionBar.extend({ job: null, + 'data-test-children-status-bar': true, + data: computed('job.{pendingChildren,runningChildren,deadChildren}', function() { if (!this.get('job')) { return []; diff --git a/ui/app/templates/components/job-page/parts/summary.hbs b/ui/app/templates/components/job-page/parts/summary.hbs index f00984304..57e2dd25e 100644 --- a/ui/app/templates/components/job-page/parts/summary.hbs +++ b/ui/app/templates/components/job-page/parts/summary.hbs @@ -12,7 +12,7 @@ allocationContainer=job job=job class="split-view" as |chart|}} -
      +
        {{#each chart.data as |datum index|}}
      1. diff --git a/ui/tests/integration/job-page/parts/summary-test.js b/ui/tests/integration/job-page/parts/summary-test.js new file mode 100644 index 000000000..d04d2e82e --- /dev/null +++ b/ui/tests/integration/job-page/parts/summary-test.js @@ -0,0 +1,150 @@ +import { getOwner } from '@ember/application'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent('job-page/parts/summary', 'Integration | Component | job-page/parts/summary', { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, +}); + +test('jobs with children use the children diagram', function(assert) { + this.server.create('job', 'periodic', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-children-status-bar]'), 'Children status bar found'); + assert.notOk(find('[data-test-allocation-status-bar]'), 'Allocation status bar not found'); + }); + }); +}); + +test('jobs without children use the allocations diagram', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.ok(find('[data-test-allocation-status-bar]'), 'Allocation status bar found'); + assert.notOk(find('[data-test-children-status-bar]'), 'Children status bar not found'); + }); + }); +}); + +test('the allocations diagram lists all allocation status figures', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.equal( + find('[data-test-legend-value="queued"]').textContent, + this.get('job.queuedAllocs'), + `${this.get('job.queuedAllocs')} are queued` + ); + + assert.equal( + find('[data-test-legend-value="starting"]').textContent, + this.get('job.startingAllocs'), + `${this.get('job.startingAllocs')} are starting` + ); + + assert.equal( + find('[data-test-legend-value="running"]').textContent, + this.get('job.runningAllocs'), + `${this.get('job.runningAllocs')} are running` + ); + + assert.equal( + find('[data-test-legend-value="complete"]').textContent, + this.get('job.completeAllocs'), + `${this.get('job.completeAllocs')} are complete` + ); + + assert.equal( + find('[data-test-legend-value="failed"]').textContent, + this.get('job.failedAllocs'), + `${this.get('job.failedAllocs')} are failed` + ); + + assert.equal( + find('[data-test-legend-value="lost"]').textContent, + this.get('job.lostAllocs'), + `${this.get('job.lostAllocs')} are lost` + ); + }); + }); +}); + +test('the children diagram lists all children status figures', function(assert) { + this.server.create('job', 'periodic', { + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + this.set('job', this.store.peekAll('job').get('firstObject')); + + this.render(hbs` + {{job-page/parts/summary job=job}} + `); + + return wait().then(() => { + assert.equal( + find('[data-test-legend-value="queued"]').textContent, + this.get('job.pendingChildren'), + `${this.get('job.pendingChildren')} are pending` + ); + + assert.equal( + find('[data-test-legend-value="running"]').textContent, + this.get('job.runningChildren'), + `${this.get('job.runningChildren')} are running` + ); + + assert.equal( + find('[data-test-legend-value="complete"]').textContent, + this.get('job.deadChildren'), + `${this.get('job.deadChildren')} are dead` + ); + }); + }); +}); From 1671786e351effeef82cf54099bb5b9e7e6a21c3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 17:21:45 -0800 Subject: [PATCH 73/79] Tests for the task groups job page part --- .../job-page/parts/task-groups-test.js | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 ui/tests/integration/job-page/parts/task-groups-test.js diff --git a/ui/tests/integration/job-page/parts/task-groups-test.js b/ui/tests/integration/job-page/parts/task-groups-test.js new file mode 100644 index 000000000..260db31c9 --- /dev/null +++ b/ui/tests/integration/job-page/parts/task-groups-test.js @@ -0,0 +1,171 @@ +import { getOwner } from '@ember/application'; +import { assign } from '@ember/polyfills'; +import hbs from 'htmlbars-inline-precompile'; +import wait from 'ember-test-helpers/wait'; +import { click, findAll, find } from 'ember-native-dom-helpers'; +import { test, moduleForComponent } from 'ember-qunit'; +import sinon from 'sinon'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; +import { initialize as fragmentSerializerInitializer } from 'nomad-ui/initializers/fragment-serializer'; + +moduleForComponent( + 'job-page/parts/task-groups', + 'Integration | Component | job-page/parts/task-groups', + { + integration: true, + beforeEach() { + fragmentSerializerInitializer(getOwner(this)); + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + + afterEach() { + this.server.shutdown(); + }, + } +); + +const props = (job, options = {}) => + assign( + { + job, + sortProperty: 'name', + sortDescending: true, + gotoTaskGroup: () => {}, + }, + options + ); + +test('the job detail page should list all task groups', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const job = this.store.peekAll('job').get('firstObject'); + this.setProperties(props(job)); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + assert.equal( + findAll('[data-test-task-group]').length, + job.get('taskGroups.length'), + 'One row per task group' + ); + }); + }); +}); + +test('each row in the task group table should show basic information about the task group', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const job = this.store.peekAll('job').get('firstObject'); + const taskGroup = job + .get('taskGroups') + .sortBy('name') + .reverse() + .get('firstObject'); + + this.setProperties(props(job)); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + const taskGroupRow = find('[data-test-task-group]'); + + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(), + taskGroup.get('name'), + 'Name' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(), + taskGroup.get('count'), + 'Count' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(), + `${taskGroup.get('reservedCPU')} MHz`, + 'Reserved CPU' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(), + `${taskGroup.get('reservedMemory')} MiB`, + 'Reserved Memory' + ); + assert.equal( + taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(), + `${taskGroup.get('reservedEphemeralDisk')} MiB`, + 'Reserved Disk' + ); + }); + }); +}); + +test('gotoTaskGroup is called when task group rows are clicked', function(assert) { + this.server.create('job', { + createAllocations: false, + }); + + this.store.findAll('job').then(jobs => { + jobs.forEach(job => job.reload()); + }); + + return wait().then(() => { + const taskGroupSpy = sinon.spy(); + const job = this.store.peekAll('job').get('firstObject'); + const taskGroup = job + .get('taskGroups') + .sortBy('name') + .reverse() + .get('firstObject'); + + this.setProperties( + props(job, { + gotoTaskGroup: taskGroupSpy, + }) + ); + + this.render(hbs` + {{job-page/parts/task-groups + job=job + sortProperty=sortProperty + sortDescending=sortDescending + gotoTaskGroup=gotoTaskGroup}} + `); + + return wait().then(() => { + click('[data-test-task-group]'); + assert.ok( + taskGroupSpy.withArgs(taskGroup).calledOnce, + 'Clicking the task group row calls the gotoTaskGroup action' + ); + }); + }); +}); From 6e3086438c65e95314c061d965bf392516b3c228 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 17:22:09 -0800 Subject: [PATCH 74/79] New module-for-job for acceptance testing job detail differences --- ui/tests/acceptance/job-detail-test.js | 356 +++---------------------- ui/tests/helpers/module-for-job.js | 45 ++++ 2 files changed, 75 insertions(+), 326 deletions(-) create mode 100644 ui/tests/helpers/module-for-job.js diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index a1df252ec..ee4377f25 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -1,26 +1,38 @@ -import { get } from '@ember/object'; import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers'; -import moment from 'moment'; -import { test } from 'qunit'; +import { skip } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; +import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; -const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); +moduleForJob('Acceptance | job detail (batch)', () => server.create('job', { type: 'batch' })); +moduleForJob('Acceptance | job detail (system)', () => server.create('job', { type: 'system' })); +moduleForJob('Acceptance | job detail (periodic)', () => server.create('job', 'periodic')); -let job; +moduleForJob('Acceptance | job detail (parameterized)', () => + server.create('job', 'parameterized') +); -moduleForAcceptance('Acceptance | job detail', { - beforeEach() { - server.create('node'); - job = server.create('job', { type: 'service' }); - visit(`/jobs/${job.id}`); +moduleForJob('Acceptance | job detail (periodic child)', () => { + const parent = server.create('job', 'periodic'); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJob('Acceptance | job detail (parameterized child)', () => { + const parent = server.create('job', 'parameterized'); + return server.db.jobs.where({ parentId: parent.id })[0]; +}); + +moduleForJob('Acceptance | job detail (service)', () => server.create('job', { type: 'service' }), { + 'the subnav links to deployment': (job, assert) => { + click(find('[data-test-tab="deployments"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/deployments`); + }); }, }); -test('visiting /jobs/:job_id', function(assert) { - assert.equal(currentURL(), `/jobs/${job.id}`); -}); +let job; -test('breadcrumbs includes job name and link back to the jobs list', function(assert) { +skip('breadcrumbs includes job name and link back to the jobs list', function(assert) { assert.equal( find('[data-test-breadcrumb="Jobs"]').textContent, 'Jobs', @@ -38,322 +50,14 @@ test('breadcrumbs includes job name and link back to the jobs list', function(as }); }); -test('the subnav includes links to definition, versions, and deployments when type = service', function( - assert -) { - const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); - assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); - assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); - assert.ok(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); -}); - -test('the subnav includes links to definition and versions when type != service', function(assert) { - job = server.create('job', { type: 'batch' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - const subnavLabels = findAll('[data-test-tab]').map(anchor => anchor.textContent); - assert.ok(subnavLabels.some(label => label === 'Definition'), 'Definition link'); - assert.ok(subnavLabels.some(label => label === 'Versions'), 'Versions link'); - assert.notOk(subnavLabels.some(label => label === 'Deployments'), 'Deployments link'); - }); -}); - -test('the job detail page should contain basic information about the job', function(assert) { +skip('the job detail page should contain basic information about the job', function(assert) { assert.ok(find('[data-test-job-status]').textContent.includes(job.status), 'Status'); assert.ok(find('[data-test-job-stat="type"]').textContent.includes(job.type), 'Type'); assert.ok(find('[data-test-job-stat="priority"]').textContent.includes(job.priority), 'Priority'); assert.notOk(find('[data-test-job-stat="namespace"]'), 'Namespace is not included'); }); -test('the job detail page should list all task groups', function(assert) { - assert.equal( - findAll('[data-test-task-group]').length, - server.db.taskGroups.where({ jobId: job.id }).length - ); -}); - -test('each row in the task group table should show basic information about the task group', function( - assert -) { - const taskGroup = job.taskGroupIds.map(id => server.db.taskGroups.find(id)).sortBy('name')[0]; - const taskGroupRow = find('[data-test-task-group]'); - const tasks = server.db.tasks.where({ taskGroupId: taskGroup.id }); - const sum = (list, key) => list.reduce((sum, item) => sum + get(item, key), 0); - - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-name]').textContent.trim(), - taskGroup.name, - 'Name' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-count]').textContent.trim(), - taskGroup.count, - 'Count' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-cpu]').textContent.trim(), - `${sum(tasks, 'Resources.CPU')} MHz`, - 'Reserved CPU' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-mem]').textContent.trim(), - `${sum(tasks, 'Resources.MemoryMB')} MiB`, - 'Reserved Memory' - ); - assert.equal( - taskGroupRow.querySelector('[data-test-task-group-disk]').textContent.trim(), - `${taskGroup.ephemeralDisk.SizeMB} MiB`, - 'Reserved Disk' - ); -}); - -test('the allocations diagram lists all allocation status figures', function(assert) { - const jobSummary = server.db.jobSummaries.findBy({ jobId: job.id }); - const statusCounts = Object.keys(jobSummary.Summary).reduce( - (counts, key) => { - const group = jobSummary.Summary[key]; - counts.queued += group.Queued; - counts.starting += group.Starting; - counts.running += group.Running; - counts.complete += group.Complete; - counts.failed += group.Failed; - counts.lost += group.Lost; - return counts; - }, - { queued: 0, starting: 0, running: 0, complete: 0, failed: 0, lost: 0 } - ); - - assert.equal( - find('[data-test-legend-value="queued"]').textContent, - statusCounts.queued, - `${statusCounts.queued} are queued` - ); - - assert.equal( - find('[data-test-legend-value="starting"]').textContent, - statusCounts.starting, - `${statusCounts.starting} are starting` - ); - - assert.equal( - find('[data-test-legend-value="running"]').textContent, - statusCounts.running, - `${statusCounts.running} are running` - ); - - assert.equal( - find('[data-test-legend-value="complete"]').textContent, - statusCounts.complete, - `${statusCounts.complete} are complete` - ); - - assert.equal( - find('[data-test-legend-value="failed"]').textContent, - statusCounts.failed, - `${statusCounts.failed} are failed` - ); - - assert.equal( - find('[data-test-legend-value="lost"]').textContent, - statusCounts.lost, - `${statusCounts.lost} are lost` - ); -}); - -test('there is no active deployment section when the job has no active deployment', function( - assert -) { - // TODO: it would be better to not visit two different job pages in one test, but this - // way is much more convenient. - job = server.create('job', { noActiveDeployment: true, type: 'service' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-active-deployment]'), 'No active deployment'); - }); -}); - -test('the active deployment section shows up for the currently running deployment', function( - assert -) { - job = server.create('job', { activeDeployment: true, type: 'service' }); - const deployment = server.db.deployments.where({ jobId: job.id })[0]; - const taskGroupSummaries = server.db.deploymentTaskGroupSummaries.where({ - deploymentId: deployment.id, - }); - const version = server.db.jobVersions.findBy({ - jobId: job.id, - version: deployment.versionNumber, - }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.ok(find('[data-test-active-deployment]'), 'Active deployment'); - assert.equal( - find('[data-test-active-deployment-stat="id"]').textContent.trim(), - deployment.id.split('-')[0], - 'The active deployment is the most recent running deployment' - ); - - assert.equal( - find('[data-test-active-deployment-stat="submit-time"]').textContent.trim(), - moment(version.submitTime / 1000000).fromNow(), - 'Time since the job was submitted is in the active deployment header' - ); - - assert.equal( - find('[data-test-deployment-metric="canaries"]').textContent.trim(), - `${sum(taskGroupSummaries, 'placedCanaries')} / ${sum( - taskGroupSummaries, - 'desiredCanaries' - )}`, - 'Canaries, both places and desired, are in the metrics' - ); - - assert.equal( - find('[data-test-deployment-metric="placed"]').textContent.trim(), - sum(taskGroupSummaries, 'placedAllocs'), - 'Placed allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="desired"]').textContent.trim(), - sum(taskGroupSummaries, 'desiredTotal'), - 'Desired allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="healthy"]').textContent.trim(), - sum(taskGroupSummaries, 'healthyAllocs'), - 'Healthy allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-metric="unhealthy"]').textContent.trim(), - sum(taskGroupSummaries, 'unhealthyAllocs'), - 'Unhealthy allocs aggregates across task groups' - ); - - assert.equal( - find('[data-test-deployment-notification]').textContent.trim(), - deployment.statusDescription, - 'Status description is in the metrics block' - ); - }); -}); - -test('the active deployment section can be expanded to show task groups and allocations', function( - assert -) { - job = server.create('job', { activeDeployment: true, type: 'service' }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-deployment-task-groups]'), 'Task groups not found'); - assert.notOk(find('[data-test-deployment-allocations]'), 'Allocations not found'); - }); - - andThen(() => { - click('[data-test-deployment-toggle-details]'); - }); - - andThen(() => { - assert.ok(find('[data-test-deployment-task-groups]'), 'Task groups found'); - assert.ok(find('[data-test-deployment-allocations]'), 'Allocations found'); - }); -}); - -test('the evaluations table lists evaluations sorted by modify index', function(assert) { - job = server.create('job'); - const evaluations = server.db.evaluations - .where({ jobId: job.id }) - .sortBy('modifyIndex') - .reverse(); - - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.equal( - findAll('[data-test-evaluation]').length, - evaluations.length, - 'A row for each evaluation' - ); - - evaluations.forEach((evaluation, index) => { - const row = findAll('[data-test-evaluation]')[index]; - assert.equal( - row.querySelector('[data-test-id]').textContent, - evaluation.id.split('-')[0], - `Short ID, row ${index}` - ); - }); - - const firstEvaluation = evaluations[0]; - const row = find('[data-test-evaluation]'); - assert.equal( - row.querySelector('[data-test-priority]').textContent, - '' + firstEvaluation.priority, - 'Priority' - ); - assert.equal( - row.querySelector('[data-test-triggered-by]').textContent, - firstEvaluation.triggeredBy, - 'Triggered By' - ); - assert.equal( - row.querySelector('[data-test-status]').textContent, - firstEvaluation.status, - 'Status' - ); - }); -}); - -test('when the job has placement failures, they are called out', function(assert) { - job = server.create('job', { failedPlacements: true }); - const failedEvaluation = server.db.evaluations - .where({ jobId: job.id }) - .filter(evaluation => evaluation.failedTGAllocs) - .sortBy('modifyIndex') - .reverse()[0]; - - const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs); - - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.ok(find('[data-test-placement-failures]'), 'Placement failures section found'); - - const taskGroupLabels = findAll('[data-test-placement-failure-task-group]').map(title => - title.textContent.trim() - ); - failedTaskGroupNames.forEach(name => { - assert.ok( - taskGroupLabels.find(label => label.includes(name)), - `${name} included in placement failures list` - ); - assert.ok( - taskGroupLabels.find(label => - label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1) - ), - 'The number of unplaced allocs = CoalescedFailures + 1' - ); - }); - }); -}); - -test('when the job has no placement failures, the placement failures section is gone', function( - assert -) { - job = server.create('job', { noFailedPlacements: true }); - visit(`/jobs/${job.id}`); - - andThen(() => { - assert.notOk(find('[data-test-placement-failures]'), 'Placement failures section not found'); - }); -}); - -test('when the job is not found, an error message is shown, but the URL persists', function( +skip('when the job is not found, an error message is shown, but the URL persists', function( assert ) { visit('/jobs/not-a-real-job'); @@ -383,7 +87,7 @@ moduleForAcceptance('Acceptance | job detail (with namespaces)', { }, }); -test('when there are namespaces, the job detail page states the namespace for the job', function( +skip('when there are namespaces, the job detail page states the namespace for the job', function( assert ) { const namespace = server.db.namespaces.find(job.namespaceId); @@ -397,7 +101,7 @@ test('when there are namespaces, the job detail page states the namespace for th }); }); -test('when switching namespaces, the app redirects to /jobs with the new namespace', function( +skip('when switching namespaces, the app redirects to /jobs with the new namespace', function( assert ) { const namespace = server.db.namespaces.find(job.namespaceId); diff --git a/ui/tests/helpers/module-for-job.js b/ui/tests/helpers/module-for-job.js new file mode 100644 index 000000000..1e1c27fa7 --- /dev/null +++ b/ui/tests/helpers/module-for-job.js @@ -0,0 +1,45 @@ +import { test } from 'qunit'; +import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; + +export default function moduleForJob(title, jobFactory, additionalTests) { + let job; + + moduleForAcceptance(title, { + beforeEach() { + server.create('node'); + job = jobFactory(); + visit(`/jobs/${job.id}`); + }, + }); + + test('visiting /jobs/:job_id', function(assert) { + assert.equal(currentURL(), `/jobs/${job.id}`); + }); + + test('the subnav links to overview', function(assert) { + click(find('[data-test-tab="overview"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}`); + }); + }); + + test('the subnav links to definition', function(assert) { + click(find('[data-test-tab="definition"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/definition`); + }); + }); + + test('the subnav links to versions', function(assert) { + click(find('[data-test-tab="versions"] a')); + andThen(() => { + assert.equal(currentURL(), `/jobs/${job.id}/versions`); + }); + }); + + for (var testName in additionalTests) { + test(testName, function(assert) { + additionalTests[testName](job, assert); + }); + } +} From b7f57ec3b6f81229128d5cc915227dd4bf4ab165 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Thu, 1 Feb 2018 18:54:23 -0800 Subject: [PATCH 75/79] Integration test for periodic job force launch --- .../components/job-page/periodic.hbs | 2 +- ui/mirage/config.js | 17 ++++ .../integration/job-page/periodic-test.js | 89 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 ui/tests/integration/job-page/periodic-test.js diff --git a/ui/app/templates/components/job-page/periodic.hbs b/ui/app/templates/components/job-page/periodic.hbs index 065c8c96e..6a815b332 100644 --- a/ui/app/templates/components/job-page/periodic.hbs +++ b/ui/app/templates/components/job-page/periodic.hbs @@ -10,7 +10,7 @@ {{job.name}} {{job.status}} periodic - +
        diff --git a/ui/mirage/config.js b/ui/mirage/config.js index b32c70b7c..c17c7d3e5 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -11,6 +11,7 @@ export function findLeader(schema) { } export default function() { + const server = this; this.timing = 0; // delay for each request, automatically set to 0 during testing this.namespace = 'v1'; @@ -58,6 +59,22 @@ export default function() { return this.serialize(deployments.where({ jobId: params.id })); }); + this.post('/job/:id/periodic/force', function(schema, { params }) { + // Create the child job + const parent = schema.jobs.find(params.id); + + // Use the server instead of the schema to leverage the job factory + server.create('job', 'periodicChild', { + parentId: parent.id, + namespaceId: parent.namespaceId, + namespace: parent.namespace, + createAllocations: parent.createAllocations, + }); + + // Return bogus, since the response is normally just eval information + return new Response(200, {}, '{}'); + }); + this.get('/deployment/:id'); this.get('/job/:id/evaluations', function({ evaluations }, { params }) { diff --git a/ui/tests/integration/job-page/periodic-test.js b/ui/tests/integration/job-page/periodic-test.js new file mode 100644 index 000000000..6c6c379a5 --- /dev/null +++ b/ui/tests/integration/job-page/periodic-test.js @@ -0,0 +1,89 @@ +import { run } from '@ember/runloop'; +import { getOwner } from '@ember/application'; +import { test, moduleForComponent } from 'ember-qunit'; +import { click, find, findAll } from 'ember-native-dom-helpers'; +import wait from 'ember-test-helpers/wait'; +import hbs from 'htmlbars-inline-precompile'; +import sinon from 'sinon'; +import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; + +moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', { + integration: true, + beforeEach() { + window.localStorage.clear(); + this.store = getOwner(this).lookup('service:store'); + this.server = startMirage(); + this.server.create('namespace'); + }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, +}); + +test('Clicking Force Launch launches a new periodic child job', function(assert) { + const childrenCount = 3; + + this.server.create('job', 'periodic', { + id: 'parent', + childrenCount, + createAllocations: false, + }); + + this.store.findAll('job'); + + return wait().then(() => { + const job = this.store.peekAll('job').findBy('plainId', 'parent'); + this.setProperties({ + job, + sortProperty: 'name', + sortDescending: true, + currentPage: 1, + gotoJob: () => {}, + }); + + this.render(hbs` + {{job-page/periodic + job=job + sortProperty=sortProperty + sortDescending=sortDescending + currentPage=currentPage + gotoJob=gotoJob}} + `); + + return wait().then(() => { + const currentJobCount = server.db.jobs.length; + + assert.equal( + findAll('[data-test-job-name]').length, + childrenCount, + 'The new periodic job launch is in the children list' + ); + + click('[data-test-force-launch]'); + + return wait().then(() => { + const id = job.get('plainId'); + const namespace = job.get('namespace.name') || 'default'; + + assert.ok( + server.pretender.handledRequests + .filterBy('method', 'POST') + .find(req => req.url === `/v1/job/${id}/periodic/force?namespace=${namespace}`), + 'POST URL was correct' + ); + + assert.ok(server.db.jobs.length, currentJobCount + 1, 'POST request was made'); + + return wait().then(() => { + assert.equal( + findAll('[data-test-job-name]').length, + childrenCount + 1, + 'The new periodic job launch is in the children list' + ); + }); + }); + }); + }); +}); From de27385801bc7ce9670f693d50428929e6694fd8 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 2 Feb 2018 09:49:18 -0800 Subject: [PATCH 76/79] Inject system so namespace shows up on job detail components --- ui/app/components/job-page/abstract.js | 3 ++ ui/tests/acceptance/job-detail-test.js | 41 +++---------------- .../integration/job-page/periodic-test.js | 5 +-- 3 files changed, 9 insertions(+), 40 deletions(-) diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index e9ea804a4..eb80479d7 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -1,7 +1,10 @@ import Component from '@ember/component'; import { computed } from '@ember/object'; +import { inject as service } from '@ember/service'; export default Component.extend({ + system: service(), + job: null, // Provide a value that is bound to a query param diff --git a/ui/tests/acceptance/job-detail-test.js b/ui/tests/acceptance/job-detail-test.js index ee4377f25..be2593488 100644 --- a/ui/tests/acceptance/job-detail-test.js +++ b/ui/tests/acceptance/job-detail-test.js @@ -1,5 +1,5 @@ import { click, findAll, currentURL, find, visit } from 'ember-native-dom-helpers'; -import { skip } from 'qunit'; +import { test } from 'qunit'; import moduleForAcceptance from 'nomad-ui/tests/helpers/module-for-acceptance'; import moduleForJob from 'nomad-ui/tests/helpers/module-for-job'; @@ -32,34 +32,7 @@ moduleForJob('Acceptance | job detail (service)', () => server.create('job', { t let job; -skip('breadcrumbs includes job name and link back to the jobs list', function(assert) { - assert.equal( - find('[data-test-breadcrumb="Jobs"]').textContent, - 'Jobs', - 'First breadcrumb says jobs' - ); - assert.equal( - find(`[data-test-breadcrumb="${job.name}"]`).textContent, - job.name, - 'Second breadcrumb says the job name' - ); - - click(find('[data-test-breadcrumb="Jobs"]')); - andThen(() => { - assert.equal(currentURL(), '/jobs', 'First breadcrumb links back to jobs'); - }); -}); - -skip('the job detail page should contain basic information about the job', function(assert) { - assert.ok(find('[data-test-job-status]').textContent.includes(job.status), 'Status'); - assert.ok(find('[data-test-job-stat="type"]').textContent.includes(job.type), 'Type'); - assert.ok(find('[data-test-job-stat="priority"]').textContent.includes(job.priority), 'Priority'); - assert.notOk(find('[data-test-job-stat="namespace"]'), 'Namespace is not included'); -}); - -skip('when the job is not found, an error message is shown, but the URL persists', function( - assert -) { +test('when the job is not found, an error message is shown, but the URL persists', function(assert) { visit('/jobs/not-a-real-job'); andThen(() => { @@ -82,14 +55,12 @@ moduleForAcceptance('Acceptance | job detail (with namespaces)', { beforeEach() { server.createList('namespace', 2); server.create('node'); - job = server.create('job', { namespaceId: server.db.namespaces[1].name }); + job = server.create('job', { type: 'service', namespaceId: server.db.namespaces[1].name }); server.createList('job', 3, { namespaceId: server.db.namespaces[0].name }); }, }); -skip('when there are namespaces, the job detail page states the namespace for the job', function( - assert -) { +test('when there are namespaces, the job detail page states the namespace for the job', function(assert) { const namespace = server.db.namespaces.find(job.namespaceId); visit(`/jobs/${job.id}?namespace=${namespace.name}`); @@ -101,9 +72,7 @@ skip('when there are namespaces, the job detail page states the namespace for th }); }); -skip('when switching namespaces, the app redirects to /jobs with the new namespace', function( - assert -) { +test('when switching namespaces, the app redirects to /jobs with the new namespace', function(assert) { const namespace = server.db.namespaces.find(job.namespaceId); const otherNamespace = server.db.namespaces.toArray().find(ns => ns !== namespace).name; const label = otherNamespace === 'default' ? 'Default Namespace' : otherNamespace; diff --git a/ui/tests/integration/job-page/periodic-test.js b/ui/tests/integration/job-page/periodic-test.js index 6c6c379a5..c259f0167 100644 --- a/ui/tests/integration/job-page/periodic-test.js +++ b/ui/tests/integration/job-page/periodic-test.js @@ -1,11 +1,8 @@ -import { run } from '@ember/runloop'; import { getOwner } from '@ember/application'; import { test, moduleForComponent } from 'ember-qunit'; -import { click, find, findAll } from 'ember-native-dom-helpers'; +import { click, findAll } from 'ember-native-dom-helpers'; import wait from 'ember-test-helpers/wait'; import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage'; moduleForComponent('job-page/periodic', 'Integration | Component | job-page/periodic', { From 9e606f389c64258f23e0194a081a020dcd91f8b7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 2 Feb 2018 09:51:44 -0800 Subject: [PATCH 77/79] Always shutdown the mirage server --- ui/tests/integration/job-page/parts/children-test.js | 4 ++++ .../integration/job-page/parts/placement-failures-test.js | 4 ++++ ui/tests/integration/job-page/parts/summary-test.js | 4 ++++ ui/tests/integration/job-page/parts/task-groups-test.js | 1 - 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ui/tests/integration/job-page/parts/children-test.js b/ui/tests/integration/job-page/parts/children-test.js index aecc32c7d..9b7149d41 100644 --- a/ui/tests/integration/job-page/parts/children-test.js +++ b/ui/tests/integration/job-page/parts/children-test.js @@ -16,6 +16,10 @@ moduleForComponent('job-page/parts/children', 'Integration | Component | job-pag this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, }); const props = (job, options = {}) => diff --git a/ui/tests/integration/job-page/parts/placement-failures-test.js b/ui/tests/integration/job-page/parts/placement-failures-test.js index d77467540..844e51c5b 100644 --- a/ui/tests/integration/job-page/parts/placement-failures-test.js +++ b/ui/tests/integration/job-page/parts/placement-failures-test.js @@ -19,6 +19,10 @@ moduleForComponent( this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, } ); diff --git a/ui/tests/integration/job-page/parts/summary-test.js b/ui/tests/integration/job-page/parts/summary-test.js index d04d2e82e..186c4ffcf 100644 --- a/ui/tests/integration/job-page/parts/summary-test.js +++ b/ui/tests/integration/job-page/parts/summary-test.js @@ -15,6 +15,10 @@ moduleForComponent('job-page/parts/summary', 'Integration | Component | job-page this.server = startMirage(); this.server.create('namespace'); }, + afterEach() { + this.server.shutdown(); + window.localStorage.clear(); + }, }); test('jobs with children use the children diagram', function(assert) { diff --git a/ui/tests/integration/job-page/parts/task-groups-test.js b/ui/tests/integration/job-page/parts/task-groups-test.js index 260db31c9..f9ac811f8 100644 --- a/ui/tests/integration/job-page/parts/task-groups-test.js +++ b/ui/tests/integration/job-page/parts/task-groups-test.js @@ -20,7 +20,6 @@ moduleForComponent( this.server = startMirage(); this.server.create('namespace'); }, - afterEach() { this.server.shutdown(); }, From 229760afb34e82f886f32796bbbd419a1c672a4a Mon Sep 17 00:00:00 2001 From: Michael Schurter Date: Tue, 6 Feb 2018 10:58:00 -0800 Subject: [PATCH 78/79] Add changelog entry for #3839 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32da02763..3077c6977 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ IMPROVEMENTS: * driver/docker: Support adding or dropping capabilities [[GH-3754](https://github.com/hashicorp/nomad/issues/3754)] * driver/docker: Support mounting root filesystem as read-only [[GH-3802](https://github.com/hashicorp/nomad/issues/3802)] * driver/lxc: Add volumes config to LXC driver [[GH-3687](https://github.com/hashicorp/nomad/issues/3687)] + * telemetry: Support DataDog tags [[GH-3839](https://github.com/hashicorp/nomad/issues/3839)] BUG FIXES: * core: Fix search endpoint forwarding for multi-region clusters [[GH-3680](https://github.com/hashicorp/nomad/issues/3680)] From 5fad2288b7ab74c0b508e0f7039b9a951c1cbaa5 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 7 Feb 2018 09:48:57 -0800 Subject: [PATCH 79/79] Switches to HashiCorp fork of jteeuwen/go-bindata. We make a HashiCorp hard fork of the jteeuwen/go-bindata hard fork that was replaced and diffed the code against a Dec 1, 2015 copy of the original repository we had as a cross-check of that hard fork. This replaces references to jteeuwen/go-bindata to point to the HashiCorp fork. --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index 951e7a81b..9ac6ba5be 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -148,7 +148,7 @@ deps: ## Install build and development dependencies @echo "==> Updating build dependencies..." go get -u github.com/kardianos/govendor go get -u github.com/ugorji/go/codec/codecgen - go get -u github.com/jteeuwen/go-bindata/... + go get -u github.com/hashicorp/go-bindata/... go get -u github.com/elazarl/go-bindata-assetfs/... go get -u github.com/a8m/tree/cmd/tree go get -u github.com/magiconair/vendorfmt/cmd/vendorfmt