From 078a025725fcd94716f1c5f224d246a5f1bab3db Mon Sep 17 00:00:00 2001 From: Carlos Diaz-Padron Date: Mon, 28 Sep 2015 16:54:32 -0700 Subject: [PATCH 001/178] Use environment to connect to Docker by default Uses the environment definition for docker by default. Docker will default to the unix/tcp socket if the environment is not set. --- client/driver/docker.go | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 6995d2b25..bb0795ca9 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -39,9 +39,10 @@ func NewDockerDriver(ctx *DriverContext) Driver { func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { // Initialize docker API client - dockerEndpoint := d.config.ReadDefault("docker.endpoint", "unix:///var/run/docker.sock") - client, err := docker.NewClient(dockerEndpoint) + client, err := d.dockerClient() + if err != nil { + d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon: %v", err) return false, nil } @@ -56,6 +57,7 @@ func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool env, err := client.Version() if err != nil { + d.logger.Printf("[DEBUG] driver.docker: could not read version from daemon: %v", err) // Check the "no such file" error if the unix file is missing if strings.Contains(err.Error(), "no such file") { return false, nil @@ -195,10 +197,9 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle } // Initialize docker API client - dockerEndpoint := d.config.ReadDefault("docker.endpoint", "unix:///var/run/docker.sock") - client, err := docker.NewClient(dockerEndpoint) + client, err := d.dockerClient() if err != nil { - return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", dockerEndpoint, err) + return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", client.Endpoint(), err) } repo, tag := docker.ParseRepositoryTag(image) @@ -292,10 +293,9 @@ func (d *DockerDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, er d.logger.Printf("[INFO] driver.docker: re-attaching to docker process: %s", handleID) // Initialize docker API client - dockerEndpoint := d.config.ReadDefault("docker.endpoint", "unix:///var/run/docker.sock") - client, err := docker.NewClient(dockerEndpoint) + client, err := d.dockerClient() if err != nil { - return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", dockerEndpoint, err) + return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", client.Endpoint(), err) } // Look for a running container with this ID @@ -333,6 +333,18 @@ func (d *DockerDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, er return h, nil } +// dockerClient returns a configured *docker.Client from the ClientConfig +func (d *DockerDriver) dockerClient() (*docker.Client, error) { + dockerEndpoint := d.config.Read("docker.endpoint") + if dockerEndpoint == "" { + client, err := docker.NewClientFromEnv() + return client, err + } else { + client, err := docker.NewClient(dockerEndpoint) + return client, err + } +} + func (h *dockerHandle) ID() string { // Return a handle to the PID pid := dockerPID{ From 9faa5530681c94908957e84f36d35aa970c73133 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 00:37:45 -0500 Subject: [PATCH 002/178] Fingerprinting code for GCE nodes This reads the following: * hostname * instance id * machine-type * zone * internal IP * external IP (if any) * tags * attributes Atributes are placed under the platform.gce.attr.* hierarchy. Tags are set up as platform.gce.tag.TagName=true. --- client/fingerprint/env_gce.go | 236 +++++++++++++++++++++++++++++ client/fingerprint/env_gce_test.go | 192 +++++++++++++++++++++++ client/fingerprint/fingerprint.go | 2 + 3 files changed, 430 insertions(+) create mode 100644 client/fingerprint/env_gce.go create mode 100644 client/fingerprint/env_gce_test.go diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go new file mode 100644 index 000000000..ab6397eef --- /dev/null +++ b/client/fingerprint/env_gce.go @@ -0,0 +1,236 @@ +package fingerprint + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +type GCEMetadataClient struct { + *http.Client + logger *log.Logger + metadataURL string +} + +type ReqError struct { + StatusCode int +} + +func (e ReqError) Error() string { + return http.StatusText(e.StatusCode) +} + +func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { + // Read the internal metadata URL from the environment, allowing test files to + // provide their own + metadataURL := os.Getenv("GCE_ENV_URL") + if metadataURL == "" { + metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/" + } + + // assume 2 seconds is enough time for inside GCE network + client := &http.Client{ + Timeout: 2 * time.Second, + } + + return GCEMetadataClient{ + Client: client, + logger: logger, + metadataURL: metadataURL, + } +} + +func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) { + reqUrl := g.metadataURL + attribute + if recursive { + reqUrl = reqUrl + "?recursive=true" + } + + parsedUrl, err := url.Parse(reqUrl) + if err != nil { + return "", err + } + + req := &http.Request{ + Method: "GET", + URL: parsedUrl, + Header: http.Header{ + "Metadata-Flavor": []string{"Google"}, + }, + } + + res, err := g.Client.Do(req) + if err != nil { + return "", err + } + + resp, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + g.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) + return "", err + } + + if res.StatusCode >= 400 { + return "", ReqError{res.StatusCode} + } + + return string(resp), nil +} + +// EnvGCEFingerprint is used to fingerprint the CPU +type EnvGCEFingerprint struct { + logger *log.Logger +} + +// NewEnvGCEFingerprint is used to create a CPU fingerprint +func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { + f := &EnvGCEFingerprint{logger: logger} + return f +} + +func checkError(err error, logger *log.Logger, desc string) error { + // If it's a URL error, assume we're not actually in an GCE environment. + // To the outer layers, this isn't an error so return nil. + if _, ok := err.(*url.Error); ok { + logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE " + desc + ", skipping") + return nil + } + // Otherwise pass the error through. + return err +} + +func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { + if !f.isGCE() { + return false, nil + } + + // newNetwork is populated and addded to the Nodes resources + newNetwork := &structs.NetworkResource{ + Device: "eth0", + } + + if node.Links == nil { + node.Links = make(map[string]string) + } + + client := NewGCEMetadataClient(f.logger) + + keys := []string{ + "hostname", + "id", + } + for _, k := range keys { + value, err := client.Get(k, false) + if err != nil { + return false, checkError(err, f.logger, k) + } + + // assume we want blank entries + node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + } + + // These keys need everything before the final slash removed to be usable. + keys = []string{ + "machine-type", + "zone", + } + for _, k := range keys { + value, err := client.Get(k, false) + if err != nil { + return false, checkError(err, f.logger, k) + } + + index := strings.LastIndex(value, "/") + value = value[index+1:] + node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + } + + // Get internal and external IP (if it exits) + value, err := client.Get("network-interfaces/0/ip", false) + if err != nil { + return false, checkError(err, f.logger, "ip") + } + newNetwork.IP = strings.Trim(value, "\n") + newNetwork.CIDR = newNetwork.IP + "/32" + node.Attributes["network.ip-address"] = newNetwork.IP + + value, err = client.Get("network-interfaces/0/access-configs/0/external-ip", false) + if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { + return false, checkError(err, f.logger, "external IP") + } + value = strings.Trim(value, "\n") + if len(value) > 0 { + node.Attributes["platform.gce.external-ip"] = value + } + + var tagList []string + value, err = client.Get("tags", false) + if err != nil { + return false, checkError(err, f.logger, "tags") + } + err = json.Unmarshal([]byte(value), &tagList) + if err == nil { + for _, tag := range tagList { + node.Attributes["platform.gce.tag."+tag] = "true" + } + } else { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) + } + + var attrDict map[string]string + value, err = client.Get("attributes/", true) + if err != nil { + return false, checkError(err, f.logger, "attributes/") + } + err = json.Unmarshal([]byte(value), &attrDict) + if err == nil { + for k, v := range attrDict { + node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") + } + } else { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) + } + + // populate Node Network Resources + if node.Resources == nil { + node.Resources = &structs.Resources{} + } + node.Resources.Networks = append(node.Resources.Networks, newNetwork) + + // populate Links + node.Links["gce"] = node.Attributes["platform.gce.id"] + + return true, nil +} + +func (f *EnvGCEFingerprint) isGCE() bool { + // TODO: better way to detect GCE? + + client := NewGCEMetadataClient(f.logger) + // Query the metadata url for the machine type, to verify we're on GCE + machineType, err := client.Get("machine-type", false) + if err != nil { + if re, ok := err.(ReqError); !ok || re.StatusCode != 404 { + // If it wasn't a 404 error, print an error message. + f.logger.Printf("[ERR] fingerprint.env_gce: Error querying GCE Metadata URL, skipping") + } + return false + } + + match, err := regexp.MatchString("projects/.+/machineTypes/.+", machineType) + if !match { + return false + } + + return true +} diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go new file mode 100644 index 000000000..402f03d7a --- /dev/null +++ b/client/fingerprint/env_gce_test.go @@ -0,0 +1,192 @@ +package fingerprint + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestGCEFingerprint_nonGCE(t *testing.T) { + f := NewEnvGCEFingerprint(testLogger()) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + ok, err := f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + if ok { + t.Fatalf("Should be false without test server") + } +} + +func testFingerprint_GCE(t *testing.T, withExternalIp bool) { + f := NewEnvGCEFingerprint(testLogger()) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + // configure mock server with fixture routes, data + routes := routes{} + if err := json.Unmarshal([]byte(GCE_routes), &routes); err != nil { + t.Fatalf("Failed to unmarshal JSON in GCE ENV test: %s", err) + } + if withExternalIp { + routes.Endpoints = append(routes.Endpoints, &endpoint{ + Uri: "/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip", + ContentType: "text/plain", + Body: "104.44.55.66", + }) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + value, ok := r.Header["Metadata-Flavor"] + if !ok { + t.Fatal("Metadata-Flavor not present in HTTP request header") + } + if value[0] != "Google" { + t.Fatalf("Expected Metadata-Flavor Google, saw %s", value[0]) + } + + found := false + for _, e := range routes.Endpoints { + if r.RequestURI == e.Uri { + w.Header().Set("Content-Type", e.ContentType) + fmt.Fprintln(w, e.Body) + } + found = true + } + + if !found { + w.WriteHeader(404) + } + })) + defer ts.Close() + os.Setenv("GCE_ENV_URL", ts.URL+"/computeMetadata/v1/instance/") + + ok, err := f.Fingerprint(&config.Config{}, node) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !ok { + t.Fatalf("should apply") + } + + keys := []string{ + "platform.gce.id", + "platform.gce.hostname", + "platform.gce.zone", + "platform.gce.machine-type", + "platform.gce.zone", + "platform.gce.tag.abc", + "platform.gce.tag.def", + "platform.gce.attr.ghi", + "platform.gce.attr.jkl", + "network.ip-address", + } + + for _, k := range keys { + assertNodeAttributeContains(t, node, k) + } + + if len(node.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) + } + + assertNodeAttributeEquals(t, node, "platform.gce.id", "12345") + assertNodeAttributeEquals(t, node, "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") + + if node.Resources == nil || len(node.Resources.Networks) == 0 { + t.Fatal("Expected to find Network Resources") + } + + // Test at least the first Network Resource + net := node.Resources.Networks[0] + if net.IP != "10.240.0.5" { + t.Fatalf("Expected Network Resource to have IP 10.240.0.5, saw %s", net.IP) + } + if net.CIDR != "10.240.0.5/32" { + t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) + } + if net.Device == "" { + t.Fatal("Expected Network Resource to have a Device Name") + } + + assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + if withExternalIp { + assertNodeAttributeEquals(t, node, "platform.gce.external-ip", "104.44.55.66") + } else if _, ok := node.Attributes["platform.gce.external-ip"]; ok { + t.Fatal("platform.gce.external-ip is set without an external IP") + } + + assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") + assertNodeAttributeEquals(t, node, "platform.gce.tag.def", "true") + assertNodeAttributeEquals(t, node, "platform.gce.attr.ghi", "111") + assertNodeAttributeEquals(t, node, "platform.gce.attr.jkl", "222") +} + +const GCE_routes = ` +{ + "endpoints": [ + { + "uri": "/computeMetadata/v1/instance/id", + "content-type": "text/plain", + "body": "12345" + }, + { + "uri": "/computeMetadata/v1/instance/hostname", + "content-type": "text/plain", + "body": "instance-1.c.project.internal" + }, + { + "uri": "/computeMetadata/v1/instance/zone", + "content-type": "text/plain", + "body": "projects/555555/zones/us-central1-f" + }, + { + "uri": "/computeMetadata/v1/instance/machine-type", + "content-type": "text/plain", + "body": "projects/555555/machineTypes/n1-standard-1" + }, + { + "uri": "/computeMetadata/v1/instance/network-interfaces/0/ip", + "content-type": "text/plain", + "body": "10.240.0.5" + }, + { + "uri": "/computeMetadata/v1/instance/tags", + "content-type": "application/json", + "body": "[\"abc\", \"def\"]" + }, + { + "uri": "/computeMetadata/v1/instance/attributes/?recursive=true", + "content-type": "application/json", + "body": "{\"ghi\":\"111\",\"jkl\":\"222\"}" + } + ] +} +` + +func TestFingerprint_GCEWithExternalIp(t *testing.T) { + testFingerprint_GCE(t, true) +} + +func TestFingerprint_GCEWithoutExternalIp(t *testing.T) { + testFingerprint_GCE(t, false) +} diff --git a/client/fingerprint/fingerprint.go b/client/fingerprint/fingerprint.go index ce69a8ac9..4a42057b2 100644 --- a/client/fingerprint/fingerprint.go +++ b/client/fingerprint/fingerprint.go @@ -18,6 +18,7 @@ var BuiltinFingerprints = []string{ "storage", "network", "env_aws", + "env_gce", } // builtinFingerprintMap contains the built in registered fingerprints @@ -30,6 +31,7 @@ var builtinFingerprintMap = map[string]Factory{ "storage": NewStorageFingerprint, "network": NewNetworkFingerprinter, "env_aws": NewEnvAWSFingerprint, + "env_gce": NewEnvGCEFingerprint, } // NewFingerprint is used to instantiate and return a new fingerprint From 444d6f63e63006b312e13d5453fcb351680d0556 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 00:42:34 -0500 Subject: [PATCH 003/178] isAWS should return false on GCE GCE and AWS both expose metadata servers, and GCE's 404 response includes the URL in the content, which maatches the regex. So, check the response code as well and if a 4xx code comes back, take that to meanit's not AWS. --- client/fingerprint/env_aws.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index 0bf92410c..d47ee1ba8 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -176,6 +176,11 @@ func isAWS() bool { } defer resp.Body.Close() + if resp.StatusCode >= 400 { + // URL not found, which indicates that this isn't AWS + return false + } + instanceID, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("[ERR] fingerprint.env_aws: Error reading AWS Instance ID, skipping") From 458adfa1248c8bde562121bb666ac13ff1e0b0c9 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 12:57:45 -0500 Subject: [PATCH 004/178] Use a constant for the default GCE metadata URL --- client/fingerprint/env_gce.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ab6397eef..c062efb49 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -15,6 +15,8 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" + type GCEMetadataClient struct { *http.Client logger *log.Logger @@ -34,7 +36,7 @@ func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { // provide their own metadataURL := os.Getenv("GCE_ENV_URL") if metadataURL == "" { - metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/" + metadataURL = DEFAULT_GCE_URL } // assume 2 seconds is enough time for inside GCE network From 69ec26e9520a3c652e4a8fd626b201da47f71d1f Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 12:59:02 -0500 Subject: [PATCH 005/178] And add a comment to the constant. --- client/fingerprint/env_gce.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index c062efb49..92c7fec0f 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -15,6 +15,8 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +// This is where the GCE metadata server normally resides. We hardcode the +// "instance" path as well since it's the only one we access here. const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" type GCEMetadataClient struct { From 983e8516e4a8e4bdd2452cced2e6c9bda2696fc7 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 5 Oct 2015 13:13:25 -0500 Subject: [PATCH 006/178] Consolidate GCEMetadataClient into EnvGCEFingerprint This allows easier reuse of the same client across multiple functions. --- client/fingerprint/env_gce.go | 53 +++++++++++++----------------- client/fingerprint/env_gce_test.go | 2 +- 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 92c7fec0f..7f2fa3784 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -20,9 +20,6 @@ import ( const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" type GCEMetadataClient struct { - *http.Client - logger *log.Logger - metadataURL string } type ReqError struct { @@ -33,7 +30,15 @@ func (e ReqError) Error() string { return http.StatusText(e.StatusCode) } -func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { +// EnvGCEFingerprint is used to fingerprint the CPU +type EnvGCEFingerprint struct { + client *http.Client + logger *log.Logger + metadataURL string +} + +// NewEnvGCEFingerprint is used to create a CPU fingerprint +func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { // Read the internal metadata URL from the environment, allowing test files to // provide their own metadataURL := os.Getenv("GCE_ENV_URL") @@ -46,15 +51,15 @@ func NewGCEMetadataClient(logger *log.Logger) GCEMetadataClient { Timeout: 2 * time.Second, } - return GCEMetadataClient{ - Client: client, + return &EnvGCEFingerprint{ + client: client, logger: logger, metadataURL: metadataURL, } } -func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) { - reqUrl := g.metadataURL + attribute +func (f *EnvGCEFingerprint) Get(attribute string, recursive bool) (string, error) { + reqUrl := f.metadataURL + attribute if recursive { reqUrl = reqUrl + "?recursive=true" } @@ -72,7 +77,7 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) }, } - res, err := g.Client.Do(req) + res, err := f.client.Do(req) if err != nil { return "", err } @@ -80,7 +85,7 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) resp, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - g.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) + f.logger.Printf("[ERR]: fingerprint.env_gce: Error reading response body for GCE %s", attribute) return "", err } @@ -91,17 +96,6 @@ func (g GCEMetadataClient) Get(attribute string, recursive bool) (string, error) return string(resp), nil } -// EnvGCEFingerprint is used to fingerprint the CPU -type EnvGCEFingerprint struct { - logger *log.Logger -} - -// NewEnvGCEFingerprint is used to create a CPU fingerprint -func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { - f := &EnvGCEFingerprint{logger: logger} - return f -} - func checkError(err error, logger *log.Logger, desc string) error { // If it's a URL error, assume we're not actually in an GCE environment. // To the outer layers, this isn't an error so return nil. @@ -127,14 +121,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) node.Links = make(map[string]string) } - client := NewGCEMetadataClient(f.logger) - keys := []string{ "hostname", "id", } for _, k := range keys { - value, err := client.Get(k, false) + value, err := f.Get(k, false) if err != nil { return false, checkError(err, f.logger, k) } @@ -149,7 +141,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) "zone", } for _, k := range keys { - value, err := client.Get(k, false) + value, err := f.Get(k, false) if err != nil { return false, checkError(err, f.logger, k) } @@ -160,7 +152,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // Get internal and external IP (if it exits) - value, err := client.Get("network-interfaces/0/ip", false) + value, err := f.Get("network-interfaces/0/ip", false) if err != nil { return false, checkError(err, f.logger, "ip") } @@ -168,7 +160,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) newNetwork.CIDR = newNetwork.IP + "/32" node.Attributes["network.ip-address"] = newNetwork.IP - value, err = client.Get("network-interfaces/0/access-configs/0/external-ip", false) + value, err = f.Get("network-interfaces/0/access-configs/0/external-ip", false) if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { return false, checkError(err, f.logger, "external IP") } @@ -178,7 +170,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } var tagList []string - value, err = client.Get("tags", false) + value, err = f.Get("tags", false) if err != nil { return false, checkError(err, f.logger, "tags") } @@ -192,7 +184,7 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } var attrDict map[string]string - value, err = client.Get("attributes/", true) + value, err = f.Get("attributes/", true) if err != nil { return false, checkError(err, f.logger, "attributes/") } @@ -220,9 +212,8 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) func (f *EnvGCEFingerprint) isGCE() bool { // TODO: better way to detect GCE? - client := NewGCEMetadataClient(f.logger) // Query the metadata url for the machine type, to verify we're on GCE - machineType, err := client.Get("machine-type", false) + machineType, err := f.Get("machine-type", false) if err != nil { if re, ok := err.(ReqError); !ok || re.StatusCode != 404 { // If it wasn't a 404 error, print an error message. diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 402f03d7a..28889ba16 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -29,7 +29,6 @@ func TestGCEFingerprint_nonGCE(t *testing.T) { } func testFingerprint_GCE(t *testing.T, withExternalIp bool) { - f := NewEnvGCEFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), } @@ -71,6 +70,7 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { })) defer ts.Close() os.Setenv("GCE_ENV_URL", ts.URL+"/computeMetadata/v1/instance/") + f := NewEnvGCEFingerprint(testLogger()) ok, err := f.Fingerprint(&config.Config{}, node) if err != nil { From 127dc127b2173726c74501a3361c4e8d6b9e34a6 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 6 Oct 2015 16:26:31 -0700 Subject: [PATCH 007/178] Use docker.NewClient; move dockerClient before it's used --- client/driver/docker.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 4e0e035ad..896529513 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -37,10 +37,17 @@ func NewDockerDriver(ctx *DriverContext) Driver { return &DockerDriver{*ctx} } +// dockerClient creates *docker.Client using ClientConfig so we can get the +// correct socket for the daemon +func (d *DockerDriver) dockerClient() (*docker.Client, error) { + dockerEndpoint := d.config.Read("docker.endpoint") + client, err := docker.NewClient(dockerEndpoint) + return client, err +} + func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { // Initialize docker API client client, err := d.dockerClient() - if err != nil { d.logger.Printf("[DEBUG] driver.docker: could not connect to docker daemon: %v", err) return false, nil @@ -350,18 +357,6 @@ func (d *DockerDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, er return h, nil } -// dockerClient returns a configured *docker.Client from the ClientConfig -func (d *DockerDriver) dockerClient() (*docker.Client, error) { - dockerEndpoint := d.config.Read("docker.endpoint") - if dockerEndpoint == "" { - client, err := docker.NewClientFromEnv() - return client, err - } else { - client, err := docker.NewClient(dockerEndpoint) - return client, err - } -} - func (h *dockerHandle) ID() string { // Return a handle to the PID pid := dockerPID{ From f3b5d553ad17571bc881890b1d2157eae0268933 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 6 Oct 2015 17:53:05 -0700 Subject: [PATCH 008/178] Support boot2docker or VM for dev/test --- client/driver/docker.go | 44 ++++++++++++++++++++++++++++++++---- client/driver/docker_test.go | 26 +++++++++++++-------- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 896529513..b7b4e3eb2 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -4,10 +4,12 @@ import ( "encoding/json" "fmt" "log" + "runtime" "strconv" "strings" docker "github.com/fsouza/go-dockerclient" + opts "github.com/fsouza/go-dockerclient/external/github.com/docker/docker/opts" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/nomad/structs" @@ -33,16 +35,47 @@ type dockerHandle struct { doneCh chan struct{} } +// getDefaultDockerHost is copied from fsouza. If it were exported we woudn't +// need this here. +func getDefaultDockerHost() (string, error) { + var defaultHost string + if runtime.GOOS == "windows" { + // If we do not have a host, default to TCP socket on Windows + defaultHost = fmt.Sprintf("tcp://%s:%d", opts.DefaultHTTPHost, opts.DefaultHTTPPort) + } else { + // If we do not have a host, default to unix socket + defaultHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket) + } + return opts.ValidateHost(defaultHost) +} + func NewDockerDriver(ctx *DriverContext) Driver { return &DockerDriver{*ctx} } -// dockerClient creates *docker.Client using ClientConfig so we can get the -// correct socket for the daemon +// dockerClient creates *docker.Client. In test / dev mode we can use ENV vars +// to connect to the docker daemon. In production mode we will read +// docker.endpoint from the config file. func (d *DockerDriver) dockerClient() (*docker.Client, error) { - dockerEndpoint := d.config.Read("docker.endpoint") - client, err := docker.NewClient(dockerEndpoint) - return client, err + // In dev mode, read DOCKER_* environment variables DOCKER_HOST, + // DOCKER_TLS_VERIFY, and DOCKER_CERT_PATH. This allows you to run tests and + // demo against boot2docker or a VM on OSX and Windows. This falls back on + // the default unix socket on linux if tests are run on linux. + // + // Also note that we need to turn on DevMode in the test configs. + if d.config.DevMode { + return docker.NewClientFromEnv() + } + + // In prod mode we'll read the docker.endpoint configuration and fall back + // on the host-specific default. We do not read from the environment. + defaultEndpoint, err := getDefaultDockerHost() + if err != nil { + return nil, fmt.Errorf("Unable to determine default docker endpoint: %s", err) + } + dockerEndpoint := d.config.ReadDefault("docker.endpoint", defaultEndpoint) + + return docker.NewClient(dockerEndpoint) } func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { @@ -408,6 +441,7 @@ func (h *dockerHandle) Kill() error { err = h.client.RemoveImage(h.imageID) if err != nil { containers, err := h.client.ListContainers(docker.ListContainersOptions{ + // The image might be in use by a stopped container, so check everything All: true, Filters: map[string][]string{ "image": []string{h.imageID}, diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index 3bea8235a..2d436092d 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -9,6 +9,12 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) +func testDockerDriverContext(task string) *DriverContext { + cfg := testConfig() + cfg.DevMode = true + return NewDriverContext(task, cfg, cfg.Node, testLogger()) +} + // dockerLocated looks to see whether docker is available on this system before // we try to run tests. We'll keep it simple and just check for the CLI. func dockerLocated() bool { @@ -33,7 +39,7 @@ func TestDockerDriver_Handle(t *testing.T) { // The fingerprinter test should always pass, even if Docker is not installed. func TestDockerDriver_Fingerprint(t *testing.T) { - d := NewDockerDriver(testDriverContext("")) + d := NewDockerDriver(testDockerDriverContext("")) node := &structs.Node{ Attributes: make(map[string]string), } @@ -56,14 +62,14 @@ func TestDockerDriver_StartOpen_Wait(t *testing.T) { } task := &structs.Task{ - Name: "python-demo", + Name: "redis-demo", Config: map[string]string{ "image": "redis", }, Resources: basicResources, } - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) @@ -93,7 +99,7 @@ func TestDockerDriver_Start_Wait(t *testing.T) { } task := &structs.Task{ - Name: "python-demo", + Name: "redis-demo", Config: map[string]string{ "image": "redis", "command": "redis-server -v", @@ -104,7 +110,7 @@ func TestDockerDriver_Start_Wait(t *testing.T) { }, } - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) @@ -140,7 +146,7 @@ func TestDockerDriver_Start_Kill_Wait(t *testing.T) { } task := &structs.Task{ - Name: "python-demo", + Name: "redis-demo", Config: map[string]string{ "image": "redis", "command": "sleep 10", @@ -148,7 +154,7 @@ func TestDockerDriver_Start_Kill_Wait(t *testing.T) { Resources: basicResources, } - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) @@ -219,7 +225,7 @@ func TestDocker_StartN(t *testing.T) { // Let's spin up a bunch of things var err error for idx, task := range taskList { - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) @@ -265,7 +271,7 @@ func TestDocker_StartNVersions(t *testing.T) { // Let's spin up a bunch of things var err error for idx, task := range taskList { - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) @@ -299,7 +305,7 @@ func TestDockerHostNet(t *testing.T) { CPU: 512, }, } - driverCtx := testDriverContext(task.Name) + driverCtx := testDockerDriverContext(task.Name) ctx := testDriverExecContext(task, driverCtx) defer ctx.AllocDir.Destroy() d := NewDockerDriver(driverCtx) From 8b0c18db5f3a9a2b7bdab68858afba790fe05729 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Tue, 6 Oct 2015 19:09:59 -0700 Subject: [PATCH 009/178] Remove panic -- client is nil when there is an error --- client/driver/docker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index ffbd05dcd..5c5a82e91 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -256,7 +256,7 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle // Initialize docker API client client, err := d.dockerClient() if err != nil { - return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", client.Endpoint(), err) + return nil, fmt.Errorf("Failed to connect to docker daemon: %s", err) } repo, tag := docker.ParseRepositoryTag(image) @@ -352,7 +352,7 @@ func (d *DockerDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, er // Initialize docker API client client, err := d.dockerClient() if err != nil { - return nil, fmt.Errorf("Failed to connect to docker.endpoint (%s): %s", client.Endpoint(), err) + return nil, fmt.Errorf("Failed to connect to docker daemon: %s", err) } // Look for a running container with this ID From 64cedc1b273da7f41643384d3461c0a156436b53 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 6 Oct 2015 19:13:04 -0700 Subject: [PATCH 010/178] Print coverage info while running tests --- scripts/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test.sh b/scripts/test.sh index 524bfbe7f..7071669a7 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -10,4 +10,4 @@ go build -o $TEMPDIR/nomad || exit 1 # Run the tests echo "--> Running tests" -go list ./... | PATH=$TEMPDIR:$PATH xargs -n1 go test -timeout=40s +go list ./... | PATH=$TEMPDIR:$PATH xargs -n1 go test -cover -timeout=40s From a55bdd3430c4a3d4a899968aaf616472515f1a6c Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Tue, 6 Oct 2015 21:16:28 -0500 Subject: [PATCH 011/178] NonXXX tests should pass when actually running in their respective environments. Fixes #224 --- client/fingerprint/env_aws_test.go | 1 + client/fingerprint/env_gce_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index acc70e689..80a86d5da 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -13,6 +13,7 @@ import ( ) func TestEnvAWSFingerprint_nonAws(t *testing.T) { + os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/") f := NewEnvAWSFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 28889ba16..c6a4868f6 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -13,6 +13,7 @@ import ( ) func TestGCEFingerprint_nonGCE(t *testing.T) { + os.Setenv("GCE_ENV_URL", "http://127.0.0.1/computeMetadata/v1/instance/") f := NewEnvGCEFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), From f985ef45fa3a4850c5dfe58294f956d3b64455e4 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Tue, 6 Oct 2015 21:21:42 -0500 Subject: [PATCH 012/178] TestNetworkFingerprint_notAWS passes even when actually on AWS --- client/fingerprint/env_aws_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/fingerprint/env_aws_test.go b/client/fingerprint/env_aws_test.go index 80a86d5da..e8494c263 100644 --- a/client/fingerprint/env_aws_test.go +++ b/client/fingerprint/env_aws_test.go @@ -200,6 +200,7 @@ func TestNetworkFingerprint_AWS(t *testing.T) { } func TestNetworkFingerprint_notAWS(t *testing.T) { + os.Setenv("AWS_ENV_URL", "http://127.0.0.1/latest/meta-data/") f := NewEnvAWSFingerprint(testLogger()) node := &structs.Node{ Attributes: make(map[string]string), From 79531fe17c237d83b32392b8afce0db0664e9b1f Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Wed, 7 Oct 2015 12:39:23 -0500 Subject: [PATCH 013/178] Compact tags and attribute reading code. --- client/fingerprint/env_gce.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 7f2fa3784..ffc6b76a5 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -174,13 +174,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) if err != nil { return false, checkError(err, f.logger, "tags") } - err = json.Unmarshal([]byte(value), &tagList) - if err == nil { + if err := json.Unmarshal([]byte(value), &tagList); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) + } else { for _, tag := range tagList { node.Attributes["platform.gce.tag."+tag] = "true" } - } else { - f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) } var attrDict map[string]string @@ -188,13 +187,12 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) if err != nil { return false, checkError(err, f.logger, "attributes/") } - err = json.Unmarshal([]byte(value), &attrDict) - if err == nil { + if err := json.Unmarshal([]byte(value), &attrDict); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) + } else { for k, v := range attrDict { node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") } - } else { - f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) } // populate Node Network Resources From a95cedbfe7caf64788af9a5278707519189d7c8e Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Wed, 7 Oct 2015 13:35:00 -0500 Subject: [PATCH 014/178] Parse information for all GCE network interface. * No longer setting Device name in the network interface since we can't match up the info here with real device names. * Add attributes for all external IPs if more than one exists. --- client/fingerprint/env_gce.go | 69 +++++++++++++++++------------- client/fingerprint/env_gce_test.go | 31 ++++++-------- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ffc6b76a5..ceff998d7 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "regexp" + "strconv" "strings" "time" @@ -19,7 +20,14 @@ import ( // "instance" path as well since it's the only one we access here. const DEFAULT_GCE_URL = "http://169.254.169.254/computeMetadata/v1/instance/" -type GCEMetadataClient struct { +type GCEMetadataNetworkInterface struct { + AccessConfigs []struct { + ExternalIp string + Type string + } + ForwardedIps []string + Ip string + Network string } type ReqError struct { @@ -30,6 +38,11 @@ func (e ReqError) Error() string { return http.StatusText(e.StatusCode) } +func lastToken(s string) string { + index := strings.LastIndex(s, "/") + return s[index+1:] +} + // EnvGCEFingerprint is used to fingerprint the CPU type EnvGCEFingerprint struct { client *http.Client @@ -112,11 +125,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) return false, nil } - // newNetwork is populated and addded to the Nodes resources - newNetwork := &structs.NetworkResource{ - Device: "eth0", - } - if node.Links == nil { node.Links = make(map[string]string) } @@ -146,27 +154,36 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) return false, checkError(err, f.logger, k) } - index := strings.LastIndex(value, "/") - value = value[index+1:] - node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + node.Attributes["platform.gce."+k] = strings.Trim(lastToken(value), "\n") } - // Get internal and external IP (if it exits) - value, err := f.Get("network-interfaces/0/ip", false) - if err != nil { - return false, checkError(err, f.logger, "ip") + // Prepare to populate Node Network Resources + if node.Resources == nil { + node.Resources = &structs.Resources{} } - newNetwork.IP = strings.Trim(value, "\n") - newNetwork.CIDR = newNetwork.IP + "/32" - node.Attributes["network.ip-address"] = newNetwork.IP - value, err = f.Get("network-interfaces/0/access-configs/0/external-ip", false) - if re, ok := err.(ReqError); err != nil && (!ok || re.StatusCode != 404) { - return false, checkError(err, f.logger, "external IP") - } - value = strings.Trim(value, "\n") - if len(value) > 0 { - node.Attributes["platform.gce.external-ip"] = value + // Get internal and external IPs (if they exist) + value, err := f.Get("network-interfaces/", true) + var interfaces []GCEMetadataNetworkInterface + if err := json.Unmarshal([]byte(value), &interfaces); err != nil { + f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) + } else { + for _, intf := range interfaces { + prefix := "platform.gce.network." + lastToken(intf.Network) + + // newNetwork is populated and addded to the Nodes resources + newNetwork := &structs.NetworkResource{ + IP: strings.Trim(intf.Ip, "\n"), + } + newNetwork.CIDR = newNetwork.IP + "/32" + node.Resources.Networks = append(node.Resources.Networks, newNetwork) + + node.Attributes["network.ip-address"] = newNetwork.IP + node.Attributes[prefix+".ip"] = newNetwork.IP + for index, accessConfig := range intf.AccessConfigs { + node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp + } + } } var tagList []string @@ -195,12 +212,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } } - // populate Node Network Resources - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - node.Resources.Networks = append(node.Resources.Networks, newNetwork) - // populate Links node.Links["gce"] = node.Attributes["platform.gce.id"] diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index c6a4868f6..460bd726f 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -39,13 +39,16 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { if err := json.Unmarshal([]byte(GCE_routes), &routes); err != nil { t.Fatalf("Failed to unmarshal JSON in GCE ENV test: %s", err) } - if withExternalIp { - routes.Endpoints = append(routes.Endpoints, &endpoint{ - Uri: "/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip", - ContentType: "text/plain", - Body: "104.44.55.66", - }) + networkEndpoint := &endpoint{ + Uri: "/computeMetadata/v1/instance/network-interfaces/?recursive=true", + ContentType: "application/json", } + if withExternalIp { + networkEndpoint.Body = `[{"accessConfigs":[{"externalIp":"104.44.55.66","type":"ONE_TO_ONE_NAT"},{"externalIp":"104.44.55.67","type":"ONE_TO_ONE_NAT"}],"forwardedIps":[],"ip":"10.240.0.5","network":"projects/555555/networks/default"}]` + } else { + networkEndpoint.Body = `[{"accessConfigs":[],"forwardedIps":[],"ip":"10.240.0.5","network":"projects/555555/networks/default"}]` + } + routes.Endpoints = append(routes.Endpoints, networkEndpoint) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { value, ok := r.Header["Metadata-Flavor"] @@ -125,15 +128,14 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { if net.CIDR != "10.240.0.5/32" { t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) } - if net.Device == "" { - t.Fatal("Expected Network Resource to have a Device Name") - } assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { - assertNodeAttributeEquals(t, node, "platform.gce.external-ip", "104.44.55.66") - } else if _, ok := node.Attributes["platform.gce.external-ip"]; ok { - t.Fatal("platform.gce.external-ip is set without an external IP") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.0", "104.44.55.66") + assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.1", "104.44.55.67") + } else if _, ok := node.Attributes["platform.gce.network.default.external-ip.0"]; ok { + t.Fatal("platform.gce.network.default.external-ip is set without an external IP") } assertNodeAttributeEquals(t, node, "platform.gce.tag.abc", "true") @@ -165,11 +167,6 @@ const GCE_routes = ` "content-type": "text/plain", "body": "projects/555555/machineTypes/n1-standard-1" }, - { - "uri": "/computeMetadata/v1/instance/network-interfaces/0/ip", - "content-type": "text/plain", - "body": "10.240.0.5" - }, { "uri": "/computeMetadata/v1/instance/tags", "content-type": "application/json", From dc1d845e2c09ee559980e1e8010e888034a0181a Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Thu, 8 Oct 2015 00:08:54 -0700 Subject: [PATCH 015/178] Added documentation for DOCKER_HOST and docker.endpoint; also filled in docs for docker.cleanup --- website/source/docs/drivers/docker.html.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index 78ca25e45..c6ea40a77 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -111,11 +111,21 @@ The `docker` driver has the following configuration options: * `docker.endpoint` - Defaults to `unix:///var/run/docker.sock`. You will need to customize this if you use a non-standard socket (http or another location). +* `docker.cleanup.container` Defaults to `true`. Changing this to `false` will + prevent Nomad from removing containers from stopped tasks. + +* `docker.cleanup.image` Defaults to `true`. Changing this to `false` will + prevent Nomad from removing images from stopped tasks. + +Note: When testing or using the `-dev` flag you can use `DOCKER_HOST`, +`DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH` to customize Nomad's behavior. In +production Nomad will always read `docker.endpoint`. + ## Client Attributes The `docker` driver will set the following client attributes: -* `driver.Docker` - This will be set to "1", indicating the +* `driver.docker` - This will be set to "1", indicating the driver is available. ## Resource Isolation From c31e1aad0e9adcedeb083fe407ce483ae453738c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 8 Oct 2015 13:48:17 -0400 Subject: [PATCH 016/178] Add shopt globs to include hidden files --- scripts/website_push.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/website_push.sh b/scripts/website_push.sh index 613df5803..91ecbb39b 100755 --- a/scripts/website_push.sh +++ b/scripts/website_push.sh @@ -16,7 +16,8 @@ while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Copy into tmpdir -cp -R $DIR/website/ $DEPLOY/ +shopt -s dotglob +cp -r $DIR/website/* $DEPLOY/ # Change into that directory pushd $DEPLOY &>/dev/null @@ -25,6 +26,7 @@ pushd $DEPLOY &>/dev/null touch .gitignore echo ".sass-cache" >> .gitignore echo "build" >> .gitignore +echo "vendor" >> .gitignore # Add everything git init -q . From 1098e562fcecc2829c7f52fce66c493e800b166a Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Wed, 7 Oct 2015 19:00:05 -0700 Subject: [PATCH 017/178] Privileged exec driver --- client/driver/driver.go | 3 +- client/driver/exec_test.go | 3 +- client/driver/pexec.go | 197 ++++++++++++++++++++++++++++++++ client/driver/pexec_test.go | 216 ++++++++++++++++++++++++++++++++++++ 4 files changed, 416 insertions(+), 3 deletions(-) create mode 100644 client/driver/pexec.go create mode 100644 client/driver/pexec_test.go diff --git a/client/driver/driver.go b/client/driver/driver.go index 46b18538c..9025b952a 100644 --- a/client/driver/driver.go +++ b/client/driver/driver.go @@ -17,6 +17,7 @@ import ( var BuiltinDrivers = map[string]Factory{ "docker": NewDockerDriver, "exec": NewExecDriver, + "pexec": NewPrivilegedExecDriver, "java": NewJavaDriver, "qemu": NewQemuDriver, "rkt": NewRktDriver, @@ -112,7 +113,7 @@ func TaskEnvironmentVariables(ctx *ExecContext, task *structs.Task) environment. env.SetMeta(task.Meta) if ctx.AllocDir != nil { - env.SetAllocDir(ctx.AllocDir.AllocDir) + env.SetAllocDir(ctx.AllocDir.SharedDir) } if task.Resources != nil { diff --git a/client/driver/exec_test.go b/client/driver/exec_test.go index 4a05d5891..feba89ae3 100644 --- a/client/driver/exec_test.go +++ b/client/driver/exec_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/hashicorp/nomad/client/allocdir" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/environment" "github.com/hashicorp/nomad/nomad/structs" @@ -159,7 +158,7 @@ func TestExecDriver_Start_Wait_AllocDir(t *testing.T) { } // Check that data was written to the shared alloc directory. - outputFile := filepath.Join(ctx.AllocDir.AllocDir, allocdir.SharedAllocName, file) + outputFile := filepath.Join(ctx.AllocDir.SharedDir, file) act, err := ioutil.ReadFile(outputFile) if err != nil { t.Fatalf("Couldn't read expected output: %v", err) diff --git a/client/driver/pexec.go b/client/driver/pexec.go new file mode 100644 index 000000000..948b3e538 --- /dev/null +++ b/client/driver/pexec.go @@ -0,0 +1,197 @@ +package driver + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/hashicorp/nomad/client/allocdir" + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/args" + "github.com/hashicorp/nomad/nomad/structs" +) + +const ( + // The option that enables this driver in the Config.Options map. + pexecConfigOption = "driver.pexec.enable" + + // Null files to use as stdin. + unixNull = "/dev/null" + windowsNull = "nul" +) + +// The PexecDriver is a privileged version of the exec driver. It provides no +// resource isolation and just fork/execs. The Exec driver should be preferred +// and this should only be used when explicitly needed. +type PexecDriver struct { + DriverContext +} + +// pexecHandle is returned from Start/Open as a handle to the PID +type pexecHandle struct { + proc *os.Process + waitCh chan error + doneCh chan struct{} +} + +// NewPrivilegedExecDriver is used to create a new privileged exec driver +func NewPrivilegedExecDriver(ctx *DriverContext) Driver { + return &PexecDriver{*ctx} +} + +func (d *PexecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { + // Check that the user has explicitly enabled this executor. + enabled := strings.ToLower(cfg.ReadDefault(pexecConfigOption, "false")) + if enabled == "1" || enabled == "true" { + d.logger.Printf("[WARN] driver.Pexec: privileged exec is enabled. Only enable if needed") + node.Attributes["driver.pexec"] = "1" + return true, nil + } + + return false, nil +} + +func (d *PexecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { + // Get the command + command, ok := task.Config["command"] + if !ok || command == "" { + return nil, fmt.Errorf("missing command for pexec driver") + } + + // Get the tasks local directory. + taskName := d.DriverContext.taskName + taskDir, ok := ctx.AllocDir.TaskDirs[taskName] + if !ok { + return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) + } + taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) + + // Get the environment variables. + envVars := TaskEnvironmentVariables(ctx, task) + + // Look for arguments + var cmdArgs []string + if argRaw, ok := task.Config["args"]; ok { + parsed, err := args.ParseAndReplace(argRaw, envVars.Map()) + if err != nil { + return nil, err + } + cmdArgs = append(cmdArgs, parsed...) + } + + // Setup the command + cmd := exec.Command(command, cmdArgs...) + cmd.Dir = taskDir + cmd.Env = envVars.List() + + // Capture the stdout/stderr and redirect stdin to /dev/null + stdoutFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stdout", taskName)) + stderrFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stderr", taskName)) + stdinFilename := unixNull + if runtime.GOOS == "windows" { + stdinFilename = windowsNull + } + + stdo, err := os.OpenFile(stdoutFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stdout: %v", err) + } + + stde, err := os.OpenFile(stderrFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stderr: %v", err) + } + + stdi, err := os.OpenFile(stdinFilename, os.O_CREATE|os.O_RDONLY, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stdin: %v", err) + } + + cmd.Stdout = stdo + cmd.Stderr = stde + cmd.Stdin = stdi + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start command: %v", err) + } + + // Return a driver handle + h := &pexecHandle{ + proc: cmd.Process, + doneCh: make(chan struct{}), + waitCh: make(chan error, 1), + } + go h.run() + return h, nil +} + +func (d *PexecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { + // Split the handle + pidStr := strings.TrimPrefix(handleID, "PID:") + pid, err := strconv.Atoi(pidStr) + if err != nil { + return nil, fmt.Errorf("failed to parse handle '%s': %v", handleID, err) + } + + // Find the process + proc, err := os.FindProcess(pid) + if proc == nil || err != nil { + return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) + } + + // Return a driver handle + h := &pexecHandle{ + proc: proc, + doneCh: make(chan struct{}), + waitCh: make(chan error, 1), + } + go h.run() + return h, nil +} + +func (h *pexecHandle) ID() string { + // Return a handle to the PID + return fmt.Sprintf("PID:%d", h.proc.Pid) +} + +func (h *pexecHandle) WaitCh() chan error { + return h.waitCh +} + +func (h *pexecHandle) Update(task *structs.Task) error { + // Update is not possible + return nil +} + +// Kill is used to terminate the task. We send an Interrupt +// and then provide a 5 second grace period before doing a Kill on supported +// OS's, otherwise we kill immediately. +func (h *pexecHandle) Kill() error { + if runtime.GOOS == "windows" { + return h.proc.Kill() + } + + h.proc.Signal(os.Interrupt) + select { + case <-h.doneCh: + return nil + case <-time.After(5 * time.Second): + return h.proc.Kill() + } +} + +func (h *pexecHandle) run() { + ps, err := h.proc.Wait() + close(h.doneCh) + if err != nil { + h.waitCh <- err + } else if !ps.Success() { + h.waitCh <- fmt.Errorf("task exited with error") + } + close(h.waitCh) +} diff --git a/client/driver/pexec_test.go b/client/driver/pexec_test.go new file mode 100644 index 000000000..719ace028 --- /dev/null +++ b/client/driver/pexec_test.go @@ -0,0 +1,216 @@ +package driver + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/environment" + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestPexecDriver_Fingerprint(t *testing.T) { + d := NewPrivilegedExecDriver(testDriverContext("")) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + // Disable privileged exec. + cfg := &config.Config{Options: map[string]string{pexecConfigOption: "false"}} + + apply, err := d.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if apply { + t.Fatalf("should not apply") + } + if node.Attributes["driver.pexec"] != "" { + t.Fatalf("driver incorrectly enabled") + } + + // Enable privileged exec. + cfg.Options[pexecConfigOption] = "true" + apply, err = d.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !apply { + t.Fatalf("should apply") + } + if node.Attributes["driver.pexec"] != "1" { + t.Fatalf("driver not enabled") + } +} + +func TestPexecDriver_StartOpen_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "2", + }, + } + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewPrivilegedExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Attempt to open + handle2, err := d.Open(ctx, handle.ID()) + handle2.(*pexecHandle).waitCh = make(chan error, 1) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle2 == nil { + t.Fatalf("missing handle") + } + + // Task should terminate quickly + select { + case err := <-handle2.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(3 * time.Second): + t.Fatalf("timeout") + } +} + +func TestPexecDriver_Start_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "1", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewPrivilegedExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Update should be a no-op + err = handle.Update(task) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } +} + +func TestPexecDriver_Start_Wait_AllocDir(t *testing.T) { + exp := []byte{'w', 'i', 'n'} + file := "output.txt" + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/bash", + "args": fmt.Sprintf(`-c "sleep 1; echo -n %s > $%s/%s"`, string(exp), environment.AllocDir, file), + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewPrivilegedExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } + + // Check that data was written to the shared alloc directory. + outputFile := filepath.Join(ctx.AllocDir.SharedDir, file) + act, err := ioutil.ReadFile(outputFile) + if err != nil { + t.Fatalf("Couldn't read expected output: %v", err) + } + + if !reflect.DeepEqual(act, exp) { + t.Fatalf("Command outputted %v; want %v", act, exp) + } +} + +func TestPexecDriver_Start_Kill_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "1", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewPrivilegedExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + go func() { + time.Sleep(100 * time.Millisecond) + err := handle.Kill() + if err != nil { + t.Fatalf("err: %v", err) + } + }() + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err == nil { + t.Fatal("should err") + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } +} From 7caa30b85939408754bb33fb8d8fc5e58352bf6b Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 11:34:42 -0700 Subject: [PATCH 018/178] Change name from pexec to raw_exec; hamming distance one seemed like a bad idea --- client/driver/driver.go | 12 +- client/driver/pexec.go | 197 -------------------------------- client/driver/pexec_test.go | 216 ------------------------------------ 3 files changed, 6 insertions(+), 419 deletions(-) delete mode 100644 client/driver/pexec.go delete mode 100644 client/driver/pexec_test.go diff --git a/client/driver/driver.go b/client/driver/driver.go index 9025b952a..31986c321 100644 --- a/client/driver/driver.go +++ b/client/driver/driver.go @@ -15,12 +15,12 @@ import ( // BuiltinDrivers contains the built in registered drivers // which are available for allocation handling var BuiltinDrivers = map[string]Factory{ - "docker": NewDockerDriver, - "exec": NewExecDriver, - "pexec": NewPrivilegedExecDriver, - "java": NewJavaDriver, - "qemu": NewQemuDriver, - "rkt": NewRktDriver, + "docker": NewDockerDriver, + "exec": NewExecDriver, + "raw_exec": NewRawExecDriver, + "java": NewJavaDriver, + "qemu": NewQemuDriver, + "rkt": NewRktDriver, } // NewDriver is used to instantiate and return a new driver diff --git a/client/driver/pexec.go b/client/driver/pexec.go deleted file mode 100644 index 948b3e538..000000000 --- a/client/driver/pexec.go +++ /dev/null @@ -1,197 +0,0 @@ -package driver - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "time" - - "github.com/hashicorp/nomad/client/allocdir" - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/client/driver/args" - "github.com/hashicorp/nomad/nomad/structs" -) - -const ( - // The option that enables this driver in the Config.Options map. - pexecConfigOption = "driver.pexec.enable" - - // Null files to use as stdin. - unixNull = "/dev/null" - windowsNull = "nul" -) - -// The PexecDriver is a privileged version of the exec driver. It provides no -// resource isolation and just fork/execs. The Exec driver should be preferred -// and this should only be used when explicitly needed. -type PexecDriver struct { - DriverContext -} - -// pexecHandle is returned from Start/Open as a handle to the PID -type pexecHandle struct { - proc *os.Process - waitCh chan error - doneCh chan struct{} -} - -// NewPrivilegedExecDriver is used to create a new privileged exec driver -func NewPrivilegedExecDriver(ctx *DriverContext) Driver { - return &PexecDriver{*ctx} -} - -func (d *PexecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { - // Check that the user has explicitly enabled this executor. - enabled := strings.ToLower(cfg.ReadDefault(pexecConfigOption, "false")) - if enabled == "1" || enabled == "true" { - d.logger.Printf("[WARN] driver.Pexec: privileged exec is enabled. Only enable if needed") - node.Attributes["driver.pexec"] = "1" - return true, nil - } - - return false, nil -} - -func (d *PexecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { - // Get the command - command, ok := task.Config["command"] - if !ok || command == "" { - return nil, fmt.Errorf("missing command for pexec driver") - } - - // Get the tasks local directory. - taskName := d.DriverContext.taskName - taskDir, ok := ctx.AllocDir.TaskDirs[taskName] - if !ok { - return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) - } - taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) - - // Get the environment variables. - envVars := TaskEnvironmentVariables(ctx, task) - - // Look for arguments - var cmdArgs []string - if argRaw, ok := task.Config["args"]; ok { - parsed, err := args.ParseAndReplace(argRaw, envVars.Map()) - if err != nil { - return nil, err - } - cmdArgs = append(cmdArgs, parsed...) - } - - // Setup the command - cmd := exec.Command(command, cmdArgs...) - cmd.Dir = taskDir - cmd.Env = envVars.List() - - // Capture the stdout/stderr and redirect stdin to /dev/null - stdoutFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stdout", taskName)) - stderrFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stderr", taskName)) - stdinFilename := unixNull - if runtime.GOOS == "windows" { - stdinFilename = windowsNull - } - - stdo, err := os.OpenFile(stdoutFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) - if err != nil { - return nil, fmt.Errorf("Error opening file to redirect stdout: %v", err) - } - - stde, err := os.OpenFile(stderrFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) - if err != nil { - return nil, fmt.Errorf("Error opening file to redirect stderr: %v", err) - } - - stdi, err := os.OpenFile(stdinFilename, os.O_CREATE|os.O_RDONLY, 0666) - if err != nil { - return nil, fmt.Errorf("Error opening file to redirect stdin: %v", err) - } - - cmd.Stdout = stdo - cmd.Stderr = stde - cmd.Stdin = stdi - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start command: %v", err) - } - - // Return a driver handle - h := &pexecHandle{ - proc: cmd.Process, - doneCh: make(chan struct{}), - waitCh: make(chan error, 1), - } - go h.run() - return h, nil -} - -func (d *PexecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { - // Split the handle - pidStr := strings.TrimPrefix(handleID, "PID:") - pid, err := strconv.Atoi(pidStr) - if err != nil { - return nil, fmt.Errorf("failed to parse handle '%s': %v", handleID, err) - } - - // Find the process - proc, err := os.FindProcess(pid) - if proc == nil || err != nil { - return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) - } - - // Return a driver handle - h := &pexecHandle{ - proc: proc, - doneCh: make(chan struct{}), - waitCh: make(chan error, 1), - } - go h.run() - return h, nil -} - -func (h *pexecHandle) ID() string { - // Return a handle to the PID - return fmt.Sprintf("PID:%d", h.proc.Pid) -} - -func (h *pexecHandle) WaitCh() chan error { - return h.waitCh -} - -func (h *pexecHandle) Update(task *structs.Task) error { - // Update is not possible - return nil -} - -// Kill is used to terminate the task. We send an Interrupt -// and then provide a 5 second grace period before doing a Kill on supported -// OS's, otherwise we kill immediately. -func (h *pexecHandle) Kill() error { - if runtime.GOOS == "windows" { - return h.proc.Kill() - } - - h.proc.Signal(os.Interrupt) - select { - case <-h.doneCh: - return nil - case <-time.After(5 * time.Second): - return h.proc.Kill() - } -} - -func (h *pexecHandle) run() { - ps, err := h.proc.Wait() - close(h.doneCh) - if err != nil { - h.waitCh <- err - } else if !ps.Success() { - h.waitCh <- fmt.Errorf("task exited with error") - } - close(h.waitCh) -} diff --git a/client/driver/pexec_test.go b/client/driver/pexec_test.go deleted file mode 100644 index 719ace028..000000000 --- a/client/driver/pexec_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package driver - -import ( - "fmt" - "io/ioutil" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/hashicorp/nomad/client/config" - "github.com/hashicorp/nomad/client/driver/environment" - "github.com/hashicorp/nomad/nomad/structs" -) - -func TestPexecDriver_Fingerprint(t *testing.T) { - d := NewPrivilegedExecDriver(testDriverContext("")) - node := &structs.Node{ - Attributes: make(map[string]string), - } - - // Disable privileged exec. - cfg := &config.Config{Options: map[string]string{pexecConfigOption: "false"}} - - apply, err := d.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if apply { - t.Fatalf("should not apply") - } - if node.Attributes["driver.pexec"] != "" { - t.Fatalf("driver incorrectly enabled") - } - - // Enable privileged exec. - cfg.Options[pexecConfigOption] = "true" - apply, err = d.Fingerprint(cfg, node) - if err != nil { - t.Fatalf("err: %v", err) - } - if !apply { - t.Fatalf("should apply") - } - if node.Attributes["driver.pexec"] != "1" { - t.Fatalf("driver not enabled") - } -} - -func TestPexecDriver_StartOpen_Wait(t *testing.T) { - task := &structs.Task{ - Name: "sleep", - Config: map[string]string{ - "command": "/bin/sleep", - "args": "2", - }, - } - driverCtx := testDriverContext(task.Name) - ctx := testDriverExecContext(task, driverCtx) - defer ctx.AllocDir.Destroy() - - d := NewPrivilegedExecDriver(driverCtx) - handle, err := d.Start(ctx, task) - if err != nil { - t.Fatalf("err: %v", err) - } - if handle == nil { - t.Fatalf("missing handle") - } - - // Attempt to open - handle2, err := d.Open(ctx, handle.ID()) - handle2.(*pexecHandle).waitCh = make(chan error, 1) - if err != nil { - t.Fatalf("err: %v", err) - } - if handle2 == nil { - t.Fatalf("missing handle") - } - - // Task should terminate quickly - select { - case err := <-handle2.WaitCh(): - if err != nil { - t.Fatalf("err: %v", err) - } - case <-time.After(3 * time.Second): - t.Fatalf("timeout") - } -} - -func TestPexecDriver_Start_Wait(t *testing.T) { - task := &structs.Task{ - Name: "sleep", - Config: map[string]string{ - "command": "/bin/sleep", - "args": "1", - }, - } - - driverCtx := testDriverContext(task.Name) - ctx := testDriverExecContext(task, driverCtx) - defer ctx.AllocDir.Destroy() - - d := NewPrivilegedExecDriver(driverCtx) - handle, err := d.Start(ctx, task) - if err != nil { - t.Fatalf("err: %v", err) - } - if handle == nil { - t.Fatalf("missing handle") - } - - // Update should be a no-op - err = handle.Update(task) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Task should terminate quickly - select { - case err := <-handle.WaitCh(): - if err != nil { - t.Fatalf("err: %v", err) - } - case <-time.After(2 * time.Second): - t.Fatalf("timeout") - } -} - -func TestPexecDriver_Start_Wait_AllocDir(t *testing.T) { - exp := []byte{'w', 'i', 'n'} - file := "output.txt" - task := &structs.Task{ - Name: "sleep", - Config: map[string]string{ - "command": "/bin/bash", - "args": fmt.Sprintf(`-c "sleep 1; echo -n %s > $%s/%s"`, string(exp), environment.AllocDir, file), - }, - } - - driverCtx := testDriverContext(task.Name) - ctx := testDriverExecContext(task, driverCtx) - defer ctx.AllocDir.Destroy() - - d := NewPrivilegedExecDriver(driverCtx) - handle, err := d.Start(ctx, task) - if err != nil { - t.Fatalf("err: %v", err) - } - if handle == nil { - t.Fatalf("missing handle") - } - - // Task should terminate quickly - select { - case err := <-handle.WaitCh(): - if err != nil { - t.Fatalf("err: %v", err) - } - case <-time.After(2 * time.Second): - t.Fatalf("timeout") - } - - // Check that data was written to the shared alloc directory. - outputFile := filepath.Join(ctx.AllocDir.SharedDir, file) - act, err := ioutil.ReadFile(outputFile) - if err != nil { - t.Fatalf("Couldn't read expected output: %v", err) - } - - if !reflect.DeepEqual(act, exp) { - t.Fatalf("Command outputted %v; want %v", act, exp) - } -} - -func TestPexecDriver_Start_Kill_Wait(t *testing.T) { - task := &structs.Task{ - Name: "sleep", - Config: map[string]string{ - "command": "/bin/sleep", - "args": "1", - }, - } - - driverCtx := testDriverContext(task.Name) - ctx := testDriverExecContext(task, driverCtx) - defer ctx.AllocDir.Destroy() - - d := NewPrivilegedExecDriver(driverCtx) - handle, err := d.Start(ctx, task) - if err != nil { - t.Fatalf("err: %v", err) - } - if handle == nil { - t.Fatalf("missing handle") - } - - go func() { - time.Sleep(100 * time.Millisecond) - err := handle.Kill() - if err != nil { - t.Fatalf("err: %v", err) - } - }() - - // Task should terminate quickly - select { - case err := <-handle.WaitCh(): - if err == nil { - t.Fatal("should err") - } - case <-time.After(2 * time.Second): - t.Fatalf("timeout") - } -} From 60346ae8ec91d2192d617e8ecb7f2e3b6cdf4cbd Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 11:36:22 -0700 Subject: [PATCH 019/178] Actually add the files --- client/driver/raw_exec.go | 197 +++++++++++++++++++++ client/driver/raw_exec_test.go | 216 +++++++++++++++++++++++ website/source/docs/drivers/exec.html.md | 2 +- 3 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 client/driver/raw_exec.go create mode 100644 client/driver/raw_exec_test.go diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go new file mode 100644 index 000000000..ae8e6ab63 --- /dev/null +++ b/client/driver/raw_exec.go @@ -0,0 +1,197 @@ +package driver + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/hashicorp/nomad/client/allocdir" + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/args" + "github.com/hashicorp/nomad/nomad/structs" +) + +const ( + // The option that enables this driver in the Config.Options map. + rawExecConfigOption = "driver.raw_exec.enable" + + // Null files to use as stdin. + unixNull = "/dev/null" + windowsNull = "nul" +) + +// The RawExecDriver is a privileged version of the exec driver. It provides no +// resource isolation and just fork/execs. The Exec driver should be preferred +// and this should only be used when explicitly needed. +type RawExecDriver struct { + DriverContext +} + +// rawExecHandle is returned from Start/Open as a handle to the PID +type rawExecHandle struct { + proc *os.Process + waitCh chan error + doneCh chan struct{} +} + +// NewRawExecDriver is used to create a new raw exec driver +func NewRawExecDriver(ctx *DriverContext) Driver { + return &RawExecDriver{*ctx} +} + +func (d *RawExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { + // Check that the user has explicitly enabled this executor. + enabled := strings.ToLower(cfg.ReadDefault(rawExecConfigOption, "false")) + if enabled == "1" || enabled == "true" { + d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") + node.Attributes["driver.raw_exec"] = "1" + return true, nil + } + + return false, nil +} + +func (d *RawExecDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { + // Get the command + command, ok := task.Config["command"] + if !ok || command == "" { + return nil, fmt.Errorf("missing command for raw_exec driver") + } + + // Get the tasks local directory. + taskName := d.DriverContext.taskName + taskDir, ok := ctx.AllocDir.TaskDirs[taskName] + if !ok { + return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) + } + taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) + + // Get the environment variables. + envVars := TaskEnvironmentVariables(ctx, task) + + // Look for arguments + var cmdArgs []string + if argRaw, ok := task.Config["args"]; ok { + parsed, err := args.ParseAndReplace(argRaw, envVars.Map()) + if err != nil { + return nil, err + } + cmdArgs = append(cmdArgs, parsed...) + } + + // Setup the command + cmd := exec.Command(command, cmdArgs...) + cmd.Dir = taskDir + cmd.Env = envVars.List() + + // Capture the stdout/stderr and redirect stdin to /dev/null + stdoutFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stdout", taskName)) + stderrFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stderr", taskName)) + stdinFilename := unixNull + if runtime.GOOS == "windows" { + stdinFilename = windowsNull + } + + stdo, err := os.OpenFile(stdoutFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stdout: %v", err) + } + + stde, err := os.OpenFile(stderrFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stderr: %v", err) + } + + stdi, err := os.OpenFile(stdinFilename, os.O_CREATE|os.O_RDONLY, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stdin: %v", err) + } + + cmd.Stdout = stdo + cmd.Stderr = stde + cmd.Stdin = stdi + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start command: %v", err) + } + + // Return a driver handle + h := &rawExecHandle{ + proc: cmd.Process, + doneCh: make(chan struct{}), + waitCh: make(chan error, 1), + } + go h.run() + return h, nil +} + +func (d *RawExecDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error) { + // Split the handle + pidStr := strings.TrimPrefix(handleID, "PID:") + pid, err := strconv.Atoi(pidStr) + if err != nil { + return nil, fmt.Errorf("failed to parse handle '%s': %v", handleID, err) + } + + // Find the process + proc, err := os.FindProcess(pid) + if proc == nil || err != nil { + return nil, fmt.Errorf("failed to find PID %d: %v", pid, err) + } + + // Return a driver handle + h := &rawExecHandle{ + proc: proc, + doneCh: make(chan struct{}), + waitCh: make(chan error, 1), + } + go h.run() + return h, nil +} + +func (h *rawExecHandle) ID() string { + // Return a handle to the PID + return fmt.Sprintf("PID:%d", h.proc.Pid) +} + +func (h *rawExecHandle) WaitCh() chan error { + return h.waitCh +} + +func (h *rawExecHandle) Update(task *structs.Task) error { + // Update is not possible + return nil +} + +// Kill is used to terminate the task. We send an Interrupt +// and then provide a 5 second grace period before doing a Kill on supported +// OS's, otherwise we kill immediately. +func (h *rawExecHandle) Kill() error { + if runtime.GOOS == "windows" { + return h.proc.Kill() + } + + h.proc.Signal(os.Interrupt) + select { + case <-h.doneCh: + return nil + case <-time.After(5 * time.Second): + return h.proc.Kill() + } +} + +func (h *rawExecHandle) run() { + ps, err := h.proc.Wait() + close(h.doneCh) + if err != nil { + h.waitCh <- err + } else if !ps.Success() { + h.waitCh <- fmt.Errorf("task exited with error") + } + close(h.waitCh) +} diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go new file mode 100644 index 000000000..8d06a7de8 --- /dev/null +++ b/client/driver/raw_exec_test.go @@ -0,0 +1,216 @@ +package driver + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "reflect" + "testing" + "time" + + "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/environment" + "github.com/hashicorp/nomad/nomad/structs" +) + +func TestRawExecDriver_Fingerprint(t *testing.T) { + d := NewRawExecDriver(testDriverContext("")) + node := &structs.Node{ + Attributes: make(map[string]string), + } + + // Disable raw exec. + cfg := &config.Config{Options: map[string]string{rawExecConfigOption: "false"}} + + apply, err := d.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if apply { + t.Fatalf("should not apply") + } + if node.Attributes["driver.raw_exec"] != "" { + t.Fatalf("driver incorrectly enabled") + } + + // Enable raw exec. + cfg.Options[rawExecConfigOption] = "true" + apply, err = d.Fingerprint(cfg, node) + if err != nil { + t.Fatalf("err: %v", err) + } + if !apply { + t.Fatalf("should apply") + } + if node.Attributes["driver.raw_exec"] != "1" { + t.Fatalf("driver not enabled") + } +} + +func TestRawExecDriver_StartOpen_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "2", + }, + } + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewRawExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Attempt to open + handle2, err := d.Open(ctx, handle.ID()) + handle2.(*rawExecHandle).waitCh = make(chan error, 1) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle2 == nil { + t.Fatalf("missing handle") + } + + // Task should terminate quickly + select { + case err := <-handle2.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(3 * time.Second): + t.Fatalf("timeout") + } +} + +func TestRawExecDriver_Start_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "1", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewRawExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Update should be a no-op + err = handle.Update(task) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } +} + +func TestRawExecDriver_Start_Wait_AllocDir(t *testing.T) { + exp := []byte{'w', 'i', 'n'} + file := "output.txt" + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/bash", + "args": fmt.Sprintf(`-c "sleep 1; echo -n %s > $%s/%s"`, string(exp), environment.AllocDir, file), + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewRawExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } + + // Check that data was written to the shared alloc directory. + outputFile := filepath.Join(ctx.AllocDir.SharedDir, file) + act, err := ioutil.ReadFile(outputFile) + if err != nil { + t.Fatalf("Couldn't read expected output: %v", err) + } + + if !reflect.DeepEqual(act, exp) { + t.Fatalf("Command outputted %v; want %v", act, exp) + } +} + +func TestRawExecDriver_Start_Kill_Wait(t *testing.T) { + task := &structs.Task{ + Name: "sleep", + Config: map[string]string{ + "command": "/bin/sleep", + "args": "1", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + defer ctx.AllocDir.Destroy() + + d := NewRawExecDriver(driverCtx) + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + + go func() { + time.Sleep(100 * time.Millisecond) + err := handle.Kill() + if err != nil { + t.Fatalf("err: %v", err) + } + }() + + // Task should terminate quickly + select { + case err := <-handle.WaitCh(): + if err == nil { + t.Fatal("should err") + } + case <-time.After(2 * time.Second): + t.Fatalf("timeout") + } +} diff --git a/website/source/docs/drivers/exec.html.md b/website/source/docs/drivers/exec.html.md index e480f38bd..dd30af74a 100644 --- a/website/source/docs/drivers/exec.html.md +++ b/website/source/docs/drivers/exec.html.md @@ -6,7 +6,7 @@ description: |- The Exec task driver is used to run binaries using OS isolation primitives. --- -# Fork/Exec Driver +# Isolated Fork/Exec Driver Name: `exec` From c4e48618894149032d42da3d79edbb2c428bdba7 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 12:18:44 -0700 Subject: [PATCH 020/178] Documentation --- website/source/docs/agent/config.html.md | 4 +- website/source/docs/drivers/exec.html.md | 7 +-- website/source/docs/drivers/raw_exec.html.md | 47 ++++++++++++++++++++ website/source/layouts/docs.erb | 10 ++++- 4 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 website/source/docs/drivers/raw_exec.html.md diff --git a/website/source/docs/agent/config.html.md b/website/source/docs/agent/config.html.md index 56532f921..261514752 100644 --- a/website/source/docs/agent/config.html.md +++ b/website/source/docs/agent/config.html.md @@ -207,8 +207,8 @@ configured on server nodes. option is not required and has no default. * `meta`: This is a key/value mapping of metadata pairs. This is a free-form map and can contain any string values. - * `options`: This is a key/value mapping of internal configuration for clients, - such as for driver configuration. + * `options`: This is a key/value mapping of internal + configuration for clients, such as for driver configuration. * `network_interface`: This is a string to force network fingerprinting to use a specific network interface * `network_speed`: This is an int that sets the diff --git a/website/source/docs/drivers/exec.html.md b/website/source/docs/drivers/exec.html.md index dd30af74a..1f3e50935 100644 --- a/website/source/docs/drivers/exec.html.md +++ b/website/source/docs/drivers/exec.html.md @@ -11,9 +11,10 @@ description: |- Name: `exec` The `exec` driver is used to simply execute a particular command for a task. -This is the simplest driver and is extremely flexible. In particlar, because -it can invoke any command, it can be used to call scripts or other wrappers -which provide higher level features. +However unlike [`raw_exec`](raw_exec.html) it uses the underlying isolation +primitives of the operating system to limit the tasks access to resources. While +simple, since the `exec` driver can invoke any command, it can be used to call +scripts or other wrappers which provide higher level features. ## Task Configuration diff --git a/website/source/docs/drivers/raw_exec.html.md b/website/source/docs/drivers/raw_exec.html.md new file mode 100644 index 000000000..fdbcb956c --- /dev/null +++ b/website/source/docs/drivers/raw_exec.html.md @@ -0,0 +1,47 @@ +--- +layout: "docs" +page_title: "Drivers: Raw Exec" +sidebar_current: "docs-drivers-raw-exec" +description: |- + The Raw Exec task driver simply fork/execs and provides no isolation. +--- + +# Raw Fork/Exec Driver + +Name: `raw_exec` + +The `raw_exec` driver is used to execute a command for a task without any +resource isolation. As such, it should be used with extreme care and is disabled +by default. + +## Task Configuration + +The `raw_exec` driver supports the following configuration in the job spec: + +* `command` - The command to execute. Must be provided. + +* `args` - The argument list to the command, space seperated. Optional. + +## Client Requirements + +The `raw_exec` driver can run on all supported operating systems. It is however +disabled by default. In order to be enabled, the Nomad client configuration must +explicitly enable the `raw_exec` driver in the +[options](../agent/config.html#options) field: + +``` +options = { + driver.raw_exec.enable = "1" +} +``` + +## Client Attributes + +The `raw_exec` driver will set the following client attributes: + +* `driver.raw_exec` - This will be set to "1", indicating the + driver is available. + +## Resource Isolation + +The `raw_exec` driver provides no isolation. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 62d1dddca..e428cb593 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -49,7 +49,11 @@ > - Fork/Exec + Isolated Fork/Exec + + + > + Raw Fork/Exec > @@ -60,6 +64,10 @@ Qemu + > + Rkt + + > Custom From 6e43a2ba3389aa4481a113f42861024625b7ca67 Mon Sep 17 00:00:00 2001 From: Chris Bednarski Date: Thu, 8 Oct 2015 12:35:19 -0700 Subject: [PATCH 021/178] Use DefaultDockerHost from fsouza upstream --- client/driver/docker.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 5c5a82e91..0fbf64714 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -4,12 +4,10 @@ import ( "encoding/json" "fmt" "log" - "runtime" "strconv" "strings" docker "github.com/fsouza/go-dockerclient" - opts "github.com/fsouza/go-dockerclient/external/github.com/docker/docker/opts" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/nomad/structs" @@ -35,20 +33,6 @@ type dockerHandle struct { doneCh chan struct{} } -// getDefaultDockerHost is copied from fsouza. If it were exported we woudn't -// need this here. -func getDefaultDockerHost() (string, error) { - var defaultHost string - if runtime.GOOS == "windows" { - // If we do not have a host, default to TCP socket on Windows - defaultHost = fmt.Sprintf("tcp://%s:%d", opts.DefaultHTTPHost, opts.DefaultHTTPPort) - } else { - // If we do not have a host, default to unix socket - defaultHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket) - } - return opts.ValidateHost(defaultHost) -} - func NewDockerDriver(ctx *DriverContext) Driver { return &DockerDriver{*ctx} } @@ -69,7 +53,7 @@ func (d *DockerDriver) dockerClient() (*docker.Client, error) { // In prod mode we'll read the docker.endpoint configuration and fall back // on the host-specific default. We do not read from the environment. - defaultEndpoint, err := getDefaultDockerHost() + defaultEndpoint, err := docker.DefaultDockerHost() if err != nil { return nil, fmt.Errorf("Unable to determine default docker endpoint: %s", err) } From 0681acbf6921e95caeec87bf518ac7bd3061ce1e Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 15:36:42 -0700 Subject: [PATCH 022/178] Fix race condition in server endpoint tests in which workers would process evals before there status could be asserted --- nomad/job_endpoint_test.go | 16 ++++++++++++---- nomad/node_endpoint_test.go | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 3f0762254..e43ed3ba2 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -11,7 +11,9 @@ import ( ) func TestJobEndpoint_Register(t *testing.T) { - s1 := testServer(t, nil) + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) defer s1.Shutdown() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) @@ -78,7 +80,9 @@ func TestJobEndpoint_Register(t *testing.T) { } func TestJobEndpoint_Register_Existing(t *testing.T) { - s1 := testServer(t, nil) + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) defer s1.Shutdown() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) @@ -162,7 +166,9 @@ func TestJobEndpoint_Register_Existing(t *testing.T) { } func TestJobEndpoint_Evaluate(t *testing.T) { - s1 := testServer(t, nil) + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) defer s1.Shutdown() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) @@ -231,7 +237,9 @@ func TestJobEndpoint_Evaluate(t *testing.T) { } func TestJobEndpoint_Deregister(t *testing.T) { - s1 := testServer(t, nil) + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) defer s1.Shutdown() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) diff --git a/nomad/node_endpoint_test.go b/nomad/node_endpoint_test.go index ac458c1e5..834d4297a 100644 --- a/nomad/node_endpoint_test.go +++ b/nomad/node_endpoint_test.go @@ -529,7 +529,9 @@ func TestClientEndpoint_CreateNodeEvals(t *testing.T) { } func TestClientEndpoint_Evaluate(t *testing.T) { - s1 := testServer(t, nil) + s1 := testServer(t, func(c *Config) { + c.NumSchedulers = 0 // Prevent automatic dequeue + }) defer s1.Shutdown() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) From c25b6b931765ff354504d8160ebd4e6f6bf2df94 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 16:32:42 -0700 Subject: [PATCH 023/178] TestRPC_forwardRegion waits for test servers to join --- nomad/rpc_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nomad/rpc_test.go b/nomad/rpc_test.go index b798e2c66..a369b1ebf 100644 --- a/nomad/rpc_test.go +++ b/nomad/rpc_test.go @@ -37,6 +37,8 @@ func TestRPC_forwardRegion(t *testing.T) { }) defer s2.Shutdown() testJoin(t, s1, s2) + testutil.WaitForLeader(t, s1.RPC) + testutil.WaitForLeader(t, s2.RPC) var out struct{} err := s1.forwardRegion("region2", "Status.Ping", struct{}{}, &out) From f58407d02af0c64471209abc427d706c3999369b Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Thu, 8 Oct 2015 16:47:29 -0700 Subject: [PATCH 024/178] Fix go vet format errors in exec_linux --- client/executor/exec_linux.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/executor/exec_linux.go b/client/executor/exec_linux.go index 3985fb5fe..29cd3b973 100644 --- a/client/executor/exec_linux.go +++ b/client/executor/exec_linux.go @@ -112,7 +112,7 @@ func (e *LinuxExecutor) ConfigureTaskDir(taskName string, alloc *allocdir.AllocD // Mount dev dev := filepath.Join(taskDir, "dev") if err := os.Mkdir(dev, 0777); err != nil { - return fmt.Errorf("Mkdir(%v) failed: %v", dev) + return fmt.Errorf("Mkdir(%v) failed: %v", dev, err) } if err := syscall.Mount("", dev, "devtmpfs", syscall.MS_RDONLY, ""); err != nil { @@ -122,7 +122,7 @@ func (e *LinuxExecutor) ConfigureTaskDir(taskName string, alloc *allocdir.AllocD // Mount proc proc := filepath.Join(taskDir, "proc") if err := os.Mkdir(proc, 0777); err != nil { - return fmt.Errorf("Mkdir(%v) failed: %v", proc) + return fmt.Errorf("Mkdir(%v) failed: %v", proc, err) } if err := syscall.Mount("", proc, "proc", syscall.MS_RDONLY, ""); err != nil { From 9b6e1b4dafbfcad0511eeb9f70f4f707a51b2416 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Thu, 8 Oct 2015 16:54:41 -0700 Subject: [PATCH 025/178] Reap process after sending kill signal Without waiting on the process after sending a kill will cause zombies and we all know what happens when we have a zombies outbreak. There are other calls to kill in this file but they are done on the main process for the task so they should have the wait method called at sometime in their lifecycle. --- client/executor/exec_linux.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/executor/exec_linux.go b/client/executor/exec_linux.go index 3985fb5fe..7a64b9e69 100644 --- a/client/executor/exec_linux.go +++ b/client/executor/exec_linux.go @@ -542,6 +542,11 @@ func (e *LinuxExecutor) destroyCgroup() error { multierror.Append(errs, fmt.Errorf("Failed to kill Pid %v: %v", pid, err)) continue } + + if _, err := process.Wait(); err != nil { + multierror.Append(errs, fmt.Errorf("Failed to wait Pid %v: %v", pid, err)) + continue + } } // Remove the cgroup. From 54fa6d76a2438f73c3cd4ab05ab6b5cf38a5f56e Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Thu, 8 Oct 2015 13:54:20 -0400 Subject: [PATCH 026/178] Add Travis testing --- .travis.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..6bf7bd245 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +sudo: false + +language: go + +go: + - 1.5.1 + - tip + +matrix: + allow_failures: + - go: tip + +branches: + only: + - master + +install: + - make bootstrap + +script: + - make test From 6acdd9da085090ca38af1f07bb21a1680bf9b858 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Fri, 9 Oct 2015 09:10:40 -0500 Subject: [PATCH 027/178] Add a few more GCE-specific attributes: * cpu-platform * scheduling.automatic-restart * scheduling.on-host-maintenance * network.NETWORKNAME=true --- client/fingerprint/env_gce.go | 10 ++++++++-- client/fingerprint/env_gce_test.go | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index ceff998d7..9c6135c82 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -132,6 +132,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) keys := []string{ "hostname", "id", + "cpu-platform", + "scheduling/automatic-restart", + "scheduling/on-host-maintenance", } for _, k := range keys { value, err := f.Get(k, false) @@ -140,7 +143,8 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } // assume we want blank entries - node.Attributes["platform.gce."+k] = strings.Trim(string(value), "\n") + key := strings.Replace(k, "/", ".", -1) + node.Attributes["platform.gce."+key] = strings.Trim(string(value), "\n") } // These keys need everything before the final slash removed to be usable. @@ -169,7 +173,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) } else { for _, intf := range interfaces { - prefix := "platform.gce.network." + lastToken(intf.Network) + network := lastToken(intf.Network) + prefix := "platform.gce.network." + network + node.Attributes[prefix] = "true" // newNetwork is populated and addded to the Nodes resources newNetwork := &structs.NetworkResource{ diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 460bd726f..47c0dece8 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -130,6 +130,7 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { } assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") + assertNodeAttributeEquals(t, node, "platform.gce.network.default", "true") assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { assertNodeAttributeEquals(t, node, "platform.gce.network.default.external-ip.0", "104.44.55.66") @@ -138,6 +139,9 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { t.Fatal("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, "platform.gce.attr.ghi", "111") @@ -176,6 +180,21 @@ const GCE_routes = ` "uri": "/computeMetadata/v1/instance/attributes/?recursive=true", "content-type": "application/json", "body": "{\"ghi\":\"111\",\"jkl\":\"222\"}" + }, + { + "uri": "/computeMetadata/v1/instance/scheduling/automatic-restart", + "content-type": "text/plain", + "body": "TRUE" + }, + { + "uri": "/computeMetadata/v1/instance/scheduling/on-host-maintenance", + "content-type": "text/plain", + "body": "MIGRATE" + }, + { + "uri": "/computeMetadata/v1/instance/cpu-platform", + "content-type": "text/plain", + "body": "Intel Ivy Bridge" } ] } From 3d5f236384104f9f7d549df741cb72b18a129030 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Fri, 9 Oct 2015 08:29:33 -0600 Subject: [PATCH 028/178] side affect -> side effect --- website/source/intro/vs/ecs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/ecs.html.md b/website/source/intro/vs/ecs.html.md index 3f0568ca4..7a47bb9f4 100644 --- a/website/source/intro/vs/ecs.html.md +++ b/website/source/intro/vs/ecs.html.md @@ -19,7 +19,7 @@ Nomad is completely open source, including both the client and server components. By contrast, only the agent code for ECS is open and the servers are closed sourced and managed by Amazon. -As a side affect of the ECS servers being managed by AWS, it is not possible +As a side effect of the ECS servers being managed by AWS, it is not possible to use ECS outside of AWS. Nomad is agnostic to the environment it is run, supporting public and private clouds, as well as bare metal datacenters. Clusters in Nomad can span multiple datacenters and regions, meaning From ff8d14cb62e13e82e0a0ad795b373f1ee785d152 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Fri, 9 Oct 2015 08:34:28 -0600 Subject: [PATCH 029/178] add space --- website/source/intro/vs/ecs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/ecs.html.md b/website/source/intro/vs/ecs.html.md index 3f0568ca4..959278f46 100644 --- a/website/source/intro/vs/ecs.html.md +++ b/website/source/intro/vs/ecs.html.md @@ -23,7 +23,7 @@ As a side affect of the ECS servers being managed by AWS, it is not possible to use ECS outside of AWS. Nomad is agnostic to the environment it is run, supporting public and private clouds, as well as bare metal datacenters. Clusters in Nomad can span multiple datacenters and regions, meaning -a single cluster could be managing machines on AWS, Azure,and GCE simultaneously. +a single cluster could be managing machines on AWS, Azure, and GCE simultaneously. The ECS service is specifically focused on containers and the Docker engine, while Nomad is more general purpose. Nomad supports virtualized, From bbdceca358978b7c9b048a4e633b8d756a0b8aa2 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 9 Oct 2015 11:29:59 -0700 Subject: [PATCH 030/178] Better parsing of raw_exec option and updated docs --- client/driver/raw_exec.go | 8 ++++++-- website/source/docs/drivers/raw_exec.html.md | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/driver/raw_exec.go b/client/driver/raw_exec.go index ae8e6ab63..cdc41e676 100644 --- a/client/driver/raw_exec.go +++ b/client/driver/raw_exec.go @@ -46,8 +46,12 @@ func NewRawExecDriver(ctx *DriverContext) Driver { func (d *RawExecDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, error) { // Check that the user has explicitly enabled this executor. - enabled := strings.ToLower(cfg.ReadDefault(rawExecConfigOption, "false")) - if enabled == "1" || enabled == "true" { + enabled, err := strconv.ParseBool(cfg.ReadDefault(rawExecConfigOption, "false")) + if err != nil { + return false, fmt.Errorf("Failed to parse %v option: %v", rawExecConfigOption, err) + } + + if enabled { d.logger.Printf("[WARN] driver.raw_exec: raw exec is enabled. Only enable if needed") node.Attributes["driver.raw_exec"] = "1" return true, nil diff --git a/website/source/docs/drivers/raw_exec.html.md b/website/source/docs/drivers/raw_exec.html.md index fdbcb956c..35b0c95bf 100644 --- a/website/source/docs/drivers/raw_exec.html.md +++ b/website/source/docs/drivers/raw_exec.html.md @@ -11,8 +11,8 @@ description: |- Name: `raw_exec` The `raw_exec` driver is used to execute a command for a task without any -resource isolation. As such, it should be used with extreme care and is disabled -by default. +isolation. Further, the task is started as the same user as the Nomad process. +As such, it should be used with extreme care and is disabled by default. ## Task Configuration From 0c266684d6940a772cd5b432e2985cf7f64c4c1c Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Fri, 9 Oct 2015 14:01:51 -0600 Subject: [PATCH 031/178] Update ecs.html.md --- website/source/intro/vs/ecs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/ecs.html.md b/website/source/intro/vs/ecs.html.md index db85a72e0..519dce95e 100644 --- a/website/source/intro/vs/ecs.html.md +++ b/website/source/intro/vs/ecs.html.md @@ -20,7 +20,7 @@ components. By contrast, only the agent code for ECS is open and the servers are closed sourced and managed by Amazon. As a side effect of the ECS servers being managed by AWS, it is not possible -to use ECS outside of AWS. Nomad is agnostic to the environment it is run, +to use ECS outside of AWS. Nomad is agnostic to the environment in which it is run, supporting public and private clouds, as well as bare metal datacenters. Clusters in Nomad can span multiple datacenters and regions, meaning a single cluster could be managing machines on AWS, Azure, and GCE simultaneously. From 203906b94a9f42a53af48e5ee996343d9d9d0a85 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Fri, 9 Oct 2015 18:34:57 -0500 Subject: [PATCH 032/178] GCE fingerprinter no longer updates network resources It has nothing to add that the generic fingerprinters aren't finding on their own already. --- client/fingerprint/env_gce.go | 19 ++----------------- client/fingerprint/env_gce_test.go | 16 ---------------- 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 9c6135c82..9b3d3bd6f 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -161,11 +161,6 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) node.Attributes["platform.gce."+k] = strings.Trim(lastToken(value), "\n") } - // Prepare to populate Node Network Resources - if node.Resources == nil { - node.Resources = &structs.Resources{} - } - // Get internal and external IPs (if they exist) value, err := f.Get("network-interfaces/", true) var interfaces []GCEMetadataNetworkInterface @@ -173,19 +168,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) } else { for _, intf := range interfaces { - network := lastToken(intf.Network) - prefix := "platform.gce.network." + network + prefix := "platform.gce.network." + lastToken(intf.Network) node.Attributes[prefix] = "true" - - // newNetwork is populated and addded to the Nodes resources - newNetwork := &structs.NetworkResource{ - IP: strings.Trim(intf.Ip, "\n"), - } - newNetwork.CIDR = newNetwork.IP + "/32" - node.Resources.Networks = append(node.Resources.Networks, newNetwork) - - node.Attributes["network.ip-address"] = newNetwork.IP - node.Attributes[prefix+".ip"] = newNetwork.IP + node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") for index, accessConfig := range intf.AccessConfigs { node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp } diff --git a/client/fingerprint/env_gce_test.go b/client/fingerprint/env_gce_test.go index 47c0dece8..159563576 100644 --- a/client/fingerprint/env_gce_test.go +++ b/client/fingerprint/env_gce_test.go @@ -95,7 +95,6 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { "platform.gce.tag.def", "platform.gce.attr.ghi", "platform.gce.attr.jkl", - "network.ip-address", } for _, k := range keys { @@ -115,21 +114,6 @@ func testFingerprint_GCE(t *testing.T, withExternalIp bool) { assertNodeAttributeEquals(t, node, "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") - - if node.Resources == nil || len(node.Resources.Networks) == 0 { - t.Fatal("Expected to find Network Resources") - } - - // Test at least the first Network Resource - net := node.Resources.Networks[0] - if net.IP != "10.240.0.5" { - t.Fatalf("Expected Network Resource to have IP 10.240.0.5, saw %s", net.IP) - } - if net.CIDR != "10.240.0.5/32" { - t.Fatalf("Expected Network Resource to have CIDR 10.240.0.5/32, saw %s", net.CIDR) - } - - assertNodeAttributeEquals(t, node, "network.ip-address", "10.240.0.5") assertNodeAttributeEquals(t, node, "platform.gce.network.default", "true") assertNodeAttributeEquals(t, node, "platform.gce.network.default.ip", "10.240.0.5") if withExternalIp { From 75eb9e28d84739b7c67731dedd1f1d67348b42df Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 9 Oct 2015 20:56:28 -0700 Subject: [PATCH 033/178] Fix raw exec test race condition --- client/driver/raw_exec_test.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client/driver/raw_exec_test.go b/client/driver/raw_exec_test.go index 8d06a7de8..f3b63e12e 100644 --- a/client/driver/raw_exec_test.go +++ b/client/driver/raw_exec_test.go @@ -52,7 +52,7 @@ func TestRawExecDriver_StartOpen_Wait(t *testing.T) { Name: "sleep", Config: map[string]string{ "command": "/bin/sleep", - "args": "2", + "args": "1", }, } driverCtx := testDriverContext(task.Name) @@ -70,7 +70,6 @@ func TestRawExecDriver_StartOpen_Wait(t *testing.T) { // Attempt to open handle2, err := d.Open(ctx, handle.ID()) - handle2.(*rawExecHandle).waitCh = make(chan error, 1) if err != nil { t.Fatalf("err: %v", err) } @@ -80,13 +79,17 @@ func TestRawExecDriver_StartOpen_Wait(t *testing.T) { // Task should terminate quickly select { - case err := <-handle2.WaitCh(): - if err != nil { - t.Fatalf("err: %v", err) - } - case <-time.After(3 * time.Second): + case <-handle2.WaitCh(): + case <-time.After(2 * time.Second): t.Fatalf("timeout") } + + // Check they are both tracking the same PID. + pid1 := handle.(*rawExecHandle).proc.Pid + pid2 := handle2.(*rawExecHandle).proc.Pid + if pid1 != pid2 { + t.Fatalf("tracking incorrect Pid; %v != %v", pid1, pid2) + } } func TestRawExecDriver_Start_Wait(t *testing.T) { From 889090be15110fe88a74e30054941fdc73bd92b5 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 00:16:39 -0600 Subject: [PATCH 034/178] Update kubernetes.html.md --- website/source/intro/vs/kubernetes.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/kubernetes.html.md b/website/source/intro/vs/kubernetes.html.md index 83537e3dc..9da604168 100644 --- a/website/source/intro/vs/kubernetes.html.md +++ b/website/source/intro/vs/kubernetes.html.md @@ -31,7 +31,7 @@ configuration but is operationally complex to setup. Nomad is architecturally much simpler. Nomad is a single binary, both for clients and servers, and requires no external services for coordination or storage. -Nomad combines a lightweight resource managers and a sophisticated scheduler +Nomad combines a lightweight resource manager and a sophisticated scheduler into a single system. By default, Nomad is distributed, highly available, and operationally simple. From 35ed293a31030a26d95d0ba9f2dd2b6e56b951c5 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 10:24:33 -0600 Subject: [PATCH 035/178] change comma to semicolon --- website/source/intro/vs/mesos.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/mesos.html.md b/website/source/intro/vs/mesos.html.md index 4266a90f3..d8034411e 100644 --- a/website/source/intro/vs/mesos.html.md +++ b/website/source/intro/vs/mesos.html.md @@ -13,7 +13,7 @@ resources of a datacenter and exposes an API to integrate with Frameworks that have scheduling and job management logic. Mesos depends on ZooKeeper to provide both coordination and storage. -There are many different frameworks that integrate with Mesos, +There are many different frameworks that integrate with Mesos; popular general purpose ones include Aurora and Marathon. These frameworks allow users to submit jobs and implement scheduling logic. They depend on Mesos for resource management, and external From 203466252baf6a3ce2d611cc285023a17864b9d2 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 10:47:40 -0600 Subject: [PATCH 036/178] instead -> instead of --- website/source/intro/vs/terraform.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/vs/terraform.html.md b/website/source/intro/vs/terraform.html.md index 429ecc417..66ac4328b 100644 --- a/website/source/intro/vs/terraform.html.md +++ b/website/source/intro/vs/terraform.html.md @@ -25,7 +25,7 @@ on that infrastructure. Another major distinction is that Terraform is an offline tool that runs to completion, while Nomad is an online system with long lived servers. Nomad allows new jobs to be submitted, existing jobs updated or deleted, and can handle node failures. This -requires operating continuously instead in a single shot like Terraform. +requires operating continuously instead of in a single shot like Terraform. For small infrastructures with only a handful of servers or applications, the complexity of Nomad may not outweigh simply using Terraform to statically assign applications to From 0a202f5e6eca3a3fd85d695ed4a9c3668049aab6 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 11:31:22 -0600 Subject: [PATCH 037/178] virtual the machine -> virtual machine --- website/source/intro/getting-started/install.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/install.html.md b/website/source/intro/getting-started/install.html.md index 639befa0b..303336f46 100644 --- a/website/source/intro/getting-started/install.html.md +++ b/website/source/intro/getting-started/install.html.md @@ -16,7 +16,7 @@ Create a new directory, and download [this `Vagrantfile`](https://raw.githubuser ## Vagrant Setup Once you have created a new directory and downloaded the `Vagrantfile` -you must create the virtual the machine: +you must create the virtual machine: $ vagrant up From a8b74a57a88cc3aea8b474f3400fce37e1f9251c Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 11:45:34 -0600 Subject: [PATCH 038/178] Update install.html.md --- website/source/intro/getting-started/install.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/intro/getting-started/install.html.md b/website/source/intro/getting-started/install.html.md index 639befa0b..2a8045416 100644 --- a/website/source/intro/getting-started/install.html.md +++ b/website/source/intro/getting-started/install.html.md @@ -64,8 +64,8 @@ Available commands are: ``` If you get an error that Nomad could not be found, then your Vagrant box -may not have provisioned correctly. Check any error messages that may have -been occurred during `vagrant up`. You can always destroy the box and +may not have provisioned correctly. Check for any error messages that may have +been emitted during `vagrant up`. You can always destroy the box and re-create it. ## Next Steps From 07d8e2a5fe1666d7c5916fc4811f898630b63f07 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 12:29:56 -0600 Subject: [PATCH 039/178] is -> is a, it's -> its --- website/source/intro/getting-started/running.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/intro/getting-started/running.html.md b/website/source/intro/getting-started/running.html.md index c9a0b2fac..60bc9be34 100644 --- a/website/source/intro/getting-started/running.html.md +++ b/website/source/intro/getting-started/running.html.md @@ -87,8 +87,8 @@ ID DC Name Class Drain Status 72d3af97-144f-1e5f-94e5-df1516fe4add dc1 nomad false ready ``` -The output shows our Node ID, which is randomly generated UUID, -it's datacenter, node name, node class, drain mode and current status. +The output shows our Node ID, which is a randomly generated UUID, +its datacenter, node name, node class, drain mode and current status. We can see that our node is in the ready state, and task draining is currently off. From 83fd8c3cdc581beab49075fae597ee6534c9c803 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 13:27:09 -0600 Subject: [PATCH 040/178] an skeleton -> a skeleton --- website/source/intro/getting-started/jobs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/jobs.html.md b/website/source/intro/getting-started/jobs.html.md index c4427eda8..d23ed181f 100644 --- a/website/source/intro/getting-started/jobs.html.md +++ b/website/source/intro/getting-started/jobs.html.md @@ -20,7 +20,7 @@ however we recommend only using JSON when the configuration is generated by a ma ## Running a Job To get started, we will use the [`init` command](/docs/commands/init.html) which -generates an skeleton job file: +generates a skeleton job file: ``` $ nomad init From 175dcd858b8485694530597bc51b98dc663d0b4b Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 13:55:09 -0600 Subject: [PATCH 041/178] minor grammar fixes overtime -> over time add comma change -> to change --- website/source/intro/getting-started/jobs.html.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/source/intro/getting-started/jobs.html.md b/website/source/intro/getting-started/jobs.html.md index c4427eda8..6f5cc505d 100644 --- a/website/source/intro/getting-started/jobs.html.md +++ b/website/source/intro/getting-started/jobs.html.md @@ -83,9 +83,9 @@ it resulted in the creation of an allocation that is now running on the local no ## Modifying a Job -The definition of a job is not static, and is meant to be updated overtime. -You may update a job to change the docker container to update the application version, -or change the count of a task group to scale with load. +The definition of a job is not static, and is meant to be updated over time. +You may update a job to change the docker container, to update the application version, +or to change the count of a task group to scale with load. For now, edit the `example.nomad` file to uncomment the count and set it to 3: From 295caf77ea1400b898ece26531481164430d105b Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 13:57:30 -0600 Subject: [PATCH 042/178] this groups -> this group --- website/source/intro/getting-started/jobs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/jobs.html.md b/website/source/intro/getting-started/jobs.html.md index c4427eda8..85b79179f 100644 --- a/website/source/intro/getting-started/jobs.html.md +++ b/website/source/intro/getting-started/jobs.html.md @@ -90,7 +90,7 @@ or change the count of a task group to scale with load. For now, edit the `example.nomad` file to uncomment the count and set it to 3: ``` -# Control the number of instances of this groups. +# Control the number of instances of this group. # Defaults to 1 count = 3 ``` From 4c14019f62d29a8e2d56cdc28a36d5efe3110049 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 14:06:00 -0600 Subject: [PATCH 043/178] lets -> let's --- website/source/intro/getting-started/jobs.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/jobs.html.md b/website/source/intro/getting-started/jobs.html.md index c4427eda8..386f91f50 100644 --- a/website/source/intro/getting-started/jobs.html.md +++ b/website/source/intro/getting-started/jobs.html.md @@ -113,7 +113,7 @@ Because we set the count of the task group to three, Nomad created two additional allocations to get to the desired state. It is idempotent to run the same job specification again and no new allocations will be created. -Now, lets try to do an application update. In this case, we will simply change +Now, let's try to do an application update. In this case, we will simply change the version of redis we want to run. Edit the `example.nomad` file and change the Docker image from "redis:latest" to "redis:2.8": From 14ba8e3c0a843726e9e2327ff642fb58a549872e Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 14:16:29 -0600 Subject: [PATCH 044/178] minor grammar fixes the updated -> the update remove redundant 'each time', 'at a time' --- website/source/intro/getting-started/jobs.html.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/website/source/intro/getting-started/jobs.html.md b/website/source/intro/getting-started/jobs.html.md index c4427eda8..9ed031331 100644 --- a/website/source/intro/getting-started/jobs.html.md +++ b/website/source/intro/getting-started/jobs.html.md @@ -141,10 +141,9 @@ $ nomad run example.nomad ==> Evaluation "d34d37f4-19b1-f4c0-b2da-c949e6ade82d" finished with status "complete" ``` -We can see that Nomad handled the updated in three phases, each -time only updating a single task group at a time. The update strategy -can be configured, but rolling updates makes it easy to upgrade -an application at large scale. +We can see that Nomad handled the update in three phases, only updating a single task +group in each phase. The update strategy can be configured, but rolling updates makes +it easy to upgrade an application at large scale. ## Stopping a Job From 04b7443dd4a969a5eda21370cf6ee54df2f722e3 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 14:51:43 -0600 Subject: [PATCH 045/178] rewording suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit have it elect as a leader -> have it elected as a leader Not completely sure if this is correct (maybe “have it elect itself as a leader” is better?) --- website/source/intro/getting-started/cluster.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/cluster.html.md b/website/source/intro/getting-started/cluster.html.md index 743b782bc..0aa63f171 100644 --- a/website/source/intro/getting-started/cluster.html.md +++ b/website/source/intro/getting-started/cluster.html.md @@ -37,7 +37,7 @@ server { This is a fairly minimal server configuration file, but it is enough to start an agent in server only mode and have it -elect as a leader. The major change that should be made for +elected as a leader. The major change that should be made for production is to run more than one server, and to change the corresponding `bootstrap_expect` value. From b3cfd06cd328aaf232e13ffb527b82ad5ed0582d Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 17:58:30 -0600 Subject: [PATCH 046/178] replace comman with 'and' --- website/source/docs/internals/architecture.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/architecture.html.md b/website/source/docs/internals/architecture.html.md index 69bf43f77..d41504708 100644 --- a/website/source/docs/internals/architecture.html.md +++ b/website/source/docs/internals/architecture.html.md @@ -121,7 +121,7 @@ specified by the job. Resource utilization is maximized by bin packing, in which the scheduling tries to make use of all the resources of a machine without exhausting any dimension. Job constraints can be used to ensure an application is running in an appropriate environment. Constraints can be technical requirements based -on hardware features such as architecture, availability of GPUs, or software features +on hardware features such as architecture and availability of GPUs, or software features like operating system and kernel version, or they can be business constraints like ensuring PCI compliant workloads run on appropriate servers. From 9323a8c7dcaac6fda6c45789eb1db4c3c70cf707 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sat, 10 Oct 2015 20:21:05 -0600 Subject: [PATCH 047/178] Add floor characters to math expression --- website/source/docs/internals/consensus.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/consensus.html.md b/website/source/docs/internals/consensus.html.md index 16b86565c..cc15c698c 100644 --- a/website/source/docs/internals/consensus.html.md +++ b/website/source/docs/internals/consensus.html.md @@ -45,7 +45,7 @@ same sequence of logs must result in the same state, meaning behavior must be de For Nomad's purposes, all server nodes are in the peer set of the local region. * **Quorum** - A quorum is a majority of members from a peer set: for a set of size `n`, -quorum requires at least `(n/2)+1` members. +quorum requires at least `⌊(n/2)+1⌋` members. For example, if there are 5 members in the peer set, we would need 3 nodes to form a quorum. If a quorum of nodes is unavailable for any reason, the cluster becomes *unavailable* and no new logs can be committed. From 592863d5122d581c6b39b0e36b40bcbc1af42498 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sun, 11 Oct 2015 12:07:08 -0600 Subject: [PATCH 048/178] schedule -> scheduler --- website/source/docs/internals/scheduling.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/scheduling.html.md b/website/source/docs/internals/scheduling.html.md index e871e5181..354e2a8f0 100644 --- a/website/source/docs/internals/scheduling.html.md +++ b/website/source/docs/internals/scheduling.html.md @@ -53,7 +53,7 @@ and ensure at least once delivery. Nomad servers run scheduling workers, defaulting to one per CPU core, which are used to process evaluations. The workers dequeue evaluations from the broker, and then invoke -the appropriate schedule as specified by the job. Nomad ships with a `service` scheduler +the appropriate scheduler as specified by the job. Nomad ships with a `service` scheduler that optimizes for long-lived services, a `batch` scheduler that is used for fast placement of batch jobs, and a `core` scheduler which is used for internal maintenance. Nomad can be extended to support custom schedulers as well. From 8616a3586268d5dd73eb7ed82edcd1bc82a6517b Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sun, 11 Oct 2015 12:25:30 -0600 Subject: [PATCH 049/178] Update scheduling.html.md --- website/source/docs/internals/scheduling.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/internals/scheduling.html.md b/website/source/docs/internals/scheduling.html.md index 354e2a8f0..d859e6132 100644 --- a/website/source/docs/internals/scheduling.html.md +++ b/website/source/docs/internals/scheduling.html.md @@ -75,8 +75,8 @@ and density of applications, but is also augmented by affinity and anti-affinity Once the scheduler has ranked enough nodes, the highest ranking node is selected and added to the allocation plan. -When planning is complete, the scheduler submits the plan to the leader and -gets added to the plan queue. The plan queue manages pending plans, provides priority +When planning is complete, the scheduler submits the plan to the leader which adds +the plan to the plan queue. The plan queue manages pending plans, provides priority ordering, and allows Nomad to handle concurrency races. Multiple schedulers are running in parallel without locking or reservations, making Nomad optimistically concurrent. As a result, schedulers might overlap work on the same node and cause resource From 54dea9fe87a7d8f4b7474395d09a8a11b3892d85 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:12:39 -0400 Subject: [PATCH 050/178] scheduler: adding version constraint logic --- scheduler/feasible.go | 39 ++++++++++++++++++++++++++++++++++++++ scheduler/feasible_test.go | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/scheduler/feasible.go b/scheduler/feasible.go index d11a2034e..b015bd24b 100644 --- a/scheduler/feasible.go +++ b/scheduler/feasible.go @@ -5,6 +5,7 @@ import ( "reflect" "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/nomad/nomad/structs" ) @@ -253,7 +254,45 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool { case "contains": // TODO: Implement return false + case "version": + return checkVersionConstraint(lVal, rVal) default: return false } } + +// checkVersionConstraint is used to compare a version on the +// left hand side with a set of constraints on the right hand side +func checkVersionConstraint(lVal, rVal interface{}) bool { + // Parse the version + var versionStr string + switch v := lVal.(type) { + case string: + versionStr = v + case int: + versionStr = fmt.Sprintf("%d", v) + default: + return false + } + + // Parse the verison + vers, err := version.NewVersion(versionStr) + if err != nil { + return false + } + + // Constraint must be a string + constraintStr, ok := rVal.(string) + if !ok { + return false + } + + // Parse the constraints + constraints, err := version.NewConstraint(constraintStr) + if err != nil { + return false + } + + // Check the constraints against the version + return constraints.Check(vers) +} diff --git a/scheduler/feasible_test.go b/scheduler/feasible_test.go index e651dec6f..071df55dd 100644 --- a/scheduler/feasible_test.go +++ b/scheduler/feasible_test.go @@ -244,6 +244,11 @@ func TestCheckConstraint(t *testing.T) { lVal: "foo", rVal: "bar", result: true, }, + { + op: "version", + lVal: "1.2.3", rVal: "~> 1.0", + result: true, + }, } for _, tc := range cases { @@ -253,6 +258,40 @@ func TestCheckConstraint(t *testing.T) { } } +func TestCheckVersionConstraint(t *testing.T) { + type tcase struct { + lVal, rVal interface{} + result bool + } + cases := []tcase{ + { + lVal: "1.2.3", rVal: "~> 1.0", + result: true, + }, + { + lVal: "1.2.3", rVal: ">= 1.0, < 1.4", + result: true, + }, + { + lVal: "2.0.1", rVal: "~> 1.0", + result: false, + }, + { + lVal: "1.4", rVal: ">= 1.0, < 1.4", + result: false, + }, + { + lVal: 1, rVal: "~> 1.0", + result: true, + }, + } + for _, tc := range cases { + if res := checkVersionConstraint(tc.lVal, tc.rVal); res != tc.result { + t.Fatalf("TC: %#v, Result: %v", tc, res) + } + } +} + func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { for { next := iter.Next() From a5342d61b7c554bbc00fc53ab0ac216e75e92940 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:20:58 -0400 Subject: [PATCH 051/178] jobspec: adding sugar for version constraint --- jobspec/parse.go | 7 +++++++ jobspec/parse_test.go | 20 ++++++++++++++++++++ jobspec/test-fixtures/version-constraint.hcl | 6 ++++++ 3 files changed, 33 insertions(+) create mode 100644 jobspec/test-fixtures/version-constraint.hcl diff --git a/jobspec/parse.go b/jobspec/parse.go index 1231b5ec0..20086d86e 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -242,6 +242,13 @@ func parseConstraints(result *[]*structs.Constraint, obj *hclobj.Object) error { m["hard"] = true } + // If "version" is provided, set the operand + // to "version" and the value to the "RTarget" + if constraint, ok := m["version"]; ok { + m["Operand"] = "version" + m["RTarget"] = constraint + } + // Build the constraint var c structs.Constraint if err := mapstructure.WeakDecode(m, &c); err != nil { diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index dea59ec26..4336b9aac 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -152,6 +152,26 @@ func TestParse(t *testing.T) { false, }, + { + "version-constraint.hcl", + &structs.Job{ + ID: "foo", + Name: "foo", + Priority: 50, + Region: "global", + Type: "service", + Constraints: []*structs.Constraint{ + &structs.Constraint{ + Hard: true, + LTarget: "$attr.kernel.version", + RTarget: "~> 3.2", + Operand: "version", + }, + }, + }, + false, + }, + { "specify-job.hcl", &structs.Job{ diff --git a/jobspec/test-fixtures/version-constraint.hcl b/jobspec/test-fixtures/version-constraint.hcl new file mode 100644 index 000000000..3ba755272 --- /dev/null +++ b/jobspec/test-fixtures/version-constraint.hcl @@ -0,0 +1,6 @@ +job "foo" { + constraint { + attribute = "$attr.kernel.version" + version = "~> 3.2" + } +} From 2aad0838fa20c5cf723395425c6b1e67ae156344 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:24:16 -0400 Subject: [PATCH 052/178] website: update the jobspec --- website/source/docs/jobspec/index.html.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/source/docs/jobspec/index.html.md b/website/source/docs/jobspec/index.html.md index 9ed167bef..d5af97036 100644 --- a/website/source/docs/jobspec/index.html.md +++ b/website/source/docs/jobspec/index.html.md @@ -223,6 +223,13 @@ The `constraint` object supports the following keys: * `value` - Specifies the value to compare the attribute against. This can be a literal value or another attribute. +* `version` - Specifies a version constraint against the attribute. + This sets the operator to "version" and the `value` to what is + specified. This supports a comma seperated list of constraints, + including the pessimistic operator. See the + [go-version](https://github.com/hashicorp/go-version) repository + for examples. + Below is a table documenting the variables that can be interpreted: From 0a2e8742451cef5343b1edd2cb367c93fcda50bd Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:35:13 -0400 Subject: [PATCH 053/178] scheduler: adding regexp constraints --- scheduler/feasible.go | 31 +++++++++++++++++++++++++++--- scheduler/feasible_test.go | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/scheduler/feasible.go b/scheduler/feasible.go index b015bd24b..5ecfbedab 100644 --- a/scheduler/feasible.go +++ b/scheduler/feasible.go @@ -3,6 +3,7 @@ package scheduler import ( "fmt" "reflect" + "regexp" "strings" "github.com/hashicorp/go-version" @@ -251,11 +252,10 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool { case "<", "<=", ">", ">=": // TODO: Implement return false - case "contains": - // TODO: Implement - return false case "version": return checkVersionConstraint(lVal, rVal) + case "regexp": + return checkRegexpConstraint(lVal, rVal) default: return false } @@ -296,3 +296,28 @@ func checkVersionConstraint(lVal, rVal interface{}) bool { // Check the constraints against the version return constraints.Check(vers) } + +// checkRegexpConstraint is used to compare a value on the +// left hand side with a regexp on the right hand side +func checkRegexpConstraint(lVal, rVal interface{}) bool { + // Ensure left-hand is string + lStr, ok := lVal.(string) + if !ok { + return false + } + + // Regexp must be a string + regexpStr, ok := rVal.(string) + if !ok { + return false + } + + // Parse the regexp + re, err := regexp.Compile(regexpStr) + if err != nil { + return false + } + + // Look for a match + return re.MatchString(lStr) +} diff --git a/scheduler/feasible_test.go b/scheduler/feasible_test.go index 071df55dd..8ee3c6dc5 100644 --- a/scheduler/feasible_test.go +++ b/scheduler/feasible_test.go @@ -249,6 +249,11 @@ func TestCheckConstraint(t *testing.T) { lVal: "1.2.3", rVal: "~> 1.0", result: true, }, + { + op: "regexp", + lVal: "foobarbaz", rVal: "[\\w]+", + result: true, + }, } for _, tc := range cases { @@ -292,6 +297,40 @@ func TestCheckVersionConstraint(t *testing.T) { } } +func TestCheckRegexpConstraint(t *testing.T) { + type tcase struct { + lVal, rVal interface{} + result bool + } + cases := []tcase{ + { + lVal: "foobar", rVal: "bar", + result: true, + }, + { + lVal: "foobar", rVal: "^foo", + result: true, + }, + { + lVal: "foobar", rVal: "^bar", + result: false, + }, + { + lVal: "zipzap", rVal: "foo", + result: false, + }, + { + lVal: 1, rVal: "foo", + result: false, + }, + } + for _, tc := range cases { + if res := checkRegexpConstraint(tc.lVal, tc.rVal); res != tc.result { + t.Fatalf("TC: %#v, Result: %v", tc, res) + } + } +} + func collectFeasible(iter FeasibleIterator) (out []*structs.Node) { for { next := iter.Next() From e2093e41891aedf7f0f6047a33e337f11207dae7 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:37:50 -0400 Subject: [PATCH 054/178] jobspec: adding sugar for regexp constraint --- jobspec/parse.go | 7 +++++++ jobspec/parse_test.go | 20 ++++++++++++++++++++ jobspec/test-fixtures/regexp-constraint.hcl | 6 ++++++ 3 files changed, 33 insertions(+) create mode 100644 jobspec/test-fixtures/regexp-constraint.hcl diff --git a/jobspec/parse.go b/jobspec/parse.go index 20086d86e..b7ca31c54 100644 --- a/jobspec/parse.go +++ b/jobspec/parse.go @@ -249,6 +249,13 @@ func parseConstraints(result *[]*structs.Constraint, obj *hclobj.Object) error { m["RTarget"] = constraint } + // If "regexp" is provided, set the operand + // to "regexp" and the value to the "RTarget" + if constraint, ok := m["regexp"]; ok { + m["Operand"] = "regexp" + m["RTarget"] = constraint + } + // Build the constraint var c structs.Constraint if err := mapstructure.WeakDecode(m, &c); err != nil { diff --git a/jobspec/parse_test.go b/jobspec/parse_test.go index 4336b9aac..60c5f1f09 100644 --- a/jobspec/parse_test.go +++ b/jobspec/parse_test.go @@ -172,6 +172,26 @@ func TestParse(t *testing.T) { false, }, + { + "regexp-constraint.hcl", + &structs.Job{ + ID: "foo", + Name: "foo", + Priority: 50, + Region: "global", + Type: "service", + Constraints: []*structs.Constraint{ + &structs.Constraint{ + Hard: true, + LTarget: "$attr.kernel.version", + RTarget: "[0-9.]+", + Operand: "regexp", + }, + }, + }, + false, + }, + { "specify-job.hcl", &structs.Job{ diff --git a/jobspec/test-fixtures/regexp-constraint.hcl b/jobspec/test-fixtures/regexp-constraint.hcl new file mode 100644 index 000000000..dfdb4ce20 --- /dev/null +++ b/jobspec/test-fixtures/regexp-constraint.hcl @@ -0,0 +1,6 @@ +job "foo" { + constraint { + attribute = "$attr.kernel.version" + regexp = "[0-9.]+" + } +} From 982edafa0190067bdd7462692487c27351555030 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:38:38 -0400 Subject: [PATCH 055/178] website: document the regexp constraint --- website/source/docs/jobspec/index.html.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/source/docs/jobspec/index.html.md b/website/source/docs/jobspec/index.html.md index d5af97036..b8dd7119a 100644 --- a/website/source/docs/jobspec/index.html.md +++ b/website/source/docs/jobspec/index.html.md @@ -230,6 +230,10 @@ The `constraint` object supports the following keys: [go-version](https://github.com/hashicorp/go-version) repository for examples. +* `regexp` - Specifies a regular expression constraint against + the attribute. This sets the operator to "regexp" and the `value` + to the regular expression. + Below is a table documenting the variables that can be interpreted:
From a3dda996fabc2e15322da3c622be9c5c1b06dda4 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:50:16 -0400 Subject: [PATCH 056/178] nomad: additional constraint validation --- nomad/structs/structs.go | 40 +++++++++++++++++++++++++++++++++++ nomad/structs/structs_test.go | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 098c57b32..09cef210a 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -4,11 +4,13 @@ import ( "bytes" "errors" "fmt" + "regexp" "strings" "time" "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-version" ) var ( @@ -809,6 +811,12 @@ func (j *Job) Validate() error { if len(j.TaskGroups) == 0 { mErr.Errors = append(mErr.Errors, errors.New("Missing job task groups")) } + for idx, constr := range j.Constraints { + if err := constr.Validate(); err != nil { + outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err) + mErr.Errors = append(mErr.Errors, outer) + } + } // Check for duplicate task groups taskGroups := make(map[string]int) @@ -918,6 +926,12 @@ func (tg *TaskGroup) Validate() error { if len(tg.Tasks) == 0 { mErr.Errors = append(mErr.Errors, errors.New("Missing tasks for task group")) } + for idx, constr := range tg.Constraints { + if err := constr.Validate(); err != nil { + outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err) + mErr.Errors = append(mErr.Errors, outer) + } + } // Check for duplicate tasks tasks := make(map[string]int) @@ -997,6 +1011,12 @@ func (t *Task) Validate() error { if t.Resources == nil { mErr.Errors = append(mErr.Errors, errors.New("Missing task resources")) } + for idx, constr := range t.Constraints { + if err := constr.Validate(); err != nil { + outer := fmt.Errorf("Constraint %d validation failed: %s", idx+1, err) + mErr.Errors = append(mErr.Errors, outer) + } + } return mErr.ErrorOrNil() } @@ -1015,6 +1035,26 @@ func (c *Constraint) String() string { return fmt.Sprintf("%s %s %s", c.LTarget, c.Operand, c.RTarget) } +func (c *Constraint) Validate() error { + var mErr multierror.Error + if c.Operand == "" { + mErr.Errors = append(mErr.Errors, errors.New("Missing constraint operand")) + } + + // Perform additional validation based on operand + switch c.Operand { + case "regexp": + if _, err := regexp.Compile(c.RTarget); err != nil { + mErr.Errors = append(mErr.Errors, fmt.Errorf("Regular expression failed to compile: %v", err)) + } + case "version": + if _, err := version.NewConstraint(c.RTarget); err != nil { + mErr.Errors = append(mErr.Errors, fmt.Errorf("Version constraint is invalid: %v", err)) + } + } + return mErr.ErrorOrNil() +} + const ( AllocDesiredStatusRun = "run" // Allocation should run AllocDesiredStatusStop = "stop" // Allocation should stop diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index ca72f540b..6df231a67 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -125,6 +125,43 @@ func TestTask_Validate(t *testing.T) { } } +func TestConstraint_Validate(t *testing.T) { + c := &Constraint{} + err := c.Validate() + mErr := err.(*multierror.Error) + if !strings.Contains(mErr.Errors[0].Error(), "Missing constraint operand") { + t.Fatalf("err: %s", err) + } + + c = &Constraint{ + LTarget: "$attr.kernel.name", + RTarget: "linux", + Operand: "=", + } + err = c.Validate() + if err != nil { + t.Fatalf("err: %v", err) + } + + // Perform additional regexp validation + c.Operand = "regexp" + c.RTarget = "(foo" + err = c.Validate() + mErr = err.(*multierror.Error) + if !strings.Contains(mErr.Errors[0].Error(), "missing closing") { + t.Fatalf("err: %s", err) + } + + // Perform version validation + c.Operand = "version" + c.RTarget = "~> foo" + err = c.Validate() + mErr = err.(*multierror.Error) + if !strings.Contains(mErr.Errors[0].Error(), "Malformed constraint") { + t.Fatalf("err: %s", err) + } +} + func TestResource_NetIndex(t *testing.T) { r := &Resources{ Networks: []*NetworkResource{ From 441971e0150e804ccce7e26d777aad85f3b25ece Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Sun, 11 Oct 2015 12:53:34 -0700 Subject: [PATCH 057/178] Add details to docker driver docs - The driver sets a bool to `true` - Clarify behavior when `network_mode` is `container` --- website/source/docs/drivers/docker.html.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index c6ea40a77..05ef6a64f 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -26,7 +26,7 @@ The `docker` driver supports the following configuration in the job specificatio * `network_mode` - (Optional) The network mode to be used for the container. Valid options are `default`, `bridge`, `host` or `none`. If nothing is specified, the container will start in `bridge` mode. The `container` - network mode is not supported right now. + network mode is not supported right now, this case also defaults to `bridge`. ### Port Mapping @@ -125,8 +125,10 @@ production Nomad will always read `docker.endpoint`. The `docker` driver will set the following client attributes: -* `driver.docker` - This will be set to "1", indicating the +* `driver.docker` - This will be set to "true", indicating the driver is available. +* `driver.docker.version` - This will be set to version of the + docker server ## Resource Isolation From 4f694933d3fd8f2f26ac394c6954a8decac7311f Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Sun, 11 Oct 2015 12:55:23 -0700 Subject: [PATCH 058/178] Fix formatting for qemu driver version --- website/source/docs/drivers/qemu.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/drivers/qemu.html.md b/website/source/docs/drivers/qemu.html.md index 9b1f99a17..dbc941234 100644 --- a/website/source/docs/drivers/qemu.html.md +++ b/website/source/docs/drivers/qemu.html.md @@ -48,7 +48,7 @@ The `Qemu` driver will set the following client attributes: * `driver.qemu` - Set to `1` if Qemu is found on the host node. Nomad determines this by executing `qemu-system-x86_64 -version` on the host and parsing the output -* `driver.qemu.version` - Version of `qemu-system-x86_64, ex: `2.4.0` +* `driver.qemu.version` - Version of `qemu-system-x86_64`, ex: `2.4.0` ## Resource Isolation @@ -59,4 +59,4 @@ better performance. Virtualization provides the highest level of isolation for workloads that require additional security, and resources use is constrained by the Qemu hypervisor rather than the host kernel. VM network traffic still flows through -the host's interface(s). \ No newline at end of file +the host's interface(s). From 641b9f3ee4133352fef6cfab43e4f85e3de4e8eb Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:57:06 -0400 Subject: [PATCH 059/178] scheduler: support lexical ordering constraints --- scheduler/feasible.go | 29 ++++++++++++++++++++++-- scheduler/feasible_test.go | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/scheduler/feasible.go b/scheduler/feasible.go index 5ecfbedab..7482a953d 100644 --- a/scheduler/feasible.go +++ b/scheduler/feasible.go @@ -250,8 +250,7 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool { case "!=", "not": return !reflect.DeepEqual(lVal, rVal) case "<", "<=", ">", ">=": - // TODO: Implement - return false + return checkLexicalOrder(operand, lVal, rVal) case "version": return checkVersionConstraint(lVal, rVal) case "regexp": @@ -261,6 +260,32 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool { } } +// checkLexicalOrder is used to check for lexical ordering +func checkLexicalOrder(op string, lVal, rVal interface{}) bool { + // Ensure the values are strings + lStr, ok := lVal.(string) + if !ok { + return false + } + rStr, ok := rVal.(string) + if !ok { + return false + } + + switch op { + case "<": + return lStr < rStr + case "<=": + return lStr <= rStr + case ">": + return lStr > rStr + case ">=": + return lStr >= rStr + default: + return false + } +} + // checkVersionConstraint is used to compare a version on the // left hand side with a set of constraints on the right hand side func checkVersionConstraint(lVal, rVal interface{}) bool { diff --git a/scheduler/feasible_test.go b/scheduler/feasible_test.go index 8ee3c6dc5..a69b4c8ce 100644 --- a/scheduler/feasible_test.go +++ b/scheduler/feasible_test.go @@ -254,6 +254,11 @@ func TestCheckConstraint(t *testing.T) { lVal: "foobarbaz", rVal: "[\\w]+", result: true, }, + { + op: "<", + lVal: "foo", rVal: "bar", + result: false, + }, } for _, tc := range cases { @@ -263,6 +268,46 @@ func TestCheckConstraint(t *testing.T) { } } +func TestCheckLexicalOrder(t *testing.T) { + type tcase struct { + op string + lVal, rVal interface{} + result bool + } + cases := []tcase{ + { + op: "<", + lVal: "bar", rVal: "foo", + result: true, + }, + { + op: "<=", + lVal: "foo", rVal: "foo", + result: true, + }, + { + op: ">", + lVal: "bar", rVal: "foo", + result: false, + }, + { + op: ">=", + lVal: "bar", rVal: "bar", + result: true, + }, + { + op: ">", + lVal: 1, rVal: "foo", + result: false, + }, + } + for _, tc := range cases { + if res := checkLexicalOrder(tc.op, tc.lVal, tc.rVal); res != tc.result { + t.Fatalf("TC: %#v, Result: %v", tc, res) + } + } +} + func TestCheckVersionConstraint(t *testing.T) { type tcase struct { lVal, rVal interface{} From 6fdbdd83ca2d0db24ccfca83a4f4860c073889d0 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 15:58:25 -0400 Subject: [PATCH 060/178] website: document lexical ordering operators --- website/source/docs/jobspec/index.html.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/source/docs/jobspec/index.html.md b/website/source/docs/jobspec/index.html.md index b8dd7119a..8f772417d 100644 --- a/website/source/docs/jobspec/index.html.md +++ b/website/source/docs/jobspec/index.html.md @@ -218,7 +218,8 @@ The `constraint` object supports the following keys: to true. Soft constraints are not currently supported. * `operator` - Specifies the comparison operator. Defaults to equality, - and can be `=`, `==`, `is`, `!=`, `not`. + and can be `=`, `==`, `is`, `!=`, `not`, `>`, `>=`, `<`, `<=`. The + ordering is compared lexically. * `value` - Specifies the value to compare the attribute against. This can be a literal value or another attribute. From 415f05c42c62b0adb3cd08cc3aab665c50532330 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sun, 11 Oct 2015 14:05:55 -0600 Subject: [PATCH 061/178] Update index.html.md --- website/source/docs/jobspec/index.html.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/source/docs/jobspec/index.html.md b/website/source/docs/jobspec/index.html.md index 9ed167bef..11c4af502 100644 --- a/website/source/docs/jobspec/index.html.md +++ b/website/source/docs/jobspec/index.html.md @@ -9,8 +9,7 @@ description: |- # Job Specification Jobs can be specified either in [HCL](https://github.com/hashicorp/hcl) or JSON. -HCL is meant to strike a balance between human readable and editable, as well -as being machine-friendly. +HCL is meant to strike a balance between human readable and editable, and machine-friendly. For machine-friendliness, Nomad can also read JSON configurations. In general, we recommend using the HCL syntax. From 985ef83ae7872a07cb03df555646469411db7972 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:35:09 -0400 Subject: [PATCH 062/178] nomad: make tests more robust --- nomad/worker_test.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nomad/worker_test.go b/nomad/worker_test.go index 81837d99e..a56dfefe1 100644 --- a/nomad/worker_test.go +++ b/nomad/worker_test.go @@ -52,7 +52,12 @@ func TestWorker_dequeueEvaluation(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} @@ -82,7 +87,12 @@ func TestWorker_dequeueEvaluation_paused(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} @@ -153,7 +163,12 @@ func TestWorker_sendAck(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} From 5a6687bdb4bd519d9bc3999b386a68e0be94c87c Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:47:00 -0400 Subject: [PATCH 063/178] nomad: make test more robust --- nomad/leader_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/nomad/leader_test.go b/nomad/leader_test.go index 475da47b0..b753b41f4 100644 --- a/nomad/leader_test.go +++ b/nomad/leader_test.go @@ -228,6 +228,7 @@ func TestLeader_EvalBroker_Reset(t *testing.T) { defer s3.Shutdown() servers := []*Server{s1, s2, s3} testJoin(t, s1, s2, s3) + testutil.WaitForLeader(t, s1.RPC) for _, s := range servers { testutil.WaitForResult(func() (bool, error) { From 1d2e8135d2c333cc08ce7df67e82da56d19c2c94 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 17:39:34 -0400 Subject: [PATCH 064/178] nomad: raftApplyFuture exposes underlying Future --- nomad/rpc.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/nomad/rpc.go b/nomad/rpc.go index dee45e47f..3d81277d5 100644 --- a/nomad/rpc.go +++ b/nomad/rpc.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/net-rpc-msgpackrpc" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" + "github.com/hashicorp/raft" "github.com/hashicorp/yamux" ) @@ -225,12 +226,11 @@ func (s *Server) forwardRegion(region, method string, args interface{}, reply in return s.connPool.RPC(region, server.Addr, server.Version, method, args, reply) } -// raftApply is used to encode a message, run it through raft, and return -// the FSM response along with any errors -func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, uint64, error) { +// raftApplyFuture is used to encode a message, run it through raft, and return the Raft future. +func (s *Server) raftApplyFuture(t structs.MessageType, msg interface{}) (raft.ApplyFuture, error) { buf, err := structs.Encode(t, msg) if err != nil { - return nil, 0, fmt.Errorf("Failed to encode request: %v", err) + return nil, fmt.Errorf("Failed to encode request: %v", err) } // Warn if the command is very large @@ -240,9 +240,18 @@ func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, future := s.raft.Apply(buf, enqueueLimit) if err := future.Error(); err != nil { + return nil, err + } + return future, nil +} + +// raftApply is used to encode a message, run it through raft, and return +// the FSM response along with any errors +func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, uint64, error) { + future, err := s.raftApplyFuture(t, msg) + if err != nil { return nil, 0, err } - return future.Response(), future.Index(), nil } From 28428e9f8613920b3a86fe33f000a7dcefb2fe54 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 17:42:21 -0400 Subject: [PATCH 065/178] nomad: raftApplyFuture does not block for error --- nomad/rpc.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nomad/rpc.go b/nomad/rpc.go index 3d81277d5..074dec0d6 100644 --- a/nomad/rpc.go +++ b/nomad/rpc.go @@ -239,9 +239,6 @@ func (s *Server) raftApplyFuture(t structs.MessageType, msg interface{}) (raft.A } future := s.raft.Apply(buf, enqueueLimit) - if err := future.Error(); err != nil { - return nil, err - } return future, nil } @@ -252,6 +249,9 @@ func (s *Server) raftApply(t structs.MessageType, msg interface{}) (interface{}, if err != nil { return nil, 0, err } + if err := future.Error(); err != nil { + return nil, 0, err + } return future.Response(), future.Index(), nil } From bb3e9406860c3f61b39e2bebbe5e02947a0a9c18 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 17:48:18 -0400 Subject: [PATCH 066/178] nomad: plan apply uses raw Raft future --- nomad/plan_apply.go | 47 +++++++++++++++++++++++++++++++++++----- nomad/plan_apply_test.go | 12 ++++++++-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 4b760cdaf..fc142cdfa 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -7,11 +7,35 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" + "github.com/hashicorp/raft" ) // planApply is a long lived goroutine that reads plan allocations from // the plan queue, determines if they can be applied safely and applies // them via Raft. +// +// Naively, we could simply dequeue a plan, verify, apply and then respond. +// However, the plan application is bounded by the Raft apply time and +// subject to some latency. This creates a stall condition, where we are +// not evaluating, but simply waiting for a transaction to complete. +// +// To avoid this, we overlap verification with apply. This means once +// we've verified plan N we attempt to apply it. However, while waiting +// for apply, we begin to verify plan N+1 under the assumption that plan +// N has succeeded. +// +// In this sense, we track two parallel versions of the world. One is +// the pessimistic one driven by the Raft log which is replicated. The +// other is optimistic and assumes our transactions will succeed. In the +// happy path, this lets us do productive work during the latency of +// apply. +// +// In the unhappy path (Raft transaction fails), effectively we only +// wasted work during a time we would have been waiting anyways. However, +// in anticipation of this case we cannot respond to the plan until +// the Raft log is updated. This means our schedulers will stall, +// but there are many of those and only a single plan verifier. +// func (s *Server) planApply() { for { // Pull the next pending plan, exit if we are no longer leader @@ -53,7 +77,13 @@ func (s *Server) planApply() { // Apply the plan if there is anything to do if !result.IsNoOp() { - allocIndex, err := s.applyPlan(result) + future, err := s.applyPlan(result) + if err != nil { + s.logger.Printf("[ERR] nomad: failed to submit plan: %v", err) + pending.respond(nil, err) + continue + } + allocIndex, err := s.planWaitFuture(future) if err != nil { s.logger.Printf("[ERR] nomad: failed to apply plan: %v", err) pending.respond(nil, err) @@ -68,8 +98,7 @@ func (s *Server) planApply() { } // applyPlan is used to apply the plan result and to return the alloc index -func (s *Server) applyPlan(result *structs.PlanResult) (uint64, error) { - defer metrics.MeasureSince([]string{"nomad", "plan", "apply"}, time.Now()) +func (s *Server) applyPlan(result *structs.PlanResult) (raft.ApplyFuture, error) { req := structs.AllocUpdateRequest{} for _, updateList := range result.NodeUpdate { req.Alloc = append(req.Alloc, updateList...) @@ -79,8 +108,16 @@ func (s *Server) applyPlan(result *structs.PlanResult) (uint64, error) { } req.Alloc = append(req.Alloc, result.FailedAllocs...) - _, index, err := s.raftApply(structs.AllocUpdateRequestType, &req) - return index, err + return s.raftApplyFuture(structs.AllocUpdateRequestType, &req) +} + +// planWaitFuture is used to wait for the Raft future to complete +func (s *Server) planWaitFuture(future raft.ApplyFuture) (uint64, error) { + defer metrics.MeasureSince([]string{"nomad", "plan", "apply"}, time.Now()) + if err := future.Error(); err != nil { + return 0, err + } + return future.Index(), nil } // evaluatePlan is used to determine what portions of a plan diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index 72f37c416..c74b6353b 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -46,7 +46,11 @@ func TestPlanApply_applyPlan(t *testing.T) { } // Apply the plan - index, err := s1.applyPlan(plan) + future, err := s1.applyPlan(plan) + if err != nil { + t.Fatalf("err: %v", err) + } + index, err := s1.planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) } @@ -87,7 +91,11 @@ func TestPlanApply_applyPlan(t *testing.T) { } // Apply the plan - index, err = s1.applyPlan(plan) + future, err = s1.applyPlan(plan) + if err != nil { + t.Fatalf("err: %v", err) + } + index, err = s1.planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) } From 712393f4bf936514360f6a1a0b6669223bc50152 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 17:57:36 -0400 Subject: [PATCH 067/178] nomad: wait for plan to apply async --- nomad/plan_apply.go | 50 +++++++++++++++++++++++----------------- nomad/plan_apply_test.go | 13 +++++++++-- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index fc142cdfa..b58f9e1ce 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -75,25 +75,24 @@ func (s *Server) planApply() { continue } - // Apply the plan if there is anything to do - if !result.IsNoOp() { - future, err := s.applyPlan(result) - if err != nil { - s.logger.Printf("[ERR] nomad: failed to submit plan: %v", err) - pending.respond(nil, err) - continue - } - allocIndex, err := s.planWaitFuture(future) - if err != nil { - s.logger.Printf("[ERR] nomad: failed to apply plan: %v", err) - pending.respond(nil, err) - continue - } - result.AllocIndex = allocIndex + // Fast-path the response if there is nothing to do + if result.IsNoOp() { + pending.respond(result, nil) + continue } - // Respond to the plan - pending.respond(result, nil) + // Dispatch the Raft transaction for the plan + future, err := s.applyPlan(result) + if err != nil { + s.logger.Printf("[ERR] nomad: failed to submit plan: %v", err) + pending.respond(nil, err) + continue + } + + // Respond to the plan in async + waitCh := make(chan struct{}) + go s.asyncPlanWait(waitCh, future, result, pending) + <-waitCh } } @@ -111,13 +110,22 @@ func (s *Server) applyPlan(result *structs.PlanResult) (raft.ApplyFuture, error) return s.raftApplyFuture(structs.AllocUpdateRequestType, &req) } -// planWaitFuture is used to wait for the Raft future to complete -func (s *Server) planWaitFuture(future raft.ApplyFuture) (uint64, error) { +// asyncPlanWait is used to apply and respond to a plan async +func (s *Server) asyncPlanWait(waitCh chan struct{}, future raft.ApplyFuture, + result *structs.PlanResult, pending *pendingPlan) { defer metrics.MeasureSince([]string{"nomad", "plan", "apply"}, time.Now()) + defer close(waitCh) + + // Wait for the plan to apply if err := future.Error(); err != nil { - return 0, err + s.logger.Printf("[ERR] nomad: failed to apply plan: %v", err) + pending.respond(nil, err) + return } - return future.Index(), nil + + // Respond to the plan + result.AllocIndex = future.Index() + pending.respond(result, nil) } // evaluatePlan is used to determine what portions of a plan diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index c74b6353b..654e22f6d 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -7,8 +7,17 @@ import ( "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" + "github.com/hashicorp/raft" ) +// planWaitFuture is used to wait for the Raft future to complete +func planWaitFuture(future raft.ApplyFuture) (uint64, error) { + if err := future.Error(); err != nil { + return 0, err + } + return future.Index(), nil +} + func testRegisterNode(t *testing.T, s *Server, n *structs.Node) { // Create the register request req := &structs.NodeRegisterRequest{ @@ -50,7 +59,7 @@ func TestPlanApply_applyPlan(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - index, err := s1.planWaitFuture(future) + index, err := planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) } @@ -95,7 +104,7 @@ func TestPlanApply_applyPlan(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - index, err = s1.planWaitFuture(future) + index, err = planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) } From 5348bd056370b2d88357a5118498df5d62b30bae Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:19:01 -0400 Subject: [PATCH 068/178] nomad: optimistically apply plan to state snapshot --- nomad/plan_apply.go | 19 ++++++++++++++++--- nomad/plan_apply_test.go | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index b58f9e1ce..ae9f85b50 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -82,7 +82,7 @@ func (s *Server) planApply() { } // Dispatch the Raft transaction for the plan - future, err := s.applyPlan(result) + future, err := s.applyPlan(result, snap) if err != nil { s.logger.Printf("[ERR] nomad: failed to submit plan: %v", err) pending.respond(nil, err) @@ -97,7 +97,7 @@ func (s *Server) planApply() { } // applyPlan is used to apply the plan result and to return the alloc index -func (s *Server) applyPlan(result *structs.PlanResult) (raft.ApplyFuture, error) { +func (s *Server) applyPlan(result *structs.PlanResult, snap *state.StateSnapshot) (raft.ApplyFuture, error) { req := structs.AllocUpdateRequest{} for _, updateList := range result.NodeUpdate { req.Alloc = append(req.Alloc, updateList...) @@ -107,7 +107,20 @@ func (s *Server) applyPlan(result *structs.PlanResult) (raft.ApplyFuture, error) } req.Alloc = append(req.Alloc, result.FailedAllocs...) - return s.raftApplyFuture(structs.AllocUpdateRequestType, &req) + // Dispatch the Raft transaction + future, err := s.raftApplyFuture(structs.AllocUpdateRequestType, &req) + if err != nil { + return nil, err + } + + // Optimistically apply to our state view + if snap != nil { + nextIdx := s.raft.AppliedIndex() + 1 + if err := snap.UpsertAllocs(nextIdx, req.Alloc); err != nil { + return future, err + } + } + return future, nil } // asyncPlanWait is used to apply and respond to a plan async diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index 654e22f6d..d4c5dde0f 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -55,7 +55,7 @@ func TestPlanApply_applyPlan(t *testing.T) { } // Apply the plan - future, err := s1.applyPlan(plan) + future, err := s1.applyPlan(plan, nil) if err != nil { t.Fatalf("err: %v", err) } @@ -100,7 +100,7 @@ func TestPlanApply_applyPlan(t *testing.T) { } // Apply the plan - future, err = s1.applyPlan(plan) + future, err = s1.applyPlan(plan, nil) if err != nil { t.Fatalf("err: %v", err) } From 7464bcb9bb704c0846b3abb0dabcba8514018359 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:34:52 -0400 Subject: [PATCH 069/178] nomad: overlap plan evaluation with apply --- nomad/plan_apply.go | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index ae9f85b50..75fff4f59 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -37,6 +37,12 @@ import ( // but there are many of those and only a single plan verifier. // func (s *Server) planApply() { + // waitCh is used to track an outstanding application + // while snap holds an optimistic state which includes + // that plan application. + var waitCh chan struct{} + var snap *state.StateSnapshot + for { // Pull the next pending plan, exit if we are no longer leader pending, err := s.planQueue.Dequeue(0) @@ -59,12 +65,23 @@ func (s *Server) planApply() { continue } + // Check if out last plan has completed + select { + case <-waitCh: + waitCh = nil + snap = nil + default: + } + // Snapshot the state so that we have a consistent view of the world - snap, err := s.fsm.State().Snapshot() - if err != nil { - s.logger.Printf("[ERR] nomad: failed to snapshot state: %v", err) - pending.respond(nil, err) - continue + // if no snapshot is available + if snap == nil { + snap, err = s.fsm.State().Snapshot() + if err != nil { + s.logger.Printf("[ERR] nomad: failed to snapshot state: %v", err) + pending.respond(nil, err) + continue + } } // Evaluate the plan @@ -81,6 +98,19 @@ func (s *Server) planApply() { continue } + // Ensure any parallel apply is complete before + // starting the next one. This also limits how out + // of date our snapshot can be. + if waitCh != nil { + <-waitCh + snap, err = s.fsm.State().Snapshot() + if err != nil { + s.logger.Printf("[ERR] nomad: failed to snapshot state: %v", err) + pending.respond(nil, err) + continue + } + } + // Dispatch the Raft transaction for the plan future, err := s.applyPlan(result, snap) if err != nil { @@ -90,9 +120,8 @@ func (s *Server) planApply() { } // Respond to the plan in async - waitCh := make(chan struct{}) + waitCh = make(chan struct{}) go s.asyncPlanWait(waitCh, future, result, pending) - <-waitCh } } From 9c14964bae0538eaccd468f6d8eb913b00aec12b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:38:07 -0400 Subject: [PATCH 070/178] nomad: refresh snapshot under error return --- nomad/plan_apply.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 75fff4f59..94c003477 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -75,7 +75,7 @@ func (s *Server) planApply() { // Snapshot the state so that we have a consistent view of the world // if no snapshot is available - if snap == nil { + if waitCh == nil || snap == nil { snap, err = s.fsm.State().Snapshot() if err != nil { s.logger.Printf("[ERR] nomad: failed to snapshot state: %v", err) From c4cc21dab4a4f74017ed731a02830718d9e29d10 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:46:46 -0400 Subject: [PATCH 071/178] nomad: test optimistic state update --- nomad/plan_apply_test.go | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/nomad/plan_apply_test.go b/nomad/plan_apply_test.go index d4c5dde0f..4fd0627c5 100644 --- a/nomad/plan_apply_test.go +++ b/nomad/plan_apply_test.go @@ -54,11 +54,24 @@ func TestPlanApply_applyPlan(t *testing.T) { FailedAllocs: []*structs.Allocation{allocFail}, } - // Apply the plan - future, err := s1.applyPlan(plan, nil) + // Snapshot the state + snap, err := s1.State().Snapshot() if err != nil { t.Fatalf("err: %v", err) } + + // Apply the plan + future, err := s1.applyPlan(plan, snap) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Verify our optimistic snapshot is updated + if out, err := snap.AllocByID(alloc.ID); err != nil || out == nil { + t.Fatalf("bad: %v %v", out, err) + } + + // Check plan does apply cleanly index, err := planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) @@ -99,11 +112,24 @@ func TestPlanApply_applyPlan(t *testing.T) { }, } - // Apply the plan - future, err = s1.applyPlan(plan, nil) + // Snapshot the state + snap, err = s1.State().Snapshot() if err != nil { t.Fatalf("err: %v", err) } + + // Apply the plan + future, err = s1.applyPlan(plan, snap) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Check that our optimistic view is updated + if out, _ := snap.AllocByID(allocEvict.ID); out.DesiredStatus != structs.AllocDesiredStatusEvict { + t.Fatalf("bad: %#v", out) + } + + // Verify plan applies cleanly index, err = planWaitFuture(future) if err != nil { t.Fatalf("err: %v", err) From d2fd952fe7e9d70e41c6b94f360d752739202112 Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sun, 11 Oct 2015 20:33:02 -0600 Subject: [PATCH 072/178] evaulation -> evaluation --- website/source/docs/jobspec/environment.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/jobspec/environment.html.md b/website/source/docs/jobspec/environment.html.md index f3dd4e6ec..858dd03af 100644 --- a/website/source/docs/jobspec/environment.html.md +++ b/website/source/docs/jobspec/environment.html.md @@ -17,7 +17,7 @@ environment variables. When you request resources for a job, Nomad creates a resource offer. The final resources for your job are not determined until it is scheduled. Nomad will -tell you which resources have been allocated after evaulation and placement. +tell you which resources have been allocated after evaluation and placement. ### CPU and Memory From ad5635f4d7a16eec27cafeec2938096a15323e5f Mon Sep 17 00:00:00 2001 From: Charlie O'Keefe Date: Sun, 11 Oct 2015 20:36:50 -0600 Subject: [PATCH 073/178] add 'is' --- website/source/docs/jobspec/environment.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/jobspec/environment.html.md b/website/source/docs/jobspec/environment.html.md index f3dd4e6ec..a44ccb159 100644 --- a/website/source/docs/jobspec/environment.html.md +++ b/website/source/docs/jobspec/environment.html.md @@ -28,7 +28,7 @@ the memory limit to inform how large your in-process cache should be, or to decide when to flush buffers to disk. Both CPU and memory are presented as integers. The unit for CPU limit is -`1024 = 1Ghz`. The unit for memory `1 = 1 megabytes`. +`1024 = 1Ghz`. The unit for memory is `1 = 1 megabytes`. Writing your applications to adjust to these values at runtime provides greater scheduling flexibility since you can adjust the resource allocations in your From fa8c5fb27c1fbf5406e4fa36f89efd67baec5156 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:35:09 -0400 Subject: [PATCH 074/178] nomad: make tests more robust --- nomad/worker_test.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nomad/worker_test.go b/nomad/worker_test.go index 81837d99e..a56dfefe1 100644 --- a/nomad/worker_test.go +++ b/nomad/worker_test.go @@ -52,7 +52,12 @@ func TestWorker_dequeueEvaluation(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} @@ -82,7 +87,12 @@ func TestWorker_dequeueEvaluation_paused(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} @@ -153,7 +163,12 @@ func TestWorker_sendAck(t *testing.T) { // Create the evaluation eval1 := mock.Eval() - s1.evalBroker.Enqueue(eval1) + testutil.WaitForResult(func() (bool, error) { + err := s1.evalBroker.Enqueue(eval1) + return err == nil, err + }, func(err error) { + t.Fatalf("err: %v", err) + }) // Create a worker w := &Worker{srv: s1, logger: s1.logger} From b5ec1e3ba1ed2d070ff6855aa6dc1b8a9ad6d09b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 11 Oct 2015 18:47:00 -0400 Subject: [PATCH 075/178] nomad: make test more robust --- nomad/leader_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/nomad/leader_test.go b/nomad/leader_test.go index 475da47b0..b753b41f4 100644 --- a/nomad/leader_test.go +++ b/nomad/leader_test.go @@ -228,6 +228,7 @@ func TestLeader_EvalBroker_Reset(t *testing.T) { defer s3.Shutdown() servers := []*Server{s1, s2, s3} testJoin(t, s1, s2, s3) + testutil.WaitForLeader(t, s1.RPC) for _, s := range servers { testutil.WaitForResult(func() (bool, error) { From 5a0551aed9ca3c4a4ae00ab4b6e58599126903c2 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 12 Oct 2015 13:15:37 -0700 Subject: [PATCH 076/178] Change Docker/Rkt to set driver to 1 not true for consistency --- client/driver/docker.go | 2 +- client/driver/docker_test.go | 2 +- client/driver/rkt.go | 2 +- client/driver/rkt_test.go | 2 +- website/source/docs/drivers/rkt.html.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 0fbf64714..29e1d635d 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -91,7 +91,7 @@ func (d *DockerDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool // is broken. return false, err } - node.Attributes["driver.docker"] = "true" + node.Attributes["driver.docker"] = "1" node.Attributes["driver.docker.version"] = env.Get("Version") return true, nil diff --git a/client/driver/docker_test.go b/client/driver/docker_test.go index 3f526b66f..028738f35 100644 --- a/client/driver/docker_test.go +++ b/client/driver/docker_test.go @@ -50,7 +50,7 @@ func TestDockerDriver_Fingerprint(t *testing.T) { if apply != dockerLocated() { t.Fatalf("Fingerprinter should detect Docker when it is installed") } - if node.Attributes["driver.docker"] == "" { + if node.Attributes["driver.docker"] != "1" { t.Log("Docker not found. The remainder of the docker tests will be skipped.") } t.Logf("Found docker version %s", node.Attributes["driver.docker.version"]) diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 23f4d29ee..e5934792c 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -69,7 +69,7 @@ func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, e return false, fmt.Errorf("Unable to parse Rkt version string: %#v", rktMatches) } - node.Attributes["driver.rkt"] = "true" + node.Attributes["driver.rkt"] = "1" node.Attributes["driver.rkt.version"] = rktMatches[0] node.Attributes["driver.rkt.appc.version"] = appcMatches[1] diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index ea42b2268..b44d0806c 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -41,7 +41,7 @@ func TestRktDriver_Fingerprint(t *testing.T) { if !apply { t.Fatalf("should apply") } - if node.Attributes["driver.rkt"] == "" { + if node.Attributes["driver.rkt"] != "1" { t.Fatalf("Missing Rkt driver") } if node.Attributes["driver.rkt.version"] == "" { diff --git a/website/source/docs/drivers/rkt.html.md b/website/source/docs/drivers/rkt.html.md index 0381c6a8f..d948e2947 100644 --- a/website/source/docs/drivers/rkt.html.md +++ b/website/source/docs/drivers/rkt.html.md @@ -34,7 +34,7 @@ over HTTP. The `Rkt` driver will set the following client attributes: -* `driver.rkt` - Set to `true` if Rkt is found on the host node. Nomad determines +* `driver.rkt` - Set to `1` if Rkt is found on the host node. Nomad determines this by executing `rkt version` on the host and parsing the output * `driver.rkt.version` - Version of `rkt` eg: `0.8.1` * `driver.rkt.appc.version` - Version of `appc` that `rkt` is using eg: `0.8.1` From d61d162799ec384345df91f3880654ffeafea52f Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Mon, 12 Oct 2015 13:40:31 -0700 Subject: [PATCH 077/178] Do not say the bool is set to true This will be harmonized across all drivers soon --- website/source/docs/drivers/docker.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index 05ef6a64f..8bfa02bcf 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -125,7 +125,7 @@ production Nomad will always read `docker.endpoint`. The `docker` driver will set the following client attributes: -* `driver.docker` - This will be set to "true", indicating the +* `driver.docker` - This will be set to "1", indicating the driver is available. * `driver.docker.version` - This will be set to version of the docker server From e2105f0643e5c0dbd28ea375b5cbfdeb14762624 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 9 Oct 2015 10:52:44 -0700 Subject: [PATCH 078/178] Separate args from exec command; inject environment variables and general cleanup of Rkt driver --- client/driver/rkt.go | 80 +++++++++++++++---------- client/driver/rkt_test.go | 5 +- website/source/docs/drivers/rkt.html.md | 1 + 3 files changed, 53 insertions(+), 33 deletions(-) diff --git a/client/driver/rkt.go b/client/driver/rkt.go index e5934792c..9d7efc7ce 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -14,6 +14,7 @@ import ( "time" "github.com/hashicorp/nomad/client/config" + "github.com/hashicorp/nomad/client/driver/args" "github.com/hashicorp/nomad/nomad/structs" ) @@ -78,60 +79,77 @@ func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, e // Run an existing Rkt image. func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { + // Validate that the config is valid. trust_prefix, ok := task.Config["trust_prefix"] if !ok || trust_prefix == "" { return nil, fmt.Errorf("Missing trust prefix for rkt") } + name, ok := task.Config["name"] + if !ok || name == "" { + return nil, fmt.Errorf("Missing ACI name for rkt") + } + // Add the given trust prefix var outBuf, errBuf bytes.Buffer cmd := exec.Command("rkt", "trust", fmt.Sprintf("--prefix=%s", trust_prefix)) cmd.Stdout = &outBuf cmd.Stderr = &errBuf - d.logger.Printf("[DEBUG] driver.rkt: starting rkt command: %q", cmd.Args) if err := cmd.Run(); err != nil { - return nil, fmt.Errorf( - "Error running rkt: %s\n\nOutput: %s\n\nError: %s", + return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", err, outBuf.String(), errBuf.String()) } d.logger.Printf("[DEBUG] driver.rkt: added trust prefix: %q", trust_prefix) - name, ok := task.Config["name"] - if !ok || name == "" { - return nil, fmt.Errorf("Missing ACI name for rkt") + // Reset the buffers + outBuf.Reset() + errBuf.Reset() + + // Build the command. + var cmd_args []string + + // Inject the environment variables. + envVars := TaskEnvironmentVariables(ctx, task) + for k, v := range envVars.Map() { + cmd_args = append(cmd_args, fmt.Sprintf("--set-env=%v=%v", k, v)) } - exec_cmd, ok := task.Config["exec"] - if !ok || exec_cmd == "" { - d.logger.Printf("[WARN] driver.rkt: could not find a command to execute in the ACI, the default command will be executed") + // Append the run command. + cmd_args = append(cmd_args, "run", "--mds-register=false", name) + + // Check if the user has overriden the exec command. + if exec_cmd, ok := task.Config["exec"]; ok { + cmd_args = append(cmd_args, fmt.Sprintf("--exec=%v", exec_cmd)) } - // Run the ACI - var aoutBuf, aerrBuf bytes.Buffer - run_cmd := []string{ - "rkt", - "run", - "--mds-register=false", - name, + // Add user passed arguments. + if userArgs, ok := task.Config["args"]; ok { + parsed, err := args.ParseAndReplace(userArgs, envVars.Map()) + if err != nil { + return nil, err + } + + // Need to start arguments with "--" + if len(parsed) > 0 { + cmd_args = append(cmd_args, "--") + } + + for _, arg := range parsed { + cmd_args = append(cmd_args, fmt.Sprintf("--%v", arg)) + } } - if exec_cmd != "" { - splitted := strings.Fields(exec_cmd) - run_cmd = append(run_cmd, "--exec=", splitted[0], "--") - run_cmd = append(run_cmd, splitted[1:]...) - run_cmd = append(run_cmd, "---") - } - acmd := exec.Command(run_cmd[0], run_cmd[1:]...) - acmd.Stdout = &aoutBuf - acmd.Stderr = &aerrBuf - d.logger.Printf("[DEBUG] driver:rkt: starting rkt command: %q", acmd.Args) - if err := acmd.Start(); err != nil { - return nil, fmt.Errorf( - "Error running rkt: %s\n\nOutput: %s\n\nError: %s", - err, aoutBuf.String(), aerrBuf.String()) + + cmd = exec.Command("rkt", cmd_args...) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", + err, outBuf.String(), errBuf.String()) } d.logger.Printf("[DEBUG] driver.rkt: started ACI: %q", name) + h := &rktHandle{ - proc: acmd.Process, + proc: cmd.Process, name: name, logger: d.logger, doneCh: make(chan struct{}), diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index b44d0806c..afc1d63f8 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -60,7 +60,7 @@ func TestRktDriver_Start(t *testing.T) { Config: map[string]string{ "trust_prefix": "coreos.com/etcd", "name": "coreos.com/etcd:v2.0.4", - "exec": "/etcd --version", + "exec": "/etcd", }, } @@ -99,7 +99,8 @@ func TestRktDriver_Start_Wait(t *testing.T) { Config: map[string]string{ "trust_prefix": "coreos.com/etcd", "name": "coreos.com/etcd:v2.0.4", - "exec": "/etcd --version", + "exec": "/etcd", + "args": "--version", }, } diff --git a/website/source/docs/drivers/rkt.html.md b/website/source/docs/drivers/rkt.html.md index d948e2947..c837c154c 100644 --- a/website/source/docs/drivers/rkt.html.md +++ b/website/source/docs/drivers/rkt.html.md @@ -22,6 +22,7 @@ The `Rkt` driver supports the following configuration in the job spec: the box running the nomad agent. * `name` - **(Required)** Fully qualified name of an image to run using rkt * `exec` - **(Optional**) A command to execute on the ACI +* `args` - **(Optional**) A string of args to pass into the image. ## Client Requirements From ab75442dd2503ec6b6097c6290e83bcf951e921f Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 9 Oct 2015 12:14:56 -0700 Subject: [PATCH 079/178] Capture Rkt logs --- client/driver/rkt.go | 35 ++++++++++++++++++++------ client/driver/rkt_test.go | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 9d7efc7ce..c6826fdd8 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -7,12 +7,14 @@ import ( "log" "os" "os/exec" + "path/filepath" "regexp" "runtime" "strings" "syscall" "time" + "github.com/hashicorp/nomad/client/allocdir" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/client/driver/args" "github.com/hashicorp/nomad/nomad/structs" @@ -90,6 +92,14 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e return nil, fmt.Errorf("Missing ACI name for rkt") } + // Get the tasks local directory. + taskName := d.DriverContext.taskName + taskDir, ok := ctx.AllocDir.TaskDirs[taskName] + if !ok { + return nil, fmt.Errorf("Could not find task directory for task: %v", d.DriverContext.taskName) + } + taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) + // Add the given trust prefix var outBuf, errBuf bytes.Buffer cmd := exec.Command("rkt", "trust", fmt.Sprintf("--prefix=%s", trust_prefix)) @@ -101,10 +111,6 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e } d.logger.Printf("[DEBUG] driver.rkt: added trust prefix: %q", trust_prefix) - // Reset the buffers - outBuf.Reset() - errBuf.Reset() - // Build the command. var cmd_args []string @@ -135,13 +141,28 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e } for _, arg := range parsed { - cmd_args = append(cmd_args, fmt.Sprintf("--%v", arg)) + cmd_args = append(cmd_args, fmt.Sprintf("%v", arg)) } } + // Create files to capture stdin and out. + stdoutFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stdout", taskName)) + stderrFilename := filepath.Join(taskLocal, fmt.Sprintf("%s.stderr", taskName)) + + stdo, err := os.OpenFile(stdoutFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stdout: %v", err) + } + + stde, err := os.OpenFile(stderrFilename, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + return nil, fmt.Errorf("Error opening file to redirect stderr: %v", err) + } + cmd = exec.Command("rkt", cmd_args...) - cmd.Stdout = &outBuf - cmd.Stderr = &errBuf + cmd.Stdout = stdo + cmd.Stderr = stde + if err := cmd.Start(); err != nil { return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", err, outBuf.String(), errBuf.String()) diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index afc1d63f8..9b7c10136 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -2,10 +2,13 @@ package driver import ( "fmt" + "io/ioutil" "os" + "path/filepath" "testing" "time" + "github.com/hashicorp/nomad/client/allocdir" "github.com/hashicorp/nomad/client/config" "github.com/hashicorp/nomad/nomad/structs" @@ -133,3 +136,53 @@ func TestRktDriver_Start_Wait(t *testing.T) { t.Fatalf("timeout") } } + +func TestRktDriver_Start_Wait_Logs(t *testing.T) { + ctestutils.RktCompatible(t) + task := &structs.Task{ + Name: "etcd", + Config: map[string]string{ + "trust_prefix": "coreos.com/etcd", + "name": "coreos.com/etcd:v2.0.4", + "exec": "/etcd", + "args": "--version", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + d := NewRktDriver(driverCtx) + defer ctx.AllocDir.Destroy() + + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + defer handle.Kill() + + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatalf("timeout") + } + + taskDir, ok := ctx.AllocDir.TaskDirs[task.Name] + if !ok { + t.Fatalf("Could not find task directory for task: %v", task) + } + stdout := filepath.Join(taskDir, allocdir.TaskLocal, fmt.Sprintf("%v.stdout", task.Name)) + data, err := ioutil.ReadFile(stdout) + if err != nil { + t.Fatalf("Failed to read tasks stdout: %v", err) + } + + if len(data) == 0 { + t.Fatal("Task's stdout is empty") + } +} From 7936102135e3ea47fa203df1c668c2234f61b01c Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Fri, 9 Oct 2015 21:28:41 -0700 Subject: [PATCH 080/178] Log starting command --- client/driver/rkt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/driver/rkt.go b/client/driver/rkt.go index c6826fdd8..641ff29d9 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -167,7 +167,7 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", err, outBuf.String(), errBuf.String()) } - d.logger.Printf("[DEBUG] driver.rkt: started ACI: %q", name) + d.logger.Printf("[DEBUG] driver.rkt: started ACI %q with: %v", name, cmd.Args) h := &rktHandle{ proc: cmd.Process, From ad76822c8d1723e0440db0574e4e1fe678258ab6 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 12 Oct 2015 14:35:17 -0700 Subject: [PATCH 081/178] nomad: comment cleanups --- nomad/plan_apply.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nomad/plan_apply.go b/nomad/plan_apply.go index 94c003477..fe7bb84a7 100644 --- a/nomad/plan_apply.go +++ b/nomad/plan_apply.go @@ -17,7 +17,7 @@ import ( // Naively, we could simply dequeue a plan, verify, apply and then respond. // However, the plan application is bounded by the Raft apply time and // subject to some latency. This creates a stall condition, where we are -// not evaluating, but simply waiting for a transaction to complete. +// not evaluating, but simply waiting for a transaction to apply. // // To avoid this, we overlap verification with apply. This means once // we've verified plan N we attempt to apply it. However, while waiting @@ -37,9 +37,8 @@ import ( // but there are many of those and only a single plan verifier. // func (s *Server) planApply() { - // waitCh is used to track an outstanding application - // while snap holds an optimistic state which includes - // that plan application. + // waitCh is used to track an outstanding application while snap + // holds an optimistic state which includes that plan application. var waitCh chan struct{} var snap *state.StateSnapshot @@ -98,9 +97,8 @@ func (s *Server) planApply() { continue } - // Ensure any parallel apply is complete before - // starting the next one. This also limits how out - // of date our snapshot can be. + // Ensure any parallel apply is complete before starting the next one. + // This also limits how out of date our snapshot can be. if waitCh != nil { <-waitCh snap, err = s.fsm.State().Snapshot() From 659e66d8f7af38eb11deb4acdba9bd394ca102e5 Mon Sep 17 00:00:00 2001 From: Alex Dadgar Date: Mon, 12 Oct 2015 14:37:56 -0700 Subject: [PATCH 082/178] Update driver config names for consistency and make the trust_prefix optional --- client/driver/rkt.go | 60 +++++++++++++------------ client/driver/rkt_test.go | 57 +++++++++++++++++++---- website/source/docs/drivers/rkt.html.md | 10 +++-- 3 files changed, 86 insertions(+), 41 deletions(-) diff --git a/client/driver/rkt.go b/client/driver/rkt.go index 641ff29d9..8bebbaf91 100644 --- a/client/driver/rkt.go +++ b/client/driver/rkt.go @@ -35,7 +35,7 @@ type RktDriver struct { // rktHandle is returned from Start/Open as a handle to the PID type rktHandle struct { proc *os.Process - name string + image string logger *log.Logger waitCh chan error doneCh chan struct{} @@ -44,8 +44,8 @@ type rktHandle struct { // rktPID is a struct to map the pid running the process to the vm image on // disk type rktPID struct { - Pid int - Name string + Pid int + Image string } // NewRktDriver is used to create a new exec driver @@ -82,14 +82,9 @@ func (d *RktDriver) Fingerprint(cfg *config.Config, node *structs.Node) (bool, e // Run an existing Rkt image. func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { // Validate that the config is valid. - trust_prefix, ok := task.Config["trust_prefix"] - if !ok || trust_prefix == "" { - return nil, fmt.Errorf("Missing trust prefix for rkt") - } - - name, ok := task.Config["name"] - if !ok || name == "" { - return nil, fmt.Errorf("Missing ACI name for rkt") + img, ok := task.Config["image"] + if !ok || img == "" { + return nil, fmt.Errorf("Missing ACI image for rkt") } // Get the tasks local directory. @@ -101,15 +96,18 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e taskLocal := filepath.Join(taskDir, allocdir.TaskLocal) // Add the given trust prefix - var outBuf, errBuf bytes.Buffer - cmd := exec.Command("rkt", "trust", fmt.Sprintf("--prefix=%s", trust_prefix)) - cmd.Stdout = &outBuf - cmd.Stderr = &errBuf - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", - err, outBuf.String(), errBuf.String()) + trust_prefix, trust_cmd := task.Config["trust_prefix"] + if trust_cmd { + var outBuf, errBuf bytes.Buffer + cmd := exec.Command("rkt", "trust", fmt.Sprintf("--prefix=%s", trust_prefix)) + cmd.Stdout = &outBuf + cmd.Stderr = &errBuf + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("Error running rkt trust: %s\n\nOutput: %s\n\nError: %s", + err, outBuf.String(), errBuf.String()) + } + d.logger.Printf("[DEBUG] driver.rkt: added trust prefix: %q", trust_prefix) } - d.logger.Printf("[DEBUG] driver.rkt: added trust prefix: %q", trust_prefix) // Build the command. var cmd_args []string @@ -120,11 +118,16 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e cmd_args = append(cmd_args, fmt.Sprintf("--set-env=%v=%v", k, v)) } + // Disble signature verification if the trust command was not run. + if !trust_cmd { + cmd_args = append(cmd_args, "--insecure-skip-verify") + } + // Append the run command. - cmd_args = append(cmd_args, "run", "--mds-register=false", name) + cmd_args = append(cmd_args, "run", "--mds-register=false", img) // Check if the user has overriden the exec command. - if exec_cmd, ok := task.Config["exec"]; ok { + if exec_cmd, ok := task.Config["command"]; ok { cmd_args = append(cmd_args, fmt.Sprintf("--exec=%v", exec_cmd)) } @@ -159,19 +162,18 @@ func (d *RktDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, e return nil, fmt.Errorf("Error opening file to redirect stderr: %v", err) } - cmd = exec.Command("rkt", cmd_args...) + cmd := exec.Command("rkt", cmd_args...) cmd.Stdout = stdo cmd.Stderr = stde if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("Error running rkt: %s\n\nOutput: %s\n\nError: %s", - err, outBuf.String(), errBuf.String()) + return nil, fmt.Errorf("Error running rkt: %v", err) } - d.logger.Printf("[DEBUG] driver.rkt: started ACI %q with: %v", name, cmd.Args) + d.logger.Printf("[DEBUG] driver.rkt: started ACI %q with: %v", img, cmd.Args) h := &rktHandle{ proc: cmd.Process, - name: name, + image: img, logger: d.logger, doneCh: make(chan struct{}), waitCh: make(chan error, 1), @@ -197,7 +199,7 @@ func (d *RktDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error // Return a driver handle h := &rktHandle{ proc: proc, - name: qpid.Name, + image: qpid.Image, logger: d.logger, doneCh: make(chan struct{}), waitCh: make(chan error, 1), @@ -210,8 +212,8 @@ func (d *RktDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, error func (h *rktHandle) ID() string { // Return a handle to the PID pid := &rktPID{ - Pid: h.proc.Pid, - Name: h.name, + Pid: h.proc.Pid, + Image: h.image, } data, err := json.Marshal(pid) if err != nil { diff --git a/client/driver/rkt_test.go b/client/driver/rkt_test.go index 9b7c10136..94d45cdcc 100644 --- a/client/driver/rkt_test.go +++ b/client/driver/rkt_test.go @@ -18,13 +18,13 @@ import ( func TestRktDriver_Handle(t *testing.T) { h := &rktHandle{ proc: &os.Process{Pid: 123}, - name: "foo", + image: "foo", doneCh: make(chan struct{}), waitCh: make(chan error, 1), } actual := h.ID() - expected := `Rkt:{"Pid":123,"Name":"foo"}` + expected := `Rkt:{"Pid":123,"Image":"foo"}` if actual != expected { t.Errorf("Expected `%s`, found `%s`", expected, actual) } @@ -62,8 +62,8 @@ func TestRktDriver_Start(t *testing.T) { Name: "etcd", Config: map[string]string{ "trust_prefix": "coreos.com/etcd", - "name": "coreos.com/etcd:v2.0.4", - "exec": "/etcd", + "image": "coreos.com/etcd:v2.0.4", + "command": "/etcd", }, } @@ -101,8 +101,8 @@ func TestRktDriver_Start_Wait(t *testing.T) { Name: "etcd", Config: map[string]string{ "trust_prefix": "coreos.com/etcd", - "name": "coreos.com/etcd:v2.0.4", - "exec": "/etcd", + "image": "coreos.com/etcd:v2.0.4", + "command": "/etcd", "args": "--version", }, } @@ -137,14 +137,55 @@ func TestRktDriver_Start_Wait(t *testing.T) { } } +func TestRktDriver_Start_Wait_Skip_Trust(t *testing.T) { + ctestutils.RktCompatible(t) + task := &structs.Task{ + Name: "etcd", + Config: map[string]string{ + "image": "coreos.com/etcd:v2.0.4", + "command": "/etcd", + "args": "--version", + }, + } + + driverCtx := testDriverContext(task.Name) + ctx := testDriverExecContext(task, driverCtx) + d := NewRktDriver(driverCtx) + defer ctx.AllocDir.Destroy() + + handle, err := d.Start(ctx, task) + if err != nil { + t.Fatalf("err: %v", err) + } + if handle == nil { + t.Fatalf("missing handle") + } + defer handle.Kill() + + // Update should be a no-op + err = handle.Update(task) + if err != nil { + t.Fatalf("err: %v", err) + } + + select { + case err := <-handle.WaitCh(): + if err != nil { + t.Fatalf("err: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatalf("timeout") + } +} + func TestRktDriver_Start_Wait_Logs(t *testing.T) { ctestutils.RktCompatible(t) task := &structs.Task{ Name: "etcd", Config: map[string]string{ "trust_prefix": "coreos.com/etcd", - "name": "coreos.com/etcd:v2.0.4", - "exec": "/etcd", + "image": "coreos.com/etcd:v2.0.4", + "command": "/etcd", "args": "--version", }, } diff --git a/website/source/docs/drivers/rkt.html.md b/website/source/docs/drivers/rkt.html.md index c837c154c..7db5707d8 100644 --- a/website/source/docs/drivers/rkt.html.md +++ b/website/source/docs/drivers/rkt.html.md @@ -18,10 +18,12 @@ containers. The `Rkt` driver supports the following configuration in the job spec: -* `trust_prefix` - **(Required)** The trust prefix to be passed to rkt. Must be reachable from -the box running the nomad agent. -* `name` - **(Required)** Fully qualified name of an image to run using rkt -* `exec` - **(Optional**) A command to execute on the ACI +* `trust_prefix` - **(Optional)** The trust prefix to be passed to rkt. Must be reachable from +the box running the nomad agent. If not specified, the image is run without +verifying the image signature. +* `image` - **(Required)** The image to run which may be specified by name, +hash, ACI address or docker registry. +* `command` - **(Optional**) A command to execute on the ACI. * `args` - **(Optional**) A string of args to pass into the image. ## Client Requirements From c43978bc4b3d74c8939b2a3ac9e9874fc929880b Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 12 Oct 2015 16:56:33 -0500 Subject: [PATCH 083/178] Fix old comments and other syntax cleanup --- client/fingerprint/env_aws.go | 4 ++-- client/fingerprint/env_gce.go | 18 ++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/client/fingerprint/env_aws.go b/client/fingerprint/env_aws.go index d47ee1ba8..a65933a1e 100644 --- a/client/fingerprint/env_aws.go +++ b/client/fingerprint/env_aws.go @@ -62,12 +62,12 @@ var ec2InstanceSpeedMap = map[string]int{ "d2.8xlarge": 10000, } -// EnvAWSFingerprint is used to fingerprint the CPU +// EnvAWSFingerprint is used to fingerprint AWS metadata type EnvAWSFingerprint struct { logger *log.Logger } -// NewEnvAWSFingerprint is used to create a CPU fingerprint +// NewEnvAWSFingerprint is used to create a fingerprint from AWS metadata func NewEnvAWSFingerprint(logger *log.Logger) Fingerprint { f := &EnvAWSFingerprint{logger: logger} return f diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 9b3d3bd6f..041a11791 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -43,14 +43,14 @@ func lastToken(s string) string { return s[index+1:] } -// EnvGCEFingerprint is used to fingerprint the CPU +// EnvGCEFingerprint is used to fingerprint GCE metadata type EnvGCEFingerprint struct { client *http.Client logger *log.Logger metadataURL string } -// NewEnvGCEFingerprint is used to create a CPU fingerprint +// NewEnvGCEFingerprint is used to create a fingerprint from GCE metadata func NewEnvGCEFingerprint(logger *log.Logger) Fingerprint { // Read the internal metadata URL from the environment, allowing test files to // provide their own @@ -184,10 +184,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } if err := json.Unmarshal([]byte(value), &tagList); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance tags: %s", err.Error()) - } else { - for _, tag := range tagList { - node.Attributes["platform.gce.tag."+tag] = "true" - } + } + for _, tag := range tagList { + node.Attributes["platform.gce.tag."+tag] = "true" } var attrDict map[string]string @@ -197,10 +196,9 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) } if err := json.Unmarshal([]byte(value), &attrDict); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding instance attributes: %s", err.Error()) - } else { - for k, v := range attrDict { - node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") - } + } + for k, v := range attrDict { + node.Attributes["platform.gce.attr."+k] = strings.Trim(v, "\n") } // populate Links From 263c5f5727711576ec5e2eb511a359c55aa7a2a6 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Mon, 12 Oct 2015 17:57:45 -0500 Subject: [PATCH 084/178] More syntax cleanup --- client/fingerprint/env_gce.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/fingerprint/env_gce.go b/client/fingerprint/env_gce.go index 041a11791..b20978db0 100644 --- a/client/fingerprint/env_gce.go +++ b/client/fingerprint/env_gce.go @@ -166,14 +166,14 @@ func (f *EnvGCEFingerprint) Fingerprint(cfg *config.Config, node *structs.Node) var interfaces []GCEMetadataNetworkInterface if err := json.Unmarshal([]byte(value), &interfaces); err != nil { f.logger.Printf("[WARN] fingerprint.env_gce: Error decoding network interface information: %s", err.Error()) - } else { - for _, intf := range interfaces { - prefix := "platform.gce.network." + lastToken(intf.Network) - node.Attributes[prefix] = "true" - node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") - for index, accessConfig := range intf.AccessConfigs { - node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp - } + } + + for _, intf := range interfaces { + prefix := "platform.gce.network." + lastToken(intf.Network) + node.Attributes[prefix] = "true" + node.Attributes[prefix+".ip"] = strings.Trim(intf.Ip, "\n") + for index, accessConfig := range intf.AccessConfigs { + node.Attributes[prefix+".external-ip."+strconv.Itoa(index)] = accessConfig.ExternalIp } } From b42db2fff1aa4c354331af2293d4969505eba45e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 12 Oct 2015 20:15:07 -0700 Subject: [PATCH 085/178] scheduler: adding regexp and version constraint cache --- scheduler/context.go | 28 +++++++++++++++++++++++++++ scheduler/context_test.go | 2 +- scheduler/feasible.go | 39 ++++++++++++++++++++++++++------------ scheduler/feasible_test.go | 9 ++++++--- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/scheduler/context.go b/scheduler/context.go index 6cffad740..58ae8931a 100644 --- a/scheduler/context.go +++ b/scheduler/context.go @@ -2,7 +2,9 @@ package scheduler import ( "log" + "regexp" + "github.com/hashicorp/go-version" "github.com/hashicorp/nomad/nomad/structs" ) @@ -27,10 +29,36 @@ type Context interface { // which is the existing allocations, removing evictions, and // adding any planned placements. ProposedAllocs(nodeID string) ([]*structs.Allocation, error) + + // RegexpCache is a cache of regular expressions + RegexpCache() map[string]*regexp.Regexp + + // ConstraintCache is a cache of version constraints + ConstraintCache() map[string]version.Constraints +} + +// EvalCache is used to cache certain things during an evaluation +type EvalCache struct { + reCache map[string]*regexp.Regexp + constraintCache map[string]version.Constraints +} + +func (e *EvalCache) RegexpCache() map[string]*regexp.Regexp { + if e.reCache == nil { + e.reCache = make(map[string]*regexp.Regexp) + } + return e.reCache +} +func (e *EvalCache) ConstraintCache() map[string]version.Constraints { + if e.constraintCache == nil { + e.constraintCache = make(map[string]version.Constraints) + } + return e.constraintCache } // EvalContext is a Context used during an Evaluation type EvalContext struct { + EvalCache state State plan *structs.Plan logger *log.Logger diff --git a/scheduler/context_test.go b/scheduler/context_test.go index 19566d311..914b54b06 100644 --- a/scheduler/context_test.go +++ b/scheduler/context_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/nomad/nomad/structs" ) -func testContext(t *testing.T) (*state.StateStore, *EvalContext) { +func testContext(t testing.TB) (*state.StateStore, *EvalContext) { state, err := state.NewStateStore(os.Stderr) if err != nil { t.Fatalf("err: %v", err) diff --git a/scheduler/feasible.go b/scheduler/feasible.go index 7482a953d..cbf811b8c 100644 --- a/scheduler/feasible.go +++ b/scheduler/feasible.go @@ -206,7 +206,7 @@ func (iter *ConstraintIterator) meetsConstraint(constraint *structs.Constraint, } // Check if satisfied - return checkConstraint(constraint.Operand, lVal, rVal) + return checkConstraint(iter.ctx, constraint.Operand, lVal, rVal) } // resolveConstraintTarget is used to resolve the LTarget and RTarget of a Constraint @@ -243,7 +243,7 @@ func resolveConstraintTarget(target string, node *structs.Node) (interface{}, bo } // checkConstraint checks if a constraint is satisfied -func checkConstraint(operand string, lVal, rVal interface{}) bool { +func checkConstraint(ctx Context, operand string, lVal, rVal interface{}) bool { switch operand { case "=", "==", "is": return reflect.DeepEqual(lVal, rVal) @@ -252,9 +252,9 @@ func checkConstraint(operand string, lVal, rVal interface{}) bool { case "<", "<=", ">", ">=": return checkLexicalOrder(operand, lVal, rVal) case "version": - return checkVersionConstraint(lVal, rVal) + return checkVersionConstraint(ctx, lVal, rVal) case "regexp": - return checkRegexpConstraint(lVal, rVal) + return checkRegexpConstraint(ctx, lVal, rVal) default: return false } @@ -288,7 +288,7 @@ func checkLexicalOrder(op string, lVal, rVal interface{}) bool { // checkVersionConstraint is used to compare a version on the // left hand side with a set of constraints on the right hand side -func checkVersionConstraint(lVal, rVal interface{}) bool { +func checkVersionConstraint(ctx Context, lVal, rVal interface{}) bool { // Parse the version var versionStr string switch v := lVal.(type) { @@ -312,10 +312,17 @@ func checkVersionConstraint(lVal, rVal interface{}) bool { return false } + // Check the cache for a match + cache := ctx.ConstraintCache() + constraints := cache[constraintStr] + // Parse the constraints - constraints, err := version.NewConstraint(constraintStr) - if err != nil { - return false + if constraints == nil { + constraints, err = version.NewConstraint(constraintStr) + if err != nil { + return false + } + cache[constraintStr] = constraints } // Check the constraints against the version @@ -324,7 +331,7 @@ func checkVersionConstraint(lVal, rVal interface{}) bool { // checkRegexpConstraint is used to compare a value on the // left hand side with a regexp on the right hand side -func checkRegexpConstraint(lVal, rVal interface{}) bool { +func checkRegexpConstraint(ctx Context, lVal, rVal interface{}) bool { // Ensure left-hand is string lStr, ok := lVal.(string) if !ok { @@ -337,10 +344,18 @@ func checkRegexpConstraint(lVal, rVal interface{}) bool { return false } + // Check the cache + cache := ctx.RegexpCache() + re := cache[regexpStr] + // Parse the regexp - re, err := regexp.Compile(regexpStr) - if err != nil { - return false + if re == nil { + var err error + re, err = regexp.Compile(regexpStr) + if err != nil { + return false + } + cache[regexpStr] = re } // Look for a match diff --git a/scheduler/feasible_test.go b/scheduler/feasible_test.go index a69b4c8ce..2f091c2f4 100644 --- a/scheduler/feasible_test.go +++ b/scheduler/feasible_test.go @@ -262,7 +262,8 @@ func TestCheckConstraint(t *testing.T) { } for _, tc := range cases { - if res := checkConstraint(tc.op, tc.lVal, tc.rVal); res != tc.result { + _, ctx := testContext(t) + if res := checkConstraint(ctx, tc.op, tc.lVal, tc.rVal); res != tc.result { t.Fatalf("TC: %#v, Result: %v", tc, res) } } @@ -336,7 +337,8 @@ func TestCheckVersionConstraint(t *testing.T) { }, } for _, tc := range cases { - if res := checkVersionConstraint(tc.lVal, tc.rVal); res != tc.result { + _, ctx := testContext(t) + if res := checkVersionConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { t.Fatalf("TC: %#v, Result: %v", tc, res) } } @@ -370,7 +372,8 @@ func TestCheckRegexpConstraint(t *testing.T) { }, } for _, tc := range cases { - if res := checkRegexpConstraint(tc.lVal, tc.rVal); res != tc.result { + _, ctx := testContext(t) + if res := checkRegexpConstraint(ctx, tc.lVal, tc.rVal); res != tc.result { t.Fatalf("TC: %#v, Result: %v", tc, res) } } From b26c11b3b453446290c91648a368f2d98d4f5893 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Mon, 12 Oct 2015 23:57:16 -0700 Subject: [PATCH 086/178] Do not default to a network mode Makes the driver error out when a wrong or un-supported network_mode is used --- client/driver/docker.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/driver/docker.go b/client/driver/docker.go index 29e1d635d..e4ab6c6db 100644 --- a/client/driver/docker.go +++ b/client/driver/docker.go @@ -135,9 +135,11 @@ func createHostConfig(task *structs.Task) *docker.HostConfig { } // createContainer initializes a struct needed to call docker.client.CreateContainer() -func createContainer(ctx *ExecContext, task *structs.Task, logger *log.Logger) docker.CreateContainerOptions { +func createContainer(ctx *ExecContext, task *structs.Task, logger *log.Logger) (docker.CreateContainerOptions, error) { + var c docker.CreateContainerOptions if task.Resources == nil { - panic("task.Resources is nil and we can't constrain resource usage. We shouldn't have been able to schedule this in the first place.") + logger.Printf("[ERR] driver.docker: task.Resources is empty") + return c, fmt.Errorf("task.Resources is nil and we can't constrain resource usage. We shouldn't have been able to schedule this in the first place.") } hostConfig := createHostConfig(task) @@ -156,8 +158,8 @@ func createContainer(ctx *ExecContext, task *structs.Task, logger *log.Logger) d case "default", "bridge", "none", "host": logger.Printf("[DEBUG] driver.docker: using %s as network mode", mode) default: - logger.Printf("[WARN] invalid setting for network mode %s, defaulting to bridge mode on docker0", mode) - mode = "bridge" + logger.Printf("[ERR] driver.docker: invalid setting for network mode: %s", mode) + return c, fmt.Errorf("Invalid setting for network mode: %s", mode) } hostConfig.NetworkMode = mode @@ -209,7 +211,7 @@ func createContainer(ctx *ExecContext, task *structs.Task, logger *log.Logger) d return docker.CreateContainerOptions{ Config: config, HostConfig: hostConfig, - } + }, nil } func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle, error) { @@ -283,8 +285,13 @@ func (d *DockerDriver) Start(ctx *ExecContext, task *structs.Task) (DriverHandle d.logger.Printf("[DEBUG] driver.docker: using image %s", dockerImage.ID) d.logger.Printf("[INFO] driver.docker: identified image %s as %s", image, dockerImage.ID) + config, err := createContainer(ctx, task, d.logger) + if err != nil { + d.logger.Printf("[ERR] driver.docker: %s", err) + return nil, fmt.Errorf("Failed to create container config for image %s", image) + } // Create a container - container, err := client.CreateContainer(createContainer(ctx, task, d.logger)) + container, err := client.CreateContainer(config) if err != nil { d.logger.Printf("[ERR] driver.docker: %s", err) return nil, fmt.Errorf("Failed to create container from image %s", image) From 311329bac05fb6bdd462f69843ac355a661b7847 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 13 Oct 2015 13:31:56 -0700 Subject: [PATCH 087/178] Clarify that container is an invalid option --- website/source/docs/drivers/docker.html.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/source/docs/drivers/docker.html.md b/website/source/docs/drivers/docker.html.md index 8bfa02bcf..bb6bdb9e6 100644 --- a/website/source/docs/drivers/docker.html.md +++ b/website/source/docs/drivers/docker.html.md @@ -26,7 +26,8 @@ The `docker` driver supports the following configuration in the job specificatio * `network_mode` - (Optional) The network mode to be used for the container. Valid options are `default`, `bridge`, `host` or `none`. If nothing is specified, the container will start in `bridge` mode. The `container` - network mode is not supported right now, this case also defaults to `bridge`. + network mode is not supported right now and is reported as an invalid + option. ### Port Mapping From 77990f9d5c487a3496941136426a631e79b04910 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Tue, 13 Oct 2015 17:50:23 -0400 Subject: [PATCH 088/178] website: add makefile and update README --- website/Makefile | 10 ++++++++++ website/README.md | 8 +------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 website/Makefile diff --git a/website/Makefile b/website/Makefile new file mode 100644 index 000000000..63bb4cab1 --- /dev/null +++ b/website/Makefile @@ -0,0 +1,10 @@ +all: build + +init: + bundle + +dev: init + bundle exec middleman server + +build: init + bundle exec middleman build \ No newline at end of file diff --git a/website/README.md b/website/README.md index fb6d2719c..a539e225e 100644 --- a/website/README.md +++ b/website/README.md @@ -12,13 +12,7 @@ requests like any normal GitHub project, and we'll merge it in. ## Running the Site Locally -Running the site locally is simple. Clone this repo and run the following -commands: - -``` -$ bundle -$ bundle exec middleman server -``` +Running the site locally is simple. Clone this repo and run `make dev`. Then open up `http://localhost:4567`. Note that some URLs you may need to append ".html" to make them work (in the navigation). From 9e0a4e24cd32992f1c4ae6668e3abccbe81ce85a Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Tue, 13 Oct 2015 17:51:01 -0400 Subject: [PATCH 089/178] website: add github_slug to config.rb --- website/config.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/website/config.rb b/website/config.rb index a6849e7b0..b37596d2a 100644 --- a/website/config.rb +++ b/website/config.rb @@ -17,6 +17,7 @@ activate :hashicorp do |h| h.bintray_repo = "mitchellh/nomad" h.bintray_user = "mitchellh" h.bintray_key = ENV["BINTRAY_API_KEY"] + h.github_slug = "hashicorp/nomad" h.minify_javascript = false end From 188e8d202cbd6b5afff06131abf0148f31f2b4ec Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Tue, 13 Oct 2015 17:51:43 -0400 Subject: [PATCH 090/178] website: bundle update middleman-hashicorp --- website/Gemfile.lock | 83 ++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/website/Gemfile.lock b/website/Gemfile.lock index 216114847..72f1c08e2 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,12 +1,12 @@ GIT remote: git://github.com/hashicorp/middleman-hashicorp.git - revision: 76f0f284ad44cea0457484ea83467192f02daf87 + revision: b152b6436348e8e1f9990436228b25b4c5c6fcb8 specs: middleman-hashicorp (0.1.0) bootstrap-sass (~> 3.3) builder (~> 3.2) less (~> 2.6) - middleman (~> 3.3) + middleman (~> 3.4) middleman-livereload (~> 3.4) middleman-minify-html (~> 3.4) middleman-syntax (~> 2.0) @@ -21,21 +21,25 @@ GIT GEM remote: https://rubygems.org/ specs: - activesupport (4.1.12) - i18n (~> 0.6, >= 0.6.9) + activesupport (4.2.4) + i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.1) + thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - autoprefixer-rails (5.2.1) + autoprefixer-rails (6.0.3) execjs json bootstrap-sass (3.3.5.1) autoprefixer-rails (>= 5.0.0.1) sass (>= 3.3.0) builder (3.2.2) - celluloid (0.16.0) - timers (~> 4.0.0) + capybara (2.4.4) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) chunky_png (1.3.4) coffee-script (2.4.1) coffee-script-source @@ -59,52 +63,50 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) erubis (2.7.0) - eventmachine (1.0.7) - execjs (2.5.2) + eventmachine (1.0.8) + execjs (2.6.0) ffi (1.9.10) git-version-bump (0.15.1) - haml (4.0.6) + haml (4.0.7) tilt hike (1.2.3) - hitimes (1.2.2) - hooks (0.4.0) - uber (~> 0.0.4) + hooks (0.4.1) + uber (~> 0.0.14) htmlcompressor (0.2.0) http_parser.rb (0.6.0) i18n (0.7.0) json (1.8.3) - kramdown (1.8.0) + kramdown (1.9.0) less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.11) - listen (2.10.1) - celluloid (~> 0.16.0) + listen (3.0.3) rb-fsevent (>= 0.9.3) rb-inotify (>= 0.9) - middleman (3.3.12) + middleman (3.4.0) coffee-script (~> 2.2) compass (>= 1.0.0, < 2.0.0) compass-import-once (= 1.0.5) execjs (~> 2.0) haml (>= 4.0.5) kramdown (~> 1.2) - middleman-core (= 3.3.12) + middleman-core (= 3.4.0) middleman-sprockets (>= 3.1.2) sass (>= 3.4.0, < 4.0) uglifier (~> 2.5) - middleman-core (3.3.12) - activesupport (~> 4.1.0) + middleman-core (3.4.0) + activesupport (~> 4.1) bundler (~> 1.1) + capybara (~> 2.4.4) erubis hooks (~> 0.3) i18n (~> 0.7.0) - listen (>= 2.7.9, < 3.0) + listen (~> 3.0.3) padrino-helpers (~> 0.12.3) rack (>= 1.4.5, < 2.0) - rack-test (~> 0.6.2) thor (>= 0.15.2, < 2.0) tilt (~> 1.4.1, < 2.0) - middleman-livereload (3.4.2) + middleman-livereload (3.4.3) em-websocket (~> 0.5.1) middleman-core (>= 3.3) rack-livereload (~> 0.3.15) @@ -119,8 +121,12 @@ GEM middleman-syntax (2.0.0) middleman-core (~> 3.2) rouge (~> 1.0) - minitest (5.7.0) + mime-types (2.6.2) + mini_portile (0.6.2) + minitest (5.8.1) multi_json (1.11.2) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) padrino-helpers (0.12.5) i18n (~> 0.6, >= 0.6.7) padrino-support (= 0.12.5) @@ -128,7 +134,7 @@ GEM padrino-support (0.12.5) activesupport (>= 3.1) rack (1.6.4) - rack-contrib (1.3.0) + rack-contrib (1.4.0) git-version-bump (~> 0.15) rack (~> 1.4) rack-livereload (0.3.16) @@ -136,16 +142,16 @@ GEM rack-protection (1.5.3) rack rack-rewrite (1.5.1) - rack-ssl-enforcer (0.2.8) + rack-ssl-enforcer (0.2.9) rack-test (0.6.3) rack (>= 1.0) - rb-fsevent (0.9.5) + rb-fsevent (0.9.6) rb-inotify (0.9.5) ffi (>= 0.5.0) - redcarpet (3.3.2) + redcarpet (3.3.3) ref (2.0.0) - rouge (1.9.1) - sass (3.4.16) + rouge (1.10.1) + sass (3.4.19) sprockets (2.12.4) hike (~> 1.2) multi_json (~> 1.0) @@ -159,24 +165,27 @@ GEM therubyracer (0.12.2) libv8 (~> 3.16.14.0) ref - thin (1.6.3) + thin (1.6.4) daemons (~> 1.0, >= 1.0.9) - eventmachine (~> 1.0) + eventmachine (~> 1.0, >= 1.0.4) rack (~> 1.0) thor (0.19.1) thread_safe (0.3.5) tilt (1.4.1) - timers (4.0.1) - hitimes tzinfo (1.2.2) thread_safe (~> 0.1) - uber (0.0.13) - uglifier (2.7.1) + uber (0.0.15) + uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES middleman-hashicorp! + +BUNDLED WITH + 1.10.6 From 28b556cf3d4c0e97d86b5cc082342f48a032b5e4 Mon Sep 17 00:00:00 2001 From: Sam Handler Date: Tue, 13 Oct 2015 17:52:44 -0400 Subject: [PATCH 091/178] website: add 'Edit this page' link everywhere but index --- website/source/layouts/_footer.erb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/source/layouts/_footer.erb b/website/source/layouts/_footer.erb index 1c965cdfa..291033025 100644 --- a/website/source/layouts/_footer.erb +++ b/website/source/layouts/_footer.erb @@ -7,6 +7,9 @@
  • Docs
  • Community
  • Security
  • + <% if current_page.url != '/' %> +
  • Edit this page
  • + <% end %>