From b394dff38f383da666fba9e4fe48b90032d2a1f5 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 16 Oct 2014 10:14:36 -0700 Subject: [PATCH 001/238] agent: optimize rpc monitor test --- command/agent/rpc_client_test.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 4e6f09252..18825613c 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -232,7 +232,7 @@ func TestRPCClientMonitor(t *testing.T) { found := false OUTER1: - for { + for i := 0; ; i++ { select { case e := <-eventCh: if strings.Contains(e, "Accepted client") { @@ -240,6 +240,10 @@ OUTER1: break OUTER1 } default: + if i > 100 { + break OUTER1 + } + time.Sleep(10 * time.Millisecond) } } if !found { @@ -249,18 +253,20 @@ OUTER1: // Join a bad thing to generate more events p1.agent.JoinLAN(nil) - time.Sleep(1 * time.Second) - found = false OUTER2: - for { + for i := 0; ; i++ { select { case e := <-eventCh: if strings.Contains(e, "joining") { found = true + break OUTER2 } default: - break OUTER2 + if i > 100 { + break OUTER2 + } + time.Sleep(10 * time.Millisecond) } } if !found { From 8dd3e3004aa232fff666327902640bbbbcffcc9f Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 11:00:59 -0700 Subject: [PATCH 002/238] Cutting v0.4.1 --- CHANGELOG.md | 2 +- bench/bench-aws.json | 4 ++-- bench/bench.json | 4 ++-- demo/vagrant-cluster/Vagrantfile | 2 +- terraform/aws/scripts/install.sh | 2 +- version.go | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e66fcfb..6794687db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.1 (Unreleased) +## 0.4.1 (October 17, 2014) FEATURES: diff --git a/bench/bench-aws.json b/bench/bench-aws.json index fd1ee975a..b9623dc64 100644 --- a/bench/bench-aws.json +++ b/bench/bench-aws.json @@ -53,8 +53,8 @@ "sudo mkdir /etc/consul.d", "sudo apt-get update", "sudo apt-get install unzip make", - "wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip", - "unzip 0.4.0_linux_amd64.zip", + "wget https://dl.bintray.com/mitchellh/consul/0.4.1_linux_amd64.zip", + "unzip 0.4.1_linux_amd64.zip", "sudo mv consul /usr/local/bin/consul", "chmod +x /usr/local/bin/consul" ] diff --git a/bench/bench.json b/bench/bench.json index 32a2741f7..7d50d80f0 100644 --- a/bench/bench.json +++ b/bench/bench.json @@ -47,8 +47,8 @@ "mkdir /etc/consul.d", "apt-get update", "apt-get install unzip make", - "wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip", - "unzip 0.4.0_linux_amd64.zip", + "wget https://dl.bintray.com/mitchellh/consul/0.4.1_linux_amd64.zip", + "unzip 0.4.1_linux_amd64.zip", "mv consul /usr/local/bin/consul", "chmod +x /usr/local/bin/consul" ] diff --git a/demo/vagrant-cluster/Vagrantfile b/demo/vagrant-cluster/Vagrantfile index fefcc7085..78270c0e6 100644 --- a/demo/vagrant-cluster/Vagrantfile +++ b/demo/vagrant-cluster/Vagrantfile @@ -7,7 +7,7 @@ sudo apt-get install -y unzip echo Fetching Consul... cd /tmp/ -wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip -O consul.zip +wget https://dl.bintray.com/mitchellh/consul/0.4.1_linux_amd64.zip -O consul.zip echo Installing Consul... unzip consul.zip diff --git a/terraform/aws/scripts/install.sh b/terraform/aws/scripts/install.sh index 3a186a91f..0686b2e23 100644 --- a/terraform/aws/scripts/install.sh +++ b/terraform/aws/scripts/install.sh @@ -10,7 +10,7 @@ sudo apt-get install -y unzip echo "Fetching Consul..." cd /tmp -wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip -O consul.zip +wget https://dl.bintray.com/mitchellh/consul/0.4.1_linux_amd64.zip -O consul.zip echo "Installing Consul..." unzip consul.zip >/dev/null diff --git a/version.go b/version.go index 6b1d30716..c4f705a4a 100644 --- a/version.go +++ b/version.go @@ -9,4 +9,4 @@ const Version = "0.4.1" // A pre-release marker for the version. If this is "" (empty string) // then it means that it is a final release. Otherwise, this is a pre-release // such as "dev" (in development), "beta", "rc1", etc. -const VersionPrerelease = "rc" +const VersionPrerelease = "" From c4951bb59836fa2983711c2f0295f9d7919bf40d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 11:24:10 -0700 Subject: [PATCH 003/238] agent: skip syslog test on windows --- command/agent/syslog_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/command/agent/syslog_test.go b/command/agent/syslog_test.go index f25ec4504..707570319 100644 --- a/command/agent/syslog_test.go +++ b/command/agent/syslog_test.go @@ -1,6 +1,7 @@ package agent import ( + "runtime" "testing" "github.com/hashicorp/go-syslog" @@ -8,6 +9,9 @@ import ( ) func TestSyslogFilter(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } l, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "LOCAL0", "consul") if err != nil { t.Fatalf("err: %s", err) From 0322bf2b7d5d035f304fbf260f03f811a89d8687 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 17 Oct 2014 14:29:12 -0700 Subject: [PATCH 004/238] agent: initialize local consul service tags to fix service sync --- command/agent/agent.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/agent/agent.go b/command/agent/agent.go index 5eb17468e..c3b34f546 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -120,6 +120,7 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { Service: consul.ConsulServiceName, ID: consul.ConsulServiceID, Port: agent.config.Ports.Server, + Tags: []string{}, } agent.state.AddService(&consulService) } else { From 6b119b98f0a96fcf621c0e0ff5256e4e947608a1 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 17 Oct 2014 14:43:52 -0700 Subject: [PATCH 005/238] agent: add test for consul service sync state --- command/agent/agent_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 7304b2229..dff36507b 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" "io" "io/ioutil" "os" @@ -326,8 +327,21 @@ func TestAgent_ConsulService(t *testing.T) { defer os.RemoveAll(dir) defer agent.Shutdown() + testutil.WaitForLeader(t, agent.RPC, "dc1") + + // Consul service is registered services := agent.state.Services() if _, ok := services[consul.ConsulServiceID]; !ok { t.Fatalf("%s service should be registered", consul.ConsulServiceID) } + + // Perform anti-entropy on consul service + if err := agent.state.syncService(consul.ConsulServiceID); err != nil { + t.Fatalf("err: %s", err) + } + + // Consul service should be in sync + if !agent.state.serviceStatus[consul.ConsulServiceID].inSync { + t.Fatalf("%s service should be in sync", consul.ConsulServiceID) + } } From c236900dbae8380cf1b8ff14f1987e5a98f1f3fb Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 17 Oct 2014 17:33:27 -0700 Subject: [PATCH 006/238] agent: test services are in sync when added from the API --- command/agent/catalog_endpoint_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/command/agent/catalog_endpoint_test.go b/command/agent/catalog_endpoint_test.go index ed5b92a2d..5b8b0cc22 100644 --- a/command/agent/catalog_endpoint_test.go +++ b/command/agent/catalog_endpoint_test.go @@ -39,6 +39,17 @@ func TestCatalogRegister(t *testing.T) { if res != true { t.Fatalf("bad: %v", res) } + + // Service should be in sync + if err := srv.agent.state.syncService("foo"); err != nil { + t.Fatalf("err: %s", err) + } + if _, ok := srv.agent.state.serviceStatus["foo"]; !ok { + t.Fatalf("bad: %#v", srv.agent.state.serviceStatus) + } + if !srv.agent.state.serviceStatus["foo"].inSync { + t.Fatalf("should be in sync") + } } func TestCatalogDeregister(t *testing.T) { From b04dc46c728cccdcbd60873567c92bd5d490670c Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 17:40:03 -0700 Subject: [PATCH 007/238] consul: Improving test reliability --- consul/leader_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/consul/leader_test.go b/consul/leader_test.go index 6cad9cd70..75535d56b 100644 --- a/consul/leader_test.go +++ b/consul/leader_test.go @@ -390,10 +390,12 @@ func TestLeader_LeftLeader(t *testing.T) { // Verify the old leader is deregistered state := remain.fsm.State() - _, found, _ := state.GetNode(leader.config.NodeName) - if found { + testutil.WaitForResult(func() (bool, error) { + _, found, _ := state.GetNode(leader.config.NodeName) + return !found, nil + }, func(err error) { t.Fatalf("leader should be deregistered") - } + }) } func TestLeader_MultiBootstrap(t *testing.T) { From 34713fe970a04f7e60b0cd66726a94552d1660f1 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 18:23:13 -0700 Subject: [PATCH 008/238] Encode/Decode test --- consul/structs/structs_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/consul/structs/structs_test.go b/consul/structs/structs_test.go index c025054cc..e5944cbe4 100644 --- a/consul/structs/structs_test.go +++ b/consul/structs/structs_test.go @@ -1 +1,34 @@ package structs + +import ( + "reflect" + "testing" +) + +func TestEncodeDecode(t *testing.T) { + arg := &RegisterRequest{ + Datacenter: "foo", + Node: "bar", + Address: "baz", + Service: &NodeService{ + Service: "test", + }, + } + buf, err := Encode(RegisterRequestType, arg) + if err != nil { + t.Fatalf("err: %v", err) + } + + var out RegisterRequest + err = Decode(buf[1:], &out) + if err != nil { + t.Fatalf("err: %v", err) + } + + if !reflect.DeepEqual(arg.Service, out.Service) { + t.Fatalf("bad: %#v %#v", arg.Service, out.Service) + } + if !reflect.DeepEqual(arg, &out) { + t.Fatalf("bad: %#v %#v", arg, out) + } +} From 3f36515544bd6d8c98b4de68fa7411f34d6b41e9 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 18:26:19 -0700 Subject: [PATCH 009/238] Switching to the pinned version of msgpack --- command/agent/rpc.go | 2 +- command/agent/rpc_client.go | 2 +- command/agent/util.go | 2 +- consul/fsm.go | 2 +- consul/mdb_table_test.go | 2 +- consul/pool.go | 2 +- consul/rpc.go | 2 +- consul/status_endpoint_test.go | 2 +- consul/structs/structs.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 2a6960d1a..784c6c46f 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -26,7 +26,7 @@ import ( "fmt" "github.com/hashicorp/logutils" "github.com/hashicorp/serf/serf" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" "io" "log" "net" diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 9c3522962..a64ec4d53 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -4,7 +4,7 @@ import ( "bufio" "fmt" "github.com/hashicorp/logutils" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" "log" "net" "sync" diff --git a/command/agent/util.go b/command/agent/util.go index c91a6dbc4..f1a88df4e 100644 --- a/command/agent/util.go +++ b/command/agent/util.go @@ -11,7 +11,7 @@ import ( "runtime" "time" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" ) const ( diff --git a/consul/fsm.go b/consul/fsm.go index ec5c72b7b..58cfe5951 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/raft" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" ) // consulFSM implements a finite state machine that is used diff --git a/consul/mdb_table_test.go b/consul/mdb_table_test.go index f67d0cc41..e70c9131f 100644 --- a/consul/mdb_table_test.go +++ b/consul/mdb_table_test.go @@ -3,7 +3,7 @@ package consul import ( "bytes" "github.com/armon/gomdb" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" "io/ioutil" "os" "reflect" diff --git a/consul/pool.go b/consul/pool.go index 84214d2a4..70afb0ff0 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/yamux" "github.com/inconshreveable/muxado" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" ) // msgpackHandle is a shared handle for encoding/decoding of RPC messages diff --git a/consul/rpc.go b/consul/rpc.go index d2d6459e0..1607bd22c 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/yamux" "github.com/inconshreveable/muxado" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" "io" "math/rand" "net" diff --git a/consul/status_endpoint_test.go b/consul/status_endpoint_test.go index 15cb2dcfc..419c174bc 100644 --- a/consul/status_endpoint_test.go +++ b/consul/status_endpoint_test.go @@ -2,7 +2,7 @@ package consul import ( "github.com/hashicorp/consul/testutil" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" "net" "net/rpc" "os" diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 5b3597bba..c2585b132 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -6,7 +6,7 @@ import ( "time" "github.com/hashicorp/consul/acl" - "github.com/ugorji/go/codec" + "github.com/hashicorp/go-msgpack/codec" ) var ( From 32b14c6f0270801aff31cb7ac015eef3858c780d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Fri, 17 Oct 2014 18:48:51 -0700 Subject: [PATCH 010/238] CHANGELOG updates --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6794687db..ccf935218 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.1 (October 17, 2014) +## 0.4.1 (Unreleased) FEATURES: @@ -23,6 +23,7 @@ BUG FIXES: * Serf snapshot compaction works on Windows [GH-332] * Raft snapshots work on Windows [GH-265] * Consul service entry clean by clients now possible + * Fixing improper deserialization IMPROVEMENTS: From c58627df41959ee193895ac4cb6464b4fc49b51a Mon Sep 17 00:00:00 2001 From: Mark Rushakoff Date: Fri, 17 Oct 2014 21:39:44 -0700 Subject: [PATCH 011/238] Install curl The getting started guide mentions has several examples using curl, so preinstall curl on the Vagrant box to make the guide easier to follow. --- demo/vagrant-cluster/Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/vagrant-cluster/Vagrantfile b/demo/vagrant-cluster/Vagrantfile index 78270c0e6..99bb3c5e7 100644 --- a/demo/vagrant-cluster/Vagrantfile +++ b/demo/vagrant-cluster/Vagrantfile @@ -3,7 +3,7 @@ $script = < - + - - + + -<%= yield_content :head %> + <%= yield_content :head %> + - - -"> - + + + + From dc9295fc3fcf2a0385fd4828e96610999c2f5baa Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 18 Oct 2014 19:19:06 -0400 Subject: [PATCH 019/238] Move more javascript into the footer --- website/source/layouts/_footer.erb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/website/source/layouts/_footer.erb b/website/source/layouts/_footer.erb index 0cc18b0ff..222d5b36a 100644 --- a/website/source/layouts/_footer.erb +++ b/website/source/layouts/_footer.erb @@ -23,8 +23,23 @@ + + <%= javascript_include_tag 'application' %> + + + From 26c2bf178810b98fcb1be444705dc5917e9a378f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 18 Oct 2014 19:19:23 -0400 Subject: [PATCH 020/238] Import fonts in CSS instead of HTML --- website/source/assets/stylesheets/application.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/website/source/assets/stylesheets/application.scss b/website/source/assets/stylesheets/application.scss index 4e419870a..7191da1c0 100644 --- a/website/source/assets/stylesheets/application.scss +++ b/website/source/assets/stylesheets/application.scss @@ -2,6 +2,9 @@ @import "bootstrap-sprockets"; @import "bootstrap"; +// Remote fonts +@import url("http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700"); + // Core variables and mixins @import "_variables"; @import "_mixins"; From 36a14019d12490529c9e16d5d7e7a767a02f4961 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sat, 18 Oct 2014 19:36:17 -0400 Subject: [PATCH 021/238] Use a description attribute for HTML descriptions --- website/source/index.html.erb | 4 ++++ website/source/layouts/_header.erb | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/website/source/index.html.erb b/website/source/index.html.erb index 070766ce7..6f09e3356 100644 --- a/website/source/index.html.erb +++ b/website/source/index.html.erb @@ -1,3 +1,7 @@ +--- +description: Service discovery and configuration made easy. Distributed, highly available, and datacenter-aware. +--- +
diff --git a/website/source/layouts/_header.erb b/website/source/layouts/_header.erb index 1e2f4d44c..c364b4255 100644 --- a/website/source/layouts/_header.erb +++ b/website/source/layouts/_header.erb @@ -5,9 +5,7 @@ <%= current_page.data.page_title ? "#{current_page.data.page_title} - " : "" %>Consul - - - + <%= stylesheet_link_tag "application" %> From e265ea050dd9a9b3e4dc6b229bab60a82e7a7e4c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sun, 19 Oct 2014 19:40:10 -0400 Subject: [PATCH 022/238] Use new Markdown syntaxes and add SEO descriptions --- website/source/assets/stylesheets/_docs.scss | 4 + website/source/community.html.erb | 4 +- .../source/docs/agent/basics.html.markdown | 4 +- .../source/docs/agent/checks.html.markdown | 38 +- website/source/docs/agent/dns.html.markdown | 93 +- .../docs/agent/encryption.html.markdown | 6 +- website/source/docs/agent/http.html.markdown | 914 ++++++++++-------- .../source/docs/agent/logging.html.markdown | 2 + .../source/docs/agent/options.html.markdown | 2 + website/source/docs/agent/rpc.html.markdown | 133 ++- .../source/docs/agent/services.html.markdown | 24 +- .../source/docs/agent/telemetry.html.markdown | 5 +- .../source/docs/agent/watches.html.markdown | 325 ++++--- .../source/docs/commands/agent.html.markdown | 2 + .../source/docs/commands/event.html.markdown | 4 +- .../source/docs/commands/exec.html.markdown | 4 +- .../docs/commands/force-leave.html.markdown | 2 + .../source/docs/commands/index.html.markdown | 7 +- .../source/docs/commands/info.html.markdown | 88 +- .../source/docs/commands/join.html.markdown | 4 +- .../source/docs/commands/keygen.html.markdown | 5 +- .../source/docs/commands/leave.html.markdown | 7 +- .../docs/commands/members.html.markdown | 4 +- .../docs/commands/monitor.html.markdown | 4 +- .../source/docs/commands/reload.html.markdown | 4 +- .../source/docs/commands/watch.html.markdown | 4 +- .../source/docs/compatibility.html.markdown | 43 +- .../docs/guides/bootstrapping.html.markdown | 23 +- .../docs/guides/datacenters.html.markdown | 11 +- .../docs/guides/dns-cache.html.markdown | 19 +- .../source/docs/guides/external.html.markdown | 49 +- .../docs/guides/forwarding.html.markdown | 124 +-- .../source/docs/guides/index.html.markdown | 21 +- .../docs/guides/leader-election.html.markdown | 15 +- .../guides/manual-bootstrap.html.markdown | 126 +-- .../source/docs/guides/outage.html.markdown | 28 +- .../source/docs/guides/servers.html.markdown | 44 +- website/source/docs/index.html.markdown | 2 + .../source/docs/internals/acl.html.markdown | 53 +- ...arkdown.erb => architecture.html.markdown} | 9 +- .../docs/internals/consensus.html.markdown | 93 +- .../docs/internals/gossip.html.markdown | 7 +- .../source/docs/internals/index.html.markdown | 10 +- .../docs/internals/jepsen.html.markdown | 5 +- .../docs/internals/security.html.markdown | 7 +- ...ml.markdown.erb => sessions.html.markdown} | 9 +- website/source/docs/upgrading.html.markdown | 6 +- website/source/downloads.html.erb | 2 + website/source/downloads_web_ui.html.erb | 2 + .../intro/getting-started/agent.html.markdown | 17 +- .../getting-started/checks.html.markdown | 13 +- .../getting-started/install.html.markdown | 14 +- .../intro/getting-started/join.html.markdown | 15 +- .../intro/getting-started/kv.html.markdown | 13 +- .../getting-started/next-steps.html.markdown | 3 +- .../getting-started/services.html.markdown | 17 +- ...{ui.html.markdown.erb => ui.html.markdown} | 6 +- website/source/intro/index.html.markdown | 2 + .../source/intro/vs/chef-puppet.html.markdown | 2 + website/source/intro/vs/custom.html.markdown | 31 +- website/source/intro/vs/index.html.markdown | 12 +- .../intro/vs/nagios-sensu.html.markdown | 3 +- website/source/intro/vs/serf.html.markdown | 3 +- website/source/intro/vs/skydns.html.markdown | 2 + .../source/intro/vs/smartstack.html.markdown | 4 +- .../source/intro/vs/zookeeper.html.markdown | 3 +- website/source/robots.txt | 1 + 67 files changed, 1428 insertions(+), 1134 deletions(-) rename website/source/docs/internals/{architecture.html.markdown.erb => architecture.html.markdown} (95%) rename website/source/docs/internals/{sessions.html.markdown.erb => sessions.html.markdown} (93%) rename website/source/intro/getting-started/{ui.html.markdown.erb => ui.html.markdown} (83%) diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 37877f514..02679b6c0 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -201,6 +201,10 @@ body.layout-intro{ #graph { margin-top: 30px; } + + .alert p { + margin-bottom: 0; + } } diff --git a/website/source/community.html.erb b/website/source/community.html.erb index efa81b18c..123721e8a 100644 --- a/website/source/community.html.erb +++ b/website/source/community.html.erb @@ -1,5 +1,7 @@ --- page_title: "Community" +description: |- + Consul is a new project with a growing community. Despite this, there are active, dedicated users willing to help you through various mediums. ---
@@ -77,7 +79,7 @@ page_title: "Community"

- +
diff --git a/website/source/docs/agent/basics.html.markdown b/website/source/docs/agent/basics.html.markdown index b99736b66..5740f3b84 100644 --- a/website/source/docs/agent/basics.html.markdown +++ b/website/source/docs/agent/basics.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Agent" sidebar_current: "docs-agent-running" +description: |- + The Consul agent is the core process of Consul. The agent maintains membership information, registers services, runs checks, responds to queries and more. The agent must run on every node that is part of a Consul cluster. --- # Consul Agent @@ -25,7 +27,7 @@ running forever or until told to quit. The agent command takes a variety of configuration options but the defaults are usually good enough. When running `consul agent`, you should see output similar to that below: -``` +```text $ consul agent -data-dir=/tmp/consul ==> Starting Consul agent... ==> Starting Consul agent RPC... diff --git a/website/source/docs/agent/checks.html.markdown b/website/source/docs/agent/checks.html.markdown index 0fa9d1915..33714d3bc 100644 --- a/website/source/docs/agent/checks.html.markdown +++ b/website/source/docs/agent/checks.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Check Definition" sidebar_current: "docs-agent-checks" +description: |- + One of the primary roles of the agent is the management of system and application level health checks. A health check is considered to be application level if it associated with a service. A check is defined in a configuration file, or added at runtime over the HTTP interface. --- # Checks @@ -30,25 +32,29 @@ There are two different kinds of checks: A check definition that is a script looks like: - { - "check": { - "id": "mem-util", - "name": "Memory utilization", - "script": "/usr/local/bin/check_mem.py", - "interval": "10s" - } - } +```javascript +{ + "check": { + "id": "mem-util", + "name": "Memory utilization", + "script": "/usr/local/bin/check_mem.py", + "interval": "10s" + } +} +``` A TTL based check is very similar: - { - "check": { - "id": "web-app", - "name": "Web App Status", - "notes": "Web app does a curl internally every 10 seconds", - "ttl": "30s" - } - } +```javascript +{ + "check": { + "id": "web-app", + "name": "Web App Status", + "notes": "Web app does a curl internally every 10 seconds", + "ttl": "30s" + } +} +``` Both types of definitions must include a `name`, and may optionally provide an `id` and `notes` field. The `id` is set to the `name` if not diff --git a/website/source/docs/agent/dns.html.markdown b/website/source/docs/agent/dns.html.markdown index 13021dbec..b1afc0f8c 100644 --- a/website/source/docs/agent/dns.html.markdown +++ b/website/source/docs/agent/dns.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "DNS Interface" sidebar_current: "docs-agent-dns" +description: |- + One of the primary query interfaces for Consul is using DNS. The DNS interface allows applications to make use of service discovery without any high-touch integration with Consul. --- # DNS Interface @@ -50,25 +52,26 @@ DNS lookup for nodes in other datacenters, with no additional effort. For a node lookup, the only records returned are A records with the IP address of the node. - $ dig @127.0.0.1 -p 8600 foobar.node.consul ANY +```text +$ dig @127.0.0.1 -p 8600 foobar.node.consul ANY - ; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 foobar.node.consul ANY - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24355 - ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 0 - ;; WARNING: recursion requested but not available +; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 foobar.node.consul ANY +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24355 +;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 0 +;; WARNING: recursion requested but not available - ;; QUESTION SECTION: - ;foobar.node.consul. IN ANY +;; QUESTION SECTION: +;foobar.node.consul. IN ANY - ;; ANSWER SECTION: - foobar.node.consul. 0 IN A 10.1.10.12 - - ;; AUTHORITY SECTION: - consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 +;; ANSWER SECTION: +foobar.node.consul. 0 IN A 10.1.10.12 +;; AUTHORITY SECTION: +consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 +``` ## Service Lookups @@ -100,24 +103,26 @@ provide the port that a service is registered on, enabling services to avoid rel on well-known ports. SRV records are only served if the client specifically requests SRV records. - $ dig @127.0.0.1 -p 8600 consul.service.consul SRV +```text +$ dig @127.0.0.1 -p 8600 consul.service.consul SRV - ; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 consul.service.consul ANY - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50483 - ;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 1, ADDITIONAL: 1 - ;; WARNING: recursion requested but not available +; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 consul.service.consul ANY +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50483 +;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 1, ADDITIONAL: 1 +;; WARNING: recursion requested but not available - ;; QUESTION SECTION: - ;consul.service.consul. IN SRV +;; QUESTION SECTION: +;consul.service.consul. IN SRV - ;; ANSWER SECTION: - consul.service.consul. 0 IN SRV 1 1 8300 foobar.node.dc1.consul. +;; ANSWER SECTION: +consul.service.consul. 0 IN SRV 1 1 8300 foobar.node.dc1.consul. - ;; ADDITIONAL SECTION: - foobar.node.dc1.consul. 0 IN A 10.1.10.12 +;; ADDITIONAL SECTION: +foobar.node.dc1.consul. 0 IN A 10.1.10.12 +``` ### RFC-2782 Style Lookup @@ -138,24 +143,26 @@ Using the RCF style lookup, If you registered the service "rabbitmq" on port 5672 and tagged it with "amqp" you would query the SRV record as "_rabbitmq._amqp.service.consul" as illustrated in the example below: - $ dig @127.0.0.1 -p 8600 _rabbitmq._amqp.service.consul SRV +```text +$ dig @127.0.0.1 -p 8600 _rabbitmq._amqp.service.consul SRV - ; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 _rabbitmq._amqp.service.consul ANY - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52838 - ;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 - ;; WARNING: recursion requested but not available +; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 _rabbitmq._amqp.service.consul ANY +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 52838 +;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 +;; WARNING: recursion requested but not available - ;; QUESTION SECTION: - ;_rabbitmq._amqp.service.consul. IN SRV +;; QUESTION SECTION: +;_rabbitmq._amqp.service.consul. IN SRV - ;; ANSWER SECTION: - _rabbitmq._amqp.service.consul. 0 IN SRV 1 1 5672 rabbitmq.node1.dc1.consul. +;; ANSWER SECTION: +_rabbitmq._amqp.service.consul. 0 IN SRV 1 1 5672 rabbitmq.node1.dc1.consul. - ;; ADDITIONAL SECTION: - rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 +;; ADDITIONAL SECTION: +rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 +``` ### UDP Based DNS Queries diff --git a/website/source/docs/agent/encryption.html.markdown b/website/source/docs/agent/encryption.html.markdown index 0b81ee98a..281dfcf39 100644 --- a/website/source/docs/agent/encryption.html.markdown +++ b/website/source/docs/agent/encryption.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Encryption" sidebar_current: "docs-agent-encryption" +description: |- + The Consul agent supports encrypting all of its network traffic. The exact method of this encryption is described on the encryption internals page. There are two seperate systems, one for gossip traffic and one for RPC. --- # Encryption @@ -19,7 +21,7 @@ in a configuration file for the agent. The key must be 16-bytes that are base64 encoded. The easiest method to obtain a cryptographically suitable key is by using `consul keygen`. -``` +```text $ consul keygen cg8StVXbQJ0gPvMd9o7yrg== ``` @@ -27,7 +29,7 @@ cg8StVXbQJ0gPvMd9o7yrg== With that key, you can enable encryption on the agent. You can verify encryption is enabled because the output will include "Encrypted: true". -``` +```text $ cat encrypt.json {"encrypt": "cg8StVXbQJ0gPvMd9o7yrg=="} diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index da1d070cb..e65422f94 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "HTTP API" sidebar_current: "docs-agent-http" +description: |- + The main interface to Consul is a RESTful HTTP API. The API can be used for CRUD for nodes, services, checks, and configuration. The endpoints are versioned to enable changes without breaking backwards compatibility. --- # HTTP API @@ -120,17 +122,19 @@ all keys with the given prefix. Each object will look like: - [ - { - "CreateIndex": 100, - "ModifyIndex": 200, - "LockIndex": 200, - "Key": "zip", - "Flags": 0, - "Value": "dGVzdA==", - "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" - } - ] +```javascript +[ + { + "CreateIndex": 100, + "ModifyIndex": 200, + "LockIndex": 200, + "Key": "zip", + "Flags": 0, + "Value": "dGVzdA==", + "Session": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" + } +] +``` The `CreateIndex` is the internal index value that represents when the entry was created. The `ModifyIndex` is the last index @@ -153,11 +157,13 @@ can be used to list only up to a given separator. For example, listing "/web/" with a "/" seperator may return: - [ - "/web/bar", - "/web/foo", - "/web/subdir/" - ] +```javascript +[ + "/web/bar", + "/web/foo", + "/web/subdir/" +] +``` Using the key listing method may be suitable when you do not need the values or flags, or want to implement a key-space explorer. @@ -242,18 +248,20 @@ anti-entropy, so in most situations everything will be in sync within a few seco This endpoint is hit with a GET and returns a JSON body like this: - { - "service:redis": { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - } - } +```javascript +{ + "service:redis": { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + } +} +``` ### /v1/agent/services @@ -266,14 +274,16 @@ anti-entropy, so in most situations everything will be in sync within a few seco This endpoint is hit with a GET and returns a JSON body like this: - { - "redis": { - "ID": "redis", - "Service": "redis", - "Tags": null, - "Port": 8000 - } - } +```javascript +{ + "redis": { + "ID": "redis", + "Service": "redis", + "Tags": null, + "Port": 8000 + } +} +``` ### /v1/agent/members @@ -287,26 +297,28 @@ the list of WAN members instead of the LAN members which is default. This endpoint returns a JSON body like: - [ - { - "Name": "foobar", - "Addr": "10.1.10.12", - "Port": 8301, - "Tags": { - "bootstrap": "1", - "dc": "dc1", - "port": "8300", - "role": "consul" - }, - "Status": 1, - "ProtocolMin": 1, - "ProtocolMax": 2, - "ProtocolCur": 2, - "DelegateMin": 1, - "DelegateMax": 3, - "DelegateCur": 3 - } - ] +```javascript +[ + { + "Name": "foobar", + "Addr": "10.1.10.12", + "Port": 8301, + "Tags": { + "bootstrap": "1", + "dc": "dc1", + "port": "8300", + "role": "consul" + }, + "Status": 1, + "ProtocolMin": 1, + "ProtocolMax": 2, + "ProtocolCur": 2, + "DelegateMin": 1, + "DelegateMax": 3, + "DelegateCur": 3 + } +] +``` ### /v1/agent/self @@ -314,65 +326,67 @@ This endpoint is used to return configuration of the local agent and member info It returns a JSON body like this: - { - "Config": { - "Bootstrap": true, - "Server": true, - "Datacenter": "dc1", - "DataDir": "/tmp/consul", - "DNSRecursor": "", - "Domain": "consul.", - "LogLevel": "INFO", - "NodeName": "foobar", - "ClientAddr": "127.0.0.1", - "BindAddr": "0.0.0.0", - "AdvertiseAddr": "10.1.10.12", - "Ports": { - "DNS": 8600, - "HTTP": 8500, - "RPC": 8400, - "SerfLan": 8301, - "SerfWan": 8302, - "Server": 8300 - }, - "LeaveOnTerm": false, - "SkipLeaveOnInt": false, - "StatsiteAddr": "", - "Protocol": 1, - "EnableDebug": false, - "VerifyIncoming": false, - "VerifyOutgoing": false, - "CAFile": "", - "CertFile": "", - "KeyFile": "", - "StartJoin": [], - "UiDir": "", - "PidFile": "", - "EnableSyslog": false, - "RejoinAfterLeave": false - }, - "Member": { - "Name": "foobar", - "Addr": "10.1.10.12", - "Port": 8301, - "Tags": { - "bootstrap": "1", - "dc": "dc1", - "port": "8300", - "role": "consul", - "vsn": "1", - "vsn_max": "1", - "vsn_min": "1" - }, - "Status": 1, - "ProtocolMin": 1, - "ProtocolMax": 2, - "ProtocolCur": 2, - "DelegateMin": 2, - "DelegateMax": 4, - "DelegateCur": 4 - } - } +```javascript +{ + "Config": { + "Bootstrap": true, + "Server": true, + "Datacenter": "dc1", + "DataDir": "/tmp/consul", + "DNSRecursor": "", + "Domain": "consul.", + "LogLevel": "INFO", + "NodeName": "foobar", + "ClientAddr": "127.0.0.1", + "BindAddr": "0.0.0.0", + "AdvertiseAddr": "10.1.10.12", + "Ports": { + "DNS": 8600, + "HTTP": 8500, + "RPC": 8400, + "SerfLan": 8301, + "SerfWan": 8302, + "Server": 8300 + }, + "LeaveOnTerm": false, + "SkipLeaveOnInt": false, + "StatsiteAddr": "", + "Protocol": 1, + "EnableDebug": false, + "VerifyIncoming": false, + "VerifyOutgoing": false, + "CAFile": "", + "CertFile": "", + "KeyFile": "", + "StartJoin": [], + "UiDir": "", + "PidFile": "", + "EnableSyslog": false, + "RejoinAfterLeave": false + }, + "Member": { + "Name": "foobar", + "Addr": "10.1.10.12", + "Port": 8301, + "Tags": { + "bootstrap": "1", + "dc": "dc1", + "port": "8300", + "role": "consul", + "vsn": "1", + "vsn_max": "1", + "vsn_min": "1" + }, + "Status": 1, + "ProtocolMin": 1, + "ProtocolMax": 2, + "ProtocolCur": 2, + "DelegateMin": 2, + "DelegateMax": 4, + "DelegateCur": 4 + } +} +``` ### /v1/agent/join/\ @@ -401,14 +415,16 @@ the status of the check and keeping the Catalog in sync. The register endpoint expects a JSON request body to be PUT. The request body must look like: - { - "ID": "mem", - "Name": "Memory utilization", - "Notes": "Ensure we don't oversubscribe memory", - "Script": "/usr/local/bin/check_mem.py", - "Interval": "10s", - "TTL": "15s" - } +```javascript +{ + "ID": "mem", + "Name": "Memory utilization", + "Notes": "Ensure we don't oversubscribe memory", + "Script": "/usr/local/bin/check_mem.py", + "Interval": "10s", + "TTL": "15s" +} +``` The `Name` field is mandatory, as is either `Script` and `Interval` or `TTL`. Only one of `Script` and `Interval` or `TTL` should be provided. @@ -474,20 +490,22 @@ the status of the check and keeping the Catalog in sync. The register endpoint expects a JSON request body to be PUT. The request body must look like: - { - "ID": "redis1", - "Name": "redis", - "Tags": [ - "master", - "v1" - ], - "Port": 8000, - "Check": { - "Script": "/usr/local/bin/check_redis.py", - "Interval": "10s", - "TTL": "15s" - } - } +```javascript +{ + "ID": "redis1", + "Name": "redis", + "Tags": [ + "master", + "v1" + ], + "Port": 8000, + "Check": { + "Script": "/usr/local/bin/check_redis.py", + "Interval": "10s", + "TTL": "15s" + } +} +``` The `Name` field is mandatory, If an `ID` is not provided, it is set to `Name`. You cannot have duplicate `ID` entries per agent, so it may be necessary to provide an ID. @@ -534,28 +552,30 @@ the agent local endpoints, as they are simpler and perform anti-entropy. The register endpoint expects a JSON request body to be PUT. The request body must look like: - { - "Datacenter": "dc1", - "Node": "foobar", - "Address": "192.168.10.10", - "Service": { - "ID": "redis1", - "Service": "redis", - "Tags": [ - "master", - "v1" - ], - "Port": 8000 - }, - "Check": { - "Node": "foobar", - "CheckID": "service:redis1", - "Name": "Redis health check", - "Notes": "Script based health check", - "Status": "passing", - "ServiceID": "redis1" - } - } +```javascript +{ + "Datacenter": "dc1", + "Node": "foobar", + "Address": "192.168.10.10", + "Service": { + "ID": "redis1", + "Service": "redis", + "Tags": [ + "master", + "v1" + ], + "Port": 8000 + }, + "Check": { + "Node": "foobar", + "CheckID": "service:redis1", + "Name": "Redis health check", + "Notes": "Script based health check", + "Status": "passing", + "ServiceID": "redis1" + } +} +``` The behavior of the endpoint depends on what keys are provided. The endpoint requires `Node` and `Address` to be provided, while `Datacenter` will be defaulted @@ -593,22 +613,28 @@ endpoints, as they are simpler and perform anti-entropy. The deregister endpoint expects a JSON request body to be PUT. The request body must look like one of the following: - { - "Datacenter": "dc1", - "Node": "foobar", - } +```javascript +{ + "Datacenter": "dc1", + "Node": "foobar", +} +``` - { - "Datacenter": "dc1", - "Node": "foobar", - "CheckID": "service:redis1" - } +```javascript +{ + "Datacenter": "dc1", + "Node": "foobar", + "CheckID": "service:redis1" +} +``` - { - "Datacenter": "dc1", - "Node": "foobar", - "ServiceID": "redis1", - } +```javascript +{ + "Datacenter": "dc1", + "Node": "foobar", + "ServiceID": "redis1", +} +``` The behavior of the endpoint depends on what keys are provided. The endpoint requires `Node` to be provided, while `Datacenter` will be defaulted @@ -627,7 +653,9 @@ datacenters that are known by the Consul server. It returns a JSON body like this: - ["dc1", "dc2"] +```javascript +["dc1", "dc2"] +``` This endpoint does not require a cluster leader, and as such will succeed even during an availability outage. It can thus be @@ -641,16 +669,18 @@ however the dc can be provided using the "?dc=" query parameter. It returns a JSON body like this: - [ - { - "Node": "baz", - "Address": "10.1.10.11" - }, - { - "Node": "foobar", - "Address": "10.1.10.12" - } - ] +```javascript +[ + { + "Node": "baz", + "Address": "10.1.10.11" + }, + { + "Node": "foobar", + "Address": "10.1.10.12" + } +] +``` This endpoint supports blocking queries and all consistency modes. @@ -662,14 +692,16 @@ however the dc can be provided using the "?dc=" query parameter. It returns a JSON body like this: - { - "consul": [], - "redis": [], - "postgresql": [ - "master", - "slave" - ] - } +```javascript +{ + "consul": [], + "redis": [], + "postgresql": [ + "master", + "slave" + ] +} +``` The main object keys are the service names, while the array provides all the known tags for a given service. @@ -688,16 +720,18 @@ by tag using the "?tag=" query parameter. It returns a JSON body like this: - [ - { - "Node": "foobar", - "Address": "10.1.10.12", - "ServiceID": "redis", - "ServiceName": "redis", - "ServiceTags": null, - "ServicePort": 8000 - } - ] +```javascript +[ + { + "Node": "foobar", + "Address": "10.1.10.12", + "ServiceID": "redis", + "ServiceName": "redis", + "ServiceTags": null, + "ServicePort": 8000 + } +] +``` This endpoint supports blocking queries and all consistency modes. @@ -710,28 +744,30 @@ The node being queried must be provided after the slash. It returns a JSON body like this: - { - "Node": { - "Node": "foobar", - "Address": "10.1.10.12" - }, - "Services": { - "consul": { - "ID": "consul", - "Service": "consul", - "Tags": null, - "Port": 8300 - }, - "redis": { - "ID": "redis", - "Service": "redis", - "Tags": [ - "v1" - ], - "Port": 8000 - } - } +```javascript +{ + "Node": { + "Node": "foobar", + "Address": "10.1.10.12" + }, + "Services": { + "consul": { + "ID": "consul", + "Service": "consul", + "Tags": null, + "Port": 8300 + }, + "redis": { + "ID": "redis", + "Service": "redis", + "Tags": [ + "v1" + ], + "Port": 8000 } + } +} +``` This endpoint supports blocking queries and all consistency modes. @@ -759,28 +795,30 @@ The node being queried must be provided after the slash. It returns a JSON body like this: - [ - { - "Node": "foobar", - "CheckID": "serfHealth", - "Name": "Serf Health Status", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "", - "ServiceName": "" - }, - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - } - ] +```javascript +[ + { + "Node": "foobar", + "CheckID": "serfHealth", + "Name": "Serf Health Status", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "", + "ServiceName": "" + }, + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + } +] +``` In this case, we can see there is a system level check (no associated `ServiceID`, as well as a service check for Redis). The "serfHealth" check @@ -801,18 +839,20 @@ The service being queried must be provided after the slash. It returns a JSON body like this: - [ - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - } - ] +```javascript +[ + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + } +] +``` This endpoint supports blocking queries and all consistency modes. @@ -841,42 +881,44 @@ by incorporating the use of health checks. It returns a JSON body like this: - [ - { - "Node": { - "Node": "foobar", - "Address": "10.1.10.12" - }, - "Service": { - "ID": "redis", - "Service": "redis", - "Tags": null, - "Port": 8000 - }, - "Checks": [ - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - }, - { - "Node": "foobar", - "CheckID": "serfHealth", - "Name": "Serf Health Status", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "", - "ServiceName": "" - } - ] - } +```javascript +[ + { + "Node": { + "Node": "foobar", + "Address": "10.1.10.12" + }, + "Service": { + "ID": "redis", + "Service": "redis", + "Tags": null, + "Port": 8000 + }, + "Checks": [ + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + }, + { + "Node": "foobar", + "CheckID": "serfHealth", + "Name": "Serf Health Status", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "", + "ServiceName": "" + } ] + } +] +``` This endpoint supports blocking queries and all consistency modes. @@ -892,28 +934,30 @@ a wildcard that can be used to return all the checks. It returns a JSON body like this: - [ - { - "Node": "foobar", - "CheckID": "serfHealth", - "Name": "Serf Health Status", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "", - "ServiceName": "" - }, - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - } - ] +```javascript +[ + { + "Node": "foobar", + "CheckID": "serfHealth", + "Name": "Serf Health Status", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "", + "ServiceName": "" + }, + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + } +] +``` This endpoint supports blocking queries and all consistency modes. @@ -945,12 +989,14 @@ to use cross-region sessions. The create endpoint expects a JSON request body to be PUT. The request body must look like: - { - "LockDelay": "15s", - "Name": "my-service-lock", - "Node": "foobar", - "Checks": ["a", "b", "c"] - } +```javascript +{ + "LockDelay": "15s", + "Name": "my-service-lock", + "Node": "foobar", + "Checks": ["a", "b", "c"] +} +``` None of the fields are mandatory, and in fact no body needs to be PUT if the defaults are to be used. The `LockDelay` field can be specified @@ -966,7 +1012,11 @@ It is highly recommended that if you override this list, you include that check. The return code is 200 on success, along with a body like: - {"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"} +```javascript +{ + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" +} +``` This is used to provide the ID of the newly created session. @@ -988,17 +1038,19 @@ The session being queried must be provided after the slash. It returns a JSON body like this: - [ - { - "LockDelay": 1.5e+10, - "Checks": [ - "serfHealth" - ], - "Node": "foobar", - "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", - "CreateIndex": 1086449 - } - ] +```javascript +[ + { + "LockDelay": 1.5e+10, + "Checks": [ + "serfHealth" + ], + "Node": "foobar", + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", + "CreateIndex": 1086449 + } +] +``` If the session is not found, null is returned instead of a JSON list. This endpoint supports blocking queries and all consistency modes. @@ -1012,18 +1064,20 @@ The node being queried must be provided after the slash. It returns a JSON body like this: - [ - { - "LockDelay": 1.5e+10, - "Checks": [ - "serfHealth" - ], - "Node": "foobar", - "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", - "CreateIndex": 1086449 - }, - ... - ] +```javascript +[ + { + "LockDelay": 1.5e+10, + "Checks": [ + "serfHealth" + ], + "Node": "foobar", + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", + "CreateIndex": 1086449 + }, + ... +] +``` This endpoint supports blocking queries and all consistency modes. @@ -1035,18 +1089,20 @@ however the dc can be provided using the "?dc=" query parameter. It returns a JSON body like this: - [ - { - "LockDelay": 1.5e+10, - "Checks": [ - "serfHealth" - ], - "Node": "foobar", - "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", - "CreateIndex": 1086449 - }, - ... - ] +```javascript +[ + { + "LockDelay": 1.5e+10, + "Checks": [ + "serfHealth" + ], + "Node": "foobar", + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e", + "CreateIndex": 1086449 + }, + ... +] +``` This endpoint supports blocking queries and all consistency modes. @@ -1080,11 +1136,13 @@ of the agent that the request is made to. The create endpoint expects a JSON request body to be PUT. The request body must look like: - { - "Name": "my-app-token", - "Type": "client", - "Rules": "" - } +```javascript +{ + "Name": "my-app-token", + "Type": "client", + "Rules": "" +} +``` None of the fields are mandatory, and in fact no body needs to be PUT if the defaults are to be used. The `Name` and `Rules` default to being @@ -1093,7 +1151,11 @@ blank, and the `Type` defaults to "client". The format of `Rules` is The return code is 200 on success, along with a body like: - {"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"} +```javascript +{ + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" +} +``` This is used to provide the ID of the newly created ACL token. @@ -1112,12 +1174,14 @@ of the agent that the request is made to. The update endpoint expects a JSON request body to be PUT. The request body must look like: - { - "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" - "Name": "my-app-token-updated", - "Type": "client", - "Rules": "# New Rules", - } +```javascript +{ + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" + "Name": "my-app-token-updated", + "Type": "client", + "Rules": "# New Rules", +} +``` Only the `ID` field is mandatory, the other fields provide defaults. The `Name` and `Rules` default to being blank, and the `Type` defaults to "client". @@ -1142,16 +1206,18 @@ The token being queried must be provided after the slash. It returns a JSON body like this: - [ - { - "CreateIndex":3, - "ModifyIndex":3, - "ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05", - "Name":"Client Token", - "Type":"client", - "Rules":"..." - } - ] +```javascript +[ + { + "CreateIndex": 3, + "ModifyIndex": 3, + "ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + "Name": "Client Token", + "Type": "client", + "Rules": "..." + } +] +``` If the session is not found, null is returned instead of a JSON list. @@ -1165,7 +1231,11 @@ after the slash. Requests to this endpoint require a management token. The return code is 200 on success, along with a body like: - {"ID":"adf4238a-882b-9ddc-4a9d-5b6758e4159e"} +```javascript +{ + "ID": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" +} +``` This is used to provide the ID of the newly created ACL token. @@ -1177,17 +1247,19 @@ management token. It returns a JSON body like this: - [ - { - "CreateIndex":3, - "ModifyIndex":3, - "ID":"8f246b77-f3e1-ff88-5b48-8ec93abf3e05", - "Name":"Client Token", - "Type":"client", - "Rules":"..." - }, - ... - ] +```javascript +[ + { + "CreateIndex": 3, + "ModifyIndex": 3, + "ID": "8f246b77-f3e1-ff88-5b48-8ec93abf3e05", + "Name": "Client Token", + "Type": "client", + "Rules": "..." + }, + ... +] +``` ## Event @@ -1218,16 +1290,18 @@ by node name, service, and service tags. The return code is 200 on success, along with a body like: - { - "ID":"b54fe110-7af5-cafc-d1fb-afc8ba432b1c", - "Name":"deploy", - "Payload":null, - "NodeFilter":"", - "ServiceFilter":"", - "TagFilter":"", - "Version":1, - "LTime":0 - } +```javascript +{ + "ID": "b54fe110-7af5-cafc-d1fb-afc8ba432b1c", + "Name": "deploy", + "Payload": null, + "NodeFilter": "", + "ServiceFilter": "", + "TagFilter": "", + "Version": 1, + "LTime": 0 +} +``` This is used to provide the ID of the newly fired event. @@ -1267,19 +1341,21 @@ enough for most clients and watches. It returns a JSON body like this: - [ - { - "ID": "b54fe110-7af5-cafc-d1fb-afc8ba432b1c", - "Name": "deploy", - "Payload": "MTYwOTAzMA=="", - "NodeFilter": "", - "ServiceFilter": "", - "TagFilter": "", - "Version": 1, - "LTime": 19 - }, - ... - ] +```javascript +[ + { + "ID": "b54fe110-7af5-cafc-d1fb-afc8ba432b1c", + "Name": "deploy", + "Payload": "MTYwOTAzMA==", + "NodeFilter": "", + "ServiceFilter": "", + "TagFilter": "", + "Version": 1, + "LTime": 19 + }, + ... +] +``` ## Status @@ -1297,11 +1373,19 @@ The following endpoints are supported: This endpoint is used to get the Raft leader for the datacenter the agent is running in. It returns only an address like: - "10.1.10.12:8300" +```text +"10.1.10.12:8300" +``` ### /v1/status/peers This endpoint is used to get the Raft peers for the datacenter the agent is running in. It returns a list of addresses like: - ["10.1.10.12:8300", "10.1.10.11:8300", "10.1.10.10:8300"] +```javascript +[ + "10.1.10.12:8300", + "10.1.10.11:8300", + "10.1.10.10:8300" +] +``` diff --git a/website/source/docs/agent/logging.html.markdown b/website/source/docs/agent/logging.html.markdown index 9af098726..f7aedcf07 100644 --- a/website/source/docs/agent/logging.html.markdown +++ b/website/source/docs/agent/logging.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Logging" sidebar_current: "docs-agent" +description: |- + TODO --- # Logging diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 1dc1b5827..32f7e53f1 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Configuration" sidebar_current: "docs-agent-config" +description: |- + The agent has various configuration options that can be specified via the command-line or via configuration files. All of the configuration options are completely optional and their defaults will be specified with their descriptions. --- # Configuration diff --git a/website/source/docs/agent/rpc.html.markdown b/website/source/docs/agent/rpc.html.markdown index 293040a99..3272956cc 100644 --- a/website/source/docs/agent/rpc.html.markdown +++ b/website/source/docs/agent/rpc.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "RPC" sidebar_current: "docs-agent-rpc" +description: |- + The Consul agent provides a complete RPC mechanism that can be used to control the agent programmatically. This RPC mechanism is the same one used by the CLI, but can be used by other applications to easily leverage the power of Consul without directly embedding. --- # RPC Protocol @@ -24,15 +26,21 @@ that is broadly available across languages. All RPC requests have a request header, and some requests have a request body. The request header looks like: -``` - {"Command": "Handshake", "Seq": 0} +```javascript +{ + "Command": "Handshake", + "Seq": 0 +} ``` All responses have a response header, and some may contain a response body. The response header looks like: -``` - {"Seq": 0, "Error": ""} +```javascript +{ + "Seq": 0, + "Error": "" +} ``` The `Command` is used to specify what command the server should @@ -65,8 +73,10 @@ the server which version the client is using. The request header must be followed with a handshake body, like: -``` - {"Version": 1} +```javascript +{ + "Version": 1 +} ``` The body specifies the IPC version being used, however only version @@ -81,8 +91,10 @@ response and check for an error. This command is used to remove failed nodes from a cluster. It takes the following body: -``` - {"Node": "failed-node-name"} +```javascript +{ + "Node": "failed-node-name" +} ``` There is no special response body. @@ -92,8 +104,14 @@ There is no special response body. This command is used to join an existing cluster using a known node. It takes the following body: -``` - {"Existing": ["192.168.0.1:6000", "192.168.0.2:6000"], "WAN": false} +```javascript +{ + "Existing": [ + "192.168.0.1:6000", + "192.168.0.2:6000" + ], + "WAN": false +} ``` The `Existing` nodes are each contacted, and `WAN` controls if we are adding a @@ -105,8 +123,10 @@ WAN. The response body in addition to the header is returned. The body looks like: -``` - {"Num": 2} +```javascript +{ + "Num": 2 +} ``` The body returns the number of nodes successfully joined. @@ -118,25 +138,27 @@ information. All agents will respond to this command. There is no request body, but the response looks like: -``` - {"Members": [ - { - "Name": "TestNode" - "Addr": [127, 0, 0, 1], - "Port": 5000, - "Tags": { - "role": "test" - }, - "Status": "alive", - "ProtocolMin": 0, - "ProtocolMax": 3, - "ProtocolCur": 2, - "DelegateMin": 0, - "DelegateMax": 1, - "DelegateCur": 1, - }, - ...] - } +```javascript +{ + "Members": [ + { + "Name": "TestNode" + "Addr": [127, 0, 0, 1], + "Port": 5000, + "Tags": { + "role": "test" + }, + "Status": "alive", + "ProtocolMin": 0, + "ProtocolMax": 3, + "ProtocolCur": 2, + "DelegateMin": 0, + "DelegateMax": 1, + "DelegateCur": 1, + }, + ... + ] +} ``` ### members-wan @@ -152,8 +174,10 @@ The monitor command subscribes the channel to log messages from the Agent. The request is like: -``` - {"LogLevel": "DEBUG"} +```javascript +{ + "LogLevel": "DEBUG" +} ``` This subscribes the client to all messages of at least DEBUG level. @@ -165,9 +189,15 @@ the same `Seq` as the monitor command that matches. Assume we issued the previous monitor command with Seq `50`, we may start getting messages like: -``` - {"Seq": 50, "Error": ""} - {"Log": "2013/12/03 13:06:53 [INFO] agent: Received event: member-join"} +```javascript +{ + "Seq": 50, + "Error": "" +} + +{ + "Log": "2013/12/03 13:06:53 [INFO] agent: Received event: member-join" +} ``` It is important to realize that these messages are sent asynchronously, @@ -184,8 +214,10 @@ To stop streaming, the `stop` command is used. The stop command is used to stop a monitor. The request looks like: -``` - {"Stop": 50} +```javascript +{ + "Stop": 50 +} ``` This unsubscribes the client from the monitor with `Seq` value of 50. @@ -202,22 +234,21 @@ There is no request body, or special response body. The stats command is used to provide operator information for debugging. There is no request body, the response body looks like: -``` - { - "agent": { - "check_monitors": 0, - ... - }, - "consul: { - "server": "true", - ... - }, - ... - } +```javascript +{ + "agent": { + "check_monitors": 0, + ... + }, + "consul: { + "server": "true", + ... + }, + ... +} ``` ### reload The reload command is used trigger a reload of configurations. There is no request body, or special response body. - diff --git a/website/source/docs/agent/services.html.markdown b/website/source/docs/agent/services.html.markdown index a047582a4..3e99fb855 100644 --- a/website/source/docs/agent/services.html.markdown +++ b/website/source/docs/agent/services.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Service Definition" sidebar_current: "docs-agent-services" +description: |- + One of the main goals of service discovery is to provide a catalog of available services. To that end, the agent provides a simple service definition format to declare the availability of a service, and to potentially associate it with a health check. A health check is considered to be application level if it associated with a service. A service is defined in a configuration file, or added at runtime over the HTTP interface. --- # Services @@ -17,17 +19,19 @@ or added at runtime over the HTTP interface. A service definition that is a script looks like: - { - "service": { - "name": "redis", - "tags": ["master"], - "port": 8000, - "check": { - "script": "/usr/local/bin/check_redis.py", - "interval": "10s" - } - } +```javascript +{ + "service": { + "name": "redis", + "tags": ["master"], + "port": 8000, + "check": { + "script": "/usr/local/bin/check_redis.py", + "interval": "10s" } + } +} +``` A service definition must include a `name`, and may optionally provide an `id`, `tags`, `port`, and `check`. The `id` is set to the `name` if not diff --git a/website/source/docs/agent/telemetry.html.markdown b/website/source/docs/agent/telemetry.html.markdown index 1035304cc..6f20d59be 100644 --- a/website/source/docs/agent/telemetry.html.markdown +++ b/website/source/docs/agent/telemetry.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Telemetry" sidebar_current: "docs-agent-telemetry" +description: |- + The Consul agent collects various metrics data at runtime about the performance of different libraries and sub-systems. These metrics are aggregated on a ten second interval and are retained for one minute. --- # Telemetry @@ -25,7 +27,7 @@ aggregate and flushed to Graphite or any other metrics store. Below is an example output: -``` +```text [2014-01-29 10:56:50 -0800 PST][G] 'consul-agent.runtime.num_goroutines': 19.000 [2014-01-29 10:56:50 -0800 PST][G] 'consul-agent.runtime.alloc_bytes': 755960.000 [2014-01-29 10:56:50 -0800 PST][G] 'consul-agent.runtime.malloc_count': 7550.000 @@ -42,4 +44,3 @@ Below is an example output: [2014-01-29 10:56:50 -0800 PST][S] 'consul-agent.serf.queue.Intent': Count: 10 Sum: 0.000 [2014-01-29 10:56:50 -0800 PST][S] 'consul-agent.serf.queue.Event': Count: 10 Min: 0.000 Mean: 2.500 Max: 5.000 Stddev: 2.121 Sum: 25.000 ``` - diff --git a/website/source/docs/agent/watches.html.markdown b/website/source/docs/agent/watches.html.markdown index 6886a6918..b5dd857dc 100644 --- a/website/source/docs/agent/watches.html.markdown +++ b/website/source/docs/agent/watches.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Watches" sidebar_current: "docs-agent-watches" +description: |- + Watches are a way of specifying a view of data (list of nodes, KV pairs, health checks, etc) which is monitored for any updates. When an update is detected, an external handler is invoked. A handler can be any executable. As an example, you could watch the status of health checks and notify an external system when a check is critical. --- # Watches @@ -74,11 +76,13 @@ This maps to the `/v1/kv/` API internally. Here is an example configuration: - { - "type": "key", - "key": "foo/bar/baz", - "handler": "/usr/bin/my-key-handler.sh" - } +```javascript +{ + "type": "key", + "key": "foo/bar/baz", + "handler": "/usr/bin/my-key-handler.sh" +} +``` Or, using the watch command: @@ -86,15 +90,17 @@ Or, using the watch command: An example of the output of this command: - { - "Key": "foo/bar/baz", - "CreateIndex": 1793, - "ModifyIndex": 1793, - "LockIndex": 0, - "Flags": 0, - "Value": "aGV5", - "Session": "" - } +```javascript +{ + "Key": "foo/bar/baz", + "CreateIndex": 1793, + "ModifyIndex": 1793, + "LockIndex": 0, + "Flags": 0, + "Value": "aGV5", + "Session": "" +} +``` ### Type: keyprefix @@ -105,11 +111,13 @@ This maps to the `/v1/kv/` API internally. Here is an example configuration: - { - "type": "keyprefix", - "prefix": "foo/", - "handler": "/usr/bin/my-prefix-handler.sh" - } +```javascript +{ + "type": "keyprefix", + "prefix": "foo/", + "handler": "/usr/bin/my-prefix-handler.sh" +} +``` Or, using the watch command: @@ -117,36 +125,37 @@ Or, using the watch command: An example of the output of this command: - [ - { - "Key": "foo/bar", - "CreateIndex": 1796, - "ModifyIndex": 1796, - "LockIndex": 0, - "Flags": 0, - "Value": "TU9BUg==", - "Session": "" - }, - { - "Key": "foo/baz", - "CreateIndex": 1795, - "ModifyIndex": 1795, - "LockIndex": 0, - "Flags": 0, - "Value": "YXNkZg==", - "Session": "" - }, - { - "Key": "foo/test", - "CreateIndex": 1793, - "ModifyIndex": 1793, - "LockIndex": 0, - "Flags": 0, - "Value": "aGV5", - "Session": "" - } - ] - +```javascript +[ + { + "Key": "foo/bar", + "CreateIndex": 1796, + "ModifyIndex": 1796, + "LockIndex": 0, + "Flags": 0, + "Value": "TU9BUg==", + "Session": "" + }, + { + "Key": "foo/baz", + "CreateIndex": 1795, + "ModifyIndex": 1795, + "LockIndex": 0, + "Flags": 0, + "Value": "YXNkZg==", + "Session": "" + }, + { + "Key": "foo/test", + "CreateIndex": 1793, + "ModifyIndex": 1793, + "LockIndex": 0, + "Flags": 0, + "Value": "aGV5", + "Session": "" + } +] +``` ### Type: services @@ -157,11 +166,13 @@ This maps to the `/v1/catalog/services` API internally. An example of the output of this command: - { - "consul": [], - "redis": [], - "web": [] - } +```javascript +{ + "consul": [], + "redis": [], + "web": [] +} +``` ### Type: nodes @@ -172,32 +183,34 @@ This maps to the `/v1/catalog/nodes` API internally. An example of the output of this command: - [ - { - "Node": "nyc1-consul-1", - "Address": "192.241.159.115" - }, - { - "Node": "nyc1-consul-2", - "Address": "192.241.158.205" - }, - { - "Node": "nyc1-consul-3", - "Address": "198.199.77.133" - }, - { - "Node": "nyc1-worker-1", - "Address": "162.243.162.228" - }, - { - "Node": "nyc1-worker-2", - "Address": "162.243.162.226" - }, - { - "Node": "nyc1-worker-3", - "Address": "162.243.162.229" - } - ] +```javascript +[ + { + "Node": "nyc1-consul-1", + "Address": "192.241.159.115" + }, + { + "Node": "nyc1-consul-2", + "Address": "192.241.158.205" + }, + { + "Node": "nyc1-consul-3", + "Address": "198.199.77.133" + }, + { + "Node": "nyc1-worker-1", + "Address": "162.243.162.228" + }, + { + "Node": "nyc1-worker-2", + "Address": "162.243.162.226" + }, + { + "Node": "nyc1-worker-3", + "Address": "162.243.162.229" + } +] +``` ### Type: service @@ -211,11 +224,13 @@ This maps to the `/v1/health/service` API internally. Here is an example configuration: - { - "type": "service", - "service": "redis", - "handler": "/usr/bin/my-service-handler.sh" - } +```javascript +{ + "type": "service", + "key": "redis", + "handler": "/usr/bin/my-service-handler.sh" +} +``` Or, using the watch command: @@ -223,42 +238,44 @@ Or, using the watch command: An example of the output of this command: - [ - { - "Node": { - "Node": "foobar", - "Address": "10.1.10.12" - }, - "Service": { - "ID": "redis", - "Service": "redis", - "Tags": null, - "Port": 8000 - }, - "Checks": [ - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - }, - { - "Node": "foobar", - "CheckID": "serfHealth", - "Name": "Serf Health Status", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "", - "ServiceName": "" - } - ] - } +```javascript +[ + { + "Node": { + "Node": "foobar", + "Address": "10.1.10.12" + }, + "Service": { + "ID": "redis", + "Service": "redis", + "Tags": null, + "Port": 8000 + }, + "Checks": [ + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + }, + { + "Node": "foobar", + "CheckID": "serfHealth", + "Name": "Serf Health Status", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "", + "ServiceName": "" + } ] + } +] +``` ### Type: checks @@ -272,19 +289,20 @@ or `/v1/health/checks/` if monitoring by service. An example of the output of this command: - [ - { - "Node": "foobar", - "CheckID": "service:redis", - "Name": "Service 'redis' check", - "Status": "passing", - "Notes": "", - "Output": "", - "ServiceID": "redis", - "ServiceName": "redis" - } - ] - +```javascript +[ + { + "Node": "foobar", + "CheckID": "service:redis", + "Name": "Service 'redis' check", + "Status": "passing", + "Notes": "", + "Output": "", + "ServiceID": "redis", + "ServiceName": "redis" + } +] +``` ### Type: event @@ -297,11 +315,13 @@ This maps to the `v1/event/list` API internally. Here is an example configuration: - { - "type": "event", - "name": "web-deploy", - "handler": "/usr/bin/my-deploy-handler.sh" - } +```javascript +{ + "type": "event", + "name": "web-deploy", + "handler": "/usr/bin/my-deploy-handler.sh" +} +``` Or, using the watch command: @@ -309,21 +329,22 @@ Or, using the watch command: An example of the output of this command: - [ - { - "ID": "f07f3fcc-4b7d-3a7c-6d1e-cf414039fcee", - "Name": "web-deploy", - "Payload": "MTYwOTAzMA==", - "NodeFilter": "", - "ServiceFilter": "", - "TagFilter": "", - "Version": 1, - "LTime": 18 - }, - ... - ] +```javascript +[ + { + "ID": "f07f3fcc-4b7d-3a7c-6d1e-cf414039fcee", + "Name": "web-deploy", + "Payload": "MTYwOTAzMA==", + "NodeFilter": "", + "ServiceFilter": "", + "TagFilter": "", + "Version": 1, + "LTime": 18 + }, + ... +] +``` To fire a new `web-deploy` event the following could be used: $ consul event -name web-deploy 1609030 - diff --git a/website/source/docs/commands/agent.html.markdown b/website/source/docs/commands/agent.html.markdown index 43e366a52..c0f81633f 100644 --- a/website/source/docs/commands/agent.html.markdown +++ b/website/source/docs/commands/agent.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Commands: Agent" sidebar_current: "docs-commands-agent" +description: |- + The `consul agent` command is the heart of Consul: it runs the agent that performs the important task of maintaining membership information, running checks, announcing services, handling queries, etc. --- # Consul Agent diff --git a/website/source/docs/commands/event.html.markdown b/website/source/docs/commands/event.html.markdown index e1048655b..cfa55234c 100644 --- a/website/source/docs/commands/event.html.markdown +++ b/website/source/docs/commands/event.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Event" sidebar_current: "docs-commands-event" +description: |- + The event command provides a mechanism to fire a custom user event to an entire datacenter. These events are opaque to Consul, but they can be used to build scripting infrastructure to do automated deploys, restart services, or perform any other orchestration action. Events can be handled by using a watch. --- # Consul Event Command: `consul event` -The event command provides a mechanism to fire a custom user event to an +The `event` command provides a mechanism to fire a custom user event to an entire datacenter. These events are opaque to Consul, but they can be used to build scripting infrastructure to do automated deploys, restart services, or perform any other orchestration action. Events can be handled by diff --git a/website/source/docs/commands/exec.html.markdown b/website/source/docs/commands/exec.html.markdown index 09365580f..332f8f477 100644 --- a/website/source/docs/commands/exec.html.markdown +++ b/website/source/docs/commands/exec.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Exec" sidebar_current: "docs-commands-exec" +description: |- + The exec command provides a mechanism for remote execution. For example, this can be used to run the `uptime` command across all machines providing the `web` service. --- # Consul Exec Command: `consul exec` -The exec command provides a mechanism for remote execution. For example, +The `exec` command provides a mechanism for remote execution. For example, this can be used to run the `uptime` command across all machines providing the `web` service. diff --git a/website/source/docs/commands/force-leave.html.markdown b/website/source/docs/commands/force-leave.html.markdown index fdd52bb3f..fe01de0aa 100644 --- a/website/source/docs/commands/force-leave.html.markdown +++ b/website/source/docs/commands/force-leave.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Commands: Force Leave" sidebar_current: "docs-commands-forceleave" +description: |- + The `force-leave` command forces a member of a Consul cluster to enter the left state. Note that if the member is still actually alive, it will eventually rejoin the cluster. The true purpose of this method is to force remove failed nodes. --- # Consul Force Leave diff --git a/website/source/docs/commands/index.html.markdown b/website/source/docs/commands/index.html.markdown index 5adf67685..7a71bca57 100644 --- a/website/source/docs/commands/index.html.markdown +++ b/website/source/docs/commands/index.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Commands" sidebar_current: "docs-commands" +description: |- + Consul is controlled via a very easy to use command-line interface (CLI). Consul is only a single command-line application: `consul`. This application then takes a subcommand such as agent or members. The complete list of subcommands is in the navigation to the left. --- # Consul Commands (CLI) @@ -19,7 +21,7 @@ as you'd most likely expect. And some commands that expect input accept To view a list of the available commands at any time, just run `consul` with no arguments: -``` +```text $ consul usage: consul [--version] [--help] [] @@ -42,7 +44,7 @@ Available commands are: To get help for any specific command, pass the `-h` flag to the relevant subcommand. For example, to see help about the `join` subcommand: -``` +```text $ consul join -h Usage: consul join [options] address ... @@ -53,5 +55,4 @@ Options: -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. -wan Joins a server to another server in the WAN pool - ``` diff --git a/website/source/docs/commands/info.html.markdown b/website/source/docs/commands/info.html.markdown index d0611cfeb..5df9bf624 100644 --- a/website/source/docs/commands/info.html.markdown +++ b/website/source/docs/commands/info.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Info" sidebar_current: "docs-commands-info" +description: |- + The `info` command provides various debugging information that can be useful to operators. Depending on if the agent is a client or server, information about different sub-systems will be returned. --- # Consul Info Command: `consul info` -The info command provides various debugging information that can be +The `info` command provides various debugging information that can be useful to operators. Depending on if the agent is a client or server, information about different sub-systems will be returned. @@ -22,47 +24,49 @@ There are currently the top-level keys for: Here is an example output: - agent: - check_monitors = 0 - check_ttls = 0 - checks = 0 - services = 0 - consul: - bootstrap = true - known_datacenters = 1 - leader = true - server = true - raft: - applied_index = 45832 - commit_index = 45832 - fsm_pending = 0 - last_log_index = 45832 - last_log_term = 4 - last_snapshot_index = 45713 - last_snapshot_term = 1 - num_peers = 2 - state = Leader - term = 4 - serf_lan: - event_queue = 0 - event_time = 2 - failed = 0 - intent_queue = 0 - left = 0 - member_time = 7 - members = 3 - query_queue = 0 - query_time = 1 - serf_wan: - event_queue = 0 - event_time = 1 - failed = 0 - intent_queue = 0 - left = 0 - member_time = 1 - members = 1 - query_queue = 0 - query_time = 1 +```text +agent: + check_monitors = 0 + check_ttls = 0 + checks = 0 + services = 0 +consul: + bootstrap = true + known_datacenters = 1 + leader = true + server = true +raft: + applied_index = 45832 + commit_index = 45832 + fsm_pending = 0 + last_log_index = 45832 + last_log_term = 4 + last_snapshot_index = 45713 + last_snapshot_term = 1 + num_peers = 2 + state = Leader + term = 4 +serf_lan: + event_queue = 0 + event_time = 2 + failed = 0 + intent_queue = 0 + left = 0 + member_time = 7 + members = 3 + query_queue = 0 + query_time = 1 +serf_wan: + event_queue = 0 + event_time = 1 + failed = 0 + intent_queue = 0 + left = 0 + member_time = 1 + members = 1 + query_queue = 0 + query_time = 1 +``` ## Usage diff --git a/website/source/docs/commands/join.html.markdown b/website/source/docs/commands/join.html.markdown index a386ab2f6..8d3100b92 100644 --- a/website/source/docs/commands/join.html.markdown +++ b/website/source/docs/commands/join.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Join" sidebar_current: "docs-commands-join" +description: |- + The `join` command tells a Consul agent to join an existing cluster. A new Consul agent must join with at least one existing member of a cluster in order to join an existing cluster. After joining that one member, the gossip layer takes over, propagating the updated membership state across the cluster. --- # Consul Join Command: `consul join` -The `consul join` command tells a Consul agent to join an existing cluster. +The `join` command tells a Consul agent to join an existing cluster. A new Consul agent must join with at least one existing member of a cluster in order to join an existing cluster. After joining that one member, the gossip layer takes over, propagating the updated membership state across diff --git a/website/source/docs/commands/keygen.html.markdown b/website/source/docs/commands/keygen.html.markdown index 8eb68702a..683db3303 100644 --- a/website/source/docs/commands/keygen.html.markdown +++ b/website/source/docs/commands/keygen.html.markdown @@ -2,13 +2,16 @@ layout: "docs" page_title: "Commands: Keygen" sidebar_current: "docs-commands-keygen" +description: |- + The `keygen` command generates an encryption key that can be used for Consul agent traffic encryption. The keygen command uses a cryptographically strong pseudo-random number generator to generate the key. + --- # Consul Keygen Command: `consul keygen` -The `consul keygen` command generates an encryption key that can be used for +The `keygen` command generates an encryption key that can be used for [Consul agent traffic encryption](/docs/agent/encryption.html). The keygen command uses a cryptographically strong pseudo-random number generator to generate the key. diff --git a/website/source/docs/commands/leave.html.markdown b/website/source/docs/commands/leave.html.markdown index 4d44397a1..80a0e50db 100644 --- a/website/source/docs/commands/leave.html.markdown +++ b/website/source/docs/commands/leave.html.markdown @@ -2,15 +2,16 @@ layout: "docs" page_title: "Commands: Leave" sidebar_current: "docs-commands-leave" +description: |- + The `leave` command triggers a graceful leave and shutdown of the agent. It is used to ensure other nodes see the agent as left instead of failed. Nodes that leave will not attempt to re-join the cluster on restarting with a snapshot. --- # Consul Leave Command: `consul leave` -The leave command triggers a graceful leave and shutdown of the agent. - -This is used to ensure other nodes see the agent as "left" instead of +The `leave` command triggers a graceful leave and shutdown of the agent. +It is used to ensure other nodes see the agent as "left" instead of "failed". Nodes that leave will not attempt to re-join the cluster on restarting with a snapshot. diff --git a/website/source/docs/commands/members.html.markdown b/website/source/docs/commands/members.html.markdown index b325933f6..1e1f4f028 100644 --- a/website/source/docs/commands/members.html.markdown +++ b/website/source/docs/commands/members.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Members" sidebar_current: "docs-commands-members" +description: |- + The `members` command outputs the current list of members that a Consul agent knows about, along with their state. The state of a node can only be alive, left, or failed. --- # Consul Members Command: `consul members` -The members command outputs the current list of members that a Consul +The `members` command outputs the current list of members that a Consul agent knows about, along with their state. The state of a node can only be "alive", "left", or "failed". diff --git a/website/source/docs/commands/monitor.html.markdown b/website/source/docs/commands/monitor.html.markdown index 09a4ddc28..8402f9860 100644 --- a/website/source/docs/commands/monitor.html.markdown +++ b/website/source/docs/commands/monitor.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Monitor" sidebar_current: "docs-commands-monitor" +description: |- + The `monitor` command is used to connect and follow the logs of a running Consul agent. Monitor will show the recent logs and then continue to follow the logs, not exiting until interrupted or until the remote agent quits. --- # Consul Monitor Command: `consul monitor` -The monitor command is used to connect and follow the logs of a running +The `monitor` command is used to connect and follow the logs of a running Consul agent. Monitor will show the recent logs and then continue to follow the logs, not exiting until interrupted or until the remote agent quits. diff --git a/website/source/docs/commands/reload.html.markdown b/website/source/docs/commands/reload.html.markdown index 001c8f2af..ca1e5aa4d 100644 --- a/website/source/docs/commands/reload.html.markdown +++ b/website/source/docs/commands/reload.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Reload" sidebar_current: "docs-commands-reload" +description: |- + The `reload` command triggers a reload of configuration files for the agent. --- # Consul Reload Command: `consul reload` -The reload command triggers a reload of configuration files for the agent. +The `reload` command triggers a reload of configuration files for the agent. The `SIGHUP` signal is usually used to trigger a reload of configurations, but in some cases it may be more convenient to trigger the CLI instead. diff --git a/website/source/docs/commands/watch.html.markdown b/website/source/docs/commands/watch.html.markdown index a0d53fc1d..4324520d2 100644 --- a/website/source/docs/commands/watch.html.markdown +++ b/website/source/docs/commands/watch.html.markdown @@ -2,13 +2,15 @@ layout: "docs" page_title: "Commands: Watch" sidebar_current: "docs-commands-watch" +description: |- + The `watch` command provides a mechanism to watch for changes in a particular data view (list of nodes, service members, key value, etc) and to invoke a process with the latest values of the view. If no process is specified, the current values are dumped to stdout which can be a useful way to inspect data in Consul. --- # Consul Watch Command: `consul watch` -The watch command provides a mechanism to watch for changes in a particular +The `watch` command provides a mechanism to watch for changes in a particular data view (list of nodes, service members, key value, etc) and to invoke a process with the latest values of the view. If no process is specified, the current values are dumped to stdout which can be a useful way to inspect diff --git a/website/source/docs/compatibility.html.markdown b/website/source/docs/compatibility.html.markdown index eacd8725a..bc6aca139 100644 --- a/website/source/docs/compatibility.html.markdown +++ b/website/source/docs/compatibility.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Consul Protocol Compatibility Promise" sidebar_current: "docs-upgrading-compatibility" +description: |- + We expect Consul to run in large clusters as long-running agents. Because upgrading agents in this sort of environment relies heavily on protocol compatibility, this page makes it clear on our promise to keeping different Consul versions compatible with each other. --- # Protocol Compatibility Promise @@ -28,25 +30,24 @@ upgrading, see the [upgrading page](/docs/upgrading.html). ## Protocol Compatibility Table - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
VersionProtocol Compatibility
0.11
0.21
0.31, 2
0.41, 2
VersionProtocol Compatibility
0.11
0.21
0.31, 2
0.41, 2
- diff --git a/website/source/docs/guides/bootstrapping.html.markdown b/website/source/docs/guides/bootstrapping.html.markdown index c197fdfe1..4741527bb 100644 --- a/website/source/docs/guides/bootstrapping.html.markdown +++ b/website/source/docs/guides/bootstrapping.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Bootstrapping" sidebar_current: "docs-guides-bootstrapping" +description: |- + Before a Consul cluster can begin to service requests, it is necessary for a server node to be elected leader. For this reason, the first nodes that are started are generally the server nodes. Remember that an agent can run in both client and server mode. Server nodes are responsible for running the consensus protocol, and storing the cluster state. The client nodes are mostly stateless and rely on the server nodes, so they can be started easily. --- # Bootstrapping a Datacenter @@ -26,22 +28,28 @@ discouraged as data loss is inevitable in a failure scenario. Suppose we are starting a 3 server cluster, we can start `Node A`, `Node B` and `Node C` providing the `-bootstrap-expect 3` flag. Once the nodes are started, you should see a message to the effect of: - [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +```text +[WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +``` This indicates that the nodes are expecting 2 peers, but none are known yet. The servers will not elect themselves leader to prevent a split-brain. We can now join these machines together. Since a join operation is symmetric it does not matter which node initiates it. From any node you can do the following: - $ consul join - Successfully joined cluster by contacting 3 nodes. +```text +$ consul join +Successfully joined cluster by contacting 3 nodes. +``` Once the join is successful, one of the nodes will output something like: - [INFO] consul: adding server foo (Addr: 127.0.0.2:8300) (DC: dc1) - [INFO] consul: adding server bar (Addr: 127.0.0.1:8300) (DC: dc1) - [INFO] consul: Attempting bootstrap with nodes: [127.0.0.3:8300 127.0.0.2:8300 127.0.0.1:8300] +```text +[INFO] consul: adding server foo (Addr: 127.0.0.2:8300) (DC: dc1) +[INFO] consul: adding server bar (Addr: 127.0.0.1:8300) (DC: dc1) +[INFO] consul: Attempting bootstrap with nodes: [127.0.0.3:8300 127.0.0.2:8300 127.0.0.1:8300] ... - [INFO] consul: cluster leadership acquired +[INFO] consul: cluster leadership acquired +``` As a sanity check, the `consul info` command is a useful tool. It can be used to verify `raft.num_peers` is now 2, and you can view the latest log index under `raft.last_log_index`. @@ -64,4 +72,3 @@ In versions of Consul previous to 0.4, bootstrapping was a more manual process. For a guide on using the `-bootstrap` flag directly, see the [manual bootstrapping guide](/docs/guides/manual-bootstrap.html). This is not recommended, as it is more error prone than automatic bootstrapping. - diff --git a/website/source/docs/guides/datacenters.html.markdown b/website/source/docs/guides/datacenters.html.markdown index 919f4c0ff..146e3cfe0 100644 --- a/website/source/docs/guides/datacenters.html.markdown +++ b/website/source/docs/guides/datacenters.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Multiple Datacenters" sidebar_current: "docs-guides-datacenters" +description: |- + One of the key features of Consul is its support for multiple datacenters. The architecture of Consul is designed to promote a low-coupling of datacenters, so that connectivity issues or failure of any datacenter does not impact the availability of Consul in other regions. This means each datacenter runs independently, with a dedicated group of servers and a private LAN gossip pool. --- # Multi-Datacenter Deploys @@ -20,7 +22,7 @@ we can refer to as `dc1` and `dc2`, although the names are opaque to Consul. The next step is to ensure that all the server nodes join the WAN gossip pool. To query the known WAN nodes, we use the `members` command: -``` +```text $ consul members -wan ... ``` @@ -31,7 +33,7 @@ to a datacenter-local server, which then forwards the request to a server in the The next step is to simply join all the servers in the WAN pool: -``` +```text $ consul join -wan ... ... ``` @@ -46,14 +48,14 @@ Once this is done the `members` command can be used to verify that all server nodes are known about. We can also verify that both datacenters are known using the HTTP API: -``` +```text $ curl http://localhost:8500/v1/catalog/datacenters ["dc1", "dc2"] ``` As a simple test, you can try to query the nodes in each datacenter: -``` +```text $ curl http://localhost:8500/v1/catalog/nodes?dc=dc1 ... $ curl http://localhost:8500/v1/catalog/nodes?dc=dc2 @@ -67,4 +69,3 @@ is to be used across datacenters, then the network must be able to route traffic between IP addresses across regions as well. Usually, this means that all datacenters must be connected using a VPN or other tunneling mechanism. Consul does not handle VPN, address rewriting, or NAT traversal for you. - diff --git a/website/source/docs/guides/dns-cache.html.markdown b/website/source/docs/guides/dns-cache.html.markdown index 9113ce3e2..30eb716c8 100644 --- a/website/source/docs/guides/dns-cache.html.markdown +++ b/website/source/docs/guides/dns-cache.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "DNS Caching" sidebar_current: "docs-guides-dns-cache" +description: |- + One of the main interfaces to Consul is DNS. Using DNS is a simple way integrate Consul into an existing infrastructure without any high-touch integration. --- # DNS Caching @@ -57,18 +59,17 @@ that matches if there is no specific service TTL provided. This is specified using the `dns_config.service_ttl` map. The "*" service is the wildcard service. For example, if we specify: -``` - { - "dns_config": { - "service_ttl": { - "*": "5s", - "web": "30s" - } - } +```javascript +{ + "dns_config": { + "service_ttl": { + "*": "5s", + "web": "30s" } + } +} ``` This sets all lookups to "web.service.consul" to use a 30 second TTL, while lookups to "db.service.consul" or "api.service.consul" will use the 5 second TTL from the wildcard. - diff --git a/website/source/docs/guides/external.html.markdown b/website/source/docs/guides/external.html.markdown index 1156a3a9c..64cbfa274 100644 --- a/website/source/docs/guides/external.html.markdown +++ b/website/source/docs/guides/external.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "External Services" sidebar_current: "docs-guides-external" +description: |- + Very few infrastructures are entirely self-contained, and often rely on a multitude of external service providers. Most services are registered in Consul through the use of a service definition, however that registers the local node as the service provider. In the case of external services, we want to register a service as being provided by an external provider. --- # Registering an External Service @@ -21,36 +23,41 @@ also appear in standard queries against the API. Let us suppose we want to register a "search" service that is provided by "www.google.com", we could do the following: - $ curl -X PUT -d '{"Datacenter": "dc1", "Node": "google", "Address": "www.google.com", - "Service": {"Service": "search", "Port": 80}}' http://127.0.0.1:8500/v1/catalog/register +```text +$ curl -X PUT -d '{"Datacenter": "dc1", "Node": "google", "Address": "www.google.com", +"Service": {"Service": "search", "Port": 80}}' http://127.0.0.1:8500/v1/catalog/register +``` If we do a DNS lookup now, we can see the new search service: - ; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 search.service.consul. - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13313 - ;; flags: qr aa rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0 +```text +; <<>> DiG 9.8.3-P1 <<>> @127.0.0.1 -p 8600 search.service.consul. +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13313 +;; flags: qr aa rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 0 - ;; QUESTION SECTION: - ;search.service.consul. IN A +;; QUESTION SECTION: +;search.service.consul. IN A - ;; ANSWER SECTION: - search.service.consul. 0 IN CNAME www.google.com. - www.google.com. 264 IN A 74.125.239.114 - www.google.com. 264 IN A 74.125.239.115 - www.google.com. 264 IN A 74.125.239.116 +;; ANSWER SECTION: +search.service.consul. 0 IN CNAME www.google.com. +www.google.com. 264 IN A 74.125.239.114 +www.google.com. 264 IN A 74.125.239.115 +www.google.com. 264 IN A 74.125.239.116 - ;; Query time: 41 msec - ;; SERVER: 127.0.0.1#8600(127.0.0.1) - ;; WHEN: Tue Feb 25 17:45:12 2014 - ;; MSG SIZE rcvd: 178 +;; Query time: 41 msec +;; SERVER: 127.0.0.1#8600(127.0.0.1) +;; WHEN: Tue Feb 25 17:45:12 2014 +;; MSG SIZE rcvd: 178 +``` If at any time we want to deregister the service, we can simply do: - $ curl -X PUT -d '{"Datacenter": "dc1", "Node": "google"}' http://127.0.0.1:8500/v1/catalog/deregister +```text +$ curl -X PUT -d '{"Datacenter": "dc1", "Node": "google"}' http://127.0.0.1:8500/v1/catalog/deregister +``` This will deregister the `google` node, along with all services it provides. To learn more, read about the [HTTP API](/docs/agent/http.html). - diff --git a/website/source/docs/guides/forwarding.html.markdown b/website/source/docs/guides/forwarding.html.markdown index 9e20ed4ff..090977238 100644 --- a/website/source/docs/guides/forwarding.html.markdown +++ b/website/source/docs/guides/forwarding.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Forwarding" sidebar_current: "docs-guides-forwarding" +description: |- + By default, DNS is served from port 53 which requires root privileges. Instead of running Consul as root, it is possible to instead run Bind and forward queries to Consul as appropriate. --- # Forwarding DNS @@ -18,36 +20,40 @@ simplicity but this is not required. First, you have to disable DNSSEC so that Consul and Bind can communicate. This is an example configuration: - options { - listen-on port 53 { 127.0.0.1; }; - listen-on-v6 port 53 { ::1; }; - directory "/var/named"; - dump-file "/var/named/data/cache_dump.db"; - statistics-file "/var/named/data/named_stats.txt"; - memstatistics-file "/var/named/data/named_mem_stats.txt"; - allow-query { localhost; }; - recursion yes; +```text +options { + listen-on port 53 { 127.0.0.1; }; + listen-on-v6 port 53 { ::1; }; + directory "/var/named"; + dump-file "/var/named/data/cache_dump.db"; + statistics-file "/var/named/data/named_stats.txt"; + memstatistics-file "/var/named/data/named_mem_stats.txt"; + allow-query { localhost; }; + recursion yes; - dnssec-enable no; - dnssec-validation no; + dnssec-enable no; + dnssec-validation no; - /* Path to ISC DLV key */ - bindkeys-file "/etc/named.iscdlv.key"; + /* Path to ISC DLV key */ + bindkeys-file "/etc/named.iscdlv.key"; - managed-keys-directory "/var/named/dynamic"; - }; + managed-keys-directory "/var/named/dynamic"; +}; - include "/etc/named/consul.conf"; +include "/etc/named/consul.conf"; +``` ### Zone File Then we set up a zone for our Consul managed records in consul.conf: - zone "consul" IN { - type forward; - forward only; - forwarders { 127.0.0.1 port 8600; }; - }; +```text +zone "consul" IN { + type forward; + forward only; + forwarders { 127.0.0.1 port 8600; }; +}; +``` Here we assume Consul is running with default settings, and is serving DNS on port 8600. @@ -56,60 +62,68 @@ DNS on port 8600. First, perform a DNS query against Consul directly to be sure that the record exists: - [root@localhost ~]# dig @localhost -p 8600 master.redis.service.dc-1.consul. A +```text +[root@localhost ~]# dig @localhost -p 8600 master.redis.service.dc-1.consul. A - ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost master.redis.service.dc-1.consul. A - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536 - ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 +; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost master.redis.service.dc-1.consul. A +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536 +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 - ;; QUESTION SECTION: - ;master.redis.service.dc-1.consul. IN A +;; QUESTION SECTION: +;master.redis.service.dc-1.consul. IN A - ;; ANSWER SECTION: - master.redis.service.dc-1.consul. 0 IN A 172.31.3.234 +;; ANSWER SECTION: +master.redis.service.dc-1.consul. 0 IN A 172.31.3.234 - ;; Query time: 4 msec - ;; SERVER: 127.0.0.1#53(127.0.0.1) - ;; WHEN: Wed Apr 9 17:36:12 2014 - ;; MSG SIZE rcvd: 76 +;; Query time: 4 msec +;; SERVER: 127.0.0.1#53(127.0.0.1) +;; WHEN: Wed Apr 9 17:36:12 2014 +;; MSG SIZE rcvd: 76 +``` Then run the same query against your Bind instance and make sure you get a result: - [root@localhost ~]# dig @localhost -p 53 master.redis.service.dc-1.consul. A +```text +[root@localhost ~]# dig @localhost -p 53 master.redis.service.dc-1.consul. A - ; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost master.redis.service.dc-1.consul. A - ; (1 server found) - ;; global options: +cmd - ;; Got answer: - ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536 - ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 +; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.32.amzn1 <<>> @localhost master.redis.service.dc-1.consul. A +; (1 server found) +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11536 +;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 - ;; QUESTION SECTION: - ;master.redis.service.dc-1.consul. IN A +;; QUESTION SECTION: +;master.redis.service.dc-1.consul. IN A - ;; ANSWER SECTION: - master.redis.service.dc-1.consul. 0 IN A 172.31.3.234 +;; ANSWER SECTION: +master.redis.service.dc-1.consul. 0 IN A 172.31.3.234 - ;; Query time: 4 msec - ;; SERVER: 127.0.0.1#53(127.0.0.1) - ;; WHEN: Wed Apr 9 17:36:12 2014 - ;; MSG SIZE rcvd: 76 +;; Query time: 4 msec +;; SERVER: 127.0.0.1#53(127.0.0.1) +;; WHEN: Wed Apr 9 17:36:12 2014 +;; MSG SIZE rcvd: 76 +``` ### Troubleshooting If you don't get an answer from Bind but you do get an answer from Consul then your best bet is to turn on the query log to see what's going on: - [root@localhost ~]# rndc querylog - [root@localhost ~]# tail -f /var/log/messages +```text +[root@localhost ~]# rndc querylog +[root@localhost ~]# tail -f /var/log/messages +``` In there if you see errors like this: - error (no valid RRSIG) resolving - error (no valid DS) resolving +```text +error (no valid RRSIG) resolving +error (no valid DS) resolving +``` Then DNSSEC is not disabled properly. If you see errors about network connections then verify that there are no firewall or routing problems between the servers diff --git a/website/source/docs/guides/index.html.markdown b/website/source/docs/guides/index.html.markdown index 9744f4a80..7db1df34e 100644 --- a/website/source/docs/guides/index.html.markdown +++ b/website/source/docs/guides/index.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Guides" sidebar_current: "docs-guides" +description: |- + This section provides various guides for common actions. Due to the nature of Consul, some of these procedures can be complex, so our goal is to provide guidance to do them safely. --- # Consul Guides @@ -12,21 +14,14 @@ guidance to do them safely. The following guides are available: - * [Adding/Removing Servers](/docs/guides/servers.html) - This guide covers how to safely add - and remove Consul servers from the cluster. This should be done carefully to avoid availability - outages. +* [Adding/Removing Servers](/docs/guides/servers.html) - This guide covers how to safely add and remove Consul servers from the cluster. This should be done carefully to avoid availability outages. - * [Bootstrapping](/docs/guides/bootstrapping.html) - This guide covers bootstrapping a new - datacenter. This covers safely adding the initial Consul servers. +* [Bootstrapping](/docs/guides/bootstrapping.html) - This guide covers bootstrapping a new datacenter. This covers safely adding the initial Consul servers. - * [DNS Forwarding](/docs/guides/forwarding.html) - Forward DNS queries from Bind to Consul +* [DNS Forwarding](/docs/guides/forwarding.html) - Forward DNS queries from Bind to Consul - * [External Services](/docs/guides/external.html) - This guide covers registering - an external service. This allows using 3rd party services within the Consul framework. +* [External Services](/docs/guides/external.html) - This guide covers registering an external service. This allows using 3rd party services within the Consul framework. - * [Multiple Datacenters](/docs/guides/datacenters.html) - Configuring Consul to support multiple - datacenters. - - * [Outage Recovery](/docs/guides/outage.html) - This guide covers recovering a cluster - that has become unavailable due to server failures. +* [Multiple Datacenters](/docs/guides/datacenters.html) - Configuring Consul to support multiple datacenters. +* [Outage Recovery](/docs/guides/outage.html) - This guide covers recovering a cluster that has become unavailable due to server failures. diff --git a/website/source/docs/guides/leader-election.html.markdown b/website/source/docs/guides/leader-election.html.markdown index 591b0dc94..8651dcc49 100644 --- a/website/source/docs/guides/leader-election.html.markdown +++ b/website/source/docs/guides/leader-election.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Leader Election" sidebar_current: "docs-guides-leader" +description: |- + The goal of this guide is to cover how to build client-side leader election using Consul. If you are interested in the leader election used internally to Consul, you want to read about the consensus protocol instead. --- # Leader Election @@ -21,7 +23,9 @@ The first flow we cover is for nodes who are attempting to acquire leadership for a given service. All nodes that are participating should agree on a given key being used to coordinate. A good choice is simply: - service//leader +```text +service//leader +``` We will refer to this as just `key` for simplicity. @@ -35,7 +39,9 @@ that may be needed. Attempt to `acquire` the `key` by doing a `PUT`. This is something like: - curl -X PUT -d body http://localhost:8500/v1/kv/key?acquire=session +```text + curl -X PUT -d body http://localhost:8500/v1/kv/key?acquire=session + ``` This will either return `true` or `false`. If `true` is returned, the lock has been acquired and the local node is now the leader. If `false` is returned, @@ -54,7 +60,9 @@ wait. This is because Consul may be enforcing a [`lock-delay`](/docs/internals/s If the leader ever wishes to step down voluntarily, this should be done by simply releasing the lock: - curl -X PUT http://localhost:8500/v1/kv/key?release=session +```text +curl -X PUT http://localhost:8500/v1/kv/key?release=session +``` ## Discovering a Leader @@ -70,4 +78,3 @@ the value of the key will provide all the application-dependent information requ Clients should also watch the key using a blocking query for any changes. If the leader steps down, or fails, then the `Session` associated with the key will be cleared. When a new leader is elected, the key value will also be updated. - diff --git a/website/source/docs/guides/manual-bootstrap.html.markdown b/website/source/docs/guides/manual-bootstrap.html.markdown index bdb2ea93a..52cb58724 100644 --- a/website/source/docs/guides/manual-bootstrap.html.markdown +++ b/website/source/docs/guides/manual-bootstrap.html.markdown @@ -2,82 +2,100 @@ layout: "docs" page_title: "Manual Bootstrapping" sidebar_current: "docs-guides-bootstrapping" +description: |- + When deploying Consul to a datacenter for the first time, there is an initial bootstrapping that must be done. As of Consul 0.4, an automatic bootstrapping is available and is the recommended approach. However, older versions only support a manual bootstrap that is documented here. --- # Manually Bootstrapping a Datacenter -When deploying Consul to a datacenter for the first time, there is an initial bootstrapping that -must be done. As of Consul 0.4, an [automatic bootstrapping](/docs/guides/bootstrapping.html) is -available and is the recommended approach. However, older versions only support a manual bootstrap -that is documented here. +When deploying Consul to a datacenter for the first time, there is an initial +bootstrapping that must be done. As of Consul 0.4, an +[automatic bootstrapping](/docs/guides/bootstrapping.html) is available and is +the recommended approach. However, older versions only support a manual +bootstrap that is documented here. -Generally, the first nodes that are started are the server nodes. Remember that an -agent can run in both client and server mode. Server nodes are responsible for running -the [consensus protocol](/docs/internals/consensus.html), and storing the cluster state. -The client nodes are mostly stateless and rely on the server nodes, so they can be started easily. +Generally, the first nodes that are started are the server nodes. Remember that +an agent can run in both client and server mode. Server nodes are responsible +for running the [consensus protocol](/docs/internals/consensus.html), and +storing the cluster state. The client nodes are mostly stateless and rely on the +server nodes, so they can be started easily. -Manual bootstrapping requires that the first server that is deployed in a new datacenter provide -the `-bootstrap` [configuration option](/docs/agent/options.html). This option allows the server to -assert leadership of the cluster without agreement from any other server. This is necessary because -at this point, there are no other servers running in the datacenter! Lets call this first server `Node A`. -When starting `Node A` something like the following will be logged: +Manual bootstrapping requires that the first server that is deployed in a new +datacenter provide the `-bootstrap` +[configuration option](/docs/agent/options.html). This option allows the server +to assert leadership of the cluster without agreement from any other server. +This is necessary because at this point, there are no other servers running in +the datacenter! Lets call this first server `Node A`. When starting `Node A` +something like the following will be logged: - 2014/02/22 19:23:32 [INFO] consul: cluster leadership acquired +```text +2014/02/22 19:23:32 [INFO] consul: cluster leadership acquired +``` -Once `Node A` is running, we can start the next set of servers. There is a [deployment table](/docs/internals/consensus.html#toc_4) -that covers various options, but it is recommended to have 3 or 5 total servers per data center. -A single server deployment is _**highly**_ discouraged as data loss is inevitable in a failure scenario. -We start the next servers **without** specifying `-bootstrap`. This is critical, since only one server -should ever be running in bootstrap mode*. Once `Node B` and `Node C` are started, you should see a +Once `Node A` is running, we can start the next set of servers. There is a +[deployment table](/docs/internals/consensus.html#toc_4) that covers various +options, but it is recommended to have 3 or 5 total servers per data center. A +single server deployment is _**highly**_ discouraged as data loss is inevitable +in a failure scenario. We start the next servers **without** specifying +`-bootstrap`. This is critical, since only one server should ever be running in +bootstrap mode*. Once `Node B` and `Node C` are started, you should see a message to the effect of: - [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +```text +[WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +``` -This indicates that the node is not in bootstrap mode, and it will not elect itself as leader. -We can now join these machines together. Since a join operation is symmetric it does not matter -which node initiates it. From `Node B` and `Node C` you can do the following: +This indicates that the node is not in bootstrap mode, and it will not elect +itself as leader. We can now join these machines together. Since a join +operation is symmetric it does not matter which node initiates it. From +`Node B` and `Node C` you can do the following: - $ consul join - Successfully joined cluster by contacting 1 nodes. +```text +$ consul join +Successfully joined cluster by contacting 1 nodes. +``` Alternatively, from `Node A` you can do the following: - $ consul join - Successfully joined cluster by contacting 2 nodes. +```text +$ consul join +Successfully joined cluster by contacting 2 nodes. +``` Once the join is successful, `Node A` should output something like: - [INFO] raft: Added peer 127.0.0.2:8300, starting replication - .... - [INFO] raft: Added peer 127.0.0.3:8300, starting replication +```text +[INFO] raft: Added peer 127.0.0.2:8300, starting replication +.... +[INFO] raft: Added peer 127.0.0.3:8300, starting replication +``` As a sanity check, the `consul info` command is a useful tool. It can be used to -verify `raft.num_peers` is now 2, and you can view the latest log index under `raft.last_log_index`. -When running `consul info` on the followers, you should see `raft.last_log_index` -converge to the same value as the leader begins replication. That value represents the last -log entry that has been stored on disk. +verify `raft.num_peers` is now 2, and you can view the latest log index under +`raft.last_log_index`. When running `consul info` on the followers, you should +see `raft.last_log_index` converge to the same value as the leader begins +replication. That value represents the last log entry that has been stored on +disk. -This indicates that `Node B` and `Node C` have been added as peers. At this point, -all three nodes see each other as peers, `Node A` is the leader, and replication -should be working. +This indicates that `Node B` and `Node C` have been added as peers. At this +point, all three nodes see each other as peers, `Node A` is the leader, and +replication should be working. -The final step is to remove the `-bootstrap` flag. This is important since we don't -want the node to be able to make unilateral decisions in the case of a failure of the -other two nodes. To do this, we send a `SIGINT` to `Node A` to allow it to perform -a graceful leave. Then we remove the `-bootstrap` flag and restart the node. The node -will need to rejoin the cluster, since the graceful exit leaves the cluster. Any transactions -that took place while `Node A` was offline will be replicated and the node will catch up. +The final step is to remove the `-bootstrap` flag. This is important since we +don't want the node to be able to make unilateral decisions in the case of a +failure of the other two nodes. To do this, we send a `SIGINT` to `Node A` to +allow it to perform a graceful leave. Then we remove the `-bootstrap` flag and +restart the node. The node will need to rejoin the cluster, since the graceful +exit leaves the cluster. Any transactions that took place while `Node A` was +offline will be replicated and the node will catch up. -Now that the servers are all started and replicating to each other, all the remaining -clients can be joined. Clients are much easier, as they can be started and perform -a `join` against any existing node. All nodes participate in a gossip protocol to -perform basic discovery, so clients will automatically find the servers and register -themselves. +Now that the servers are all started and replicating to each other, all the +remaining clients can be joined. Clients are much easier, as they can be started +and perform a `join` against any existing node. All nodes participate in a +gossip protocol to perform basic discovery, so clients will automatically find +the servers and register themselves. -
-* If you accidentally start another server with the flag set, do not fret. -Shutdown the node, and remove the `raft/` folder from the data directory. This will -remove the bad state caused by being in `-bootstrap` mode. Then restart the +-> If you accidentally start another server with the flag set, do not fret. +Shutdown the node, and remove the `raft/` folder from the data directory. This +will remove the bad state caused by being in `-bootstrap` mode. Then restart the node and join the cluster normally. -
- diff --git a/website/source/docs/guides/outage.html.markdown b/website/source/docs/guides/outage.html.markdown index 22ce3b1a6..aedee390d 100644 --- a/website/source/docs/guides/outage.html.markdown +++ b/website/source/docs/guides/outage.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Outage Recovery" sidebar_current: "docs-guides-outage" +description: |- + Do not panic! This is a critical first step. Depending on your deployment configuration, it may take only a single server failure for cluster unavailability. Recovery requires an operator to intervene, but is straightforward. --- # Outage Recovery @@ -11,15 +13,14 @@ Do not panic! This is a critical first step. Depending on your take only a single server failure for cluster unavailability. Recovery requires an operator to intervene, but is straightforward. -
-This page covers recovery from Consul becoming unavailable due to a majority +~> This page covers recovery from Consul becoming unavailable due to a majority of server nodes in a datacenter being lost. If you are just looking to -add or remove a server see this page. -
+add or remove a server [see this page](/docs/guides/servers.html). If you had only a single server and it has failed, simply restart it. -Note that a single server configuration requires the `-bootstrap` or `-bootstrap-expect 1` flag. -If that server cannot be recovered, you need to bring up a new server. +Note that a single server configuration requires the `-bootstrap` or +`-bootstrap-expect 1` flag. If that server cannot be recovered, you need to +bring up a new server. See the [bootstrapping guide](/docs/guides/bootstrapping.html). Data loss is inevitable, since data was not replicated to any other servers. This is why a single server deploy is never recommended. Any services registered @@ -35,8 +36,12 @@ The next step is to go to the `-data-dir` of each Consul server. Inside that directory, there will be a `raft/` sub-directory. We need to edit the `raft/peers.json` file. It should be something like: -``` -["10.0.1.8:8300","10.0.1.6:8300","10.0.1.7:8300"] +```javascript +[ + "10.0.1.8:8300", + "10.0.1.6:8300", + "10.0.1.7:8300" +] ``` Simply delete the entries for all the failed servers. You must confirm @@ -47,7 +52,7 @@ At this point, you can restart all the remaining servers. If any servers managed to perform a graceful leave, you may need to have then rejoin the cluster using the `join` command: -``` +```text $ consul join Successfully joined cluster by contacting 1 nodes. ``` @@ -58,13 +63,13 @@ as the gossip protocol will take care of discovering the server nodes. At this point the cluster should be in an operable state again. One of the nodes should claim leadership and emit a log like: -``` +```text [INFO] consul: cluster leadership acquired ``` Additional, the `info` command can be a useful debugging tool: -``` +```text $ consul info ... raft: @@ -85,4 +90,3 @@ You should verify that one server claims to be the `Leader`, and all the others should be in the `Follower` state. All the nodes should agree on the peer count as well. This count is (N-1), since a server does not count itself as a peer. - diff --git a/website/source/docs/guides/servers.html.markdown b/website/source/docs/guides/servers.html.markdown index 5e62e4922..148e4e292 100644 --- a/website/source/docs/guides/servers.html.markdown +++ b/website/source/docs/guides/servers.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Adding/Removing Servers" sidebar_current: "docs-guides-servers" +description: |- + Consul is designed to require minimal operator involvement, however any changes to the set of Consul servers must be handled carefully. To better understand why, reading about the consensus protocol will be useful. In short, the Consul servers perform leader election and replication. For changes to be processed, a minimum quorum of servers (N/2)+1 must be available. That means if there are 3 server nodes, at least 2 must be available. --- # Adding/Removing Servers @@ -22,26 +24,32 @@ Adding new servers is generally straightforward. Simply start the new server with the `-server` flag. At this point, the server will not be a member of any cluster, and should emit something like: - [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +```text +[WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election. +``` This means that it does not know about any peers and is not configured to elect itself. This is expected, and we can now add this node to the existing cluster using `join`. From the new server, we can join any member of the existing cluster: - $ consul join - Successfully joined cluster by contacting 1 nodes. +```text +$ consul join +Successfully joined cluster by contacting 1 nodes. +``` It is important to note that any node, including a non-server may be specified for join. The gossip protocol is used to properly discover all the nodes in the cluster. Once the node has joined, the existing cluster leader should log something like: - [INFO] raft: Added peer 127.0.0.2:8300, starting replication +```text +[INFO] raft: Added peer 127.0.0.2:8300, starting replication +``` This means that raft, the underlying consensus protocol, has added the peer and begun replicating state. Since the existing cluster may be very far ahead, it can take some time for the new node to catch up. To check on this, run `info` on the leader: -``` +```text $ consul info ... raft: @@ -86,18 +94,22 @@ can handle a node leaving, the actual process is simple. You simply issue a The server leaving should contain logs like: - ... - [INFO] consul: server starting leave - ... - [INFO] raft: Removed ourself, transitioning to follower - ... +```text +... +[INFO] consul: server starting leave +... +[INFO] raft: Removed ourself, transitioning to follower +... +``` The leader should also emit various logs including: - ... - [INFO] consul: member 'node-10-0-1-8' left, deregistering - [INFO] raft: Removed peer 10.0.1.8:8300, stopping replication - ... +```text +... +[INFO] consul: member 'node-10-0-1-8' left, deregistering +[INFO] raft: Removed peer 10.0.1.8:8300, stopping replication +... +``` At this point the node has been gracefully removed from the cluster, and will shut down. @@ -113,6 +125,4 @@ leave the cluster. However, if this is not a possibility, then the `force-leave` can be used to force removal of a server. This is done by invoking that command with the name of the failed node. At this point, -the cluster leader will mark the node as having left the cluster and it will stop attempting -to replicate. - +the cluster leader will mark the node as having left the cluster and it will stop attempting to replicate. diff --git a/website/source/docs/index.html.markdown b/website/source/docs/index.html.markdown index 72a92a5d0..d0be08c74 100644 --- a/website/source/docs/index.html.markdown +++ b/website/source/docs/index.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Documentation" sidebar_current: "docs-home" +description: |- + Welcome to the Consul documentation! This documentation is more of a reference guide for all available features and options of Consul. If you're just getting started with Consul, please start with the introduction and getting started guide instead. --- # Consul Documentation diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 29c2d0660..45acaa81e 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "ACL System" sidebar_current: "docs-internals-acl" +description: |- + Consul provides an optional Access Control List (ACL) system which can be used to control access to data and APIs. The ACL system is a Capability-based system that relies on tokens which can have fine grained rules applied to them. It is very similar to AWS IAM in many ways. --- # ACL System @@ -76,37 +78,40 @@ with JSON making it easy to machine generate. As of Consul 0.4, it is only possible to specify policies for the KV store. Specification in the HCL format looks like: - # Default all keys to read-only - key "" { - policy = "read" - } - key "foo/" { - policy = "write" - } - key "foo/private/" { - # Deny access to the private dir - policy = "deny" - } +```javascript +# Default all keys to read-only +key "" { + policy = "read" +} +key "foo/" { + policy = "write" +} +key "foo/private/" { + # Deny access to the private dir + policy = "deny" +} +``` This is equivalent to the following JSON input: - { - "key": { - "": { - "policy": "read", - }, - "foo/": { - "policy": "write", - }, - "foo/private": { - "policy": "deny", - } - } +```javascript +{ + "key": { + "": { + "policy": "read", + }, + "foo/": { + "policy": "write", + }, + "foo/private": { + "policy": "deny", } + } +} +``` Key policies provide both a prefix and a policy. The rules are enforced using a longest-prefix match policy. This means we pick the most specific policy possible. The policy is either "read", "write" or "deny". A "write" policy implies "read", and there is no way to specify write-only. If there is no applicable rule, the `acl_default_policy` is applied. - diff --git a/website/source/docs/internals/architecture.html.markdown.erb b/website/source/docs/internals/architecture.html.markdown similarity index 95% rename from website/source/docs/internals/architecture.html.markdown.erb rename to website/source/docs/internals/architecture.html.markdown index 4c6af9f1e..ff0cb4669 100644 --- a/website/source/docs/internals/architecture.html.markdown.erb +++ b/website/source/docs/internals/architecture.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Consul Architecture" sidebar_current: "docs-internals-architecture" +description: |- + Consul is a complex system that has many different moving parts. To help users and developers of Consul form a mental model of how it works, this page documents the system architecture. --- # Consul Architecture @@ -10,12 +12,10 @@ Consul is a complex system that has many different moving parts. To help users and developers of Consul form a mental model of how it works, this page documents the system architecture. -
-Advanced Topic! This page covers technical details of +~> **Advanced Topic!** This page covers technical details of the internals of Consul. You don't need to know these details to effectively operate and use Consul. These details are documented here for those who wish to learn about them without having to go spelunking through the source code. -
## Glossary @@ -70,7 +70,7 @@ allowing a client to make a request from a server. From a 10,000 foot altitude the architecture of Consul looks like this:
- <%= link_to image_tag('consul-arch.png', alt: 'Consul Architecture'), image_path('consul-arch.png') %> +![Consul Architecture](consul-arch.png)
Lets break down this image and describe each piece. First of all we can see @@ -116,4 +116,3 @@ documented in detail, as is the [gossip protocol](/docs/internals/gossip.html). for the security model and protocols used are also available. For other details, either consult the code, ask in IRC or reach out to the mailing list. - diff --git a/website/source/docs/internals/consensus.html.markdown b/website/source/docs/internals/consensus.html.markdown index e389c65db..6e04e5dd2 100644 --- a/website/source/docs/internals/consensus.html.markdown +++ b/website/source/docs/internals/consensus.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Consensus Protocol" sidebar_current: "docs-internals-consensus" +description: |- + Consul uses a consensus protocol to provide Consistency as defined by CAP. This page documents the details of this internal protocol. The consensus protocol is based on Raft: In search of an Understandable Consensus Algorithm. For a visual explanation of Raft, see the The Secret Lives of Data. --- # Consensus Protocol @@ -11,12 +13,10 @@ to provide [Consistency](http://en.wikipedia.org/wiki/CAP_theorem) as defined by This page documents the details of this internal protocol. The consensus protocol is based on ["Raft: In search of an Understandable Consensus Algorithm"](https://ramcloud.stanford.edu/wiki/download/attachments/11370504/raft.pdf). For a visual explanation of Raft, see the [The Secret Lives of Data](http://thesecretlivesofdata.com/raft). -
-Advanced Topic! This page covers technical details of +~> **Advanced Topic!** This page covers technical details of the internals of Consul. You don't need to know these details to effectively operate and use Consul. These details are documented here for those who wish to learn about them without having to go spelunking through the source code. -
## Raft Protocol Overview @@ -139,7 +139,7 @@ supports 3 different consistency modes for reads. The three read modes are: -* default - Raft makes use of leader leasing, providing a time window +* `default` - Raft makes use of leader leasing, providing a time window in which the leader assumes its role is stable. However, if a leader is partitioned from the remaining peers, a new leader may be elected while the old leader is holding the lease. This means there are 2 leader @@ -151,12 +151,12 @@ The three read modes are: only stale in a hard to trigger situation. The time window of stale reads is also bounded, since the leader will step down due to the partition. -* consistent - This mode is strongly consistent without caveats. It requires +* `consistent` - This mode is strongly consistent without caveats. It requires that a leader verify with a quorum of peers that it is still leader. This introduces an additional round-trip to all server nodes. The trade off is always consistent reads, but increased latency due to an extra round trip. -* stale - This mode allows any server to service the read, regardless of if +* `stale` - This mode allows any server to service the read, regardless of if it is the leader. This means reads can be arbitrarily stale, but are generally within 50 milliseconds of the leader. The trade off is very fast and scalable reads but values will be stale. This mode allows reads without a leader, meaning @@ -172,45 +172,44 @@ recommended deployment is either 3 or 5 servers. A single server deployment is _**highly**_ discouraged as data loss is inevitable in a failure scenario. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ServersQuorum SizeFailure Tolerance
110
220
321
431
532
642
743
ServersQuorum SizeFailure Tolerance
110
220
321
431
532
642
743
- diff --git a/website/source/docs/internals/gossip.html.markdown b/website/source/docs/internals/gossip.html.markdown index 8690eb9e9..2d01df337 100644 --- a/website/source/docs/internals/gossip.html.markdown +++ b/website/source/docs/internals/gossip.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Gossip Protocol" sidebar_current: "docs-internals-gossip" +description: |- + Consul uses a gossip protocol to manage membership and broadcast messages to the cluster. All of this is provided through the use of the Serf library. The gossip protocol used by Serf is based on SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol, with a few minor adaptations. --- # Gossip Protocol @@ -13,12 +15,10 @@ used by Serf is based on ["SWIM: Scalable Weakly-consistent Infection-style Process Group Membership Protocol"](http://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf), with a few minor adaptations. There are more details about [Serf's protocol here](http://www.serfdom.io/docs/internals/gossip.html). -
-Advanced Topic! This page covers technical details of +~> **Advanced Topic!** This page covers technical details of the internals of Consul. You don't need to know these details to effectively operate and use Consul. These details are documented here for those who wish to learn about them without having to go spelunking through the source code. -
## Gossip in Consul @@ -41,4 +41,3 @@ All of these features are provided by leveraging [Serf](http://www.serfdom.io/). is used as an embedded library to provide these features. From a user perspective, this is not important, since the abstraction should be masked by Consul. It can be useful however as a developer to understand how this library is leveraged. - diff --git a/website/source/docs/internals/index.html.markdown b/website/source/docs/internals/index.html.markdown index 01374e44c..be28ef8d0 100644 --- a/website/source/docs/internals/index.html.markdown +++ b/website/source/docs/internals/index.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Internals" sidebar_current: "docs-internals" +description: |- + This section goes over some of the internals of Consul, such as the architecture, consensus and gossip protocol, security model, etc. --- # Consul Internals @@ -9,8 +11,6 @@ sidebar_current: "docs-internals" This section goes over some of the internals of Consul, such as the architecture, consensus and gossip protocol, security model, etc. -
-Note that knowing about the internals of Consul is not necessary to -successfully use it, but we document it here to be completely transparent -about how Consul works. -
+-> **Note:** Knowing about the internals of Consul is not necessary to successfully +use it, but we document it here to be completely transparent about how Consul +works. diff --git a/website/source/docs/internals/jepsen.html.markdown b/website/source/docs/internals/jepsen.html.markdown index 7e68df0ba..abf7973a4 100644 --- a/website/source/docs/internals/jepsen.html.markdown +++ b/website/source/docs/internals/jepsen.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Jepsen Testing" sidebar_current: "docs-internals-jepsen" +description: |- + Jepsen is a tool written by Kyle Kingsbury that is designed to test the partition tolerance of distributed systems. It creates network partitions while fuzzing the system with random operations. The results are analyzed to see if the system violates any of the consistency properties it claims to have. --- # Jepsen Testing @@ -30,7 +32,7 @@ Below is the output captured from Jepsen. We ran Jepsen multiple times, and it passed each time. This output is only representative of a single run. -``` +```text $ lein test :only jepsen.system.consul-test lein test jepsen.system.consul-test @@ -4018,4 +4020,3 @@ INFO jepsen.system.consul - :n5 consul nuked Ran 1 tests containing 1 assertions. 0 failures, 0 errors. ``` - diff --git a/website/source/docs/internals/security.html.markdown b/website/source/docs/internals/security.html.markdown index d2a9f48ab..5c8e2f195 100644 --- a/website/source/docs/internals/security.html.markdown +++ b/website/source/docs/internals/security.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Security Model" sidebar_current: "docs-internals-security" +description: |- + Consul relies on both a lightweight gossip mechanism and an RPC system to provide various features. Both of the systems have different security mechanisms that stem from their designs. However, the goals of Consuls security are to provide confidentiality, integrity and authentication. --- # Security Model @@ -23,12 +25,10 @@ This means Consul communication is protected against eavesdropping, tampering, or spoofing. This makes it possible to run Consul over untrusted networks such as EC2 and other shared hosting providers. -
-Advanced Topic! This page covers the technical details of +~> **Advanced Topic!** This page covers the technical details of the security model of Consul. You don't need to know these details to operate and use Consul. These details are documented here for those who wish to learn about them without having to go spelunking through the source code. -
## Threat Model @@ -50,4 +50,3 @@ When designing security into a system you design it to fit the threat model. Our goal is not to protect top secret data but to provide a "reasonable" level of security that would require an attacker to commit a considerable amount of resources to defeat. - diff --git a/website/source/docs/internals/sessions.html.markdown.erb b/website/source/docs/internals/sessions.html.markdown similarity index 93% rename from website/source/docs/internals/sessions.html.markdown.erb rename to website/source/docs/internals/sessions.html.markdown index 098135bfb..fb8d3cba2 100644 --- a/website/source/docs/internals/sessions.html.markdown.erb +++ b/website/source/docs/internals/sessions.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Sessions" sidebar_current: "docs-internals-sessions" +description: |- + Consul provides a session mechansim which can be used to build distributed locks. Sessions act as a binding layer between nodes, health checks, and key/value data. They are designed to provide granular locking, and are heavily inspired by The Chubby Lock Service for Loosely-Coupled Distributed Systems. --- # Sessions @@ -11,12 +13,10 @@ Sessions act as a binding layer between nodes, health checks, and key/value data They are designed to provide granular locking, and are heavily inspired by [The Chubby Lock Service for Loosely-Coupled Distributed Systems](http://research.google.com/archive/chubby.html). -
-Advanced Topic! This page covers technical details of +~> **Advanced Topic!** This page covers technical details of the internals of Consul. You don't need to know these details to effectively operate and use Consul. These details are documented here for those who wish to learn about them without having to go spelunking through the source code. -
## Session Design @@ -28,7 +28,7 @@ store to acquire locks, which are advisory mechanisms for mutual exclusion. Below is a diagram showing the relationship between these components:
- <%= link_to image_tag('consul-sessions.png'), image_path('consul-sessions.png') %> +![Consul Sessions](consul-sessions.png)
The contract that Consul provides is that under any of the folllowing @@ -113,4 +113,3 @@ the goal of Consul to protect against misbehaving clients. The primitives provided by sessions and the locking mechanisms of the KV store can be used to build client-side leader election algorithms. These are covered in more detail in the [Leader Election guide](/docs/guides/leader-election.html). - diff --git a/website/source/docs/upgrading.html.markdown b/website/source/docs/upgrading.html.markdown index fdac5ff96..778df3707 100644 --- a/website/source/docs/upgrading.html.markdown +++ b/website/source/docs/upgrading.html.markdown @@ -2,6 +2,8 @@ layout: "docs" page_title: "Upgrading Consul" sidebar_current: "docs-upgrading-upgrading" +description: |- + Consul is meant to be a long-running agent on any nodes participating in a Consul cluster. These nodes consistently communicate with each other. As such, protocol level compatibility and ease of upgrades is an important thing to keep in mind when using Consul. --- # Upgrading Consul @@ -85,9 +87,7 @@ only specifies the protocol version to _speak_. Every Consul agent can always understand the entire range of protocol versions it claims to on `consul -v`. -
-By running a previous protocol version, some features +~> **By running a previous protocol version**, some features of Consul, especially newer features, may not be available. If this is the case, Consul will typically warn you. In general, you should always upgrade your cluster so that you can run the latest protocol version. -
diff --git a/website/source/downloads.html.erb b/website/source/downloads.html.erb index 9036e4db4..585fe5090 100644 --- a/website/source/downloads.html.erb +++ b/website/source/downloads.html.erb @@ -2,6 +2,8 @@ layout: "downloads" page_title: "Download Consul" sidebar_current: "downloads-consul" +description: |- + Below are all available downloads for the latest version of Consul. Please download the proper package for your operating system and architecture. ---

Download Consul

diff --git a/website/source/downloads_web_ui.html.erb b/website/source/downloads_web_ui.html.erb index dbff26399..98ed76098 100644 --- a/website/source/downloads_web_ui.html.erb +++ b/website/source/downloads_web_ui.html.erb @@ -2,6 +2,8 @@ layout: "downloads" page_title: "Download Consul Web UI" sidebar_current: "downloads-ui" +description: |- + From this page you can download the web UI for Consul. This is distributed as a separate ZIP package. ---

Download Consul Web UI

diff --git a/website/source/intro/getting-started/agent.html.markdown b/website/source/intro/getting-started/agent.html.markdown index 1a75c7544..cb3508e48 100644 --- a/website/source/intro/getting-started/agent.html.markdown +++ b/website/source/intro/getting-started/agent.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Run the Agent" sidebar_current: "gettingstarted-agent" +description: |- + After Consul is installed, the agent must be run. The agent can either run in a server or client mode. Each datacenter must have at least one server, although 3 or 5 is recommended. A single server deployment is highly discouraged as data loss is inevitable in a failure scenario. --- # Run the Consul Agent @@ -19,7 +21,7 @@ will be part of the cluster. For simplicity, we'll run a single Consul agent in server mode right now: -``` +```text $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul ==> WARNING: BootstrapExpect Mode is specified as 1; this is the same as Bootstrap mode. ==> WARNING: Bootstrap mode enabled! Do not enable unless necessary @@ -53,12 +55,10 @@ data. From the log data, you can see that our agent is running in server mode, and has claimed leadership of the cluster. Additionally, the local member has been marked as a healthy member of the cluster. -
-Note for OS X Users: Consul uses your hostname as the +~> **Note for OS X Users:** Consul uses your hostname as the default node name. If your hostname contains periods, DNS queries to that node will not work with Consul. To avoid this, explicitly set -the name of your node with the -node flag. -
+the name of your node with the `-node` flag. ## Cluster Members @@ -66,7 +66,7 @@ If you run `consul members` in another terminal, you can see the members of the Consul cluster. You should only see one member (yourself). We'll cover joining clusters in the next section. -``` +```text $ consul members Node Address Status Type Build Protocol Armons-MacBook-Air 10.1.10.38:8301 alive server 0.3.0 2 @@ -82,7 +82,7 @@ For a strongly consistent view of the world, use the [HTTP API](/docs/agent/http.html), which forwards the request to the Consul servers: -``` +```text $ curl localhost:8500/v1/catalog/nodes [{"Node":"Armons-MacBook-Air","Address":"10.1.10.38"}] ``` @@ -93,7 +93,7 @@ that you have to make sure to point your DNS lookups to the Consul agent's DNS server, which runs on port 8600 by default. The format of the DNS entries (such as "Armons-MacBook-Air.node.consul") will be covered later. -``` +```text $ dig @127.0.0.1 -p 8600 Armons-MacBook-Air.node.consul ... @@ -121,4 +121,3 @@ to recover from certain network conditions, while _left_ nodes are no longer con Additionally, if an agent is operating as a server, a graceful leave is important to avoid causing a potential availability outage affecting the [consensus protocol](/docs/internals/consensus.html). See the [guides section](/docs/guides/index.html) to safely add and remove servers. - diff --git a/website/source/intro/getting-started/checks.html.markdown b/website/source/intro/getting-started/checks.html.markdown index d3ec5fd3f..a072c2a64 100644 --- a/website/source/intro/getting-started/checks.html.markdown +++ b/website/source/intro/getting-started/checks.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Registering Health Checks" sidebar_current: "gettingstarted-checks" +description: |- + We've now seen how simple it is to run Consul, add nodes and services, and query those nodes and services. In this section we will continue by adding health checks to both nodes and services, a critical component of service discovery that prevents using services that are unhealthy. --- # Health Checks @@ -29,7 +31,7 @@ the second node. The first file will add a host-level check, and the second will modify the web service definition to add a service-level check. -``` +```text $ echo '{"check": {"name": "ping", "script": "ping -c1 google.com >/dev/null", "interval": "30s"}}' >/etc/consul.d/ping.json $ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80, @@ -46,7 +48,7 @@ curl every 10 seconds to verify that the web server is running. Restart the second agent, or send a `SIGHUP` to it. We should now see the following log lines: -``` +```text ==> Starting Consul agent... ... [INFO] agent: Synced service 'web' @@ -66,7 +68,7 @@ Now that we've added some simple checks, we can use the HTTP API to check them. First, we can look for any failing checks. You can run this curl on either node: -``` +```text $ curl http://localhost:8500/v1/health/state/critical [{"Node":"agent-two","CheckID":"service:web","Name":"Service 'web' check","Status":"critical","Notes":"","ServiceID":"web","ServiceName":"web"}] ``` @@ -77,8 +79,8 @@ our `web` service check. Additionally, we can attempt to query the web service using DNS. Consul will not return any results, since the service is unhealthy: -``` - dig @127.0.0.1 -p 8600 web.service.consul +```text +dig @127.0.0.1 -p 8600 web.service.consul ... ;; QUESTION SECTION: @@ -91,4 +93,3 @@ Alternatively the HTTP API can be used to add, remove and modify checks dynamica The API allows for a "dead man's switch" or [TTL based check](/docs/agent/checks.html). TTL checks can be used to integrate an application more tightly with Consul, enabling business logic to be evaluated as part of passing a check. - diff --git a/website/source/intro/getting-started/install.html.markdown b/website/source/intro/getting-started/install.html.markdown index e224f09eb..ab44f9446 100644 --- a/website/source/intro/getting-started/install.html.markdown +++ b/website/source/intro/getting-started/install.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Installing Consul" sidebar_current: "gettingstarted-install" +description: |- + Consul must first be installed on every node that will be a member of a Consul cluster. To make installation easy, Consul is distributed as a binary package for all supported platforms and architectures. This page will not cover how to compile Consul from source. --- # Install Consul @@ -28,13 +30,15 @@ you would like. If you are using [homebrew](http://brew.sh/#install) as a package manager, than you can install consul as simple as: -``` -brew cask install consul + +```text +$ brew cask install consul ``` if you are missing the [cask plugin](http://caskroom.io/) you can install it with: -``` -brew install caskroom/cask/brew-cask + +```text +$ brew install caskroom/cask/brew-cask ``` ## Verifying the Installation @@ -43,7 +47,7 @@ After installing Consul, verify the installation worked by opening a new terminal session and checking that `consul` is available. By executing `consul` you should see help output similar to that below: -``` +```text $ consul usage: consul [--version] [--help] [] diff --git a/website/source/intro/getting-started/join.html.markdown b/website/source/intro/getting-started/join.html.markdown index 74405cff2..bf0f4bacf 100644 --- a/website/source/intro/getting-started/join.html.markdown +++ b/website/source/intro/getting-started/join.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul Cluster" sidebar_current: "gettingstarted-join" +description: |- + By this point, we've started our first agent and registered and queried one or more services on that agent. This showed how easy it is to use Consul, but didn't show how this could be extended to a scalable production service discovery infrastructure. On this page, we'll create our first real cluster with multiple members. --- # Consul Cluster @@ -33,7 +35,7 @@ and it *must* be accessible by all other nodes in the cluster. The first node will act as our server in this cluster. We're still not making a cluster of servers. -``` +```text $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul \ -node=agent-one -bind=172.20.20.10 ... @@ -44,7 +46,7 @@ This time, we set the bind address to match the IP of the second node as specified in the Vagrantfile. In production, you will generally want to provide a bind address or interface as well. -``` +```text $ consul agent -data-dir /tmp/consul -node=agent-two -bind=172.20.20.11 ... ``` @@ -59,7 +61,7 @@ against each agent and noting that only one member is a part of each. Now, let's tell the first agent to join the second agent by running the following command in a new terminal: -``` +```text $ consul join 172.20.20.11 Successfully joined cluster by contacting 1 nodes. ``` @@ -69,19 +71,16 @@ carefully, you'll see that they received join information. If you run `consul members` against each agent, you'll see that both agents now know about each other: -``` +```text $ consul members -detailed Node Address Status Tags agent-one 172.20.20.10:8301 alive role=consul,dc=dc1,vsn=2,vsn_min=1,vsn_max=2,port=8300,bootstrap=1 agent-two 172.20.20.11:8301 alive role=node,dc=dc1,vsn=2,vsn_min=1,vsn_max=2 ``` -
-

Remember: To join a cluster, a Consul agent needs to only +-> **Remember:** To join a cluster, a Consul agent needs to only learn about one existing member. After joining the cluster, the agents gossip with each other to propagate full membership information. -

-
In addition to using `consul join` you can use the `-join` flag on `consul agent` to join a cluster as part of starting up the agent. diff --git a/website/source/intro/getting-started/kv.html.markdown b/website/source/intro/getting-started/kv.html.markdown index f55b76470..7c2249ce8 100644 --- a/website/source/intro/getting-started/kv.html.markdown +++ b/website/source/intro/getting-started/kv.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Key/Value Data" sidebar_current: "gettingstarted-kv" +description: |- + In addition to providing service discovery and integrated health checking, Consul provides an easy to use Key/Value store. This can be used to hold dynamic configuration, assist in service coordination, build leader election, and anything else a developer can think to build. --- # Key/Value Data @@ -22,7 +24,7 @@ in the K/V store. Querying the agent we started in a prior page, we can first verify that there are no existing keys in the k/v store: -``` +```text $ curl -v http://localhost:8500/v1/kv/?recurse * About to connect() to localhost port 8500 (#0) * Trying 127.0.0.1... connected @@ -68,7 +70,7 @@ keys using the `?recurse` parameter. You can also fetch a single key just as easily: -``` +```text $ curl http://localhost:8500/v1/kv/web/key1 [{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}] ``` @@ -76,7 +78,7 @@ $ curl http://localhost:8500/v1/kv/web/key1 Deleting keys is simple as well. We can delete a single key by specifying the full path, or we can recursively delete all keys under a root using "?recurse": -``` +```text $ curl -X DELETE http://localhost:8500/v1/kv/web/sub?recurse $ curl http://localhost:8500/v1/kv/web?recurse [{"CreateIndex":97,"ModifyIndex":97,"Key":"web/key1","Flags":0,"Value":"dGVzdA=="}, @@ -89,7 +91,7 @@ key updates. This is done by providing the `?cas=` parameter with the last `ModifyIndex` value from the GET request. For example, suppose we wanted to update "web/key1": -``` +```text $ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97 true $ curl -X PUT -d 'newval' http://localhost:8500/v1/kv/web/key1?cas=97 @@ -102,7 +104,7 @@ However the second operation fails because the `ModifyIndex` is no longer 97. We can also make use of the `ModifyIndex` to wait for a key's value to change. For example, suppose we wanted to wait for key2 to be modified: -``` +```text $ curl "http://localhost:8500/v1/kv/web/key2?index=101&wait=5s" [{"CreateIndex":98,"ModifyIndex":101,"Key":"web/key2","Flags":42,"Value":"dGVzdA=="}] ``` @@ -115,4 +117,3 @@ of keys, waiting only until any of the keys has a newer modification time. This is only a few example of what the API supports. For full documentation, please reference the [HTTP API](/docs/agent/http.html). - diff --git a/website/source/intro/getting-started/next-steps.html.markdown b/website/source/intro/getting-started/next-steps.html.markdown index 4d0af9224..6eeffe7de 100644 --- a/website/source/intro/getting-started/next-steps.html.markdown +++ b/website/source/intro/getting-started/next-steps.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Next Steps" sidebar_current: "gettingstarted-nextsteps" +description: |- + That concludes the getting started guide for Consul. Hopefully you're able to see that while Consul is simple to use, it has a powerful set of features. We've covered the basics for all of these features in this guide. --- # Next Steps @@ -26,4 +28,3 @@ As a next step, the following resources are available: The work-in-progress examples folder within the GitHub repository for Consul contains functional examples of various use cases of Consul to help you get started with exactly what you need. - diff --git a/website/source/intro/getting-started/services.html.markdown b/website/source/intro/getting-started/services.html.markdown index 8d1329cdb..9b5e212bc 100644 --- a/website/source/intro/getting-started/services.html.markdown +++ b/website/source/intro/getting-started/services.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Registering Services" sidebar_current: "gettingstarted-services" +description: |- + In the previous page, we ran our first agent, saw the cluster members, and queried that node. On this page, we'll register our first service and query that service. We're not yet running a cluster of Consul agents. --- # Registering Services @@ -26,7 +28,7 @@ First, create a directory for Consul configurations. A good directory is typically `/etc/consul.d`. Consul loads all configuration files in the configuration directory. -``` +```text $ sudo mkdir /etc/consul.d ``` @@ -35,14 +37,14 @@ pretend we have a service named "web" running on port 80. Additionally, we'll give it some tags, which we can use as additional ways to query it later. -``` +```text $ echo '{"service": {"name": "web", "tags": ["rails"], "port": 80}}' \ >/etc/consul.d/web.json ``` Now, restart the agent we're running, providing the configuration directory: -``` +```text $ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -config-dir /etc/consul.d ==> Starting Consul agent... ... @@ -69,7 +71,7 @@ for services is `NAME.service.consul`. All DNS names are always in the querying services, and the `NAME` is the name of the service. For the web service we registered, that would be `web.service.consul`: -``` +```text $ dig @127.0.0.1 -p 8600 web.service.consul ... @@ -85,7 +87,7 @@ the service is available on. A records can only hold IP addresses. You can also use the DNS API to retrieve the entire address/port pair using SRV records: -``` +```text $ dig @127.0.0.1 -p 8600 web.service.consul SRV ... @@ -108,7 +110,7 @@ format for tag-based service queries is `TAG.NAME.service.consul`. In the example below, we ask Consul for all web services with the "rails" tag. We get a response since we registered our service with that tag. -``` +```text $ dig @127.0.0.1 -p 8600 rails.web.service.consul ... @@ -123,7 +125,7 @@ rails.web.service.consul. 0 IN A 172.20.20.11 In addition to the DNS API, the HTTP API can be used to query services: -``` +```text $ curl http://localhost:8500/v1/catalog/service/web [{"Node":"agent-one","Address":"172.20.20.11","ServiceID":"web","ServiceName":"web","ServiceTags":["rails"],"ServicePort":80}] ``` @@ -136,4 +138,3 @@ any downtime or unavailability to service queries. Alternatively the HTTP API can be used to add, remove, and modify services dynamically. - diff --git a/website/source/intro/getting-started/ui.html.markdown.erb b/website/source/intro/getting-started/ui.html.markdown similarity index 83% rename from website/source/intro/getting-started/ui.html.markdown.erb rename to website/source/intro/getting-started/ui.html.markdown index 2c6433357..954ffb787 100644 --- a/website/source/intro/getting-started/ui.html.markdown.erb +++ b/website/source/intro/getting-started/ui.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Web UI" sidebar_current: "gettingstarted-ui" +description: |- + Consul comes with support for a beautiful, functional web UI out of the box. This UI can be used for viewing all services and nodes, viewing all health checks and their current status, and for reading and setting key/value data. The UI automatically supports multi-datacenter. --- # Consul Web UI @@ -33,7 +35,7 @@ A screenshot of one page of the demo is shown below so you can get an idea of what the web UI is like. Click the screenshot for the full size.
- <%= link_to image_tag('consul_web_ui.png'), image_path('consul_web_ui.png') %> +![Consul Web UI](consul_web_ui.png)
## Set Up @@ -45,7 +47,7 @@ is also being run. Then, just append the `-ui-dir` to the `consul agent` command pointing to the directory where you unzipped the UI (the directory with the `index.html` file): -``` +```text $ consul agent -ui-dir /path/to/ui ... ``` diff --git a/website/source/intro/index.html.markdown b/website/source/intro/index.html.markdown index 46d771f09..3129fef2a 100644 --- a/website/source/intro/index.html.markdown +++ b/website/source/intro/index.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Introduction" sidebar_current: "what" +description: |- + Welcome to the intro guide to Consul! This guide is the best place to start with Consul. We cover what Consul is, what problems it can solve, how it compares to existing software, and a quick start for using Consul. If you are already familiar with the basics of Consul, the documentation provides more of a reference for all available features. --- # Introduction to Consul diff --git a/website/source/intro/vs/chef-puppet.html.markdown b/website/source/intro/vs/chef-puppet.html.markdown index 0b59d15d7..7dcc4df77 100644 --- a/website/source/intro/vs/chef-puppet.html.markdown +++ b/website/source/intro/vs/chef-puppet.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul vs. Chef, Puppet, etc." sidebar_current: "vs-other-chef" +description: |- + It is not uncommon to find people using Chef, Puppet, and other configuration management tools to build service discovery mechanisms. This is usually done by querying global state to construct configuration files on each node during a periodic convergence run. --- # Consul vs. Chef, Puppet, etc. diff --git a/website/source/intro/vs/custom.html.markdown b/website/source/intro/vs/custom.html.markdown index a931d144d..ef57800d4 100644 --- a/website/source/intro/vs/custom.html.markdown +++ b/website/source/intro/vs/custom.html.markdown @@ -2,24 +2,27 @@ layout: "intro" page_title: "Consul vs. Custom Solutions" sidebar_current: "vs-other-custom" +description: |- + As a code base grows, a monolithic app usually evolves into a Service Oriented Architecture (SOA). A universal pain point for SOA is service discovery and configuration. In many cases, this leads to organizations building home grown solutions. It is an undisputed fact that distributed systems are hard; building one is error prone and time consuming. Most systems cut corners by introducing single points of failure such as a single Redis or RDBMS to maintain cluster state. These solutions may work in the short term, but they are rarely fault tolerant or scalable. Besides these limitations, they require time and resources to build and maintain. --- # Consul vs. Custom Solutions -As a code base grows, a monolithic app usually evolves into a Service Oriented Architecture (SOA). -A universal pain point for SOA is service discovery and configuration. In many -cases, this leads to organizations building home grown solutions. -It is an undisputed fact that distributed systems are hard; building one is error prone and time consuming. -Most systems cut corners by introducing single points of failure such -as a single Redis or RDBMS to maintain cluster state. These solutions may work in the short term, -but they are rarely fault tolerant or scalable. Besides these limitations, -they require time and resources to build and maintain. +As a code base grows, a monolithic app usually evolves into a Service Oriented +Architecture (SOA). A universal pain point for SOA is service discovery and +configuration. In many cases, this leads to organizations building home grown +solutions. It is an undisputed fact that distributed systems are hard; building +one is error prone and time consuming. Most systems cut corners by introducing +single points of failure such as a single Redis or RDBMS to maintain cluster +state. These solutions may work in the short term, but they are rarely fault +tolerant or scalable. Besides these limitations, they require time and resources +to build and maintain. -Consul provides the core set of features needed by a SOA out of the box. By using Consul, -organizations can leverage open source work to reduce their time and resource commitment to -re-inventing the wheel and focus on their business applications. +Consul provides the core set of features needed by a SOA out of the box. By +using Consul, organizations can leverage open source work to reduce their time +and resource commitment to re-inventing the wheel and focus on their business +applications. Consul is built on well-cited research, and is designed with the constraints of -distributed systems in mind. At every step, Consul takes efforts to provide a robust -and scalable solution for organizations of any size. - +distributed systems in mind. At every step, Consul takes efforts to provide a +robust and scalable solution for organizations of any size. diff --git a/website/source/intro/vs/index.html.markdown b/website/source/intro/vs/index.html.markdown index 3100524cd..a3bf3f333 100644 --- a/website/source/intro/vs/index.html.markdown +++ b/website/source/intro/vs/index.html.markdown @@ -2,15 +2,19 @@ layout: "intro" page_title: "Consul vs. Other Software" sidebar_current: "vs-other" +description: |- + The problems Consul solves are varied, but each individual feature has been solved by many different systems. Although there is no single system that provides all the features of Consul, there are other options available to solve some of these problems. --- # Consul vs. Other Software The problems Consul solves are varied, but each individual feature has been -solved by many different systems. Although there is no single system that provides -all the features of Consul, there are other options available to solve some of these problems. -In this section, we compare Consul to some other options. In most cases, Consul is not -mutually exclusive with any other system. +solved by many different systems. Although there is no single system that +provides all the features of Consul, there are other options available to solve +some of these problems. + +In this section, we compare Consul to some other options. In most cases, Consul +is not mutually exclusive with any other system. Use the navigation to the left to read the comparison of Consul to specific systems. diff --git a/website/source/intro/vs/nagios-sensu.html.markdown b/website/source/intro/vs/nagios-sensu.html.markdown index 9f4dff32d..9f2631770 100644 --- a/website/source/intro/vs/nagios-sensu.html.markdown +++ b/website/source/intro/vs/nagios-sensu.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul vs. Nagios, Sensu" sidebar_current: "vs-other-nagios-sensu" +description: |- + Nagios and Sensu are both tools built for monitoring. They are used to quickly notify operators when an issue occurs. --- # Consul vs. Nagios, Sensu @@ -46,4 +48,3 @@ integrates a distributed failure detector. This means that if a Consul agent fai the failure will be detected, and thus all checks being run by that node can be assumed failed. This failure detector distributes the work among the entire cluster, and critically enables the edge triggered architecture to work. - diff --git a/website/source/intro/vs/serf.html.markdown b/website/source/intro/vs/serf.html.markdown index b2ca20c18..2e3c3b3fb 100644 --- a/website/source/intro/vs/serf.html.markdown +++ b/website/source/intro/vs/serf.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul vs. Serf" sidebar_current: "vs-other-serf" +description: |- + Serf is a node discovery and orchestration tool and is the only tool discussed so far that is built on an eventually consistent gossip model, with no centralized servers. It provides a number of features, including group membership, failure detection, event broadcasts and a query mechanism. However, Serf does not provide any high-level features such as service discovery, health checking or key/value storage. To clarify, the discovery feature of Serf is at a node level, while Consul provides a service and node level abstraction. --- # Consul vs. Serf @@ -43,4 +45,3 @@ general purpose tool. Consul uses a CP architecture, favoring consistency over availability. Serf is a AP system, and sacrifices consistency for availability. This means Consul cannot operate if the central servers cannot form a quorum, while Serf will continue to function under almost all circumstances. - diff --git a/website/source/intro/vs/skydns.html.markdown b/website/source/intro/vs/skydns.html.markdown index a81fc8380..eb69ab6ab 100644 --- a/website/source/intro/vs/skydns.html.markdown +++ b/website/source/intro/vs/skydns.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul vs. SkyDNS" sidebar_current: "vs-other-skydns" +description: |- + SkyDNS is a relatively new tool designed to solve service discovery. It uses multiple central servers that are strongly consistent and fault tolerant. Nodes register services using an HTTP API, and queries can be made over HTTP or DNS to perform discovery. --- # Consul vs. SkyDNS diff --git a/website/source/intro/vs/smartstack.html.markdown b/website/source/intro/vs/smartstack.html.markdown index f28f2fadc..4644a704b 100644 --- a/website/source/intro/vs/smartstack.html.markdown +++ b/website/source/intro/vs/smartstack.html.markdown @@ -2,6 +2,9 @@ layout: "intro" page_title: "Consul vs. SmartStack" sidebar_current: "vs-other-smartstack" +description: |- + SmartStack is another tool which tackles the service discovery problem. It has a rather unique architecture, and has 4 major components: ZooKeeper, HAProxy, Synapse, and Nerve. The ZooKeeper servers are responsible for storing cluster state in a consistent and fault tolerant manner. Each node in the SmartStack cluster then runs both Nerves and Synapses. The Nerve is responsible for running health checks against a service, and registering with the ZooKeeper servers. Synapse queries ZooKeeper for service providers and dynamically configures HAProxy. Finally, clients speak to HAProxy, which does health checking and load balancing across service providers. + --- # Consul vs. SmartStack @@ -54,4 +57,3 @@ an integrated key/value store for configuration and multi-datacenter support. While it may be possible to configure SmartStack for multiple datacenters, the central ZooKeeper cluster would be a serious impediment to a fault tolerant deployment. - diff --git a/website/source/intro/vs/zookeeper.html.markdown b/website/source/intro/vs/zookeeper.html.markdown index 62c3fda32..1e59edc93 100644 --- a/website/source/intro/vs/zookeeper.html.markdown +++ b/website/source/intro/vs/zookeeper.html.markdown @@ -2,6 +2,8 @@ layout: "intro" page_title: "Consul vs. ZooKeeper, doozerd, etcd" sidebar_current: "vs-other-zk" +description: |- + ZooKeeper, doozerd and etcd are all similar in their architecture. All three have server nodes that require a quorum of nodes to operate (usually a simple majority). They are strongly consistent, and expose various primitives that can be used through client libraries within applications to build complex distributed systems. --- # Consul vs. ZooKeeper, doozerd, etcd @@ -58,4 +60,3 @@ all these other systems require additional tools and libraries to be built on top. By using client nodes, Consul provides a simple API that only requires thin clients. Additionally, the API can be avoided entirely by using configuration files and the DNS interface to have a complete service discovery solution with no development at all. - diff --git a/website/source/robots.txt b/website/source/robots.txt index 9f07ef7c2..190c6ce04 100644 --- a/website/source/robots.txt +++ b/website/source/robots.txt @@ -1,5 +1,6 @@ --- layout: false +noindex: true --- User-agent: * From 2217f93fc690ffa455b037d0866a0dabc9c73488 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sun, 19 Oct 2014 19:40:44 -0400 Subject: [PATCH 023/238] Update gems to latest versions --- website/Gemfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/Gemfile.lock b/website/Gemfile.lock index f3e4c90e4..9b44be5fb 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: git://github.com/hashicorp/middleman-hashicorp.git - revision: dd7197276a8b9d86f3c06954a49747cd75b7e29a + revision: dc9f2d6b986842622796a569bf9358f6f3e3f634 specs: middleman-hashicorp (0.1.0) bootstrap-sass (~> 3.2) @@ -113,11 +113,11 @@ GEM rouge (~> 1.0) minitest (5.4.2) multi_json (1.10.1) - padrino-helpers (0.12.3) + padrino-helpers (0.12.4) i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.12.3) + padrino-support (= 0.12.4) tilt (~> 1.4.1) - padrino-support (0.12.3) + padrino-support (0.12.4) activesupport (>= 3.1) rack (1.5.2) rack-contrib (1.1.0) From 03e359987291a42af2b79edaf681499a26d9c029 Mon Sep 17 00:00:00 2001 From: Erik Dubbelboer Date: Mon, 20 Oct 2014 06:26:38 +0000 Subject: [PATCH 024/238] Fix small doc mistake --- website/source/docs/faq.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/faq.html.markdown b/website/source/docs/faq.html.markdown index 01e76d0b2..56fdd67f5 100644 --- a/website/source/docs/faq.html.markdown +++ b/website/source/docs/faq.html.markdown @@ -19,7 +19,7 @@ physical memory used is much lower. ## Q: What is Checkpoint? / Does Consul call home? Consul makes use of a HashiCorp service called [Checkpoint](http://checkpoint.hashicorp.com) -which is used to used to check for updates and critical security bulletins. +which is used to check for updates and critical security bulletins. Only anonymous information is sent to Checkpoint, and cannot be used to identify the user or host. An anonymous ID is sent which helps de-duplicate warning messages and can be disabled. Using the Checkpoint service is optional From ae9f280f2bdf8c75ef6d9e2e3edec195f7d9f533 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 20 Oct 2014 10:20:13 -0700 Subject: [PATCH 025/238] CHANGELOG updates --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf935218..59c44ffd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.4.1 (Unreleased) +## 0.4.1 (October 20, 2014) FEATURES: From af90aa802614c7935b623406554134e2e646b265 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 20 Oct 2014 10:21:31 -0700 Subject: [PATCH 026/238] Gofmt --- command/agent/rpc.go | 2 +- command/agent/rpc_client.go | 2 +- consul/fsm.go | 2 +- consul/pool.go | 2 +- consul/rpc.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 784c6c46f..caf97cef1 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -24,9 +24,9 @@ package agent import ( "bufio" "fmt" + "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/logutils" "github.com/hashicorp/serf/serf" - "github.com/hashicorp/go-msgpack/codec" "io" "log" "net" diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index a64ec4d53..6cd0fc19f 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -3,8 +3,8 @@ package agent import ( "bufio" "fmt" - "github.com/hashicorp/logutils" "github.com/hashicorp/go-msgpack/codec" + "github.com/hashicorp/logutils" "log" "net" "sync" diff --git a/consul/fsm.go b/consul/fsm.go index 58cfe5951..2ce6719a4 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -8,8 +8,8 @@ import ( "time" "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/raft" "github.com/hashicorp/go-msgpack/codec" + "github.com/hashicorp/raft" ) // consulFSM implements a finite state machine that is used diff --git a/consul/pool.go b/consul/pool.go index 70afb0ff0..91fe035f2 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -11,9 +11,9 @@ import ( "sync/atomic" "time" + "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/yamux" "github.com/inconshreveable/muxado" - "github.com/hashicorp/go-msgpack/codec" ) // msgpackHandle is a shared handle for encoding/decoding of RPC messages diff --git a/consul/rpc.go b/consul/rpc.go index 1607bd22c..cd5c36ebd 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -5,9 +5,9 @@ import ( "fmt" "github.com/armon/go-metrics" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/yamux" "github.com/inconshreveable/muxado" - "github.com/hashicorp/go-msgpack/codec" "io" "math/rand" "net" From 4515f82691eb2d49fade88282b9cf8f44292819f Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 20 Oct 2014 10:42:06 -0700 Subject: [PATCH 027/238] Bump website version --- website/config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/config.rb b/website/config.rb index 88fc6c039..5bb6a8574 100644 --- a/website/config.rb +++ b/website/config.rb @@ -5,7 +5,7 @@ set :base_url, "https://www.consul.io/" activate :hashicorp do |h| - h.version = '0.4.0' + h.version = '0.4.1' h.bintray_repo = 'mitchellh/consul' h.bintray_user = 'mitchellh' h.bintray_key = ENV['BINTRAY_API_KEY'] From 92c5f174987614e6a5804a79f0245d9bec33b82e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 20 Oct 2014 13:18:17 -0700 Subject: [PATCH 028/238] website: Fixing link to demo cluster --- website/source/intro/getting-started/ui.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/intro/getting-started/ui.html.markdown b/website/source/intro/getting-started/ui.html.markdown index 954ffb787..ce13da876 100644 --- a/website/source/intro/getting-started/ui.html.markdown +++ b/website/source/intro/getting-started/ui.html.markdown @@ -29,7 +29,7 @@ While the live demo is able to access data from all datacenters, we've also setup demo endpoints in the specific datacenters: [AMS2](http://ams2.demo.consul.io) (Amsterdam), [SFO1](http://sfo1.demo.consul.io) (San Francisco), -and [NYC1](http://nyc1.demo.consul.io) (New York). +and [NYC3](http://nyc3.demo.consul.io) (New York). A screenshot of one page of the demo is shown below so you can get an idea of what the web UI is like. Click the screenshot for the full size. From e702810774aa1f28f51dbfda9e5c74f826dc2a7c Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 21 Oct 2014 16:24:00 -0400 Subject: [PATCH 029/238] Add Consul Template to the download list --- .../source/downloads_consul_template.html.erb | 23 +++++++++++++++++++ website/source/layouts/downloads.erb | 4 ++++ 2 files changed, 27 insertions(+) create mode 100644 website/source/downloads_consul_template.html.erb diff --git a/website/source/downloads_consul_template.html.erb b/website/source/downloads_consul_template.html.erb new file mode 100644 index 000000000..ff5d48a44 --- /dev/null +++ b/website/source/downloads_consul_template.html.erb @@ -0,0 +1,23 @@ +--- +layout: "downloads" +page_title: "Download Consul Template" +sidebar_current: "downloads-consul-template" +description: |- + From this page you can download Consul Template. This is distributed as a separate tarball. +--- + +

Download Consul Web UI

+ +
+
+
+

+ From this page you can download the Consul Template. This is distributed as a separate tarball. +

+

+ + Download Consul Template +

+
+
+
diff --git a/website/source/layouts/downloads.erb b/website/source/layouts/downloads.erb index d8abc5661..fd13c8e79 100644 --- a/website/source/layouts/downloads.erb +++ b/website/source/layouts/downloads.erb @@ -9,6 +9,10 @@ > Download Web UI + + > + Download Consul Template +
<% end %> From b31a72e29c39cbc8b92727a624f42aafc30c4f15 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 21 Oct 2014 16:32:58 -0400 Subject: [PATCH 030/238] Fix copy-paste fail --- website/source/downloads_consul_template.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/downloads_consul_template.html.erb b/website/source/downloads_consul_template.html.erb index ff5d48a44..825642607 100644 --- a/website/source/downloads_consul_template.html.erb +++ b/website/source/downloads_consul_template.html.erb @@ -6,7 +6,7 @@ description: |- From this page you can download Consul Template. This is distributed as a separate tarball. --- -

Download Consul Web UI

+

Download Consul Template

From 22196dc47aa145c690bbcc386e448265778dd41f Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 21 Oct 2014 16:38:44 -0400 Subject: [PATCH 031/238] Fix small typo in link --- website/source/downloads_consul_template.html.erb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/source/downloads_consul_template.html.erb b/website/source/downloads_consul_template.html.erb index 825642607..8b655a432 100644 --- a/website/source/downloads_consul_template.html.erb +++ b/website/source/downloads_consul_template.html.erb @@ -15,8 +15,7 @@ description: |- From this page you can download the Consul Template. This is distributed as a separate tarball.

- - Download Consul Template + Download Consul Template

From 3ef35f75ffbd334cb662f4d928069066348d2ecb Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 21 Oct 2014 16:42:38 -0400 Subject: [PATCH 032/238] Fix the duplicate navigation active classes --- website/source/downloads_consul_template.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/downloads_consul_template.html.erb b/website/source/downloads_consul_template.html.erb index 8b655a432..1c09ad301 100644 --- a/website/source/downloads_consul_template.html.erb +++ b/website/source/downloads_consul_template.html.erb @@ -1,7 +1,7 @@ --- layout: "downloads" page_title: "Download Consul Template" -sidebar_current: "downloads-consul-template" +sidebar_current: "downloads-template" description: |- From this page you can download Consul Template. This is distributed as a separate tarball. --- From dd720956596bb94e83f99581328a2b212b25eba8 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Tue, 21 Oct 2014 18:07:39 -0400 Subject: [PATCH 033/238] Create unified "Consul Tools" page to showcase our tools and the communities --- website/source/assets/stylesheets/_docs.scss | 5 -- website/source/community.html.erb | 9 ++- website/source/downloads.html.erb | 9 +++ .../source/downloads_consul_template.html.erb | 22 ------- website/source/downloads_tools.html.erb | 66 +++++++++++++++++++ website/source/downloads_web_ui.html.erb | 35 ---------- .../intro/getting-started/ui.html.markdown | 4 +- website/source/layouts/downloads.erb | 8 +-- 8 files changed, 87 insertions(+), 71 deletions(-) delete mode 100644 website/source/downloads_consul_template.html.erb create mode 100644 website/source/downloads_tools.html.erb delete mode 100644 website/source/downloads_web_ui.html.erb diff --git a/website/source/assets/stylesheets/_docs.scss b/website/source/assets/stylesheets/_docs.scss index 02679b6c0..ef66bff4b 100755 --- a/website/source/assets/stylesheets/_docs.scss +++ b/website/source/assets/stylesheets/_docs.scss @@ -161,11 +161,6 @@ body.layout-intro{ li p a, li a { text-decoration: none; - - &:hover { - cursor: default; - text-decoration: none; - } } pre{ diff --git a/website/source/community.html.erb b/website/source/community.html.erb index 123721e8a..8245e1a0b 100644 --- a/website/source/community.html.erb +++ b/website/source/community.html.erb @@ -10,7 +10,7 @@ description: |-

Community

- Consul is a new project with a growing community. Despite this, + Consul is a new project with a growing community. Despite this, there are active, dedicated users willing to help you through various mediums.

@@ -26,6 +26,13 @@ description: |- Issue tracker on GitHub. Please only use this for reporting bugs. Do not ask for general help here. Use IRC or the mailing list for that. +

+

+ Community Tools: + Download Community Tools. Please + check out some of the awesome Consul tooling our amazing community has + helped build. +

People

diff --git a/website/source/downloads.html.erb b/website/source/downloads.html.erb index 585fe5090..4bc6a08ca 100644 --- a/website/source/downloads.html.erb +++ b/website/source/downloads.html.erb @@ -19,6 +19,15 @@ description: |-

+ + + <% product_versions.each do |os, versions| %>
diff --git a/website/source/downloads_consul_template.html.erb b/website/source/downloads_consul_template.html.erb deleted file mode 100644 index 1c09ad301..000000000 --- a/website/source/downloads_consul_template.html.erb +++ /dev/null @@ -1,22 +0,0 @@ ---- -layout: "downloads" -page_title: "Download Consul Template" -sidebar_current: "downloads-template" -description: |- - From this page you can download Consul Template. This is distributed as a separate tarball. ---- - -

Download Consul Template

- -
-
-
-

- From this page you can download the Consul Template. This is distributed as a separate tarball. -

-

- Download Consul Template -

-
-
-
diff --git a/website/source/downloads_tools.html.erb b/website/source/downloads_tools.html.erb new file mode 100644 index 000000000..974a03bdd --- /dev/null +++ b/website/source/downloads_tools.html.erb @@ -0,0 +1,66 @@ +--- +layout: "downloads" +page_title: "Download Consul Tools" +sidebar_current: "downloads-tools" +description: |- + From this page you can download various tools for Consul. These tools are maintained by HashiCorp and the Consul Community. +--- + +

Download Consul Tools

+ +
+
+

+ From this page you can download various tools for Consul. These tools are maintained by HashiCorp and the Consul Community. +

+ +

HashiCorp Tools

+

+ These Consul tools are created and managed by the dedicated engineers at HashiCorp: +

+ +
    +
  • + Envconsul - Read and set environmental variables for processes from Consul. +
  • +
  • + Consul Replicate - Consul cross-DC KV replication daemon. +
  • +
  • + Consul Template - Generic template rendering and notifications with Consul +
  • +
+
+ +
+

Community Tools

+

+ These Consul tools are created and managed by the amazing members of the Consul community: +

+ +
    +
  • + confd - Manage local application configuration files using templates and data from etcd or consul +
  • +
  • + consulate - Python client for the Consul HTTP API +
  • +
  • + crypt - Store and retrieve encrypted configuration parameters from etcd or consul +
  • +
  • + docker-consul - Dockerized Consul Agent +
  • +
  • + git2consul - Mirror the contents of a git repository into Consul KVs +
  • +
  • + registrator -Service registry bridge for Docker +
  • +
+ +

+ Are you the author of a tool and you would like to be featured on this page? Email us at hello@hashicorp.com! +

+
+
diff --git a/website/source/downloads_web_ui.html.erb b/website/source/downloads_web_ui.html.erb deleted file mode 100644 index 98ed76098..000000000 --- a/website/source/downloads_web_ui.html.erb +++ /dev/null @@ -1,35 +0,0 @@ ---- -layout: "downloads" -page_title: "Download Consul Web UI" -sidebar_current: "downloads-ui" -description: |- - From this page you can download the web UI for Consul. This is distributed as a separate ZIP package. ---- - -

Download Consul Web UI

- -
-
-
-

- From this page you can download the web UI for Consul. This is - distributed as a separate ZIP package. You can view a - demo of the web UI here or - you can - read the docs on how to set up the UI here. -

-

- - Download Consul Web UI <%= latest_version %> -

-
-
- -
-
- - - -
-
-
diff --git a/website/source/intro/getting-started/ui.html.markdown b/website/source/intro/getting-started/ui.html.markdown index ce13da876..e51fde069 100644 --- a/website/source/intro/getting-started/ui.html.markdown +++ b/website/source/intro/getting-started/ui.html.markdown @@ -15,7 +15,7 @@ health checks and their current status, and for reading and setting key/value data. The UI automatically supports multi-datacenter. For ease of deployment, the UI is -[distributed](/downloads_web_ui.html) +[distributed](/downloads_tools.html) as static HTML and JavaScript. You don't need a separate web server to run the web UI. The Consul agent itself can be configured to serve the UI. @@ -41,7 +41,7 @@ idea of what the web UI is like. Click the screenshot for the full size. ## Set Up To set up the web UI, -[download the web UI package](/downloads_web_ui.html) +[download the web UI package](/downloads_tools.html) and unzip it to a directory somewhere on the server where the Consul agent is also being run. Then, just append the `-ui-dir` to the `consul agent` command pointing to the directory where you unzipped the UI (the diff --git a/website/source/layouts/downloads.erb b/website/source/layouts/downloads.erb index fd13c8e79..fae758634 100644 --- a/website/source/layouts/downloads.erb +++ b/website/source/layouts/downloads.erb @@ -6,13 +6,9 @@ Download Consul - > - Download Web UI + > + Download Consul Tools - - > - Download Consul Template -
<% end %> From d43a89fdd76dff47542e37b0a7227ce375f66a99 Mon Sep 17 00:00:00 2001 From: Leo Cassarani Date: Thu, 23 Oct 2014 12:31:41 +0100 Subject: [PATCH 034/238] Fix spelling of "separator" in the HTTP API docs --- website/source/docs/agent/http.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index e65422f94..88e555c82 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -155,7 +155,7 @@ It is possible to also only list keys without their values by using the a list of the keys under the given prefix. The optional "?separator=" can be used to list only up to a given separator. -For example, listing "/web/" with a "/" seperator may return: +For example, listing "/web/" with a "/" separator may return: ```javascript [ From 4ca29beec3046087f143458e506d1f38b6eddd9e Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:44:20 -0400 Subject: [PATCH 035/238] Improve navigability of agent HTTP API categories --- website/source/docs/agent/http.html.markdown | 41 ++++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index e65422f94..f6ad5a261 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -14,14 +14,14 @@ versioned to enable changes without breaking backwards compatibility. All endpoints fall into one of several categories: -* kv - Key/Value store -* agent - Agent control -* catalog - Manages nodes and services -* health - Manages health checks -* session - Session manipulation -* acl - ACL creations and management -* event - User Events -* status - Consul system status +* [kv][kv] - Key/Value store +* [agent][agent] - Agent control +* [catalog][catalog] - Manages nodes and services +* [health][health] - Manages health checks +* [session][session] - Session manipulation +* [acl][acl] - ACL creations and management +* [event][event] - User Events +* [status][status] - Consul system status * internal - Internal APIs. Purposely undocumented, subject to change. Each of the categories and their respective endpoints are documented below. @@ -97,7 +97,7 @@ configuration option. However, the token can also be specified per-request by using the "?token=" query parameter. This will take precedence over the default token. -## KV +## KV The KV endpoint is used to expose a simple key/value store. This can be used to store service configurations or other meta data in a simple way. It has only @@ -213,7 +213,7 @@ keys sharing a prefix. If the "?recurse" query parameter is provided, then all keys with the prefix are deleted, otherwise only the specified key. -## Agent +## Agent The Agent endpoints are used to interact with a local Consul agent. Usually, services and checks are registered with an agent, which then takes on the @@ -525,7 +525,7 @@ check, that is also deregistered. The return code is 200 on success. -## Catalog +## Catalog The Catalog is the endpoint used to register and deregister nodes, services, and checks. It also provides a number of query endpoints. @@ -771,7 +771,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -## Health +## Health The Health used to query health related information. It is provided separately from the Catalog, since users may prefer to not use the health checking mechanisms @@ -961,7 +961,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -## Session +## Session The Session endpoints are used to create, destroy and query sessions. The following endpoints are supported: @@ -1106,7 +1106,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -## ACL +## ACL The ACL endpoints are used to create, update, destroy and query ACL tokens. The following endpoints are supported: @@ -1261,7 +1261,7 @@ It returns a JSON body like this: ] ``` -## Event +## Event The Event endpoints are used to fire new events and to query the available events. @@ -1357,7 +1357,7 @@ It returns a JSON body like this: ] ``` -## Status +## Status The Status endpoints are used to get information about the status of the Consul cluster. This are generally very low level, and not really @@ -1389,3 +1389,12 @@ the agent is running in. It returns a list of addresses like: "10.1.10.10:8300" ] ``` + +[kv]: #kv +[agent]: #agent +[catalog]: #catalog +[health]: #health +[session]: #session +[acl]: #acl +[event]: #event +[status]: #status From 3ecb9ec8dfd4d69bbfba49e5a32476ab3b47f23b Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:50:54 -0400 Subject: [PATCH 036/238] Add links for agent endpoints --- website/source/docs/agent/http.html.markdown | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index f6ad5a261..c1ae3f2b8 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -223,21 +223,21 @@ msgpack RPC protocol. The following endpoints are supported: -* /v1/agent/checks : Returns the checks the local agent is managing -* /v1/agent/services : Returns the services local agent is managing -* /v1/agent/members : Returns the members as seen by the local serf agent -* /v1/agent/self : Returns the local node configuration -* /v1/agent/join/\ : Trigger local agent to join a node -* /v1/agent/force-leave/\: Force remove node -* /v1/agent/check/register : Registers a new local check -* /v1/agent/check/deregister/\ : Deregister a local check -* /v1/agent/check/pass/\ : Mark a local test as passing -* /v1/agent/check/warn/\ : Mark a local test as warning -* /v1/agent/check/fail/\ : Mark a local test as critical -* /v1/agent/service/register : Registers a new local service -* /v1/agent/service/deregister/\ : Deregister a local service +* [`/v1/agent/checks`](#agent_checks) : Returns the checks the local agent is managing +* [`/v1/agent/services`](#agent_services) : Returns the services local agent is managing +* [`/v1/agent/members`](#agent_members) : Returns the members as seen by the local serf agent +* [`/v1/agent/self`](#agent_self) : Returns the local node configuration +* [`/v1/agent/join/\`](#agent_join) : Trigger local agent to join a node +* [`/v1/agent/force-leave/\: Force remove node +* [`/v1/agent/check/register`](#agent_check_register) : Registers a new local check +* [`/v1/agent/check/deregister/\`](#agent_check_deregister) : Deregister a local check +* [`/v1/agent/check/pass/\`](#agent_check_pass) : Mark a local test as passing +* [`/v1/agent/check/warn/\`](#agent_check_warn) : Mark a local test as warning +* [`/v1/agent/check/fail/\`](#agent_check_fail) : Mark a local test as critical +* [`/v1/agent/service/register`](#agent_service_register) : Registers a new local service +* [`/v1/agent/service/deregister/\`](#agent_service_deregister) : Deregister a local service -### /v1/agent/checks +### /v1/agent/checks This endpoint is used to return the all the checks that are registered with the local agent. These checks were either provided through configuration files, @@ -263,7 +263,7 @@ This endpoint is hit with a GET and returns a JSON body like this: } ``` -### /v1/agent/services +### /v1/agent/services This endpoint is used to return the all the services that are registered with the local agent. These services were either provided through configuration files, @@ -285,7 +285,7 @@ This endpoint is hit with a GET and returns a JSON body like this: } ``` -### /v1/agent/members +### /v1/agent/members This endpoint is hit with a GET and returns the members the agent sees in the cluster gossip pool. Due to the nature of gossip, this is eventually consistent @@ -320,7 +320,7 @@ This endpoint returns a JSON body like: ] ``` -### /v1/agent/self +### /v1/agent/self This endpoint is used to return configuration of the local agent and member information. @@ -388,7 +388,7 @@ It returns a JSON body like this: } ``` -### /v1/agent/join/\ +### /v1/agent/join/\ This endpoint is hit with a GET and is used to instruct the agent to attempt to connect to a given address. For agents running in server mode, providing a "?wan=1" @@ -396,7 +396,7 @@ query parameter causes the agent to attempt to join using the WAN pool. The endpoint returns 200 on successful join. -### /v1/agent/force-leave/\ +### /v1/agent/force-leave/\ This endpoint is hit with a GET and is used to instructs the agent to force a node into the left state. If a node fails unexpectedly, then it will be in a "failed" state. Once in this state, Consul will @@ -405,7 +405,7 @@ cleaned up. Forcing a node into the left state allows its old entries to be remo The endpoint always returns 200. -### /v1/agent/check/register +### /v1/agent/check/register The register endpoint is used to add a new check to the local agent. There is more documentation on checks [here](/docs/agent/checks.html). @@ -439,7 +439,7 @@ the state of the check. The return code is 200 on success. -### /v1/agent/check/deregister/\ +### /v1/agent/check/deregister/\ The deregister endpoint is used to remove a check from the local agent. The CheckID must be passed after the slash. The agent will take care @@ -447,7 +447,7 @@ of deregistering the check with the Catalog. The return code is 200 on success. -### /v1/agent/check/pass/\ +### /v1/agent/check/pass/\ This endpoint is used with a check that is of the [TTL type](/docs/agent/checks.html). When this endpoint is accessed via a GET, the status of the check is set to "passing", @@ -458,7 +458,7 @@ the status of the check. This should be human readable for operators. The return code is 200 on success. -### /v1/agent/check/warn/\ +### /v1/agent/check/warn/\ This endpoint is used with a check that is of the [TTL type](/docs/agent/checks.html). When this endpoint is accessed via a GET, the status of the check is set to "warning", @@ -469,7 +469,7 @@ the status of the check. This should be human readable for operators. The return code is 200 on success. -### /v1/agent/check/fail/\ +### /v1/agent/check/fail/\ This endpoint is used with a check that is of the [TTL type](/docs/agent/checks.html). When this endpoint is accessed via a GET, the status of the check is set to "critical", @@ -480,7 +480,7 @@ the status of the check. This should be human readable for operators. The return code is 200 on success. -### /v1/agent/service/register +### /v1/agent/service/register The register endpoint is used to add a new service to the local agent. There is more documentation on services [here](/docs/agent/services.html). @@ -516,7 +516,7 @@ The created check will be named "service:\". The return code is 200 on success. -### /v1/agent/service/deregister/\ +### /v1/agent/service/deregister/\ The deregister endpoint is used to remove a service from the local agent. The ServiceID must be passed after the slash. The agent will take care From 4401fbf0e3ffbeef432a62dee13e1b4b3cbe3306 Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:53:11 -0400 Subject: [PATCH 037/238] Add links for catalog endpoints --- website/source/docs/agent/http.html.markdown | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index c1ae3f2b8..8ff68dfb0 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -532,18 +532,18 @@ services, and checks. It also provides a number of query endpoints. The following endpoints are supported: -* /v1/catalog/register : Registers a new node, service, or check -* /v1/catalog/deregister : Deregisters a node, service, or check -* /v1/catalog/datacenters : Lists known datacenters -* /v1/catalog/nodes : Lists nodes in a given DC -* /v1/catalog/services : Lists services in a given DC -* /v1/catalog/service/\ : Lists the nodes in a given service -* /v1/catalog/node/\ : Lists the services provided by a node +* [`/v1/catalog/register`](#catalog_register) : Registers a new node, service, or check +* [`/v1/catalog/deregister`](#catalog_deregister) : Deregisters a node, service, or check +* [`/v1/catalog/datacenters`](#catalog_datacenters) : Lists known datacenters +* [`/v1/catalog/nodes`](#catalog_nodes) : Lists nodes in a given DC +* [`/v1/catalog/services`](#catalog_services) : Lists services in a given DC +* [`/v1/catalog/service/\`](#catalog_service) : Lists the nodes in a given service +* [`/v1/catalog/node/\`](#catalog_nodes) : Lists the services provided by a node The last 4 endpoints of the catalog support blocking queries and consistency modes. -### /v1/catalog/register +### /v1/catalog/register The register endpoint is a low level mechanism for directly registering or updating entries in the catalog. It is usually recommended to use @@ -604,7 +604,7 @@ and visa-versa. They can be provided or omitted at will. If the API call succeeds a 200 status code is returned. -### /v1/catalog/deregister +### /v1/catalog/deregister The deregister endpoint is a low level mechanism for direclty removing entries in the catalog. It is usually recommended to use the agent local @@ -646,7 +646,7 @@ service along with its associated health check (if any) is removed. If the API call succeeds a 200 status code is returned. -### /v1/catalog/datacenters +### /v1/catalog/datacenters This endpoint is hit with a GET and is used to return all the datacenters that are known by the Consul server. @@ -661,7 +661,7 @@ This endpoint does not require a cluster leader, and as such will succeed even during an availability outage. It can thus be a simple check to see if any Consul servers are routable. -### /v1/catalog/nodes +### /v1/catalog/nodes This endpoint is hit with a GET and returns the nodes known about in a given DC. By default the datacenter of the agent is queried, @@ -684,7 +684,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -### /v1/catalog/services +### /v1/catalog/services This endpoint is hit with a GET and returns the services known about in a given DC. By default the datacenter of the agent is queried, @@ -708,7 +708,7 @@ provides all the known tags for a given service. This endpoint supports blocking queries and all consistency modes. -### /v1/catalog/service/\ +### /v1/catalog/service/\ This endpoint is hit with a GET and returns the nodes providing a service in a given DC. By default the datacenter of the agent is queried, @@ -735,7 +735,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -### /v1/catalog/node/\ +### /v1/catalog/node/\ This endpoint is hit with a GET and returns the node provided services. By default the datacenter of the agent is queried, From 9a29e1b60ea09168cb8d911bcfeeba068242bd81 Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:54:46 -0400 Subject: [PATCH 038/238] Add links for health endpoints --- website/source/docs/agent/http.html.markdown | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 8ff68dfb0..e797a265c 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -779,14 +779,14 @@ as they are totally optional. Additionally, some of the query results from the H The following endpoints are supported: -* /v1/health/node/\: Returns the health info of a node -* /v1/health/checks/\: Returns the checks of a service -* /v1/health/service/\: Returns the nodes and health info of a service -* /v1/health/state/\: Returns the checks in a given state +* [`/v1/health/node/\`](#health_node): Returns the health info of a node +* [`/v1/health/checks/\`](#health_checks): Returns the checks of a service +* [`/v1/health/service/\`](#health_service): Returns the nodes and health info of a service +* [`/v1/health/state/\`](#health_state): Returns the checks in a given state All of the health endpoints supports blocking queries and all consistency modes. -### /v1/health/node/\ +### /v1/health/node/\ This endpoint is hit with a GET and returns the node specific checks known. By default the datacenter of the agent is queried, @@ -829,7 +829,7 @@ changed to "critical". This endpoint supports blocking queries and all consistency modes. -### /v1/health/checks/\ +### /v1/health/checks/\ This endpoint is hit with a GET and returns the checks associated with a service in a given datacenter. @@ -856,7 +856,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -### /v1/health/service/\ +### /v1/health/service/\ This endpoint is hit with a GET and returns the service nodes providing a given service in a given datacenter. @@ -922,7 +922,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -### /v1/health/state/\ +### /v1/health/state/\ This endpoint is hit with a GET and returns the checks in a specific state for a given datacenter. By default the datacenter of the agent is queried, From 39e44c79685282269f49987e8c5b09f6de0aed32 Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:56:28 -0400 Subject: [PATCH 039/238] Add links for session endpoints --- website/source/docs/agent/http.html.markdown | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index e797a265c..2878bba86 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -966,15 +966,15 @@ This endpoint supports blocking queries and all consistency modes. The Session endpoints are used to create, destroy and query sessions. The following endpoints are supported: -* /v1/session/create: Creates a new session -* /v1/session/destroy/\: Destroys a given session -* /v1/session/info/\: Queries a given session -* /v1/session/node/\: Lists sessions belonging to a node -* /v1/session/list: Lists all the active sessions +* [`/v1/session/create`](#session_create): Creates a new session +* [`/v1/session/destroy/\`](#session_destroy): Destroys a given session +* [`/v1/session/info/\`](#session_info): Queries a given session +* [`/v1/session/node/\`](#session_node): Lists sessions belonging to a node +* [`/v1/session/list`](#session_list): Lists all the active sessions All of the read session endpoints supports blocking queries and all consistency modes. -### /v1/session/create +### /v1/session/create The create endpoint is used to initialize a new session. There is more documentation on sessions [here](/docs/internals/sessions.html). @@ -1020,7 +1020,7 @@ The return code is 200 on success, along with a body like: This is used to provide the ID of the newly created session. -### /v1/session/destroy/\ +### /v1/session/destroy/\ The destroy endpoint is hit with a PUT and destroys the given session. By default the local datacenter is used, but the "?dc=" query parameter @@ -1029,7 +1029,7 @@ be provided after the slash. The return code is 200 on success. -### /v1/session/info/\ +### /v1/session/info/\ This endpoint is hit with a GET and returns the session information by ID within a given datacenter. By default the datacenter of the agent is queried, @@ -1055,7 +1055,7 @@ It returns a JSON body like this: If the session is not found, null is returned instead of a JSON list. This endpoint supports blocking queries and all consistency modes. -### /v1/session/node/\ +### /v1/session/node/\ This endpoint is hit with a GET and returns the active sessions for a given node and datacenter. By default the datacenter of the agent is queried, @@ -1081,7 +1081,7 @@ It returns a JSON body like this: This endpoint supports blocking queries and all consistency modes. -### /v1/session/list +### /v1/session/list This endpoint is hit with a GET and returns the active sessions for a given datacenter. By default the datacenter of the agent is queried, From d45fdd15ddec2cd85743cbacc180f66b633c4c2b Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:57:55 -0400 Subject: [PATCH 040/238] Add links for ACL endpoints --- website/source/docs/agent/http.html.markdown | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 2878bba86..19d446a5d 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -1111,14 +1111,14 @@ This endpoint supports blocking queries and all consistency modes. The ACL endpoints are used to create, update, destroy and query ACL tokens. The following endpoints are supported: -* /v1/acl/create: Creates a new token with policy -* /v1/acl/update: Update the policy of a token -* /v1/acl/destroy/\: Destroys a given token -* /v1/acl/info/\: Queries the policy of a given token -* /v1/acl/clone/\: Creates a new token by cloning an existing token -* /v1/acl/list: Lists all the active tokens +* [`/v1/acl/create`](#acl_create): Creates a new token with policy +* [`/v1/acl/update`](#acl_update): Update the policy of a token +* [`/v1/acl/destroy/\`](#acl_destroy): Destroys a given token +* [`/v1/acl/info/\`](#acl_info): Queries the policy of a given token +* [`/v1/acl/clone/\`](#acl_clone): Creates a new token by cloning an existing token +* [`/v1/acl/list`](#acl_list): Lists all the active tokens -### /v1/acl/create +### /v1/acl/create The create endpoint is used to make a new token. A token has a name, type, and a set of ACL rules. The name is opaque to Consul, and type @@ -1159,7 +1159,7 @@ The return code is 200 on success, along with a body like: This is used to provide the ID of the newly created ACL token. -### /v1/acl/update +### /v1/acl/update The update endpoint is used to modify the policy for a given ACL token. It is very similar to the create endpoint, however @@ -1189,7 +1189,7 @@ The format of `Rules` is [documented here](/docs/internals/acl.html). The return code is 200 on success. -### /v1/acl/destroy/\ +### /v1/acl/destroy/\ The destroy endpoint is hit with a PUT and destroys the given ACL token. The request is automatically routed to the authoritative ACL datacenter. @@ -1198,7 +1198,7 @@ to the endpoint must be made with a management token. The return code is 200 on success. -### /v1/acl/info/\ +### /v1/acl/info/\ This endpoint is hit with a GET and returns the token information by ID. All requests are routed to the authoritative ACL datacenter @@ -1221,7 +1221,7 @@ It returns a JSON body like this: If the session is not found, null is returned instead of a JSON list. -### /v1/acl/clone/\ +### /v1/acl/clone/\ The clone endpoint is hit with a PUT and returns a token ID that is cloned from an existing token. This allows a token to serve @@ -1239,7 +1239,7 @@ The return code is 200 on success, along with a body like: This is used to provide the ID of the newly created ACL token. -### /v1/acl/list +### /v1/acl/list The list endpoint is hit with a GET and lists all the active ACL tokens. This is a privileged endpoint, and requires a From 3082b3f95f8e6dc3f23c6ddde70342ffb07cf659 Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:59:05 -0400 Subject: [PATCH 041/238] Add links for event endpoints --- website/source/docs/agent/http.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 19d446a5d..9952f3d77 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -1268,10 +1268,10 @@ events. The following endpoints are supported: -* /v1/event/fire/\: Fires a new user event -* /v1/event/list: Lists the most recent events an agent has seen. +* [`/v1/event/fire/\`](#event_fire): Fires a new user event +* [`/v1/event/list`](#event_list): Lists the most recent events an agent has seen. -### /v1/event/fire/\ +### /v1/event/fire/\ The fire endpoint is used to trigger a new user event. A user event needs a name, and optionally takes a number of parameters. @@ -1305,7 +1305,7 @@ The return code is 200 on success, along with a body like: This is used to provide the ID of the newly fired event. -### /v1/event/list +### /v1/event/list This endpoint is hit with a GET and returns the most recent events known by the agent. As a consequence of how the From d2985d3c04596b436e3093f6330e84f4ffc3a92a Mon Sep 17 00:00:00 2001 From: Brian Lalor Date: Thu, 23 Oct 2014 07:59:58 -0400 Subject: [PATCH 042/238] Add links for status endpoints --- website/source/docs/agent/http.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 9952f3d77..a3c25fa63 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -1365,10 +1365,10 @@ useful for clients. The following endpoints are supported: -* /v1/status/leader : Returns the current Raft leader -* /v1/status/peers : Returns the current Raft peer set +* [`/v1/status/leader`](#status_leader) : Returns the current Raft leader +* [`/v1/status/peers`](#status_peers) : Returns the current Raft peer set -### /v1/status/leader +### /v1/status/leader This endpoint is used to get the Raft leader for the datacenter the agent is running in. It returns only an address like: @@ -1377,7 +1377,7 @@ the agent is running in. It returns only an address like: "10.1.10.12:8300" ``` -### /v1/status/peers +### /v1/status/peers This endpoint is used to get the Raft peers for the datacenter the agent is running in. It returns a list of addresses like: From 9df9eb09835f9c6fcd0961caa756a3918dfb5395 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 23 Oct 2014 14:32:11 -0400 Subject: [PATCH 043/238] Small typo in the doc --- website/source/docs/internals/acl.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 45acaa81e..f2f0dfe07 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -37,7 +37,7 @@ on legacy clients. Enforcement is always done by the server nodes. All servers must be [configured to provide](/docs/agent/options.html) an `acl_datacenter`, which enables -ACL enforcement but also specified the authoritative datacenter. Consul does not +ACL enforcement but also specifies the authoritative datacenter. Consul does not replicate data cross-WAN, and instead relies on [RPC forwarding](/docs/internal/architecture.html) to support Multi-Datacenter configurations. However, because requests can be made across datacenter boundaries, ACL tokens must be valid globally. To avoid From d83053e8abe5c7a93bb54551a2fd76188a799487 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 23 Oct 2014 16:39:23 -0700 Subject: [PATCH 044/238] website: document retry_join and retry_interval flags for config file --- website/source/docs/agent/options.html.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 32f7e53f1..98fa59be8 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -323,6 +323,12 @@ definitions support being updated during a reload. * `rejoin_after_leave` - Equivalent to the `-rejoin` command-line flag. +* `retry_join` - Equivalent to the `-retry-join` command-line flag. Takes a list + of addresses to attempt joining every `retry_interval` until at least one + join works. + +* `retry_interval` - Equivalent to the `-retry-interval` command-line flag. + * `server` - Equivalent to the `-server` command-line flag. * `server_name` - When give, this overrides the `node_name` for the TLS certificate. From df43c42c94fe7695a10ad90db5a728756a61f559 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Thu, 23 Oct 2014 17:14:40 -0700 Subject: [PATCH 045/238] website: Fixing formatting --- website/source/docs/agent/http.html.markdown | 40 ++++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 7ae4988d8..de1117fb2 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -227,15 +227,15 @@ The following endpoints are supported: * [`/v1/agent/services`](#agent_services) : Returns the services local agent is managing * [`/v1/agent/members`](#agent_members) : Returns the members as seen by the local serf agent * [`/v1/agent/self`](#agent_self) : Returns the local node configuration -* [`/v1/agent/join/\`](#agent_join) : Trigger local agent to join a node -* [`/v1/agent/force-leave/\: Force remove node +* [`/v1/agent/join/
`](#agent_join) : Trigger local agent to join a node +* [`/v1/agent/force-leave/`](#agent_force_leave)>: Force remove node * [`/v1/agent/check/register`](#agent_check_register) : Registers a new local check -* [`/v1/agent/check/deregister/\`](#agent_check_deregister) : Deregister a local check -* [`/v1/agent/check/pass/\`](#agent_check_pass) : Mark a local test as passing -* [`/v1/agent/check/warn/\`](#agent_check_warn) : Mark a local test as warning -* [`/v1/agent/check/fail/\`](#agent_check_fail) : Mark a local test as critical +* [`/v1/agent/check/deregister/`](#agent_check_deregister) : Deregister a local check +* [`/v1/agent/check/pass/`](#agent_check_pass) : Mark a local test as passing +* [`/v1/agent/check/warn/`](#agent_check_warn) : Mark a local test as warning +* [`/v1/agent/check/fail/`](#agent_check_fail) : Mark a local test as critical * [`/v1/agent/service/register`](#agent_service_register) : Registers a new local service -* [`/v1/agent/service/deregister/\`](#agent_service_deregister) : Deregister a local service +* [`/v1/agent/service/deregister/`](#agent_service_deregister) : Deregister a local service ### /v1/agent/checks @@ -537,8 +537,8 @@ The following endpoints are supported: * [`/v1/catalog/datacenters`](#catalog_datacenters) : Lists known datacenters * [`/v1/catalog/nodes`](#catalog_nodes) : Lists nodes in a given DC * [`/v1/catalog/services`](#catalog_services) : Lists services in a given DC -* [`/v1/catalog/service/\`](#catalog_service) : Lists the nodes in a given service -* [`/v1/catalog/node/\`](#catalog_nodes) : Lists the services provided by a node +* [`/v1/catalog/service/`](#catalog_service) : Lists the nodes in a given service +* [`/v1/catalog/node/`](#catalog_nodes) : Lists the services provided by a node The last 4 endpoints of the catalog support blocking queries and consistency modes. @@ -779,10 +779,10 @@ as they are totally optional. Additionally, some of the query results from the H The following endpoints are supported: -* [`/v1/health/node/\`](#health_node): Returns the health info of a node -* [`/v1/health/checks/\`](#health_checks): Returns the checks of a service -* [`/v1/health/service/\`](#health_service): Returns the nodes and health info of a service -* [`/v1/health/state/\`](#health_state): Returns the checks in a given state +* [`/v1/health/node/`](#health_node): Returns the health info of a node +* [`/v1/health/checks/`](#health_checks): Returns the checks of a service +* [`/v1/health/service/`](#health_service): Returns the nodes and health info of a service +* [`/v1/health/state/`](#health_state): Returns the checks in a given state All of the health endpoints supports blocking queries and all consistency modes. @@ -967,9 +967,9 @@ The Session endpoints are used to create, destroy and query sessions. The following endpoints are supported: * [`/v1/session/create`](#session_create): Creates a new session -* [`/v1/session/destroy/\`](#session_destroy): Destroys a given session -* [`/v1/session/info/\`](#session_info): Queries a given session -* [`/v1/session/node/\`](#session_node): Lists sessions belonging to a node +* [`/v1/session/destroy/`](#session_destroy): Destroys a given session +* [`/v1/session/info/`](#session_info): Queries a given session +* [`/v1/session/node/`](#session_node): Lists sessions belonging to a node * [`/v1/session/list`](#session_list): Lists all the active sessions All of the read session endpoints supports blocking queries and all consistency modes. @@ -1113,9 +1113,9 @@ The following endpoints are supported: * [`/v1/acl/create`](#acl_create): Creates a new token with policy * [`/v1/acl/update`](#acl_update): Update the policy of a token -* [`/v1/acl/destroy/\`](#acl_destroy): Destroys a given token -* [`/v1/acl/info/\`](#acl_info): Queries the policy of a given token -* [`/v1/acl/clone/\`](#acl_clone): Creates a new token by cloning an existing token +* [`/v1/acl/destroy/`](#acl_destroy): Destroys a given token +* [`/v1/acl/info/`](#acl_info): Queries the policy of a given token +* [`/v1/acl/clone/`](#acl_clone): Creates a new token by cloning an existing token * [`/v1/acl/list`](#acl_list): Lists all the active tokens ### /v1/acl/create @@ -1268,7 +1268,7 @@ events. The following endpoints are supported: -* [`/v1/event/fire/\`](#event_fire): Fires a new user event +* [`/v1/event/fire/`](#event_fire): Fires a new user event * [`/v1/event/list`](#event_list): Lists the most recent events an agent has seen. ### /v1/event/fire/\ From ad4598959e8b2f90b24a185288a734459bf88560 Mon Sep 17 00:00:00 2001 From: Alexander Simmerl Date: Thu, 23 Oct 2014 22:22:25 -0400 Subject: [PATCH 046/238] Add multiple service definition support This change-set adds another key to the configuration decoding called `services`, which is expected to be a list of service definitions. It follows the established convention of only allowing one of the keys: `service`, `check`, `services`. For every entry in the list it calls the corresponding decode method and appends it to the Servics of the resulting Config. While a similar result could be achieved with changing the Services member of the Config struct to have named mapstruct tag it lacks the proper time conversions provided by DecodeServiceDefinition. --- command/agent/config.go | 15 +++- command/agent/config_test.go | 80 +++++++++++++++++++ .../source/docs/agent/services.html.markdown | 38 +++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 9fe653a33..791f5ede9 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -392,9 +392,20 @@ func DecodeConfig(r io.Reader) (*Config, error) { // Check the result type if obj, ok := raw.(map[string]interface{}); ok { - // Check for a "service" or "check" key, meaning + // Check for a "services", "service" or "check" key, meaning // this is actually a definition entry - if sub, ok := obj["service"]; ok { + if sub, ok := obj["services"]; ok { + if list, ok := sub.([]interface{}); ok { + for _, srv := range list { + service, err := DecodeServiceDefinition(srv) + if err != nil { + return nil, err + } + result.Services = append(result.Services, service) + } + return &result, nil + } + } else if sub, ok := obj["service"]; ok { service, err := DecodeServiceDefinition(sub) result.Services = append(result.Services, service) return &result, err diff --git a/command/agent/config_test.go b/command/agent/config_test.go index f1c109b0f..e4a8b779e 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -516,6 +516,86 @@ func TestDecodeConfig(t *testing.T) { } } +func TestDecodeConfig_Services(t *testing.T) { + input := `{ + "services": [ + { + "id": "red0", + "name": "redis", + "tags": [ + "master" + ], + "port": 6000, + "check": { + "script": "/bin/check_redis -p 6000", + "interval": "5s", + "ttl": "20s" + } + }, + { + "id": "red1", + "name": "redis", + "tags": [ + "delayed", + "slave" + ], + "port": 7000, + "check": { + "script": "/bin/check_redis -p 7000", + "interval": "30s", + "ttl": "60s" + } + } + ] + }` + + config, err := DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(config.Services) != 2 { + t.Fatalf("missing services") + } + + expected := &ServiceDefinition{ + Check: CheckType{ + Interval: 5 * time.Second, + Script: "/bin/check_redis -p 6000", + TTL: 20 * time.Second, + }, + ID: "red0", + Name: "redis", + Tags: []string{ + "master", + }, + Port: 6000, + } + + if !reflect.DeepEqual(config.Services[0], expected) { + t.Fatalf("services do not match:\n%+v\n%+v", config.Services[0], expected) + } + + expected = &ServiceDefinition{ + Check: CheckType{ + Interval: 30 * time.Second, + Script: "/bin/check_redis -p 7000", + TTL: 60 * time.Second, + }, + ID: "red1", + Name: "redis", + Tags: []string{ + "delayed", + "slave", + }, + Port: 7000, + } + + if !reflect.DeepEqual(config.Services[1], expected) { + t.Fatalf("services do not match:\n%+v\n%+v", config.Services[1], expected) + } +} + func TestDecodeConfig_Service(t *testing.T) { // Basics input := `{"service": {"id": "red1", "name": "redis", "tags": ["master"], "port":8000, "check": {"script": "/bin/check_redis", "interval": "10s", "ttl": "15s" }}}` diff --git a/website/source/docs/agent/services.html.markdown b/website/source/docs/agent/services.html.markdown index 3e99fb855..6dc8e30ee 100644 --- a/website/source/docs/agent/services.html.markdown +++ b/website/source/docs/agent/services.html.markdown @@ -62,3 +62,41 @@ end in the ".json" extension to be loaded by Consul. Check definitions can also be updated by sending a `SIGHUP` to the agent. Alternatively, the service can be registered dynamically using the [HTTP API](/docs/agent/http.html). +## Multiple Service Definitions + +Multiple services definitions can be provided at once. Single and mutiple service definitions can't be provided together in one configuration file. + +```javascript +{ + "services": [ + { + "id": "red0", + "name": "redis", + "tags": [ + "master" + ], + "port": 6000, + "check": { + "script": "/bin/check_redis -p 6000", + "interval": "5s", + "ttl": "20s" + } + }, + { + "id": "red1", + "name": "redis", + "tags": [ + "delayed", + "slave" + ], + "port": 7000, + "check": { + "script": "/bin/check_redis -p 7000", + "interval": "30s", + "ttl": "60s" + } + }, + ... + ] +} +``` From d4e55923605c860ade869b59993272cf21fdaaad Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 26 Oct 2014 12:00:11 -0700 Subject: [PATCH 047/238] agent: test config as a whole in services test --- command/agent/config_test.go | 69 +++++++++++++++++------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index e4a8b779e..3b162ea08 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -554,45 +554,40 @@ func TestDecodeConfig_Services(t *testing.T) { t.Fatalf("err: %s", err) } - if len(config.Services) != 2 { - t.Fatalf("missing services") + expected := &Config{ + Services: []*ServiceDefinition{ + &ServiceDefinition{ + Check: CheckType{ + Interval: 5 * time.Second, + Script: "/bin/check_redis -p 6000", + TTL: 20 * time.Second, + }, + ID: "red0", + Name: "redis", + Tags: []string{ + "master", + }, + Port: 6000, + }, + &ServiceDefinition{ + Check: CheckType{ + Interval: 30 * time.Second, + Script: "/bin/check_redis -p 7000", + TTL: 60 * time.Second, + }, + ID: "red1", + Name: "redis", + Tags: []string{ + "delayed", + "slave", + }, + Port: 7000, + }, + }, } - expected := &ServiceDefinition{ - Check: CheckType{ - Interval: 5 * time.Second, - Script: "/bin/check_redis -p 6000", - TTL: 20 * time.Second, - }, - ID: "red0", - Name: "redis", - Tags: []string{ - "master", - }, - Port: 6000, - } - - if !reflect.DeepEqual(config.Services[0], expected) { - t.Fatalf("services do not match:\n%+v\n%+v", config.Services[0], expected) - } - - expected = &ServiceDefinition{ - Check: CheckType{ - Interval: 30 * time.Second, - Script: "/bin/check_redis -p 7000", - TTL: 60 * time.Second, - }, - ID: "red1", - Name: "redis", - Tags: []string{ - "delayed", - "slave", - }, - Port: 7000, - } - - if !reflect.DeepEqual(config.Services[1], expected) { - t.Fatalf("services do not match:\n%+v\n%+v", config.Services[1], expected) + if !reflect.DeepEqual(config, expected) { + t.Fatalf("bad: %#v", config) } } From 97ba2703f12288bcbdec777856256830e0e2ecf7 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 26 Oct 2014 13:11:25 -0700 Subject: [PATCH 048/238] agent: add support for multiple checks and config mixing --- command/agent/config.go | 26 ++++++-- command/agent/config_test.go | 114 +++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 791f5ede9..7037a81d2 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -403,16 +403,32 @@ func DecodeConfig(r io.Reader) (*Config, error) { } result.Services = append(result.Services, service) } - return &result, nil } - } else if sub, ok := obj["service"]; ok { + } + if sub, ok := obj["service"]; ok { service, err := DecodeServiceDefinition(sub) + if err != nil { + return nil, err + } result.Services = append(result.Services, service) - return &result, err - } else if sub, ok := obj["check"]; ok { + } + if sub, ok := obj["checks"]; ok { + if list, ok := sub.([]interface{}); ok { + for _, chk := range list { + check, err := DecodeCheckDefinition(chk) + if err != nil { + return nil, err + } + result.Checks = append(result.Checks, check) + } + } + } + if sub, ok := obj["check"]; ok { check, err := DecodeCheckDefinition(sub) + if err != nil { + return nil, err + } result.Checks = append(result.Checks, check) - return &result, err } } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 3b162ea08..f73fd9299 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -591,6 +591,120 @@ func TestDecodeConfig_Services(t *testing.T) { } } +func TestDecodeConfig_Checks(t *testing.T) { + input := `{ + "checks": [ + { + "id": "chk1", + "name": "mem", + "script": "/bin/check_mem", + "interval": "5s" + }, + { + "id": "chk2", + "name": "cpu", + "script": "/bin/check_cpu", + "interval": "10s" + } + ] + }` + + config, err := DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &Config{ + Checks: []*CheckDefinition{ + &CheckDefinition{ + ID: "chk1", + Name: "mem", + CheckType: CheckType{ + Script: "/bin/check_mem", + Interval: 5 * time.Second, + }, + }, + &CheckDefinition{ + ID: "chk2", + Name: "cpu", + CheckType: CheckType{ + Script: "/bin/check_cpu", + Interval: 10 * time.Second, + }, + }, + }, + } + + if !reflect.DeepEqual(config, expected) { + t.Fatalf("bad: %#v", config) + } +} + +func TestDecodeConfig_Multiples(t *testing.T) { + input := `{ + "services": [ + { + "id": "red0", + "name": "redis", + "tags": [ + "master" + ], + "port": 6000, + "check": { + "script": "/bin/check_redis -p 6000", + "interval": "5s", + "ttl": "20s" + } + } + ], + "checks": [ + { + "id": "chk1", + "name": "mem", + "script": "/bin/check_mem", + "interval": "10s" + } + ] + }` + + config, err := DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := &Config{ + Services: []*ServiceDefinition{ + &ServiceDefinition{ + Check: CheckType{ + Interval: 5 * time.Second, + Script: "/bin/check_redis -p 6000", + TTL: 20 * time.Second, + }, + ID: "red0", + Name: "redis", + Tags: []string{ + "master", + }, + Port: 6000, + }, + }, + Checks: []*CheckDefinition{ + &CheckDefinition{ + ID: "chk1", + Name: "mem", + CheckType: CheckType{ + Script: "/bin/check_mem", + Interval: 10 * time.Second, + }, + }, + }, + } + + if !reflect.DeepEqual(config, expected) { + t.Fatalf("bad: %#v", config) + } +} + func TestDecodeConfig_Service(t *testing.T) { // Basics input := `{"service": {"id": "red1", "name": "redis", "tags": ["master"], "port":8000, "check": {"script": "/bin/check_redis", "interval": "10s", "ttl": "15s" }}}` From 95fa599937f8ddc6d787eccb22973db4f50f4f1a Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 26 Oct 2014 13:24:23 -0700 Subject: [PATCH 049/238] website: update docs for multiple checks in config --- .../source/docs/agent/checks.html.markdown | 25 +++++++++++++++++++ .../source/docs/agent/services.html.markdown | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/website/source/docs/agent/checks.html.markdown b/website/source/docs/agent/checks.html.markdown index 33714d3bc..277a89c4b 100644 --- a/website/source/docs/agent/checks.html.markdown +++ b/website/source/docs/agent/checks.html.markdown @@ -85,3 +85,28 @@ a specific meaning. Specifically: This is the only convention that Consul depends on. Any output of the script will be captured and stored in the `notes` field so that it can be viewed by human operators. + +## Multiple Check Definitions + +Multiple check definitions can be provided at once using the `checks` (plural) +key in your configuration file. + +```javascript +{ + "checks": [ + { + "id": "chk1", + "name": "mem", + "script": "/bin/check_mem", + "interval": "5s", + }, + { + "id": "chk2", + "name": "cpu", + "script": "/bin/check_cpu", + "interval": "10s", + }, + ... + ] +} +``` diff --git a/website/source/docs/agent/services.html.markdown b/website/source/docs/agent/services.html.markdown index 6dc8e30ee..ad66da571 100644 --- a/website/source/docs/agent/services.html.markdown +++ b/website/source/docs/agent/services.html.markdown @@ -64,7 +64,8 @@ service can be registered dynamically using the [HTTP API](/docs/agent/http.html ## Multiple Service Definitions -Multiple services definitions can be provided at once. Single and mutiple service definitions can't be provided together in one configuration file. +Multiple services definitions can be provided at once using the `services` +(plural) key in your configuration file. ```javascript { From affdf4548ae5f23faf3914201851b84ac5e08e94 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sun, 26 Oct 2014 22:21:45 -0400 Subject: [PATCH 050/238] Redirect old WebUI downloads page to new one There are currently a number of sites on the Internet that link to the old downloads_web_ui.html page, including some popular blog posts. Since the WebUI has been moved onto the Consul page itself, these links are now broken. This commit adds a 301 (to preserve SEO) redirect from the old page to the new one. --- website/config.ru | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/website/config.ru b/website/config.ru index fd8f01d6c..9311cefc4 100644 --- a/website/config.ru +++ b/website/config.ru @@ -4,6 +4,11 @@ require "rack/contrib/response_headers" require "rack/contrib/static_cache" require "rack/contrib/try_static" +require "rake/rewrite" +use Rack::Rewrite do + r301 "/downloads_web_ui.html", "/downloads.html" +end + # Properly compress the output if the client can handle it. use Rack::Deflater From e57ea7df786d24852aac04c79e34369d96ada718 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Sun, 26 Oct 2014 22:14:56 -0400 Subject: [PATCH 051/238] Do not index "Jespen" code block The word "Jespen" is one of our top keywords because it appears so many times in the Jespen Testing page. This commit adds a comment to disable Google indexing in that code block so we can better target keywords. --- website/source/docs/internals/jepsen.html.markdown | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/source/docs/internals/jepsen.html.markdown b/website/source/docs/internals/jepsen.html.markdown index abf7973a4..d91a26505 100644 --- a/website/source/docs/internals/jepsen.html.markdown +++ b/website/source/docs/internals/jepsen.html.markdown @@ -32,6 +32,8 @@ Below is the output captured from Jepsen. We ran Jepsen multiple times, and it passed each time. This output is only representative of a single run. + + ```text $ lein test :only jepsen.system.consul-test @@ -4020,3 +4022,5 @@ INFO jepsen.system.consul - :n5 consul nuked Ran 1 tests containing 1 assertions. 0 failures, 0 errors. ``` + + From 4e3a634239a1d4b4800ab42e0f51b3c9fb628c3a Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 27 Oct 2014 11:02:02 -0400 Subject: [PATCH 052/238] Force a bundle update for rack-rewrite --- website/Gemfile.lock | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/website/Gemfile.lock b/website/Gemfile.lock index 9b44be5fb..170034efc 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,6 +1,6 @@ GIT remote: git://github.com/hashicorp/middleman-hashicorp.git - revision: dc9f2d6b986842622796a569bf9358f6f3e3f634 + revision: b82c2c2fdc244cd0bd529ff27cfab24e43f07708 specs: middleman-hashicorp (0.1.0) bootstrap-sass (~> 3.2) @@ -11,6 +11,8 @@ GIT middleman-minify-html (~> 3.4) middleman-syntax (~> 2.0) rack-contrib (~> 1.1) + rack-rewrite (~> 1.5) + rack-ssl-enforcer (~> 0.2) redcarpet (~> 3.1) therubyracer (~> 0.12) thin (~> 1.6) @@ -29,7 +31,7 @@ GEM builder (3.2.2) celluloid (0.16.0) timers (~> 4.0.0) - chunky_png (1.3.2) + chunky_png (1.3.3) coffee-script (2.3.0) coffee-script-source execjs @@ -65,7 +67,7 @@ GEM http_parser.rb (0.6.0) i18n (0.6.11) json (1.8.1) - kramdown (1.4.2) + kramdown (1.5.0) less (2.6.0) commonjs (~> 0.2.7) libv8 (3.16.14.7) @@ -124,6 +126,8 @@ GEM rack (>= 0.9.1) rack-livereload (0.3.15) rack + rack-rewrite (1.5.0) + rack-ssl-enforcer (0.2.8) rack-test (0.6.2) rack (>= 1.0) rb-fsevent (0.9.4) @@ -157,7 +161,7 @@ GEM hitimes tzinfo (1.2.2) thread_safe (~> 0.1) - uber (0.0.9) + uber (0.0.10) uglifier (2.5.3) execjs (>= 0.3.0) json (>= 1.8.0) From c751a4ff5f9edf4c1b74f7dfa35ee42c44cb285c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 27 Oct 2014 11:58:01 -0700 Subject: [PATCH 053/238] website: fix JSON in multiple checks documentation --- website/source/docs/agent/checks.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/agent/checks.html.markdown b/website/source/docs/agent/checks.html.markdown index 277a89c4b..a5a4bdb56 100644 --- a/website/source/docs/agent/checks.html.markdown +++ b/website/source/docs/agent/checks.html.markdown @@ -98,13 +98,13 @@ key in your configuration file. "id": "chk1", "name": "mem", "script": "/bin/check_mem", - "interval": "5s", + "interval": "5s" }, { "id": "chk2", "name": "cpu", "script": "/bin/check_cpu", - "interval": "10s", + "interval": "10s" }, ... ] From 09af3cb2fc1b68fdd3e7837ee7bd1d120e14ea84 Mon Sep 17 00:00:00 2001 From: Tom Lanyon Date: Tue, 28 Oct 2014 15:21:32 +1030 Subject: [PATCH 054/238] website: fix download link for web UI in intro guide. --- website/source/intro/getting-started/ui.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/intro/getting-started/ui.html.markdown b/website/source/intro/getting-started/ui.html.markdown index e51fde069..2c12ed973 100644 --- a/website/source/intro/getting-started/ui.html.markdown +++ b/website/source/intro/getting-started/ui.html.markdown @@ -15,7 +15,7 @@ health checks and their current status, and for reading and setting key/value data. The UI automatically supports multi-datacenter. For ease of deployment, the UI is -[distributed](/downloads_tools.html) +[distributed](/downloads.html) as static HTML and JavaScript. You don't need a separate web server to run the web UI. The Consul agent itself can be configured to serve the UI. @@ -41,7 +41,7 @@ idea of what the web UI is like. Click the screenshot for the full size. ## Set Up To set up the web UI, -[download the web UI package](/downloads_tools.html) +[download the web UI package](/downloads.html) and unzip it to a directory somewhere on the server where the Consul agent is also being run. Then, just append the `-ui-dir` to the `consul agent` command pointing to the directory where you unzipped the UI (the From 7a2a01f9dece6d4ab32775c31bab2461e0a6ed38 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 30 Oct 2014 21:44:23 -0400 Subject: [PATCH 055/238] ACL doc clarification Fixes #443 --- website/source/docs/agent/options.html.markdown | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 98fa59be8..b34c555e9 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -178,7 +178,12 @@ definitions support being updated during a reload. * `acl_datacenter` - Only used by servers. This designates the datacenter which is authoritative for ACL information. It must be provided to enable ACLs. - All servers and datacenters must agree on the ACL datacenter. + All servers and datacenters must agree on the ACL datacenter. Setting it on + the servers is all you need for enforcement, but for the APIs to work on the + clients, it must be set (to forward properly). Also, if we want to enhance + the ACL support for other features like service discovery, enforcement + might move to the edges, so it's best to just set the acl_datacenter on all + the nodes. * `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The default policy controls the behavior of a token when there is no matching From 7d01816c1f0eec45861011ef808b94bcae3c37b8 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 30 Oct 2014 21:46:19 -0400 Subject: [PATCH 056/238] Rephrase --- website/source/docs/agent/options.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index b34c555e9..f2860c74e 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -180,10 +180,10 @@ definitions support being updated during a reload. is authoritative for ACL information. It must be provided to enable ACLs. All servers and datacenters must agree on the ACL datacenter. Setting it on the servers is all you need for enforcement, but for the APIs to work on the - clients, it must be set (to forward properly). Also, if we want to enhance - the ACL support for other features like service discovery, enforcement - might move to the edges, so it's best to just set the acl_datacenter on all - the nodes. + clients, it must be set on them too (to forward properly). Also, if we want + to enhance the ACL support for other features like service discovery, + enforcement might move to the edges, so it's best to just set the + `acl_datacenter` on all the nodes. * `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The default policy controls the behavior of a token when there is no matching From 4d8f1c21336956eb8be116da0302308a4d86ff15 Mon Sep 17 00:00:00 2001 From: foostan Date: Sat, 1 Nov 2014 04:19:41 +0900 Subject: [PATCH 057/238] Add multiple recursor definition support --- command/agent/command.go | 2 +- command/agent/config.go | 13 +-- command/agent/config_test.go | 13 ++- command/agent/dns.go | 84 +++++++++++-------- command/agent/dns_test.go | 2 +- website/source/docs/agent/dns.html.markdown | 4 +- website/source/docs/agent/http.html.markdown | 2 +- .../source/docs/agent/options.html.markdown | 4 +- 8 files changed, 75 insertions(+), 49 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index b23e3bc96..6aa51ac7c 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -302,7 +302,7 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log } server, err := NewDNSServer(agent, &config.DNSConfig, logOutput, - config.Domain, dnsAddr.String(), config.DNSRecursor) + config.Domain, dnsAddr.String(), config.DNSRecursors) if err != nil { agent.Shutdown() c.Ui.Error(fmt.Sprintf("Error starting dns server: %s", err)) diff --git a/command/agent/config.go b/command/agent/config.go index 7037a81d2..9fc2619e8 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -97,9 +97,9 @@ type Config struct { // DataDir is the directory to store our state in DataDir string `mapstructure:"data_dir"` - // DNSRecursor can be set to allow the DNS server to recursively + // DNSRecursors can be set to allow the DNS servers to recursively // resolve non-consul domains - DNSRecursor string `mapstructure:"recursor"` + DNSRecursors []string `mapstructure:"recursors"` // DNS configuration DNSConfig DNSConfig `mapstructure:"dns_config"` @@ -623,9 +623,12 @@ func MergeConfig(a, b *Config) *Config { if b.DataDir != "" { result.DataDir = b.DataDir } - if b.DNSRecursor != "" { - result.DNSRecursor = b.DNSRecursor - } + + // Copy the dns recursors + result.DNSRecursors = make([]string, 0, len(a.DNSRecursors)+len(b.DNSRecursors)) + result.DNSRecursors = append(result.DNSRecursors, a.DNSRecursors...) + result.DNSRecursors = append(result.DNSRecursors, b.DNSRecursors...) + if b.Domain != "" { result.Domain = b.Domain } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index f73fd9299..0c09e02d4 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -109,7 +109,7 @@ func TestDecodeConfig(t *testing.T) { } // DNS setup - input = `{"ports": {"dns": 8500}, "recursor": "8.8.8.8", "domain": "foobar"}` + input = `{"ports": {"dns": 8500}, "recursor": ["8.8.8.8","8.8.4.4"], "domain": "foobar"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -119,7 +119,13 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } - if config.DNSRecursor != "8.8.8.8" { + if len(config.DNSRecursors) != 2 { + t.Fatalf("bad: %#v", config) + } + if config.DNSRecursors[0] != "8.8.8.8" { + t.Fatalf("bad: %#v", config) + } + if config.DNSRecursors[1] != "8.8.4.4" { t.Fatalf("bad: %#v", config) } @@ -791,7 +797,6 @@ func TestMergeConfig(t *testing.T) { BootstrapExpect: 0, Datacenter: "dc1", DataDir: "/tmp/foo", - DNSRecursor: "127.0.0.1:1001", Domain: "basic", LogLevel: "debug", NodeName: "foo", @@ -811,7 +816,7 @@ func TestMergeConfig(t *testing.T) { BootstrapExpect: 3, Datacenter: "dc2", DataDir: "/tmp/bar", - DNSRecursor: "127.0.0.2:1001", + DNSRecursors: []string{"127.0.0.2:1001"}, DNSConfig: DNSConfig{ NodeTTL: 10 * time.Second, ServiceTTL: map[string]time.Duration{ diff --git a/command/agent/dns.go b/command/agent/dns.go index 8a1522f75..0e5f8f195 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -28,12 +28,12 @@ type DNSServer struct { dnsServer *dns.Server dnsServerTCP *dns.Server domain string - recursor string + recursors []string logger *log.Logger } // NewDNSServer starts a new DNS server to provide an agent interface -func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain, bind, recursor string) (*DNSServer, error) { +func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain string, bind string, recursors []string) (*DNSServer, error) { // Make sure domain is FQDN domain = dns.Fqdn(domain) @@ -61,7 +61,7 @@ func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain, dnsServer: server, dnsServerTCP: serverTCP, domain: domain, - recursor: recursor, + recursors: recursors, logger: log.New(logOutput, "", log.LstdFlags), } @@ -70,12 +70,19 @@ func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain, if domain != consulDomain { mux.HandleFunc(consulDomain, srv.handleTest) } - if recursor != "" { - recursor, err := recursorAddr(recursor) - if err != nil { - return nil, fmt.Errorf("Invalid recursor address: %v", err) + if len(recursors) > 0 { + validatedRecursors := []string{} + + for _, recursor := range recursors { + recursor, err := recursorAddr(recursor) + if err != nil { + return nil, fmt.Errorf("Invalid recursor address: %v", err) + } + + validatedRecursors = append(validatedRecursors, recursor) } - srv.recursor = recursor + + srv.recursors = validatedRecursors mux.HandleFunc(".", srv.handleRecurse) } @@ -178,7 +185,7 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { m := new(dns.Msg) m.SetReply(req) m.Authoritative = true - m.RecursionAvailable = (d.recursor != "") + m.RecursionAvailable = (len(d.recursors) > 0) // Only add the SOA if requested if req.Question[0].Qtype == dns.TypeSOA { @@ -587,30 +594,34 @@ func (d *DNSServer) handleRecurse(resp dns.ResponseWriter, req *dns.Msg) { // Recursively resolve c := &dns.Client{Net: network} - r, rtt, err := c.Exchange(req, d.recursor) + for i,recursor := range d.recursors { + r, rtt, err := c.Exchange(req, recursor) - // On failure, return a SERVFAIL message - if err != nil { - d.logger.Printf("[ERR] dns: recurse failed: %v", err) - m := &dns.Msg{} - m.SetReply(req) - m.RecursionAvailable = true - m.SetRcode(req, dns.RcodeServerFailure) - resp.WriteMsg(m) - return - } - d.logger.Printf("[DEBUG] dns: recurse RTT for %v (%v)", q, rtt) + if i < len(d.recursors) && err != nil { + continue + } else if err != nil { + // On all of failure, return a SERVFAIL message + d.logger.Printf("[ERR] dns: recurse failed: %v", err) + m := &dns.Msg{} + m.SetReply(req) + m.RecursionAvailable = true + m.SetRcode(req, dns.RcodeServerFailure) + resp.WriteMsg(m) + return + } + d.logger.Printf("[DEBUG] dns: recurse RTT for %v (%v)", q, rtt) - // Forward the response - if err := resp.WriteMsg(r); err != nil { - d.logger.Printf("[WARN] dns: failed to respond: %v", err) + // Forward the response + if err := resp.WriteMsg(r); err != nil { + d.logger.Printf("[WARN] dns: failed to respond: %v", err) + } } } // resolveCNAME is used to recursively resolve CNAME records func (d *DNSServer) resolveCNAME(name string) []dns.RR { // Do nothing if we don't have a recursor - if d.recursor == "" { + if len(d.recursors) > 0 { return nil } @@ -620,13 +631,20 @@ func (d *DNSServer) resolveCNAME(name string) []dns.RR { // Make a DNS lookup request c := &dns.Client{Net: "udp"} - r, rtt, err := c.Exchange(m, d.recursor) - if err != nil { - d.logger.Printf("[ERR] dns: cname recurse failed: %v", err) - return nil - } - d.logger.Printf("[DEBUG] dns: cname recurse RTT for %v (%v)", name, rtt) + for i,recursor := range d.recursors { + r, rtt, err := c.Exchange(m, recursor) - // Return all the answers - return r.Answer + if i < len(d.recursors) && err != nil { + continue + } else if err != nil { + d.logger.Printf("[ERR] dns: cname recurse failed: %v", err) + return nil + } + d.logger.Printf("[DEBUG] dns: cname recurse RTT for %v (%v)", name, rtt) + + // Return all the answers + return r.Answer + } + + return nil } diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index b28fc7db1..e25329497 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -22,7 +22,7 @@ func makeDNSServerConfig(t *testing.T, config *DNSConfig) (string, *DNSServer) { addr, _ := conf.ClientListener(conf.Addresses.DNS, conf.Ports.DNS) dir, agent := makeAgent(t, conf) server, err := NewDNSServer(agent, config, agent.logOutput, - conf.Domain, addr.String(), "8.8.8.8:53") + conf.Domain, addr.String(), []string{"8.8.8.8:53"}) if err != nil { t.Fatalf("err: %v", err) } diff --git a/website/source/docs/agent/dns.html.markdown b/website/source/docs/agent/dns.html.markdown index b1afc0f8c..ab6995a35 100644 --- a/website/source/docs/agent/dns.html.markdown +++ b/website/source/docs/agent/dns.html.markdown @@ -20,14 +20,14 @@ provide the redis service, located in the "east-aws" datacenter, with no failing health checks. It's that simple! There are a number of [configuration options](/docs/agent/options.html) that -are important for the DNS interface. They are `client_addr`, `ports.dns`, `recursor`, +are important for the DNS interface. They are `client_addr`, `ports.dns`, `recursors`, `domain`, and `dns_config`. By default Consul will listen on 127.0.0.1:8600 for DNS queries in the "consul." domain, without support for DNS recursion. All queries are case-insensitive, a name lookup for `PostgreSQL.node.dc1.consul` will find all nodes named `postgresql`, no matter of case. There are a few ways to use the DNS interface. One option is to use a custom DNS resolver library and point it at Consul. Another option is to set Consul -as the DNS server for a node, and provide a `recursor` so that non-Consul queries +as the DNS server for a node, and provide `recursors` so that non-Consul queries can also be resolved. The last method is to forward all queries for the "consul." domain to a Consul agent from the existing DNS server. To play with the DNS server on the command line, dig can be used: diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index de1117fb2..1dc999eb1 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -333,7 +333,7 @@ It returns a JSON body like this: "Server": true, "Datacenter": "dc1", "DataDir": "/tmp/consul", - "DNSRecursor": "", + "DNSRecursors": [], "Domain": "consul.", "LogLevel": "INFO", "NodeName": "foobar", diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 98fa59be8..9a5893f95 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -316,10 +316,10 @@ definitions support being updated during a reload. * `protocol` - Equivalent to the `-protocol` command-line flag. -* `recursor` - This flag provides an address of an upstream DNS server that is used to +* `recursors` - This flag provides addresses of upstream DNS servers that are used to recursively resolve queries if they are not inside the service domain for consul. For example, a node can use Consul directly as a DNS server, and if the record is outside of the "consul." domain, - the query will be resolved upstream using this server. + the query will be resolved upstream using their servers. * `rejoin_after_leave` - Equivalent to the `-rejoin` command-line flag. From 0222ed9eb95bf2ab8f957fb938d166b6d4b33464 Mon Sep 17 00:00:00 2001 From: Emil Hessman Date: Sat, 1 Nov 2014 22:56:48 +0100 Subject: [PATCH 058/238] Fix missing arguments --- consul/kvs_endpoint_test.go | 2 +- consul/serf.go | 2 +- consul/state_store_test.go | 146 ++++++++++++++++++------------------ consul/util_test.go | 2 +- watch/watch.go | 4 +- 5 files changed, 78 insertions(+), 78 deletions(-) diff --git a/consul/kvs_endpoint_test.go b/consul/kvs_endpoint_test.go index 90d92058f..89615a76d 100644 --- a/consul/kvs_endpoint_test.go +++ b/consul/kvs_endpoint_test.go @@ -510,7 +510,7 @@ func TestKVS_Apply_LockDelay(t *testing.T) { // Create and invalidate a session with a lock state := s1.fsm.State() if err := state.EnsureNode(1, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ ID: generateUUID(), diff --git a/consul/serf.go b/consul/serf.go index 0a6dc18ca..ae1dacc33 100644 --- a/consul/serf.go +++ b/consul/serf.go @@ -147,7 +147,7 @@ func (s *Server) nodeJoin(me serf.MemberEvent, wan bool) { ok, parts := isConsulServer(m) if !ok { if wan { - s.logger.Printf("[WARN] consul: non-server in WAN pool: %s %s", m.Name) + s.logger.Printf("[WARN] consul: non-server in WAN pool: %s", m.Name) } continue } diff --git a/consul/state_store_test.go b/consul/state_store_test.go index 7d36f4066..323fe1b6e 100644 --- a/consul/state_store_test.go +++ b/consul/state_store_test.go @@ -35,7 +35,7 @@ func TestEnsureRegistration(t *testing.T) { } if err := store.EnsureRegistration(13, reg); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, found, addr := store.GetNode("foo") @@ -73,7 +73,7 @@ func TestEnsureNode(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, found, addr := store.GetNode("foo") @@ -82,7 +82,7 @@ func TestEnsureNode(t *testing.T) { } if err := store.EnsureNode(4, structs.Node{"foo", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, found, addr = store.GetNode("foo") @@ -99,11 +99,11 @@ func TestGetNodes(t *testing.T) { defer store.Close() if err := store.EnsureNode(40, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(41, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, nodes := store.Nodes() @@ -126,11 +126,11 @@ func BenchmarkGetNodes(b *testing.B) { defer store.Close() if err := store.EnsureNode(100, structs.Node{"foo", "127.0.0.1"}); err != nil { - b.Fatalf("err: %v") + b.Fatalf("err: %v", err) } if err := store.EnsureNode(101, structs.Node{"bar", "127.0.0.2"}); err != nil { - b.Fatalf("err: %v") + b.Fatalf("err: %v", err) } for i := 0; i < b.N; i++ { @@ -259,7 +259,7 @@ func TestDeleteNodeService(t *testing.T) { ServiceID: "api", } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.DeleteNodeService(14, "foo", "api"); err != nil { @@ -329,11 +329,11 @@ func TestDeleteNode(t *testing.T) { defer store.Close() if err := store.EnsureNode(20, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(21, "foo", &structs.NodeService{"api", "api", nil, 5000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ @@ -384,23 +384,23 @@ func TestGetServices(t *testing.T) { defer store.Close() if err := store.EnsureNode(30, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(31, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(32, "foo", &structs.NodeService{"api", "api", nil, 5000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(33, "foo", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(34, "bar", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, services := store.Services() @@ -434,31 +434,31 @@ func TestServiceNodes(t *testing.T) { defer store.Close() if err := store.EnsureNode(10, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(11, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(12, "foo", &structs.NodeService{"api", "api", nil, 5000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(13, "bar", &structs.NodeService{"api", "api", nil, 5000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(14, "foo", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(15, "bar", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(16, "bar", &structs.NodeService{"db2", "db", []string{"slave"}, 8001}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, nodes := store.ServiceNodes("db") @@ -525,23 +525,23 @@ func TestServiceTagNodes(t *testing.T) { defer store.Close() if err := store.EnsureNode(15, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(16, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(17, "foo", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(18, "foo", &structs.NodeService{"db2", "db", []string{"slave"}, 8001}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(19, "bar", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, nodes := store.ServiceTagNodes("db", "master") @@ -573,23 +573,23 @@ func TestServiceTagNodes_MultipleTags(t *testing.T) { defer store.Close() if err := store.EnsureNode(15, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(16, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(17, "foo", &structs.NodeService{"db", "db", []string{"master", "v2"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(18, "foo", &structs.NodeService{"db2", "db", []string{"slave", "v2", "dev"}, 8001}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(19, "bar", &structs.NodeService{"db", "db", []string{"slave", "v2"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, nodes := store.ServiceTagNodes("db", "master") @@ -649,23 +649,23 @@ func TestStoreSnapshot(t *testing.T) { defer store.Close() if err := store.EnsureNode(8, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(9, structs.Node{"bar", "127.0.0.2"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(10, "foo", &structs.NodeService{"db", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(11, "foo", &structs.NodeService{"db2", "db", []string{"slave"}, 8001}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureService(12, "bar", &structs.NodeService{"db", "db", []string{"slave"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ @@ -676,7 +676,7 @@ func TestStoreSnapshot(t *testing.T) { ServiceID: "db", } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } // Add some KVS entries @@ -725,7 +725,7 @@ func TestStoreSnapshot(t *testing.T) { // Take a snapshot snap, err := store.Snapshot() if err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } defer snap.Close() @@ -913,7 +913,7 @@ func TestEnsureCheck(t *testing.T) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -923,7 +923,7 @@ func TestEnsureCheck(t *testing.T) { ServiceID: "db1", } if err := store.EnsureCheck(3, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check2 := &structs.HealthCheck{ @@ -933,7 +933,7 @@ func TestEnsureCheck(t *testing.T) { Status: structs.HealthWarning, } if err := store.EnsureCheck(4, check2); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, checks := store.NodeChecks("foo") @@ -1009,7 +1009,7 @@ func TestDeleteNodeCheck(t *testing.T) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1019,7 +1019,7 @@ func TestDeleteNodeCheck(t *testing.T) { ServiceID: "db1", } if err := store.EnsureCheck(3, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check2 := &structs.HealthCheck{ @@ -1029,7 +1029,7 @@ func TestDeleteNodeCheck(t *testing.T) { Status: structs.HealthWarning, } if err := store.EnsureCheck(4, check2); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.DeleteNodeCheck(5, "foo", "db"); err != nil { @@ -1059,7 +1059,7 @@ func TestCheckServiceNodes(t *testing.T) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1069,7 +1069,7 @@ func TestCheckServiceNodes(t *testing.T) { ServiceID: "db1", } if err := store.EnsureCheck(3, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check = &structs.HealthCheck{ Node: "foo", @@ -1078,7 +1078,7 @@ func TestCheckServiceNodes(t *testing.T) { Status: structs.HealthPassing, } if err := store.EnsureCheck(4, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, nodes := store.CheckServiceNodes("db") @@ -1140,7 +1140,7 @@ func BenchmarkCheckServiceNodes(t *testing.B) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1150,7 +1150,7 @@ func BenchmarkCheckServiceNodes(t *testing.B) { ServiceID: "db1", } if err := store.EnsureCheck(3, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check = &structs.HealthCheck{ Node: "foo", @@ -1159,7 +1159,7 @@ func BenchmarkCheckServiceNodes(t *testing.B) { Status: structs.HealthPassing, } if err := store.EnsureCheck(4, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } for i := 0; i < t.N; i++ { @@ -1184,7 +1184,7 @@ func TestSS_Register_Deregister_Query(t *testing.T) { nil, 0} if err := store.EnsureService(2, "foo", srv); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } srv = &structs.NodeService{ @@ -1193,7 +1193,7 @@ func TestSS_Register_Deregister_Query(t *testing.T) { nil, 0} if err := store.EnsureService(3, "foo", srv); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.DeleteNode(4, "foo"); err != nil { @@ -1220,7 +1220,7 @@ func TestNodeInfo(t *testing.T) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1230,7 +1230,7 @@ func TestNodeInfo(t *testing.T) { ServiceID: "db1", } if err := store.EnsureCheck(3, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check = &structs.HealthCheck{ Node: "foo", @@ -1239,7 +1239,7 @@ func TestNodeInfo(t *testing.T) { Status: structs.HealthPassing, } if err := store.EnsureCheck(4, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, dump := store.NodeInfo("foo") @@ -1279,13 +1279,13 @@ func TestNodeDump(t *testing.T) { t.Fatalf("err: %v", err) } if err := store.EnsureService(2, "foo", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.EnsureNode(3, structs.Node{"baz", "127.0.0.2"}); err != nil { t.Fatalf("err: %v", err) } if err := store.EnsureService(4, "baz", &structs.NodeService{"db1", "db", []string{"master"}, 8000}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } idx, dump := store.NodeDump() @@ -1711,7 +1711,7 @@ func TestSessionCreate(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1719,7 +1719,7 @@ func TestSessionCreate(t *testing.T) { Status: structs.HealthPassing, } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ @@ -1756,7 +1756,7 @@ func TestSessionCreate_Invalid(t *testing.T) { // Check not registered if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } if err := store.SessionCreate(1000, session); err.Error() != "Missing check 'bar' registration" { t.Fatalf("err: %v", err) @@ -1785,7 +1785,7 @@ func TestSession_Lookups(t *testing.T) { // Create a session if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ ID: generateUUID(), @@ -1870,7 +1870,7 @@ func TestSessionInvalidate_CriticalHealthCheck(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1878,7 +1878,7 @@ func TestSessionInvalidate_CriticalHealthCheck(t *testing.T) { Status: structs.HealthPassing, } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ @@ -1914,7 +1914,7 @@ func TestSessionInvalidate_DeleteHealthCheck(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } check := &structs.HealthCheck{ Node: "foo", @@ -1922,7 +1922,7 @@ func TestSessionInvalidate_DeleteHealthCheck(t *testing.T) { Status: structs.HealthPassing, } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ @@ -1957,7 +1957,7 @@ func TestSessionInvalidate_DeleteNode(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ @@ -1970,7 +1970,7 @@ func TestSessionInvalidate_DeleteNode(t *testing.T) { // Delete the node if err := store.DeleteNode(15, "foo"); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } // Lookup by ID, should be nil @@ -2004,7 +2004,7 @@ func TestSessionInvalidate_DeleteNodeService(t *testing.T) { ServiceID: "api", } if err := store.EnsureCheck(13, check); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ @@ -2039,7 +2039,7 @@ func TestKVSLock(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ID: generateUUID(), Node: "foo"} if err := store.SessionCreate(4, session); err != nil { @@ -2112,7 +2112,7 @@ func TestKVSUnlock(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ID: generateUUID(), Node: "foo"} if err := store.SessionCreate(4, session); err != nil { @@ -2170,7 +2170,7 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) { defer store.Close() if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } session := &structs.Session{ ID: generateUUID(), @@ -2198,7 +2198,7 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) { // Delete the node if err := store.DeleteNode(6, "foo"); err != nil { - t.Fatalf("err: %v") + t.Fatalf("err: %v", err) } // Key should be unlocked diff --git a/consul/util_test.go b/consul/util_test.go index 91b7fd2f5..24d9f7299 100644 --- a/consul/util_test.go +++ b/consul/util_test.go @@ -97,7 +97,7 @@ func TestIsConsulNode(t *testing.T) { } valid, dc := isConsulNode(m) if !valid || dc != "east-aws" { - t.Fatalf("bad: %v %v %v", valid, dc) + t.Fatalf("bad: %v %v", valid, dc) } } diff --git a/watch/watch.go b/watch/watch.go index 0b0a69a32..c6fcc243c 100644 --- a/watch/watch.go +++ b/watch/watch.go @@ -107,7 +107,7 @@ func assignValue(params map[string]interface{}, name string, out *string) error if raw, ok := params[name]; ok { val, ok := raw.(string) if !ok { - return fmt.Errorf("Expecting %s to be a string") + return fmt.Errorf("Expecting %s to be a string", name) } *out = val delete(params, name) @@ -120,7 +120,7 @@ func assignValueBool(params map[string]interface{}, name string, out *bool) erro if raw, ok := params[name]; ok { val, ok := raw.(bool) if !ok { - return fmt.Errorf("Expecting %s to be a boolean") + return fmt.Errorf("Expecting %s to be a boolean", name) } *out = val delete(params, name) From 982b177e691dc3a3c4f0a8a013b7e2ddf19c887e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 3 Nov 2014 11:28:21 -0800 Subject: [PATCH 059/238] Support old recursor config for backwards compatibility --- command/agent/config.go | 10 ++++++++++ command/agent/config_test.go | 7 +++++-- website/source/docs/agent/http.html.markdown | 1 + website/source/docs/agent/options.html.markdown | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 9fc2619e8..3a926499f 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -97,6 +97,11 @@ type Config struct { // DataDir is the directory to store our state in DataDir string `mapstructure:"data_dir"` + // DNSRecursors can be set to allow the DNS servers to recursively + // resolve non-consul domains. It is deprecated, and merges into the + // recursors array. + DNSRecursor string `mapstructure:"recursor"` + // DNSRecursors can be set to allow the DNS servers to recursively // resolve non-consul domains DNSRecursors []string `mapstructure:"recursors"` @@ -500,6 +505,11 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.RetryInterval = dur } + // Merge the single recursor + if result.DNSRecursor != "" { + result.DNSRecursors = append(result.DNSRecursors, result.DNSRecursor) + } + return &result, nil } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 0c09e02d4..77e67a172 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -109,7 +109,7 @@ func TestDecodeConfig(t *testing.T) { } // DNS setup - input = `{"ports": {"dns": 8500}, "recursor": ["8.8.8.8","8.8.4.4"], "domain": "foobar"}` + input = `{"ports": {"dns": 8500}, "recursors": ["8.8.8.8","8.8.4.4"], "recursor":"127.0.0.1", "domain": "foobar"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -119,7 +119,7 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } - if len(config.DNSRecursors) != 2 { + if len(config.DNSRecursors) != 3 { t.Fatalf("bad: %#v", config) } if config.DNSRecursors[0] != "8.8.8.8" { @@ -128,6 +128,9 @@ func TestDecodeConfig(t *testing.T) { if config.DNSRecursors[1] != "8.8.4.4" { t.Fatalf("bad: %#v", config) } + if config.DNSRecursors[2] != "127.0.0.1" { + t.Fatalf("bad: %#v", config) + } if config.Domain != "foobar" { t.Fatalf("bad: %#v", config) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 1dc999eb1..96055c7c6 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -333,6 +333,7 @@ It returns a JSON body like this: "Server": true, "Datacenter": "dc1", "DataDir": "/tmp/consul", + "DNSRecursor": "", "DNSRecursors": [], "Domain": "consul.", "LogLevel": "INFO", diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 5264900f3..dbbda8d33 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -321,6 +321,9 @@ definitions support being updated during a reload. * `protocol` - Equivalent to the `-protocol` command-line flag. +* `recursor` - Provides a single recursor address. This has been deprecated, and + the value is appended to the `recursors` list for backwards compatibility. + * `recursors` - This flag provides addresses of upstream DNS servers that are used to recursively resolve queries if they are not inside the service domain for consul. For example, a node can use Consul directly as a DNS server, and if the record is outside of the "consul." domain, From f5acbace17708265fcebe84ea3b8257311185d33 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 3 Nov 2014 11:40:55 -0800 Subject: [PATCH 060/238] Fixing unit tests --- command/agent/dns.go | 76 +++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index 0e5f8f195..63810913c 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -2,14 +2,15 @@ package agent import ( "fmt" - "github.com/hashicorp/consul/consul/structs" - "github.com/miekg/dns" "io" "log" "math/rand" "net" "strings" "time" + + "github.com/hashicorp/consul/consul/structs" + "github.com/miekg/dns" ) const ( @@ -71,15 +72,14 @@ func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain s mux.HandleFunc(consulDomain, srv.handleTest) } if len(recursors) > 0 { - validatedRecursors := []string{} + validatedRecursors := make([]string, len(recursors)) - for _, recursor := range recursors { + for idx, recursor := range recursors { recursor, err := recursorAddr(recursor) if err != nil { return nil, fmt.Errorf("Invalid recursor address: %v", err) } - - validatedRecursors = append(validatedRecursors, recursor) + validatedRecursors[idx] = recursor } srv.recursors = validatedRecursors @@ -594,34 +594,35 @@ func (d *DNSServer) handleRecurse(resp dns.ResponseWriter, req *dns.Msg) { // Recursively resolve c := &dns.Client{Net: network} - for i,recursor := range d.recursors { - r, rtt, err := c.Exchange(req, recursor) - - if i < len(d.recursors) && err != nil { - continue - } else if err != nil { - // On all of failure, return a SERVFAIL message - d.logger.Printf("[ERR] dns: recurse failed: %v", err) - m := &dns.Msg{} - m.SetReply(req) - m.RecursionAvailable = true - m.SetRcode(req, dns.RcodeServerFailure) - resp.WriteMsg(m) + var r *dns.Msg + var rtt time.Duration + var err error + for _, recursor := range d.recursors { + r, rtt, err = c.Exchange(req, recursor) + if err == nil { + // Forward the response + d.logger.Printf("[DEBUG] dns: recurse RTT for %v (%v)", q, rtt) + if err := resp.WriteMsg(r); err != nil { + d.logger.Printf("[WARN] dns: failed to respond: %v", err) + } return } - d.logger.Printf("[DEBUG] dns: recurse RTT for %v (%v)", q, rtt) - - // Forward the response - if err := resp.WriteMsg(r); err != nil { - d.logger.Printf("[WARN] dns: failed to respond: %v", err) - } + d.logger.Printf("[ERR] dns: recurse failed: %v", err) } + + // If all resolvers fail, return a SERVFAIL message + d.logger.Printf("[ERR] dns: all resolvers failed for %v", q) + m := &dns.Msg{} + m.SetReply(req) + m.RecursionAvailable = true + m.SetRcode(req, dns.RcodeServerFailure) + resp.WriteMsg(m) } // resolveCNAME is used to recursively resolve CNAME records func (d *DNSServer) resolveCNAME(name string) []dns.RR { // Do nothing if we don't have a recursor - if len(d.recursors) > 0 { + if len(d.recursors) == 0 { return nil } @@ -631,20 +632,17 @@ func (d *DNSServer) resolveCNAME(name string) []dns.RR { // Make a DNS lookup request c := &dns.Client{Net: "udp"} - for i,recursor := range d.recursors { - r, rtt, err := c.Exchange(m, recursor) - - if i < len(d.recursors) && err != nil { - continue - } else if err != nil { - d.logger.Printf("[ERR] dns: cname recurse failed: %v", err) - return nil + var r *dns.Msg + var rtt time.Duration + var err error + for _, recursor := range d.recursors { + r, rtt, err = c.Exchange(m, recursor) + if err == nil { + d.logger.Printf("[DEBUG] dns: cname recurse RTT for %v (%v)", name, rtt) + return r.Answer } - d.logger.Printf("[DEBUG] dns: cname recurse RTT for %v (%v)", name, rtt) - - // Return all the answers - return r.Answer + d.logger.Printf("[ERR] dns: cname recurse failed for %v: %v", name, err) } - + d.logger.Printf("[ERR] dns: all resolvers failed for %v", name) return nil } From 4347b9394da76c09d61f2b681619fcfd9213c10c Mon Sep 17 00:00:00 2001 From: Danny Berger Date: Tue, 4 Nov 2014 21:01:45 -0700 Subject: [PATCH 061/238] Fix some typos in website docs --- website/source/docs/agent/encryption.html.markdown | 6 +++--- website/source/docs/agent/http.html.markdown | 4 ++-- website/source/docs/agent/options.html.markdown | 14 +++++++------- website/source/docs/agent/watches.html.markdown | 2 +- website/source/docs/commands/event.html.markdown | 4 ++-- website/source/docs/commands/exec.html.markdown | 2 +- .../source/docs/internals/sessions.html.markdown | 8 ++++---- website/source/intro/vs/chef-puppet.html.markdown | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/website/source/docs/agent/encryption.html.markdown b/website/source/docs/agent/encryption.html.markdown index 281dfcf39..6de76cbe0 100644 --- a/website/source/docs/agent/encryption.html.markdown +++ b/website/source/docs/agent/encryption.html.markdown @@ -3,7 +3,7 @@ layout: "docs" page_title: "Encryption" sidebar_current: "docs-agent-encryption" description: |- - The Consul agent supports encrypting all of its network traffic. The exact method of this encryption is described on the encryption internals page. There are two seperate systems, one for gossip traffic and one for RPC. + The Consul agent supports encrypting all of its network traffic. The exact method of this encryption is described on the encryption internals page. There are two separate systems, one for gossip traffic and one for RPC. --- # Encryption @@ -11,7 +11,7 @@ description: |- The Consul agent supports encrypting all of its network traffic. The exact method of this encryption is described on the [encryption internals page](/docs/internals/security.html). There are two -seperate systems, one for gossip traffic and one for RPC. +separate systems, one for gossip traffic and one for RPC. ## Gossip Encryption @@ -74,7 +74,7 @@ key pair set using `cert_file` and `key_file`. If `verify_incoming` is set, then the servers verify the authenticity of all incoming connections. Servers will also disallow any non-TLS connections. If this is set, then all clients must have a valid key pair set using `cert_file` and `key_file`. To force clients to -use TLs, `verify_outgoing` must also be set. +use TLS, `verify_outgoing` must also be set. TLS is used to secure the RPC calls between agents, but gossip between nodes is done over UDP and is secured using a symmetric key. See above for enabling gossip encryption. diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 96055c7c6..0211ebae7 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -188,7 +188,7 @@ be used with a PUT request: * ?cas=\ : This flag is used to turn the `PUT` into a Check-And-Set operation. This is very useful as it allows clients to build more complex - syncronization primitives on top. If the index is 0, then Consul will only + synchronization primitives on top. If the index is 0, then Consul will only put the key if it does not already exist. If the index is non-zero, then the key is only set if the index matches the `ModifyIndex` of that key. @@ -607,7 +607,7 @@ If the API call succeeds a 200 status code is returned. ### /v1/catalog/deregister -The deregister endpoint is a low level mechanism for direclty removing +The deregister endpoint is a low level mechanism for directly removing entries in the catalog. It is usually recommended to use the agent local endpoints, as they are simpler and perform anti-entropy. diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index dbbda8d33..9790c8f9d 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -152,7 +152,7 @@ at a single JSON object with configuration within it. Configuration files are used for more than just setting up the agent, they are also used to provide check and service definitions. These are used to announce the availability of system servers to the rest of the cluster. -They are documented seperately under [check configuration](/docs/agent/checks.html) and +They are documented separately under [check configuration](/docs/agent/checks.html) and [service configuration](/docs/agent/services.html) respectively. The service and check definitions support being updated during a reload. @@ -188,7 +188,7 @@ definitions support being updated during a reload. * `acl_default_policy` - Either "allow" or "deny", defaults to "allow". The default policy controls the behavior of a token when there is no matching rule. In "allow" mode, ACLs are a blacklist: any operation not specifically - prohibited is allowed. In "deny" mode, ACLs are a whilelist: any operation not + prohibited is allowed. In "deny" mode, ACLs are a whitelist: any operation not specifically allowed is blocked. * `acl_down_policy` - Either "allow", "deny" or "extend-cache" which is the @@ -237,12 +237,12 @@ definitions support being updated during a reload. Must be provided along with the `key_file`. * `check_update_interval` - This interval controls how often check output from - checks in a steady state is syncronized with the server. By default, this is + checks in a steady state is synchronized with the server. By default, this is set to 5 minutes ("5m"). Many checks which are in a steady state produce slightly different output per run (timestamps, etc) which cause constant writes. - This configuration allows defering the sync of check output for a given interval to + This configuration allows deferring the sync of check output for a given interval to reduce write pressure. If a check ever changes state, the new state and associated - output is syncronized immediately. To disable this behavior, set the value to "0s". + output is synchronized immediately. To disable this behavior, set the value to "0s". * `client_addr` - Equivalent to the `-client` command-line flag. @@ -260,7 +260,7 @@ definitions support being updated during a reload. new version releases. * `dns_config` - This object allows a number of sub-keys to be set which can tune - how DNS queries are perfomed. See this guide on [DNS caching](/docs/guides/dns-cache.html). + how DNS queries are performed. See this guide on [DNS caching](/docs/guides/dns-cache.html). The following sub-keys are available: * `allow_stale` - Enables a stale query for DNS information. This allows any Consul @@ -395,7 +395,7 @@ port. * Serf LAN (Default 8301). This is used to handle gossip in the LAN. Required by all agents, TCP and UDP. -* Serf WAN( Default 8302). This is used by servers to gossip over the +* Serf WAN (Default 8302). This is used by servers to gossip over the WAN to other servers. TCP and UDP. * CLI RPC (Default 8400). This is used by all agents to handle RPC diff --git a/website/source/docs/agent/watches.html.markdown b/website/source/docs/agent/watches.html.markdown index b5dd857dc..5ab470721 100644 --- a/website/source/docs/agent/watches.html.markdown +++ b/website/source/docs/agent/watches.html.markdown @@ -32,7 +32,7 @@ in a JSON body when using agent configuration, or as CLI flags for the watch com ## Handlers -The watch specifiation specifies the view of data to be monitored. +The watch specification specifies the view of data to be monitored. Once that view is updated the specified handler is invoked. The handler can be any executable. diff --git a/website/source/docs/commands/event.html.markdown b/website/source/docs/commands/event.html.markdown index cfa55234c..15372d83e 100644 --- a/website/source/docs/commands/event.html.markdown +++ b/website/source/docs/commands/event.html.markdown @@ -16,7 +16,7 @@ to build scripting infrastructure to do automated deploys, restart services, or perform any other orchestration action. Events can be handled by [using a watch](/docs/agent/watches.html). -Under the hood, events are propogated using the [gossip protocol](/docs/internals/gossip.html). +Under the hood, events are propagated using the [gossip protocol](/docs/internals/gossip.html). While the details are not important for using events, an understanding of the semantics is useful. The gossip layer will make a best-effort to deliver the event, but there is **no guarantee** delivery. Unlike most Consul data, which is @@ -24,7 +24,7 @@ replicated using [consensus](/docs/internals/consensus.html), event data is purely peer-to-peer over gossip. This means it is not persisted and does not have a total ordering. In practice, this means you cannot rely on the order of message delivery. An advantage however is that events can still -be used even in the absense of server nodes or during an outage. +be used even in the absence of server nodes or during an outage. The underlying gossip also sets limits on the size of a user event message. It is hard to give an exact number, as it depends on various diff --git a/website/source/docs/commands/exec.html.markdown b/website/source/docs/commands/exec.html.markdown index 332f8f477..98c0af18e 100644 --- a/website/source/docs/commands/exec.html.markdown +++ b/website/source/docs/commands/exec.html.markdown @@ -16,7 +16,7 @@ the `web` service. Remote execution works by specifying a job which is stored in the KV store. Agent's are informed about the new job using the [event system](/docs/commands/event.html), -which propogates messages via the [gossip protocol](/docs/internals/gossip.html). +which propagates messages via the [gossip protocol](/docs/internals/gossip.html). As a result, delivery is best-effort, and there is **no guarantee** of execution. While events are purely gossip driven, remote execution relies on the KV store diff --git a/website/source/docs/internals/sessions.html.markdown b/website/source/docs/internals/sessions.html.markdown index fb8d3cba2..62f451e64 100644 --- a/website/source/docs/internals/sessions.html.markdown +++ b/website/source/docs/internals/sessions.html.markdown @@ -3,12 +3,12 @@ layout: "docs" page_title: "Sessions" sidebar_current: "docs-internals-sessions" description: |- - Consul provides a session mechansim which can be used to build distributed locks. Sessions act as a binding layer between nodes, health checks, and key/value data. They are designed to provide granular locking, and are heavily inspired by The Chubby Lock Service for Loosely-Coupled Distributed Systems. + Consul provides a session mechanism which can be used to build distributed locks. Sessions act as a binding layer between nodes, health checks, and key/value data. They are designed to provide granular locking, and are heavily inspired by The Chubby Lock Service for Loosely-Coupled Distributed Systems. --- # Sessions -Consul provides a session mechansim which can be used to build distributed locks. +Consul provides a session mechanism which can be used to build distributed locks. Sessions act as a binding layer between nodes, health checks, and key/value data. They are designed to provide granular locking, and are heavily inspired by [The Chubby Lock Service for Loosely-Coupled Distributed Systems](http://research.google.com/archive/chubby.html). @@ -31,7 +31,7 @@ Below is a diagram showing the relationship between these components: ![Consul Sessions](consul-sessions.png)
-The contract that Consul provides is that under any of the folllowing +The contract that Consul provides is that under any of the following situations the session will be *invalidated*: * Node is deregistered @@ -79,7 +79,7 @@ mechanism by providing a zero delay value. Integration between the Key/Value store and sessions are the primary place where sessions are used. A session must be created prior to use, -and is then refered to by it's ID. +and is then referred to by it's ID. The Key/Value API is extended to support an `acquire` and `release` operation. The `acquire` operation acts like a Check-And-Set operation, except it diff --git a/website/source/intro/vs/chef-puppet.html.markdown b/website/source/intro/vs/chef-puppet.html.markdown index 7dcc4df77..8f3443f54 100644 --- a/website/source/intro/vs/chef-puppet.html.markdown +++ b/website/source/intro/vs/chef-puppet.html.markdown @@ -30,7 +30,7 @@ integrated health checking, Consul can route traffic away from unhealthy nodes, allowing systems and services to gracefully recover. Static configuration that may be provided by configuration management tools can be moved into the dynamic key/value store. This allows application configuration to be updated -without a slow convergence run. Lastly, because each datacenter runs indepedently, +without a slow convergence run. Lastly, because each datacenter runs independently, supporting multiple datacenters is no different than a single datacenter. That said, Consul is not a replacement for configuration management tools. From 8486f0ddc169a30c740de155e9f399d089b717e8 Mon Sep 17 00:00:00 2001 From: Neil Taylor Date: Wed, 5 Nov 2014 12:08:35 -0500 Subject: [PATCH 062/238] improved Parallax scrolling performance in webkit --- website/source/assets/stylesheets/_jumbotron.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/source/assets/stylesheets/_jumbotron.scss b/website/source/assets/stylesheets/_jumbotron.scss index 3db16d7ab..9b776e58d 100755 --- a/website/source/assets/stylesheets/_jumbotron.scss +++ b/website/source/assets/stylesheets/_jumbotron.scss @@ -16,6 +16,7 @@ padding-bottom: 0; margin-top: $negative-hero-margin; color: $jumbotron-color; + -webkit-backface-visibility:hidden; @include consul-gradient-bg(); &.mobile-hero{ @@ -40,7 +41,8 @@ position: relative; height: 100%; margin-top: $header-height; - + -webkit-backface-visibility:hidden; + .jumbo-logo-wrap{ margin-top: 135px; From 398179dcad7dd36de8fb4c9e529b1686902b8437 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 6 Nov 2014 18:24:04 -0800 Subject: [PATCH 063/238] agent: pass notes field through for checks inside of service definitions. Fixes #449 --- command/agent/agent.go | 2 +- command/agent/agent_test.go | 10 +++++++++- command/agent/check.go | 2 ++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index c3b34f546..9ef071137 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -447,7 +447,7 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) err CheckID: fmt.Sprintf("service:%s", service.ID), Name: fmt.Sprintf("Service '%s' check", service.Service), Status: structs.HealthCritical, - Notes: "", + Notes: chkType.Notes, ServiceID: service.ID, ServiceName: service.Service, } diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index dff36507b..2901d67bc 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -115,7 +115,10 @@ func TestAgent_AddService(t *testing.T) { Tags: []string{"foo"}, Port: 8000, } - chk := &CheckType{TTL: time.Minute} + chk := &CheckType{ + TTL: time.Minute, + Notes: "redis health check", + } err := agent.AddService(srv, chk) if err != nil { t.Fatalf("err: %v", err) @@ -135,6 +138,11 @@ func TestAgent_AddService(t *testing.T) { if _, ok := agent.checkTTLs["service:redis"]; !ok { t.Fatalf("missing redis check ttl") } + + // Ensure the notes are passed through + if agent.state.Checks()["service:redis"].Notes == "" { + t.Fatalf("missing redis check notes") + } } func TestAgent_RemoveService(t *testing.T) { diff --git a/command/agent/check.go b/command/agent/check.go index dce7d8f75..a2cb588dd 100644 --- a/command/agent/check.go +++ b/command/agent/check.go @@ -30,6 +30,8 @@ type CheckType struct { Interval time.Duration TTL time.Duration + + Notes string } // Valid checks if the CheckType is valid From a0f954b0ae9b74c7a7ded053e33c317f22331286 Mon Sep 17 00:00:00 2001 From: Jamey Owens Date: Mon, 10 Nov 2014 16:37:18 -0500 Subject: [PATCH 064/238] Typo fix for events command doc --- website/source/docs/commands/event.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/commands/event.html.markdown b/website/source/docs/commands/event.html.markdown index 15372d83e..3a274cc3a 100644 --- a/website/source/docs/commands/event.html.markdown +++ b/website/source/docs/commands/event.html.markdown @@ -54,5 +54,5 @@ The list of available flags are: * `-tag` - Regular expression to filter to only nodes with a service that has a matching tag. This must be used with `-service`. As an example, you may - do "-server mysql -tag slave". + do "-service mysql -tag slave". From 0540605110c9e99efa7236623f82968bd67f989b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 12 Nov 2014 17:55:45 -0800 Subject: [PATCH 065/238] consul: Fixing key list index calculation --- consul/fsm_test.go | 19 ++++++++++-- consul/state_store.go | 24 +++++++++++++++ consul/state_store_test.go | 61 +++++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/consul/fsm_test.go b/consul/fsm_test.go index 5e6b1ad37..db01580e4 100644 --- a/consul/fsm_test.go +++ b/consul/fsm_test.go @@ -414,23 +414,38 @@ func TestFSM_SnapshotRestore(t *testing.T) { t.Fatalf("bad: %v", d) } + // Verify the index is restored + idx, _, err := fsm.state.KVSListKeys("/blah", "") + if err != nil { + t.Fatalf("err: %v", err) + } + if idx <= 1 { + t.Fatalf("bad index: %d", idx) + } + // Verify session is restored - _, s, err := fsm.state.SessionGet(session.ID) + idx, s, err := fsm.state.SessionGet(session.ID) if err != nil { t.Fatalf("err: %v", err) } if s.Node != "foo" { t.Fatalf("bad: %v", s) } + if idx <= 1 { + t.Fatalf("bad index: %d", idx) + } // Verify ACL is restored - _, a, err := fsm.state.ACLGet(acl.ID) + idx, a, err := fsm.state.ACLGet(acl.ID) if err != nil { t.Fatalf("err: %v", err) } if a.Name != "User Token" { t.Fatalf("bad: %v", a) } + if idx <= 1 { + t.Fatalf("bad index: %d", idx) + } } func TestFSM_KVSSet(t *testing.T) { diff --git a/consul/state_store.go b/consul/state_store.go index 030f1d55c..7ae9e4735 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -1060,6 +1060,9 @@ func (s *StateStore) KVSRestore(d *structs.DirEntry) error { if err := s.kvsTable.InsertTxn(tx, d); err != nil { return err } + if err := s.kvsTable.SetMaxLastIndexTxn(tx, d.ModifyIndex); err != nil { + return err + } return tx.Commit() } @@ -1096,10 +1099,18 @@ func (s *StateStore) KVSListKeys(prefix, seperator string) (uint64, []string, er return 0, nil, err } + // Ensure a non-zero index + if idx == 0 { + // Must provide non-zero index to prevent blocking + // Index 1 is impossible anyways (due to Raft internals) + idx = 1 + } + // Aggregate the stream stream := make(chan interface{}, 128) done := make(chan struct{}) var keys []string + var maxIndex uint64 go func() { prefixLen := len(prefix) sepLen := len(seperator) @@ -1108,6 +1119,11 @@ func (s *StateStore) KVSListKeys(prefix, seperator string) (uint64, []string, er ent := raw.(*structs.DirEntry) after := ent.Key[prefixLen:] + // Update the hightest index we've seen + if ent.ModifyIndex > maxIndex { + maxIndex = ent.ModifyIndex + } + // If there is no seperator, always accumulate if sepLen == 0 { keys = append(keys, ent.Key) @@ -1131,6 +1147,11 @@ func (s *StateStore) KVSListKeys(prefix, seperator string) (uint64, []string, er // Start the stream, and wait for completion err = s.kvsTable.StreamTxn(stream, tx, "id_prefix", prefix) <-done + + // Use the maxIndex if we have any keys + if maxIndex != 0 { + idx = maxIndex + } return idx, keys, err } @@ -1618,6 +1639,9 @@ func (s *StateStore) ACLRestore(acl *structs.ACL) error { if err := s.aclTable.InsertTxn(tx, acl); err != nil { return err } + if err := s.aclTable.SetMaxLastIndexTxn(tx, acl.ModifyIndex); err != nil { + return err + } return tx.Commit() } diff --git a/consul/state_store_test.go b/consul/state_store_test.go index 323fe1b6e..9480cf1b1 100644 --- a/consul/state_store_test.go +++ b/consul/state_store_test.go @@ -1554,7 +1554,7 @@ func TestKVS_ListKeys(t *testing.T) { if err != nil { t.Fatalf("err: %v", err) } - if idx != 0 { + if idx != 1 { t.Fatalf("bad: %v", idx) } if len(keys) != 0 { @@ -1657,6 +1657,65 @@ func TestKVS_ListKeys(t *testing.T) { } } +func TestKVS_ListKeys_Index(t *testing.T) { + store, err := testStateStore() + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + + // Create the entries + d := &structs.DirEntry{Key: "/foo/a", Flags: 42, Value: []byte("test")} + if err := store.KVSSet(1000, d); err != nil { + t.Fatalf("err: %v", err) + } + d = &structs.DirEntry{Key: "/bar/b", Flags: 42, Value: []byte("test")} + if err := store.KVSSet(1001, d); err != nil { + t.Fatalf("err: %v", err) + } + d = &structs.DirEntry{Key: "/baz/c", Flags: 42, Value: []byte("test")} + if err := store.KVSSet(1002, d); err != nil { + t.Fatalf("err: %v", err) + } + d = &structs.DirEntry{Key: "/other/d", Flags: 42, Value: []byte("test")} + if err := store.KVSSet(1003, d); err != nil { + t.Fatalf("err: %v", err) + } + + idx, keys, err := store.KVSListKeys("/foo", "") + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 1000 { + t.Fatalf("bad: %v", idx) + } + if len(keys) != 1 { + t.Fatalf("bad: %v", keys) + } + + idx, keys, err = store.KVSListKeys("/ba", "") + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 1002 { + t.Fatalf("bad: %v", idx) + } + if len(keys) != 2 { + t.Fatalf("bad: %v", keys) + } + + idx, keys, err = store.KVSListKeys("/nope", "") + if err != nil { + t.Fatalf("err: %v", err) + } + if idx != 1003 { + t.Fatalf("bad: %v", idx) + } + if len(keys) != 0 { + t.Fatalf("bad: %v", keys) + } +} + func TestKVSDeleteTree(t *testing.T) { store, err := testStateStore() if err != nil { From 36c44d4386397fcf9d33279f97f7b6079e248ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 13 Nov 2014 16:56:07 +0100 Subject: [PATCH 066/238] [Doc] Added missing link to leader-election --- website/source/docs/guides/index.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/source/docs/guides/index.html.markdown b/website/source/docs/guides/index.html.markdown index 7db1df34e..af804320b 100644 --- a/website/source/docs/guides/index.html.markdown +++ b/website/source/docs/guides/index.html.markdown @@ -22,6 +22,8 @@ The following guides are available: * [External Services](/docs/guides/external.html) - This guide covers registering an external service. This allows using 3rd party services within the Consul framework. +* [Leader Election](/docs/guides/leader-election.html) - The goal of this guide is to cover how to build client-side leader election using Consul. + * [Multiple Datacenters](/docs/guides/datacenters.html) - Configuring Consul to support multiple datacenters. * [Outage Recovery](/docs/guides/outage.html) - This guide covers recovering a cluster that has become unavailable due to server failures. From c9de806ab9dfbdcac96022a6059aa917438ce5d8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 13 Nov 2014 12:02:41 -0800 Subject: [PATCH 067/238] website: better description of config merging in agent options. Closes #470 --- website/source/docs/agent/options.html.markdown | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 9790c8f9d..de3bbd78a 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -63,13 +63,15 @@ The options below are all specified on the command-line. the format of this file, read the "Configuration Files" section below. This option can be specified multiple times to load multiple configuration files. If it is specified multiple times, configuration files loaded later - will merge with configuration files loaded earlier, with the later values - overriding the earlier values. + will merge with configuration files loaded earlier. During a config merge, + single-value keys (string, int, bool) will simply have their values replaced, + while list types will be appended together. * `-config-dir` - A directory of configuration files to load. Consul will load all files in this directory ending in ".json" as configuration files - in alphabetical order. For more information on the format of the configuration - files, see the "Configuration Files" section below. + in alphabetical order using the same merge routine as the `config-file` + option above. For more information on the format of the configuration files, + see the "Configuration Files" section below. * `-data-dir` - This flag provides a data directory for the agent to store state. This is required for all agents. The directory should be durable across reboots. From 19820d9794a3e8bf9d0e8b5b961e0924f102a46d Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 13 Nov 2014 17:42:39 -0500 Subject: [PATCH 068/238] Doc explaining the blacklist mode and consul exec --- .../source/docs/internals/acl.html.markdown | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index f2f0dfe07..0e9ad633b 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -63,6 +63,30 @@ to deny all actions, then token rules can be set to allow or whitelist actions. In the inverse, the allow all default behavior is a blacklist, where rules are used to prohibit actions. +### Blacklist mode and `consul exec` + +If you set `acl_default_policy` to `deny`, the `anonymous` token won't have the +permission to read the default `_rexec` prefix, and therefore token-less consul +agents (using the `anonymous` token) won't be able to perform `consul exec` +actions. + +There is a subtle interaction there. The agents will need permission to +read/write to the `_rexec` prefix for `consul exec` to work properly. They use +that as the transport for most data, only the edge trigger uses the event +system. + +You can do this by allowing the `anonymous` token to access that prefix, or by +providing tokens to the agents that enable it. The formar can be done by giving +this rule to the `anonymous` token`: + +```javascript +key "_rexec/" { + policy = "write" +} +``` + +### Bootstrapping ACLs + Bootstrapping the ACL system is done by providing an initial `acl_master_token` [configuration](/docs/agent/options.html), which will be created as a "management" type token if it does not exist. From dfc094a5d0f51bbd4906f6bba1e5c35c18f29585 Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 13 Nov 2014 17:43:25 -0500 Subject: [PATCH 069/238] extra ' --- website/source/docs/internals/acl.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 0e9ad633b..36d4a385a 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -77,7 +77,7 @@ system. You can do this by allowing the `anonymous` token to access that prefix, or by providing tokens to the agents that enable it. The formar can be done by giving -this rule to the `anonymous` token`: +this rule to the `anonymous` token: ```javascript key "_rexec/" { From bb7eb29ade3778151f87ac50fdaf2a0b09964e5d Mon Sep 17 00:00:00 2001 From: Laurent Raufaste Date: Thu, 13 Nov 2014 17:44:20 -0500 Subject: [PATCH 070/238] Another typo --- website/source/docs/internals/acl.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 36d4a385a..08522a707 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -76,7 +76,7 @@ that as the transport for most data, only the edge trigger uses the event system. You can do this by allowing the `anonymous` token to access that prefix, or by -providing tokens to the agents that enable it. The formar can be done by giving +providing tokens to the agents that enable it. The former can be done by giving this rule to the `anonymous` token: ```javascript From df8d099f49fac0f94418dc781cc457089e0de573 Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Fri, 14 Nov 2014 10:02:42 -0500 Subject: [PATCH 071/238] Add start-wan-join, retry-wan-join and related configuration options and commandline options --- command/agent/command.go | 88 ++++++++++++++++++- command/agent/command_test.go | 83 +++++++++++++++++ command/agent/config.go | 44 ++++++++++ command/agent/config_test.go | 63 +++++++++++++ .../source/docs/agent/options.html.markdown | 24 +++++ 5 files changed, 300 insertions(+), 2 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 6aa51ac7c..768d337c3 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -53,6 +53,7 @@ func (c *Command) readConfig() *Config { var cmdConfig Config var configFiles []string var retryInterval string + var retryWanInterval string cmdFlags := flag.NewFlagSet("agent", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -83,12 +84,20 @@ func (c *Command) readConfig() *Config { "enable re-joining after a previous leave") cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartJoin), "join", "address of agent to join on startup") + cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartWanJoin), "join-wan", + "address of agent to join -wan on startup") cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryJoin), "retry-join", "address of agent to join on startup with retry") cmdFlags.IntVar(&cmdConfig.RetryMaxAttempts, "retry-max", 0, "number of retries for joining") cmdFlags.StringVar(&retryInterval, "retry-interval", "", "interval between join attempts") + cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryWanJoin), "retry-wan-join", + "address of agent to join -wan on startup with retry") + cmdFlags.IntVar(&cmdConfig.RetryWanMaxAttempts, "retry-wan-max", 0, + "number of retries for joining -wan") + cmdFlags.StringVar(&retryWanInterval, "retry-wan-interval", "", + "interval between join -wan attempts") if err := cmdFlags.Parse(c.args); err != nil { return nil @@ -103,6 +112,15 @@ func (c *Command) readConfig() *Config { cmdConfig.RetryInterval = dur } + if retryWanInterval != "" { + dur, err := time.ParseDuration(retryWanInterval) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return nil + } + cmdConfig.RetryWanInterval = dur + } + config := DefaultConfig() if len(configFiles) > 0 { fileConfig, err := ReadConfigPaths(configFiles) @@ -369,6 +387,22 @@ func (c *Command) startupJoin(config *Config) error { return nil } +// startupWanJoin is invoked to handle any joins -wan specified to take place at start time +func (c *Command) startupWanJoin(config *Config) error { + if len(config.StartWanJoin) == 0 { + return nil + } + + c.Ui.Output("Joining -wan cluster...") + n, err := c.agent.JoinWAN(config.StartWanJoin) + if err != nil { + return err + } + + c.Ui.Info(fmt.Sprintf("Join -wan completed. Synced with %d initial agents", n)) + return nil +} + // retryJoin is used to handle retrying a join until it succeeds or all // retries are exhausted. func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) { @@ -400,6 +434,37 @@ func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) { } } +// retryWanJoin is used to handle retrying a join -wan until it succeeds or all +// retries are exhausted. +func (c *Command) retryWanJoin(config *Config, errCh chan<- struct{}) { + if len(config.RetryWanJoin) == 0 { + return + } + + logger := c.agent.logger + logger.Printf("[INFO] agent: Joining WAN cluster...") + + attempt := 0 + for { + n, err := c.agent.JoinWAN(config.RetryWanJoin) + if err == nil { + logger.Printf("[INFO] agent: Join -wan completed. Synced with %d initial agents", n) + return + } + + attempt++ + if config.RetryWanMaxAttempts > 0 && attempt > config.RetryWanMaxAttempts { + logger.Printf("[ERROR] agent: max join -wan retry exhausted, exiting") + close(errCh) + return + } + + logger.Printf("[WARN] agent: Join -wan failed: %v, retrying in %v", err, + config.RetryWanInterval) + time.Sleep(config.RetryWanInterval) + } +} + func (c *Command) Run(args []string) int { c.Ui = &cli.PrefixedUi{ OutputPrefix: "==> ", @@ -482,6 +547,12 @@ func (c *Command) Run(args []string) int { return 1 } + // Join startup nodes if specified + if err := c.startupWanJoin(config); err != nil { + c.Ui.Error(err.Error()) + return 1 + } + // Register the services for _, service := range config.Services { ns := service.NodeService() @@ -542,12 +613,16 @@ func (c *Command) Run(args []string) int { errCh := make(chan struct{}) go c.retryJoin(config, errCh) + // Start retry -wan join process + errWanCh := make(chan struct{}) + go c.retryWanJoin(config, errWanCh) + // Wait for exit - return c.handleSignals(config, errCh) + return c.handleSignals(config, errCh, errWanCh) } // handleSignals blocks until we get an exit-causing signal -func (c *Command) handleSignals(config *Config, retryJoin <-chan struct{}) int { +func (c *Command) handleSignals(config *Config, retryJoin <-chan struct{}, retryWanJoin <-chan struct{}) int { signalCh := make(chan os.Signal, 4) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) @@ -563,6 +638,8 @@ WAIT: sig = os.Interrupt case <-retryJoin: return 1 + case <-retryWanJoin: + return 1 case <-c.agent.ShutdownCh(): // Agent is already shutdown! return 0 @@ -721,11 +798,18 @@ Options: -encrypt=key Provides the gossip encryption key -join=1.2.3.4 Address of an agent to join at start time. Can be specified multiple times. + -join-wan=1.2.3.4 Address of an agent to join -wan at start time. + Can be specified multiple times. -retry-join=1.2.3.4 Address of an agent to join at start time with retries enabled. Can be specified multiple times. -retry-interval=30s Time to wait between join attempts. -retry-max=0 Maximum number of join attempts. Defaults to 0, which will retry indefinitely. + -retry-wan-join=1.2.3.4 Address of an agent to join -wan at start time with + retries enabled. Can be specified multiple times. + -retry-wan-interval=30s Time to wait between join -wan attempts. + -retry-wan-max=0 Maximum number of join -wan attempts. Defaults to 0, which + will retry indefinitely. -log-level=info Log level of the agent. -node=hostname Name of this node. Must be unique in the cluster -protocol=N Sets the protocol version. Defaults to latest. diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 81934a1ae..e2dd634d5 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -121,3 +121,86 @@ func TestRetryJoinFail(t *testing.T) { t.Fatalf("bad: %d", code) } } +func TestRetryWanJoin(t *testing.T) { + dir, agent := makeAgent(t, nextConfig()) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + conf2 := nextConfig() + tmpDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tmpDir) + + doneCh := make(chan struct{}) + shutdownCh := make(chan struct{}) + + defer func() { + close(shutdownCh) + <-doneCh + }() + + cmd := &Command{ + ShutdownCh: shutdownCh, + Ui: new(cli.MockUi), + } + + serfAddr := fmt.Sprintf( + "%s:%d", + agent.config.BindAddr, + agent.config.Ports.SerfLan) + + args := []string{ + "-data-dir", tmpDir, + "-node", fmt.Sprintf(`"%s"`, conf2.NodeName), + "-retry-wan-join", serfAddr, + "-retry-interval", "1s", + } + + go func() { + if code := cmd.Run(args); code != 0 { + log.Printf("bad: %d", code) + } + close(doneCh) + }() + + testutil.WaitForResult(func() (bool, error) { + mem := agent.WANMembers() + if len(mem) != 2 { + return false, fmt.Errorf("bad: %#v", mem) + } + return true, nil + }, func(err error) { + t.Fatalf(err.Error()) + }) +} + +func TestRetryWanJoinFail(t *testing.T) { + conf := nextConfig() + tmpDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tmpDir) + + shutdownCh := make(chan struct{}) + defer close(shutdownCh) + + cmd := &Command{ + ShutdownCh: shutdownCh, + Ui: new(cli.MockUi), + } + + serfAddr := fmt.Sprintf("%s:%d", conf.BindAddr, conf.Ports.SerfWan) + + args := []string{ + "-data-dir", tmpDir, + "-retry-wan-join", serfAddr, + "-retry-max", "1", + } + + if code := cmd.Run(args); code == 0 { + t.Fatalf("bad: %d", code) + } +} diff --git a/command/agent/config.go b/command/agent/config.go index 3a926499f..ddff17164 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -194,6 +194,11 @@ type Config struct { // addresses, then the agent will error and exit. StartJoin []string `mapstructure:"start_join"` + // StartWanJoin is a list of addresses to attempt to join -wan when the + // agent starts. If Serf is unable to communicate with any of these + // addresses, then the agent will error and exit. + StartWanJoin []string `mapstructure:"start_wan_join"` + // RetryJoin is a list of addresses to join with retry enabled. RetryJoin []string `mapstructure:"retry_join"` @@ -208,6 +213,20 @@ type Config struct { RetryInterval time.Duration `mapstructure:"-" json:"-"` RetryIntervalRaw string `mapstructure:"retry_interval"` + // RetryWanJoin is a list of addresses to join -wan with retry enabled. + RetryWanJoin []string `mapstructure:"retry_wan_join"` + + // RetryWanMaxAttempts specifies the maximum number of times to retry joining a + // -wan host on startup. This is useful for cases where we know the node will be + // online eventually. + RetryWanMaxAttempts int `mapstructure:"retry_wan_max"` + + // RetryWanInterval specifies the amount of time to wait in between join + // -wan attempts on agent start. The minimum allowed value is 1 second and + // the default is 30s. + RetryWanInterval time.Duration `mapstructure:"-" json:"-"` + RetryWanIntervalRaw string `mapstructure:"retry_wan_interval"` + // UiDir is the directory containing the Web UI resources. // If provided, the UI endpoints will be enabled. UiDir string `mapstructure:"ui_dir"` @@ -348,6 +367,7 @@ func DefaultConfig() *Config { ACLDownPolicy: "extend-cache", ACLDefaultPolicy: "allow", RetryInterval: 30 * time.Second, + RetryWanInterval: 30 * time.Second, } } @@ -505,6 +525,14 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.RetryInterval = dur } + if raw := result.RetryWanIntervalRaw; raw != "" { + dur, err := time.ParseDuration(raw) + if err != nil { + return nil, fmt.Errorf("RetryWanInterval invalid: %v", err) + } + result.RetryWanInterval = dur + } + // Merge the single recursor if result.DNSRecursor != "" { result.DNSRecursors = append(result.DNSRecursors, result.DNSRecursor) @@ -750,6 +778,12 @@ func MergeConfig(a, b *Config) *Config { if b.RetryInterval != 0 { result.RetryInterval = b.RetryInterval } + if b.RetryWanMaxAttempts != 0 { + result.RetryWanMaxAttempts = b.RetryWanMaxAttempts + } + if b.RetryWanInterval != 0 { + result.RetryWanInterval = b.RetryWanInterval + } if b.DNSConfig.NodeTTL != 0 { result.DNSConfig.NodeTTL = b.DNSConfig.NodeTTL } @@ -816,11 +850,21 @@ func MergeConfig(a, b *Config) *Config { result.StartJoin = append(result.StartJoin, a.StartJoin...) result.StartJoin = append(result.StartJoin, b.StartJoin...) + // Copy the start join addresses + result.StartWanJoin = make([]string, 0, len(a.StartWanJoin)+len(b.StartWanJoin)) + result.StartWanJoin = append(result.StartWanJoin, a.StartWanJoin...) + result.StartWanJoin = append(result.StartWanJoin, b.StartWanJoin...) + // Copy the retry join addresses result.RetryJoin = make([]string, 0, len(a.RetryJoin)+len(b.RetryJoin)) result.RetryJoin = append(result.RetryJoin, a.RetryJoin...) result.RetryJoin = append(result.RetryJoin, b.RetryJoin...) + // Copy the retry join -wan addresses + result.RetryWanJoin = make([]string, 0, len(a.RetryWanJoin)+len(b.RetryWanJoin)) + result.RetryWanJoin = append(result.RetryWanJoin, a.RetryWanJoin...) + result.RetryWanJoin = append(result.RetryWanJoin, b.RetryWanJoin...) + return &result } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 77e67a172..420c6c4a6 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -274,6 +274,23 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + // Start Wan join + input = `{"start_wan_join": ["1.1.1.1", "2.2.2.2"]}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(config.StartWanJoin) != 2 { + t.Fatalf("bad: %#v", config) + } + if config.StartWanJoin[0] != "1.1.1.1" { + t.Fatalf("bad: %#v", config) + } + if config.StartWanJoin[1] != "2.2.2.2" { + t.Fatalf("bad: %#v", config) + } + // Retry join input = `{"retry_join": ["1.1.1.1", "2.2.2.2"]}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) @@ -316,6 +333,48 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + // Retry WAN join + input = `{"retry_wan_join": ["1.1.1.1", "2.2.2.2"]}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if len(config.RetryWanJoin) != 2 { + t.Fatalf("bad: %#v", config) + } + if config.RetryWanJoin[0] != "1.1.1.1" { + t.Fatalf("bad: %#v", config) + } + if config.RetryWanJoin[1] != "2.2.2.2" { + t.Fatalf("bad: %#v", config) + } + + // Retry WAN interval + input = `{"retry_wan_interval": "10s"}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if config.RetryWanIntervalRaw != "10s" { + t.Fatalf("bad: %#v", config) + } + if config.RetryWanInterval.String() != "10s" { + t.Fatalf("bad: %#v", config) + } + + // Retry WAN Max + input = `{"retry_wan_max": 3}` + config, err = DecodeConfig(bytes.NewReader([]byte(input))) + if err != nil { + t.Fatalf("err: %s", err) + } + + if config.RetryWanMaxAttempts != 3 { + t.Fatalf("bad: %#v", config) + } + // UI Dir input = `{"ui_dir": "/opt/consul-ui"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) @@ -860,12 +919,16 @@ func TestMergeConfig(t *testing.T) { Checks: []*CheckDefinition{nil}, Services: []*ServiceDefinition{nil}, StartJoin: []string{"1.1.1.1"}, + StartWanJoin: []string{"1.1.1.1"}, UiDir: "/opt/consul-ui", EnableSyslog: true, RejoinAfterLeave: true, RetryJoin: []string{"1.1.1.1"}, RetryIntervalRaw: "10s", RetryInterval: 10 * time.Second, + RetryWanJoin: []string{"1.1.1.1"}, + RetryWanIntervalRaw: "10s", + RetryWanInterval: 10 * time.Second, CheckUpdateInterval: 8 * time.Minute, CheckUpdateIntervalRaw: "8m", ACLToken: "1234", diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index de3bbd78a..bf817f085 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -105,6 +105,21 @@ The options below are all specified on the command-line. with return code 1. By default, this is set to 0, which will continue to retry the join indefinitely. +* `-join-wan` - Address of another wan agent to join upon starting up. This can be + specified multiple times to specify multiple agents that are on the WAN to join. If Consul is + unable to join with any of the specified addresses, agent startup will + fail. By default, the agent won't join -wan any nodes when it starts up. + +* `-retry-wan-join` - Similar to `retry-join`, but allows retrying a wan join if the first + attempt fails. This is useful for cases where we know the address will become + available eventually. + +* `-retry-wan-interval` - Time to wait between join -wan attempts. Defaults to 30s. + +* `-retry-wan-max` - The maximum number of join -wan attempts to be made before exiting + with return code 1. By default, this is set to 0, which will continue to + retry the join -wan indefinitely. + * `-log-level` - The level of logging to show after the Consul agent has started. This defaults to "info". The available log levels are "trace", "debug", "info", "warn", "err". This is the log level that will be shown @@ -339,6 +354,12 @@ definitions support being updated during a reload. * `retry_interval` - Equivalent to the `-retry-interval` command-line flag. +* `retry_wan_join` - Equivalent to the `-retry-wan-join` command-line flag. Takes a list + of addresses to attempt joining to WAN every `retry_wan_interval` until at least one + join -wan works. + +* `retry_wan_interval` - Equivalent to the `-retry-wan-interval` command-line flag. + * `server` - Equivalent to the `-server` command-line flag. * `server_name` - When give, this overrides the `node_name` for the TLS certificate. @@ -353,6 +374,9 @@ definitions support being updated during a reload. * `start_join` - An array of strings specifying addresses of nodes to join upon startup. +* `start_wan_join` - An array of strings specifying addresses of WAN nodes to + join -wan upon startup. + * `statsd_addr` - This provides the address of a statsd instance. If provided Consul will send various telemetry information to that instance for aggregation. This can be used to capture various runtime information. This sends UDP packets From 83515da89010ce11c8e2e3f3cd04662b914e0ac1 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 15 Nov 2014 22:40:31 -0800 Subject: [PATCH 072/238] deps: add 0.4.1 deps file, rename 0-4.1 to match naming convention --- deps/{v0-4.0.json => v0-4-0.json} | 0 deps/v0-4-1.json | 97 +++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) rename deps/{v0-4.0.json => v0-4-0.json} (100%) create mode 100644 deps/v0-4-1.json diff --git a/deps/v0-4.0.json b/deps/v0-4-0.json similarity index 100% rename from deps/v0-4.0.json rename to deps/v0-4-0.json diff --git a/deps/v0-4-1.json b/deps/v0-4-1.json new file mode 100644 index 000000000..087054e8f --- /dev/null +++ b/deps/v0-4-1.json @@ -0,0 +1,97 @@ +{ + "ImportPath": "github.com/hashicorp/consul", + "GoVersion": "go1.3.3", + "Deps": [ + { + "ImportPath": "github.com/armon/circbuf", + "Rev": "f092b4f207b6e5cce0569056fba9e1a2735cb6cf" + }, + { + "ImportPath": "github.com/armon/consul-api", + "Rev": "1b81c8e0c4cbf1d382310e4c0dc11221632e79d1" + }, + { + "ImportPath": "github.com/armon/go-metrics", + "Rev": "2b75159ce5d3641fb35b5a159cff309ac3cf4177" + }, + { + "ImportPath": "github.com/armon/go-radix", + "Rev": "b045fc0ad3587e8620fb42a0dea882cf8c08aef9" + }, + { + "ImportPath": "github.com/armon/gomdb", + "Rev": "a8e036c4dabe7437014ecf9dbc03c6f6f0766ef8" + }, + { + "ImportPath": "github.com/hashicorp/go-checkpoint", + "Rev": "89ef2a697dd8cdb4623097d5bb9acdb19a470767" + }, + { + "ImportPath": "github.com/hashicorp/go-msgpack/codec", + "Rev": "71c2886f5a673a35f909803f38ece5810165097b" + }, + { + "ImportPath": "github.com/hashicorp/go-syslog", + "Rev": "ac3963b72ac367e48b1e68a831e62b93fb69091c" + }, + { + "ImportPath": "github.com/hashicorp/golang-lru", + "Rev": "253b2dc1ca8bae42c3b5b6e53dd2eab1a7551116" + }, + { + "ImportPath": "github.com/hashicorp/hcl", + "Rev": "e51eabcdf801f663738fa12f4340fbad13062738" + }, + { + "ImportPath": "github.com/hashicorp/logutils", + "Rev": "23b0af5510a2d1442103ef83ffcf53eb82f3debc" + }, + { + "ImportPath": "github.com/hashicorp/memberlist", + "Rev": "16d947e2d4b3f1fe508ee1d9b6ec34b8fd2e96d8" + }, + { + "ImportPath": "github.com/hashicorp/raft", + "Rev": "cc9710ab540985954a67c108f414aa3152f5916f" + }, + { + "ImportPath": "github.com/hashicorp/raft-mdb", + "Rev": "6f52d0ce62a34e3f5bd29aa4d7068030d700d94a" + }, + { + "ImportPath": "github.com/hashicorp/serf/serf", + "Comment": "v0.6.3-60-g0479bc1", + "Rev": "0479bc1b942fd84205587f7e73867ac78809966b" + }, + { + "ImportPath": "github.com/hashicorp/terraform/helper/multierror", + "Comment": "v0.3.1-25-g2d11732", + "Rev": "2d117326edb33b7155d1ec9d0ab9d3542ba1b230" + }, + { + "ImportPath": "github.com/hashicorp/yamux", + "Rev": "9feabe6854fadca1abec9cd3bd2a613fe9a34000" + }, + { + "ImportPath": "github.com/inconshreveable/muxado", + "Rev": "f693c7e88ba316d1a0ae3e205e22a01aa3ec2848" + }, + { + "ImportPath": "github.com/miekg/dns", + "Rev": "dc30c7cd4ed2fc8af73d49da4ee285404958b8bd" + }, + { + "ImportPath": "github.com/mitchellh/cli", + "Rev": "e3c2e3d39391e9beb9660ccd6b4bd9a2f38dd8a0" + }, + { + "ImportPath": "github.com/mitchellh/mapstructure", + "Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf" + }, + { + "ImportPath": "github.com/ryanuber/columnize", + "Comment": "v2.0.1-6-g44cb478", + "Rev": "44cb4788b2ec3c3d158dd3d1b50aba7d66f4b59a" + } + ] +} From 59a68ecc26183ea7fb84b8abb0d44075947210cf Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Fri, 14 Nov 2014 14:39:19 -0500 Subject: [PATCH 073/238] Added HTTPS support via a new HTTPS Port configuration option similar to the HTTP Port. --- command/agent/command.go | 32 ++--- command/agent/config.go | 91 ++++++++++++- command/agent/config_test.go | 19 ++- command/agent/http.go | 128 +++++++++++++++--- .../source/docs/agent/options.html.markdown | 1 + 5 files changed, 222 insertions(+), 49 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 6aa51ac7c..de800e673 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -43,7 +43,7 @@ type Command struct { logOutput io.Writer agent *Agent rpcServer *AgentRPC - httpServer *HTTPServer + httpServers []*HTTPServer dnsServer *DNSServer } @@ -71,7 +71,7 @@ func (c *Command) readConfig() *Config { cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode") cmdFlags.IntVar(&cmdConfig.BootstrapExpect, "bootstrap-expect", 0, "enable automatic bootstrap via expect mode") - cmdFlags.StringVar(&cmdConfig.ClientAddr, "client", "", "address to bind client listeners to (DNS, HTTP, RPC)") + cmdFlags.StringVar(&cmdConfig.ClientAddr, "client", "", "address to bind client listeners to (DNS, HTTP, HTTPS, RPC)") cmdFlags.StringVar(&cmdConfig.BindAddr, "bind", "", "address to bind server listeners to") cmdFlags.StringVar(&cmdConfig.AdvertiseAddr, "advertise", "", "address to advertise instead of bind addr") @@ -278,20 +278,14 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log c.Ui.Output("Starting Consul agent RPC...") c.rpcServer = NewAgentRPC(agent, rpcListener, logOutput, logWriter) - if config.Ports.HTTP > 0 { - httpAddr, err := config.ClientListener(config.Addresses.HTTP, config.Ports.HTTP) - if err != nil { - c.Ui.Error(fmt.Sprintf("Invalid HTTP bind address: %s", err)) - return err - } - - server, err := NewHTTPServer(agent, config.UiDir, config.EnableDebug, logOutput, httpAddr.String()) + if config.Ports.HTTP > 0 || config.Ports.HTTPS > 0 { + servers, err := NewHTTPServers(agent, config, logOutput) if err != nil { agent.Shutdown() - c.Ui.Error(fmt.Sprintf("Error starting http server: %s", err)) + c.Ui.Error(fmt.Sprintf("Error starting http servers:", err)) return err } - c.httpServer = server + c.httpServers = servers } if config.Ports.DNS > 0 { @@ -472,8 +466,10 @@ func (c *Command) Run(args []string) int { if c.rpcServer != nil { defer c.rpcServer.Shutdown() } - if c.httpServer != nil { - defer c.httpServer.Shutdown() + if c.httpServers != nil { + for _, server := range c.httpServers { + defer server.Shutdown() + } } // Join startup nodes if specified @@ -502,7 +498,7 @@ func (c *Command) Run(args []string) int { } } - // Get the new client listener addr + // Get the new client http listener addr httpAddr, err := config.ClientListenerAddr(config.Addresses.HTTP, config.Ports.HTTP) if err != nil { c.Ui.Error(fmt.Sprintf("Failed to determine HTTP address: %v", err)) @@ -526,8 +522,8 @@ func (c *Command) Run(args []string) int { c.Ui.Info(fmt.Sprintf(" Node name: '%s'", config.NodeName)) c.Ui.Info(fmt.Sprintf(" Datacenter: '%s'", config.Datacenter)) c.Ui.Info(fmt.Sprintf(" Server: %v (bootstrap: %v)", config.Server, config.Bootstrap)) - c.Ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, DNS: %d, RPC: %d)", config.ClientAddr, - config.Ports.HTTP, config.Ports.DNS, config.Ports.RPC)) + c.Ui.Info(fmt.Sprintf(" Client Addr: %v (HTTP: %d, HTTPS: %d, DNS: %d, RPC: %d)", config.ClientAddr, + config.Ports.HTTP, config.Ports.HTTPS, config.Ports.DNS, config.Ports.RPC)) c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr, config.Ports.SerfLan, config.Ports.SerfWan)) c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v", @@ -709,7 +705,7 @@ Options: -bind=0.0.0.0 Sets the bind address for cluster communication -bootstrap-expect=0 Sets server to expect bootstrap mode. -client=127.0.0.1 Sets the address to bind for client access. - This includes RPC, DNS and HTTP + This includes RPC, DNS, HTTP and HTTPS (if configured) -config-file=foo Path to a JSON file to read configuration from. This can be specified multiple times. -config-dir=foo Path to a directory to read configuration files diff --git a/command/agent/config.go b/command/agent/config.go index 3a926499f..de9cb13d7 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -1,10 +1,13 @@ package agent import ( + "crypto/tls" + "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" + "io/ioutil" "net" "os" "path/filepath" @@ -23,6 +26,7 @@ import ( type PortConfig struct { DNS int // DNS Query interface HTTP int // HTTP API + HTTPS int // HTTPS API RPC int // CLI RPC SerfLan int `mapstructure:"serf_lan"` // LAN gossip (Client + Server) SerfWan int `mapstructure:"serf_wan"` // WAN gossip (Server onlyg) @@ -33,9 +37,10 @@ type PortConfig struct { // for specific services. By default, either ClientAddress // or ServerAddress is used. type AddressConfig struct { - DNS string // DNS Query interface - HTTP string // HTTP API - RPC string // CLI RPC + DNS string // DNS Query interface + HTTP string // HTTP API + HTTPS string // HTTPS API + RPC string // CLI RPC } // DNSConfig is used to fine tune the DNS sub-system. @@ -122,7 +127,7 @@ type Config struct { NodeName string `mapstructure:"node_name"` // ClientAddr is used to control the address we bind to for - // client services (DNS, HTTP, RPC) + // client services (DNS, HTTP, HTTPS, RPC) ClientAddr string `mapstructure:"client_addr"` // BindAddr is used to control the address we bind to. @@ -332,6 +337,7 @@ func DefaultConfig() *Config { Ports: PortConfig{ DNS: 8600, HTTP: 8500, + HTTPS: -1, RPC: 8400, SerfLan: consul.DefaultLANSerfPort, SerfWan: consul.DefaultWANSerfPort, @@ -385,6 +391,77 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } +// AppendCA opens and parses the CA file and adds the certificates to +// the provided CertPool. +func (c *Config) AppendCA(pool *x509.CertPool) error { + if c.CAFile == "" { + return nil + } + + // Read the file + data, err := ioutil.ReadFile(c.CAFile) + if err != nil { + return fmt.Errorf("Failed to read CA file: %v", err) + } + + if !pool.AppendCertsFromPEM(data) { + return fmt.Errorf("Failed to parse any CA certificates") + } + + return nil +} + +// KeyPair is used to open and parse a certificate and key file +func (c *Config) KeyPair() (*tls.Certificate, error) { + if c.CertFile == "" || c.KeyFile == "" { + return nil, nil + } + cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("Failed to load cert/key pair: %v", err) + } + return &cert, err +} + +// IncomingTLSConfig generates a TLS configuration for incoming requests +func (c *Config) IncomingTLSConfig() (*tls.Config, error) { + // Create the tlsConfig + tlsConfig := &tls.Config{ + ServerName: c.ServerName, + ClientCAs: x509.NewCertPool(), + ClientAuth: tls.NoClientCert, + } + if tlsConfig.ServerName == "" { + tlsConfig.ServerName = c.NodeName + } + + // Parse the CA cert if any + err := c.AppendCA(tlsConfig.ClientCAs) + if err != nil { + return nil, err + } + + // Add cert/key + cert, err := c.KeyPair() + if err != nil { + return nil, err + } else if cert != nil { + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + // Check if we require verification + if c.VerifyIncoming { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if c.CAFile == "" { + return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") + } + if cert == nil { + return nil, fmt.Errorf("VerifyIncoming set, and no Cert/Key pair provided!") + } + } + return tlsConfig, nil +} + // DecodeConfig reads the configuration from the given reader in JSON // format and decodes it into a proper Config structure. func DecodeConfig(r io.Reader) (*Config, error) { @@ -711,6 +788,9 @@ func MergeConfig(a, b *Config) *Config { if b.Ports.HTTP != 0 { result.Ports.HTTP = b.Ports.HTTP } + if b.Ports.HTTPS != 0 { + result.Ports.HTTPS = b.Ports.HTTPS + } if b.Ports.RPC != 0 { result.Ports.RPC = b.Ports.RPC } @@ -729,6 +809,9 @@ func MergeConfig(a, b *Config) *Config { if b.Addresses.HTTP != "" { result.Addresses.HTTP = b.Addresses.HTTP } + if b.Addresses.HTTPS != "" { + result.Addresses.HTTPS = b.Addresses.HTTPS + } if b.Addresses.RPC != "" { result.Addresses.RPC = b.Addresses.RPC } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 77e67a172..80ca0ec5a 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -137,7 +137,7 @@ func TestDecodeConfig(t *testing.T) { } // RPC configs - input = `{"ports": {"http": 1234, "rpc": 8100}, "client_addr": "0.0.0.0"}` + input = `{"ports": {"http": 1234, "https": 1243, "rpc": 8100}, "client_addr": "0.0.0.0"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -151,6 +151,10 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } + if config.Ports.HTTPS != 1243 { + t.Fatalf("bad: %#v", config) + } + if config.Ports.RPC != 8100 { t.Fatalf("bad: %#v", config) } @@ -494,7 +498,7 @@ func TestDecodeConfig(t *testing.T) { } // Address overrides - input = `{"addresses": {"dns": "0.0.0.0", "http": "127.0.0.1", "rpc": "127.0.0.1"}}` + input = `{"addresses": {"dns": "0.0.0.0", "http": "127.0.0.1", "https": "127.0.0.1", "rpc": "127.0.0.1"}}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) @@ -506,6 +510,9 @@ func TestDecodeConfig(t *testing.T) { if config.Addresses.HTTP != "127.0.0.1" { t.Fatalf("bad: %#v", config) } + if config.Addresses.HTTPS != "127.0.0.1" { + t.Fatalf("bad: %#v", config) + } if config.Addresses.RPC != "127.0.0.1" { t.Fatalf("bad: %#v", config) } @@ -842,11 +849,13 @@ func TestMergeConfig(t *testing.T) { SerfLan: 4, SerfWan: 5, Server: 6, + HTTPS: 7, }, Addresses: AddressConfig{ - DNS: "127.0.0.1", - HTTP: "127.0.0.2", - RPC: "127.0.0.3", + DNS: "127.0.0.1", + HTTP: "127.0.0.2", + RPC: "127.0.0.3", + HTTPS: "127.0.0.4", }, Server: true, LeaveOnTerm: true, diff --git a/command/agent/http.go b/command/agent/http.go index 1954559ac..5d07f23de 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -1,6 +1,7 @@ package agent import ( + "crypto/tls" "encoding/json" "io" "log" @@ -23,38 +24,121 @@ type HTTPServer struct { listener net.Listener logger *log.Logger uiDir string + addr string } -// NewHTTPServer starts a new HTTP server to provide an interface to +// NewHTTPServers starts new HTTP servers to provide an interface to // the agent. -func NewHTTPServer(agent *Agent, uiDir string, enableDebug bool, logOutput io.Writer, bind string) (*HTTPServer, error) { - // Create the mux - mux := http.NewServeMux() +func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPServer, error) { + var tlsConfig *tls.Config + var list net.Listener + var httpAddr *net.TCPAddr + var err error + var servers []*HTTPServer - // Create listener - list, err := net.Listen("tcp", bind) + if config.Ports.HTTPS > 0 { + httpAddr, err = config.ClientListener(config.Addresses.HTTPS, config.Ports.HTTPS) + if err != nil { + return nil, err + } + + tlsConfig, err = config.IncomingTLSConfig() + if err != nil { + return nil, err + } + + ln, err := net.Listen("tcp", httpAddr.String()) + if err != nil { + return nil, err + } + + list = tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, tlsConfig) + + // Create the mux + mux := http.NewServeMux() + + // Create the server + srv := &HTTPServer{ + agent: agent, + mux: mux, + listener: list, + logger: log.New(logOutput, "", log.LstdFlags), + uiDir: config.UiDir, + addr: httpAddr.String(), + } + srv.registerHandlers(config.EnableDebug) + + // Start the server + go http.Serve(list, mux) + + servers := make([]*HTTPServer, 1) + servers[0] = srv + } + + if config.Ports.HTTP > 0 { + httpAddr, err = config.ClientListener(config.Addresses.HTTP, config.Ports.HTTP) + if err != nil { + return nil, err + } + + // Create non-TLS listener + list, err = net.Listen("tcp", httpAddr.String()) + if err != nil { + return nil, err + } + + // Create the mux + mux := http.NewServeMux() + + // Create the server + srv := &HTTPServer{ + agent: agent, + mux: mux, + listener: list, + logger: log.New(logOutput, "", log.LstdFlags), + uiDir: config.UiDir, + addr: httpAddr.String(), + } + srv.registerHandlers(config.EnableDebug) + + // Start the server + go http.Serve(list, mux) + + if servers != nil { + // we already have the https server in servers, append + servers = append(servers, srv) + } else { + servers := make([]*HTTPServer, 1) + servers[0] = srv + } + } + + return servers, nil +} + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by NewHttpServer so +// dead TCP connections eventually go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() if err != nil { - return nil, err + return } - - // Create the server - srv := &HTTPServer{ - agent: agent, - mux: mux, - listener: list, - logger: log.New(logOutput, "", log.LstdFlags), - uiDir: uiDir, - } - srv.registerHandlers(enableDebug) - - // Start the server - go http.Serve(list, mux) - return srv, nil + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil } // Shutdown is used to shutdown the HTTP server func (s *HTTPServer) Shutdown() { - s.listener.Close() + if s != nil { + s.logger.Printf("[DEBUG] http: Shutting down http server(%v)", s.addr) + s.listener.Close() + } } // registerHandlers is used to attach our handlers to the mux diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index de3bbd78a..825eb8d6c 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -316,6 +316,7 @@ definitions support being updated during a reload. for the following keys: * `dns` - The DNS server, -1 to disable. Default 8600. * `http` - The HTTP api, -1 to disable. Default 8500. + * `https` - The HTTPS api, -1 to disable. Default -1 (disabled). * `rpc` - The RPC endpoint. Default 8400. * `serf_lan` - The Serf LAN port. Default 8301. * `serf_wan` - The Serf WAN port. Default 8302. From 46178dbb37741bcc62d96034875c2bc9cf30e97d Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Mon, 17 Nov 2014 17:14:59 -0500 Subject: [PATCH 074/238] Change names to StartJoinWan, RetryJoinWan etc --- command/agent/command.go | 52 +++++++++---------- command/agent/command_test.go | 8 +-- command/agent/config.go | 46 ++++++++-------- command/agent/config_test.go | 42 +++++++-------- .../source/docs/agent/options.html.markdown | 14 ++--- 5 files changed, 81 insertions(+), 81 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 768d337c3..7ef886fdf 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -53,7 +53,7 @@ func (c *Command) readConfig() *Config { var cmdConfig Config var configFiles []string var retryInterval string - var retryWanInterval string + var retryIntervalWan string cmdFlags := flag.NewFlagSet("agent", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -84,7 +84,7 @@ func (c *Command) readConfig() *Config { "enable re-joining after a previous leave") cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartJoin), "join", "address of agent to join on startup") - cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartWanJoin), "join-wan", + cmdFlags.Var((*AppendSliceValue)(&cmdConfig.StartJoinWan), "join-wan", "address of agent to join -wan on startup") cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryJoin), "retry-join", "address of agent to join on startup with retry") @@ -92,11 +92,11 @@ func (c *Command) readConfig() *Config { "number of retries for joining") cmdFlags.StringVar(&retryInterval, "retry-interval", "", "interval between join attempts") - cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryWanJoin), "retry-wan-join", + cmdFlags.Var((*AppendSliceValue)(&cmdConfig.RetryJoinWan), "retry-join-wan", "address of agent to join -wan on startup with retry") - cmdFlags.IntVar(&cmdConfig.RetryWanMaxAttempts, "retry-wan-max", 0, + cmdFlags.IntVar(&cmdConfig.RetryMaxAttemptsWan, "retry-max-wan", 0, "number of retries for joining -wan") - cmdFlags.StringVar(&retryWanInterval, "retry-wan-interval", "", + cmdFlags.StringVar(&retryIntervalWan, "retry-interval-wan", "", "interval between join -wan attempts") if err := cmdFlags.Parse(c.args); err != nil { @@ -112,13 +112,13 @@ func (c *Command) readConfig() *Config { cmdConfig.RetryInterval = dur } - if retryWanInterval != "" { - dur, err := time.ParseDuration(retryWanInterval) + if retryIntervalWan != "" { + dur, err := time.ParseDuration(retryIntervalWan) if err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return nil } - cmdConfig.RetryWanInterval = dur + cmdConfig.RetryIntervalWan = dur } config := DefaultConfig() @@ -387,14 +387,14 @@ func (c *Command) startupJoin(config *Config) error { return nil } -// startupWanJoin is invoked to handle any joins -wan specified to take place at start time -func (c *Command) startupWanJoin(config *Config) error { - if len(config.StartWanJoin) == 0 { +// startupJoinWan is invoked to handle any joins -wan specified to take place at start time +func (c *Command) startupJoinWan(config *Config) error { + if len(config.StartJoinWan) == 0 { return nil } c.Ui.Output("Joining -wan cluster...") - n, err := c.agent.JoinWAN(config.StartWanJoin) + n, err := c.agent.JoinWAN(config.StartJoinWan) if err != nil { return err } @@ -434,10 +434,10 @@ func (c *Command) retryJoin(config *Config, errCh chan<- struct{}) { } } -// retryWanJoin is used to handle retrying a join -wan until it succeeds or all +// retryJoinWan is used to handle retrying a join -wan until it succeeds or all // retries are exhausted. -func (c *Command) retryWanJoin(config *Config, errCh chan<- struct{}) { - if len(config.RetryWanJoin) == 0 { +func (c *Command) retryJoinWan(config *Config, errCh chan<- struct{}) { + if len(config.RetryJoinWan) == 0 { return } @@ -446,22 +446,22 @@ func (c *Command) retryWanJoin(config *Config, errCh chan<- struct{}) { attempt := 0 for { - n, err := c.agent.JoinWAN(config.RetryWanJoin) + n, err := c.agent.JoinWAN(config.RetryJoinWan) if err == nil { logger.Printf("[INFO] agent: Join -wan completed. Synced with %d initial agents", n) return } attempt++ - if config.RetryWanMaxAttempts > 0 && attempt > config.RetryWanMaxAttempts { + if config.RetryMaxAttemptsWan > 0 && attempt > config.RetryMaxAttemptsWan { logger.Printf("[ERROR] agent: max join -wan retry exhausted, exiting") close(errCh) return } logger.Printf("[WARN] agent: Join -wan failed: %v, retrying in %v", err, - config.RetryWanInterval) - time.Sleep(config.RetryWanInterval) + config.RetryIntervalWan) + time.Sleep(config.RetryIntervalWan) } } @@ -548,7 +548,7 @@ func (c *Command) Run(args []string) int { } // Join startup nodes if specified - if err := c.startupWanJoin(config); err != nil { + if err := c.startupJoinWan(config); err != nil { c.Ui.Error(err.Error()) return 1 } @@ -615,14 +615,14 @@ func (c *Command) Run(args []string) int { // Start retry -wan join process errWanCh := make(chan struct{}) - go c.retryWanJoin(config, errWanCh) + go c.retryJoinWan(config, errWanCh) // Wait for exit return c.handleSignals(config, errCh, errWanCh) } // handleSignals blocks until we get an exit-causing signal -func (c *Command) handleSignals(config *Config, retryJoin <-chan struct{}, retryWanJoin <-chan struct{}) int { +func (c *Command) handleSignals(config *Config, retryJoin <-chan struct{}, retryJoinWan <-chan struct{}) int { signalCh := make(chan os.Signal, 4) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) @@ -638,7 +638,7 @@ WAIT: sig = os.Interrupt case <-retryJoin: return 1 - case <-retryWanJoin: + case <-retryJoinWan: return 1 case <-c.agent.ShutdownCh(): // Agent is already shutdown! @@ -805,10 +805,10 @@ Options: -retry-interval=30s Time to wait between join attempts. -retry-max=0 Maximum number of join attempts. Defaults to 0, which will retry indefinitely. - -retry-wan-join=1.2.3.4 Address of an agent to join -wan at start time with + -retry-join-wan=1.2.3.4 Address of an agent to join -wan at start time with retries enabled. Can be specified multiple times. - -retry-wan-interval=30s Time to wait between join -wan attempts. - -retry-wan-max=0 Maximum number of join -wan attempts. Defaults to 0, which + -retry-interval-wan=30s Time to wait between join -wan attempts. + -retry-max-wan=0 Maximum number of join -wan attempts. Defaults to 0, which will retry indefinitely. -log-level=info Log level of the agent. -node=hostname Name of this node. Must be unique in the cluster diff --git a/command/agent/command_test.go b/command/agent/command_test.go index e2dd634d5..bd7f29a17 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -121,7 +121,7 @@ func TestRetryJoinFail(t *testing.T) { t.Fatalf("bad: %d", code) } } -func TestRetryWanJoin(t *testing.T) { +func TestRetryJoinWan(t *testing.T) { dir, agent := makeAgent(t, nextConfig()) defer os.RemoveAll(dir) defer agent.Shutdown() @@ -154,7 +154,7 @@ func TestRetryWanJoin(t *testing.T) { args := []string{ "-data-dir", tmpDir, "-node", fmt.Sprintf(`"%s"`, conf2.NodeName), - "-retry-wan-join", serfAddr, + "-retry-join-wan", serfAddr, "-retry-interval", "1s", } @@ -176,7 +176,7 @@ func TestRetryWanJoin(t *testing.T) { }) } -func TestRetryWanJoinFail(t *testing.T) { +func TestRetryJoinWanFail(t *testing.T) { conf := nextConfig() tmpDir, err := ioutil.TempDir("", "consul") if err != nil { @@ -196,7 +196,7 @@ func TestRetryWanJoinFail(t *testing.T) { args := []string{ "-data-dir", tmpDir, - "-retry-wan-join", serfAddr, + "-retry-join-wan", serfAddr, "-retry-max", "1", } diff --git a/command/agent/config.go b/command/agent/config.go index ddff17164..c995cbe56 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -194,10 +194,10 @@ type Config struct { // addresses, then the agent will error and exit. StartJoin []string `mapstructure:"start_join"` - // StartWanJoin is a list of addresses to attempt to join -wan when the + // StartJoinWan is a list of addresses to attempt to join -wan when the // agent starts. If Serf is unable to communicate with any of these // addresses, then the agent will error and exit. - StartWanJoin []string `mapstructure:"start_wan_join"` + StartJoinWan []string `mapstructure:"start_join_wan"` // RetryJoin is a list of addresses to join with retry enabled. RetryJoin []string `mapstructure:"retry_join"` @@ -213,19 +213,19 @@ type Config struct { RetryInterval time.Duration `mapstructure:"-" json:"-"` RetryIntervalRaw string `mapstructure:"retry_interval"` - // RetryWanJoin is a list of addresses to join -wan with retry enabled. - RetryWanJoin []string `mapstructure:"retry_wan_join"` + // RetryJoinWan is a list of addresses to join -wan with retry enabled. + RetryJoinWan []string `mapstructure:"retry_join_wan"` - // RetryWanMaxAttempts specifies the maximum number of times to retry joining a + // RetryMaxAttemptsWan specifies the maximum number of times to retry joining a // -wan host on startup. This is useful for cases where we know the node will be // online eventually. - RetryWanMaxAttempts int `mapstructure:"retry_wan_max"` + RetryMaxAttemptsWan int `mapstructure:"retry_max_wan"` - // RetryWanInterval specifies the amount of time to wait in between join + // RetryIntervalWan specifies the amount of time to wait in between join // -wan attempts on agent start. The minimum allowed value is 1 second and // the default is 30s. - RetryWanInterval time.Duration `mapstructure:"-" json:"-"` - RetryWanIntervalRaw string `mapstructure:"retry_wan_interval"` + RetryIntervalWan time.Duration `mapstructure:"-" json:"-"` + RetryIntervalWanRaw string `mapstructure:"retry_interval_wan"` // UiDir is the directory containing the Web UI resources. // If provided, the UI endpoints will be enabled. @@ -367,7 +367,7 @@ func DefaultConfig() *Config { ACLDownPolicy: "extend-cache", ACLDefaultPolicy: "allow", RetryInterval: 30 * time.Second, - RetryWanInterval: 30 * time.Second, + RetryIntervalWan: 30 * time.Second, } } @@ -525,12 +525,12 @@ func DecodeConfig(r io.Reader) (*Config, error) { result.RetryInterval = dur } - if raw := result.RetryWanIntervalRaw; raw != "" { + if raw := result.RetryIntervalWanRaw; raw != "" { dur, err := time.ParseDuration(raw) if err != nil { - return nil, fmt.Errorf("RetryWanInterval invalid: %v", err) + return nil, fmt.Errorf("RetryIntervalWan invalid: %v", err) } - result.RetryWanInterval = dur + result.RetryIntervalWan = dur } // Merge the single recursor @@ -778,11 +778,11 @@ func MergeConfig(a, b *Config) *Config { if b.RetryInterval != 0 { result.RetryInterval = b.RetryInterval } - if b.RetryWanMaxAttempts != 0 { - result.RetryWanMaxAttempts = b.RetryWanMaxAttempts + if b.RetryMaxAttemptsWan != 0 { + result.RetryMaxAttemptsWan = b.RetryMaxAttemptsWan } - if b.RetryWanInterval != 0 { - result.RetryWanInterval = b.RetryWanInterval + if b.RetryIntervalWan != 0 { + result.RetryIntervalWan = b.RetryIntervalWan } if b.DNSConfig.NodeTTL != 0 { result.DNSConfig.NodeTTL = b.DNSConfig.NodeTTL @@ -851,9 +851,9 @@ func MergeConfig(a, b *Config) *Config { result.StartJoin = append(result.StartJoin, b.StartJoin...) // Copy the start join addresses - result.StartWanJoin = make([]string, 0, len(a.StartWanJoin)+len(b.StartWanJoin)) - result.StartWanJoin = append(result.StartWanJoin, a.StartWanJoin...) - result.StartWanJoin = append(result.StartWanJoin, b.StartWanJoin...) + result.StartJoinWan = make([]string, 0, len(a.StartJoinWan)+len(b.StartJoinWan)) + result.StartJoinWan = append(result.StartJoinWan, a.StartJoinWan...) + result.StartJoinWan = append(result.StartJoinWan, b.StartJoinWan...) // Copy the retry join addresses result.RetryJoin = make([]string, 0, len(a.RetryJoin)+len(b.RetryJoin)) @@ -861,9 +861,9 @@ func MergeConfig(a, b *Config) *Config { result.RetryJoin = append(result.RetryJoin, b.RetryJoin...) // Copy the retry join -wan addresses - result.RetryWanJoin = make([]string, 0, len(a.RetryWanJoin)+len(b.RetryWanJoin)) - result.RetryWanJoin = append(result.RetryWanJoin, a.RetryWanJoin...) - result.RetryWanJoin = append(result.RetryWanJoin, b.RetryWanJoin...) + result.RetryJoinWan = make([]string, 0, len(a.RetryJoinWan)+len(b.RetryJoinWan)) + result.RetryJoinWan = append(result.RetryJoinWan, a.RetryJoinWan...) + result.RetryJoinWan = append(result.RetryJoinWan, b.RetryJoinWan...) return &result } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 420c6c4a6..9197447d9 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -274,20 +274,20 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } - // Start Wan join - input = `{"start_wan_join": ["1.1.1.1", "2.2.2.2"]}` + // Start Join wan + input = `{"start_join_wan": ["1.1.1.1", "2.2.2.2"]}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) } - if len(config.StartWanJoin) != 2 { + if len(config.StartJoinWan) != 2 { t.Fatalf("bad: %#v", config) } - if config.StartWanJoin[0] != "1.1.1.1" { + if config.StartJoinWan[0] != "1.1.1.1" { t.Fatalf("bad: %#v", config) } - if config.StartWanJoin[1] != "2.2.2.2" { + if config.StartJoinWan[1] != "2.2.2.2" { t.Fatalf("bad: %#v", config) } @@ -333,45 +333,45 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("bad: %#v", config) } - // Retry WAN join - input = `{"retry_wan_join": ["1.1.1.1", "2.2.2.2"]}` + // Retry Join wan + input = `{"retry_join_wan": ["1.1.1.1", "2.2.2.2"]}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) } - if len(config.RetryWanJoin) != 2 { + if len(config.RetryJoinWan) != 2 { t.Fatalf("bad: %#v", config) } - if config.RetryWanJoin[0] != "1.1.1.1" { + if config.RetryJoinWan[0] != "1.1.1.1" { t.Fatalf("bad: %#v", config) } - if config.RetryWanJoin[1] != "2.2.2.2" { + if config.RetryJoinWan[1] != "2.2.2.2" { t.Fatalf("bad: %#v", config) } - // Retry WAN interval - input = `{"retry_wan_interval": "10s"}` + // Retry Interval wan + input = `{"retry_interval_wan": "10s"}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) } - if config.RetryWanIntervalRaw != "10s" { + if config.RetryIntervalWanRaw != "10s" { t.Fatalf("bad: %#v", config) } - if config.RetryWanInterval.String() != "10s" { + if config.RetryIntervalWan.String() != "10s" { t.Fatalf("bad: %#v", config) } - // Retry WAN Max - input = `{"retry_wan_max": 3}` + // Retry Max wan + input = `{"retry_max_wan": 3}` config, err = DecodeConfig(bytes.NewReader([]byte(input))) if err != nil { t.Fatalf("err: %s", err) } - if config.RetryWanMaxAttempts != 3 { + if config.RetryMaxAttemptsWan != 3 { t.Fatalf("bad: %#v", config) } @@ -919,16 +919,16 @@ func TestMergeConfig(t *testing.T) { Checks: []*CheckDefinition{nil}, Services: []*ServiceDefinition{nil}, StartJoin: []string{"1.1.1.1"}, - StartWanJoin: []string{"1.1.1.1"}, + StartJoinWan: []string{"1.1.1.1"}, UiDir: "/opt/consul-ui", EnableSyslog: true, RejoinAfterLeave: true, RetryJoin: []string{"1.1.1.1"}, RetryIntervalRaw: "10s", RetryInterval: 10 * time.Second, - RetryWanJoin: []string{"1.1.1.1"}, - RetryWanIntervalRaw: "10s", - RetryWanInterval: 10 * time.Second, + RetryJoinWan: []string{"1.1.1.1"}, + RetryIntervalWanRaw: "10s", + RetryIntervalWan: 10 * time.Second, CheckUpdateInterval: 8 * time.Minute, CheckUpdateIntervalRaw: "8m", ACLToken: "1234", diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index bf817f085..51e346ad2 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -110,13 +110,13 @@ The options below are all specified on the command-line. unable to join with any of the specified addresses, agent startup will fail. By default, the agent won't join -wan any nodes when it starts up. -* `-retry-wan-join` - Similar to `retry-join`, but allows retrying a wan join if the first +* `-retry-join-wan` - Similar to `retry-join`, but allows retrying a wan join if the first attempt fails. This is useful for cases where we know the address will become available eventually. -* `-retry-wan-interval` - Time to wait between join -wan attempts. Defaults to 30s. +* `-retry-interval-wan` - Time to wait between join -wan attempts. Defaults to 30s. -* `-retry-wan-max` - The maximum number of join -wan attempts to be made before exiting +* `-retry-max-wan` - The maximum number of join -wan attempts to be made before exiting with return code 1. By default, this is set to 0, which will continue to retry the join -wan indefinitely. @@ -354,11 +354,11 @@ definitions support being updated during a reload. * `retry_interval` - Equivalent to the `-retry-interval` command-line flag. -* `retry_wan_join` - Equivalent to the `-retry-wan-join` command-line flag. Takes a list - of addresses to attempt joining to WAN every `retry_wan_interval` until at least one +* `retry_join_wan` - Equivalent to the `-retry-join-wan` command-line flag. Takes a list + of addresses to attempt joining to WAN every `retry_interval_wan` until at least one join -wan works. -* `retry_wan_interval` - Equivalent to the `-retry-wan-interval` command-line flag. +* `retry_interval_wan` - Equivalent to the `-retry-interval-wan` command-line flag. * `server` - Equivalent to the `-server` command-line flag. @@ -374,7 +374,7 @@ definitions support being updated during a reload. * `start_join` - An array of strings specifying addresses of nodes to join upon startup. -* `start_wan_join` - An array of strings specifying addresses of WAN nodes to +* `start_join_wan` - An array of strings specifying addresses of WAN nodes to join -wan upon startup. * `statsd_addr` - This provides the address of a statsd instance. If provided From b4424a1a5012052e2777b96f190f8c1c9da8dc28 Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Tue, 18 Nov 2014 11:03:36 -0500 Subject: [PATCH 075/238] Moved TLS Config stuff to tlsutil package --- command/agent/config.go | 74 ----------- command/agent/http.go | 17 ++- command/agent/http_test.go | 10 +- command/util_test.go | 11 +- consul/client.go | 12 +- consul/config.go | 166 ----------------------- consul/pool.go | 3 +- consul/raft_rpc.go | 3 +- consul/server.go | 14 +- tlsutil/config.go | 206 +++++++++++++++++++++++++++++ {consul => tlsutil}/config_test.go | 6 +- 11 files changed, 267 insertions(+), 255 deletions(-) create mode 100644 tlsutil/config.go rename {consul => tlsutil}/config_test.go (98%) diff --git a/command/agent/config.go b/command/agent/config.go index de9cb13d7..be5a3d4d0 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -1,13 +1,10 @@ package agent import ( - "crypto/tls" - "crypto/x509" "encoding/base64" "encoding/json" "fmt" "io" - "io/ioutil" "net" "os" "path/filepath" @@ -391,77 +388,6 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } -// AppendCA opens and parses the CA file and adds the certificates to -// the provided CertPool. -func (c *Config) AppendCA(pool *x509.CertPool) error { - if c.CAFile == "" { - return nil - } - - // Read the file - data, err := ioutil.ReadFile(c.CAFile) - if err != nil { - return fmt.Errorf("Failed to read CA file: %v", err) - } - - if !pool.AppendCertsFromPEM(data) { - return fmt.Errorf("Failed to parse any CA certificates") - } - - return nil -} - -// KeyPair is used to open and parse a certificate and key file -func (c *Config) KeyPair() (*tls.Certificate, error) { - if c.CertFile == "" || c.KeyFile == "" { - return nil, nil - } - cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) - if err != nil { - return nil, fmt.Errorf("Failed to load cert/key pair: %v", err) - } - return &cert, err -} - -// IncomingTLSConfig generates a TLS configuration for incoming requests -func (c *Config) IncomingTLSConfig() (*tls.Config, error) { - // Create the tlsConfig - tlsConfig := &tls.Config{ - ServerName: c.ServerName, - ClientCAs: x509.NewCertPool(), - ClientAuth: tls.NoClientCert, - } - if tlsConfig.ServerName == "" { - tlsConfig.ServerName = c.NodeName - } - - // Parse the CA cert if any - err := c.AppendCA(tlsConfig.ClientCAs) - if err != nil { - return nil, err - } - - // Add cert/key - cert, err := c.KeyPair() - if err != nil { - return nil, err - } else if cert != nil { - tlsConfig.Certificates = []tls.Certificate{*cert} - } - - // Check if we require verification - if c.VerifyIncoming { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - if c.CAFile == "" { - return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") - } - if cert == nil { - return nil, fmt.Errorf("VerifyIncoming set, and no Cert/Key pair provided!") - } - } - return tlsConfig, nil -} - // DecodeConfig reads the configuration from the given reader in JSON // format and decodes it into a proper Config structure. func DecodeConfig(r io.Reader) (*Config, error) { diff --git a/command/agent/http.go b/command/agent/http.go index 5d07f23de..79b847a34 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -3,6 +3,7 @@ package agent import ( "crypto/tls" "encoding/json" + "fmt" "io" "log" "net" @@ -13,6 +14,7 @@ import ( "time" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/tlsutil" "github.com/mitchellh/mapstructure" ) @@ -42,7 +44,16 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS return nil, err } - tlsConfig, err = config.IncomingTLSConfig() + tlsConf := &tlsutil.Config{ + VerifyIncoming: config.VerifyIncoming, + VerifyOutgoing: config.VerifyOutgoing, + CAFile: config.CAFile, + CertFile: config.CertFile, + KeyFile: config.KeyFile, + NodeName: config.NodeName, + ServerName: config.ServerName} + + tlsConfig, err = tlsConf.IncomingTLSConfig() if err != nil { return nil, err } @@ -78,13 +89,13 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS if config.Ports.HTTP > 0 { httpAddr, err = config.ClientListener(config.Addresses.HTTP, config.Ports.HTTP) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to get ClientListener address:port: %v", err) } // Create non-TLS listener list, err = net.Listen("tcp", httpAddr.String()) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to get Listen on %s: %v", httpAddr.String(), err) } // Create the mux diff --git a/command/agent/http_test.go b/command/agent/http_test.go index d2ee63e7d..c52b822d7 100644 --- a/command/agent/http_test.go +++ b/command/agent/http_test.go @@ -25,12 +25,18 @@ func makeHTTPServer(t *testing.T) (string, *HTTPServer) { if err := os.Mkdir(uiDir, 755); err != nil { t.Fatalf("err: %v", err) } + conf.Addresses.HTTP = "" + conf.Ports.HTTP = agent.config.Ports.HTTP + conf.Ports.HTTPS = -1 addr, _ := agent.config.ClientListener("", agent.config.Ports.HTTP) - server, err := NewHTTPServer(agent, uiDir, true, agent.logOutput, addr.String()) + servers, err := NewHTTPServers(agent, conf, agent.logOutput) if err != nil { t.Fatalf("err: %v", err) } - return dir, server + if servers == nil || len(servers) == 0 { + t.Fatalf(fmt.Sprintf("Could not create HTTP server to listen on: %s", addr.String())) + } + return dir, servers[0] } func encodeReq(obj interface{}) io.ReadCloser { diff --git a/command/util_test.go b/command/util_test.go index 0366f760b..bb0966473 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -63,19 +63,25 @@ func testAgent(t *testing.T) *agentWrapper { rpc := agent.NewAgentRPC(a, l, mult, lw) + conf.Addresses.HTTP = "127.0.0.1" httpAddr := fmt.Sprintf("127.0.0.1:%d", conf.Ports.HTTP) - http, err := agent.NewHTTPServer(a, "", false, os.Stderr, httpAddr) + http, err := agent.NewHTTPServers(a, conf, os.Stderr) if err != nil { os.RemoveAll(dir) t.Fatalf(fmt.Sprintf("err: %v", err)) } + if http == nil || len(http) == 0 { + os.RemoveAll(dir) + t.Fatalf(fmt.Sprintf("Could not create HTTP server to listen on: %s", httpAddr)) + } + return &agentWrapper{ dir: dir, config: conf, agent: a, rpc: rpc, - http: http, + http: http[0], addr: l.Addr().String(), httpAddr: httpAddr, } @@ -92,6 +98,7 @@ func nextConfig() *agent.Config { conf.Server = true conf.Ports.HTTP = 10000 + 10*idx + conf.Ports.HTTPS = 10400 + 10*idx conf.Ports.RPC = 10100 + 10*idx conf.Ports.SerfLan = 10201 + 10*idx conf.Ports.SerfWan = 10202 + 10*idx diff --git a/consul/client.go b/consul/client.go index 050d147c2..54bd4056a 100644 --- a/consul/client.go +++ b/consul/client.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "fmt" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/serf/serf" "log" "math/rand" @@ -93,7 +94,16 @@ func NewClient(config *Config) (*Client, error) { // Create the tlsConfig var tlsConfig *tls.Config var err error - if tlsConfig, err = config.OutgoingTLSConfig(); err != nil { + tlsConf := &tlsutil.Config{ + VerifyIncoming: config.VerifyIncoming, + VerifyOutgoing: config.VerifyOutgoing, + CAFile: config.CAFile, + CertFile: config.CertFile, + KeyFile: config.KeyFile, + NodeName: config.NodeName, + ServerName: config.ServerName} + + if tlsConfig, err = tlsConf.OutgoingTLSConfig(); err != nil { return nil, err } diff --git a/consul/config.go b/consul/config.go index 5404e71e6..e623dcade 100644 --- a/consul/config.go +++ b/consul/config.go @@ -1,11 +1,8 @@ package consul import ( - "crypto/tls" - "crypto/x509" "fmt" "io" - "io/ioutil" "net" "os" "time" @@ -199,169 +196,6 @@ func (c *Config) CheckACL() error { return nil } -// AppendCA opens and parses the CA file and adds the certificates to -// the provided CertPool. -func (c *Config) AppendCA(pool *x509.CertPool) error { - if c.CAFile == "" { - return nil - } - - // Read the file - data, err := ioutil.ReadFile(c.CAFile) - if err != nil { - return fmt.Errorf("Failed to read CA file: %v", err) - } - - if !pool.AppendCertsFromPEM(data) { - return fmt.Errorf("Failed to parse any CA certificates") - } - - return nil -} - -// KeyPair is used to open and parse a certificate and key file -func (c *Config) KeyPair() (*tls.Certificate, error) { - if c.CertFile == "" || c.KeyFile == "" { - return nil, nil - } - cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) - if err != nil { - return nil, fmt.Errorf("Failed to load cert/key pair: %v", err) - } - return &cert, err -} - -// OutgoingTLSConfig generates a TLS configuration for outgoing -// requests. It will return a nil config if this configuration should -// not use TLS for outgoing connections. -func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { - if !c.VerifyOutgoing { - return nil, nil - } - // Create the tlsConfig - tlsConfig := &tls.Config{ - RootCAs: x509.NewCertPool(), - InsecureSkipVerify: true, - } - if c.ServerName != "" { - tlsConfig.ServerName = c.ServerName - tlsConfig.InsecureSkipVerify = false - } - - // Ensure we have a CA if VerifyOutgoing is set - if c.VerifyOutgoing && c.CAFile == "" { - return nil, fmt.Errorf("VerifyOutgoing set, and no CA certificate provided!") - } - - // Parse the CA cert if any - err := c.AppendCA(tlsConfig.RootCAs) - if err != nil { - return nil, err - } - - // Add cert/key - cert, err := c.KeyPair() - if err != nil { - return nil, err - } else if cert != nil { - tlsConfig.Certificates = []tls.Certificate{*cert} - } - - return tlsConfig, nil -} - -// Wrap a net.Conn into a client tls connection, performing any -// additional verification as needed. -// -// As of go 1.3, crypto/tls only supports either doing no certificate -// verification, or doing full verification including of the peer's -// DNS name. For consul, we want to validate that the certificate is -// signed by a known CA, but because consul doesn't use DNS names for -// node names, we don't verify the certificate DNS names. Since go 1.3 -// no longer supports this mode of operation, we have to do it -// manually. -func wrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { - var err error - var tlsConn *tls.Conn - - tlsConn = tls.Client(conn, tlsConfig) - - // If crypto/tls is doing verification, there's no need to do - // our own. - if tlsConfig.InsecureSkipVerify == false { - return tlsConn, nil - } - - if err = tlsConn.Handshake(); err != nil { - tlsConn.Close() - return nil, err - } - - // The following is lightly-modified from the doFullHandshake - // method in crypto/tls's handshake_client.go. - opts := x509.VerifyOptions{ - Roots: tlsConfig.RootCAs, - CurrentTime: time.Now(), - DNSName: "", - Intermediates: x509.NewCertPool(), - } - - certs := tlsConn.ConnectionState().PeerCertificates - for i, cert := range certs { - if i == 0 { - continue - } - opts.Intermediates.AddCert(cert) - } - - _, err = certs[0].Verify(opts) - if err != nil { - tlsConn.Close() - return nil, err - } - - return tlsConn, err -} - -// IncomingTLSConfig generates a TLS configuration for incoming requests -func (c *Config) IncomingTLSConfig() (*tls.Config, error) { - // Create the tlsConfig - tlsConfig := &tls.Config{ - ServerName: c.ServerName, - ClientCAs: x509.NewCertPool(), - ClientAuth: tls.NoClientCert, - } - if tlsConfig.ServerName == "" { - tlsConfig.ServerName = c.NodeName - } - - // Parse the CA cert if any - err := c.AppendCA(tlsConfig.ClientCAs) - if err != nil { - return nil, err - } - - // Add cert/key - cert, err := c.KeyPair() - if err != nil { - return nil, err - } else if cert != nil { - tlsConfig.Certificates = []tls.Certificate{*cert} - } - - // Check if we require verification - if c.VerifyIncoming { - tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert - if c.CAFile == "" { - return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") - } - if cert == nil { - return nil, fmt.Errorf("VerifyIncoming set, and no Cert/Key pair provided!") - } - } - return tlsConfig, nil -} - // DefaultConfig is used to return a sane default configuration func DefaultConfig() *Config { hostname, err := os.Hostname() diff --git a/consul/pool.go b/consul/pool.go index 91fe035f2..4ab75fbc6 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -11,6 +11,7 @@ import ( "sync/atomic" "time" + "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/yamux" "github.com/inconshreveable/muxado" @@ -222,7 +223,7 @@ func (p *ConnPool) getNewConn(addr net.Addr, version int) (*Conn, error) { } // Wrap the connection in a TLS client - tlsConn, err := wrapTLSClient(conn, p.tlsConfig) + tlsConn, err := tlsutil.WrapTLSClient(conn, p.tlsConfig) if err != nil { conn.Close() return nil, err diff --git a/consul/raft_rpc.go b/consul/raft_rpc.go index 1024cd987..e0ee4c68e 100644 --- a/consul/raft_rpc.go +++ b/consul/raft_rpc.go @@ -3,6 +3,7 @@ package consul import ( "crypto/tls" "fmt" + "github.com/hashicorp/consul/tlsutil" "net" "sync" "time" @@ -94,7 +95,7 @@ func (l *RaftLayer) Dial(address string, timeout time.Duration) (net.Conn, error } // Wrap the connection in a TLS client - conn, err = wrapTLSClient(conn, l.tlsConfig) + conn, err = tlsutil.WrapTLSClient(conn, l.tlsConfig) if err != nil { return nil, err } diff --git a/consul/server.go b/consul/server.go index 789db198e..4b6aa3a93 100644 --- a/consul/server.go +++ b/consul/server.go @@ -16,6 +16,7 @@ import ( "time" "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/golang-lru" "github.com/hashicorp/raft" "github.com/hashicorp/raft-mdb" @@ -168,13 +169,22 @@ func NewServer(config *Config) (*Server, error) { } // Create the tlsConfig for outgoing connections - tlsConfig, err := config.OutgoingTLSConfig() + tlsConf := &tlsutil.Config{ + VerifyIncoming: config.VerifyIncoming, + VerifyOutgoing: config.VerifyOutgoing, + CAFile: config.CAFile, + CertFile: config.CertFile, + KeyFile: config.KeyFile, + NodeName: config.NodeName, + ServerName: config.ServerName} + + tlsConfig, err := tlsConf.OutgoingTLSConfig() if err != nil { return nil, err } // Get the incoming tls config - incomingTLS, err := config.IncomingTLSConfig() + incomingTLS, err := tlsConf.IncomingTLSConfig() if err != nil { return nil, err } diff --git a/tlsutil/config.go b/tlsutil/config.go new file mode 100644 index 000000000..ab781de13 --- /dev/null +++ b/tlsutil/config.go @@ -0,0 +1,206 @@ +package tlsutil + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "net" + "time" +) + +// Config used to create tls.Config +type Config struct { + // VerifyIncoming is used to verify the authenticity of incoming connections. + // This means that TCP requests are forbidden, only allowing for TLS. TLS connections + // must match a provided certificate authority. This can be used to force client auth. + VerifyIncoming bool + + // VerifyOutgoing is used to verify the authenticity of outgoing connections. + // This means that TLS requests are used, and TCP requests are not made. TLS connections + // must match a provided certificate authority. This is used to verify authenticity of + // server nodes. + VerifyOutgoing bool + + // CAFile is a path to a certificate authority file. This is used with VerifyIncoming + // or VerifyOutgoing to verify the TLS connection. + CAFile string + + // CertFile is used to provide a TLS certificate that is used for serving TLS connections. + // Must be provided to serve TLS connections. + CertFile string + + // KeyFile is used to provide a TLS key that is used for serving TLS connections. + // Must be provided to serve TLS connections. + KeyFile string + + // Node name is the name we use to advertise. Defaults to hostname. + NodeName string + + // ServerName is used with the TLS certificate to ensure the name we + // provide matches the certificate + ServerName string +} + +// AppendCA opens and parses the CA file and adds the certificates to +// the provided CertPool. +func (c *Config) AppendCA(pool *x509.CertPool) error { + if c.CAFile == "" { + return nil + } + + // Read the file + data, err := ioutil.ReadFile(c.CAFile) + if err != nil { + return fmt.Errorf("Failed to read CA file: %v", err) + } + + if !pool.AppendCertsFromPEM(data) { + return fmt.Errorf("Failed to parse any CA certificates") + } + + return nil +} + +// KeyPair is used to open and parse a certificate and key file +func (c *Config) KeyPair() (*tls.Certificate, error) { + if c.CertFile == "" || c.KeyFile == "" { + return nil, nil + } + cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("Failed to load cert/key pair: %v", err) + } + return &cert, err +} + +// OutgoingTLSConfig generates a TLS configuration for outgoing +// requests. It will return a nil config if this configuration should +// not use TLS for outgoing connections. +func (c *Config) OutgoingTLSConfig() (*tls.Config, error) { + if !c.VerifyOutgoing { + return nil, nil + } + // Create the tlsConfig + tlsConfig := &tls.Config{ + RootCAs: x509.NewCertPool(), + InsecureSkipVerify: true, + } + if c.ServerName != "" { + tlsConfig.ServerName = c.ServerName + tlsConfig.InsecureSkipVerify = false + } + + // Ensure we have a CA if VerifyOutgoing is set + if c.VerifyOutgoing && c.CAFile == "" { + return nil, fmt.Errorf("VerifyOutgoing set, and no CA certificate provided!") + } + + // Parse the CA cert if any + err := c.AppendCA(tlsConfig.RootCAs) + if err != nil { + return nil, err + } + + // Add cert/key + cert, err := c.KeyPair() + if err != nil { + return nil, err + } else if cert != nil { + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + return tlsConfig, nil +} + +// Wrap a net.Conn into a client tls connection, performing any +// additional verification as needed. +// +// As of go 1.3, crypto/tls only supports either doing no certificate +// verification, or doing full verification including of the peer's +// DNS name. For consul, we want to validate that the certificate is +// signed by a known CA, but because consul doesn't use DNS names for +// node names, we don't verify the certificate DNS names. Since go 1.3 +// no longer supports this mode of operation, we have to do it +// manually. +func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) { + var err error + var tlsConn *tls.Conn + + tlsConn = tls.Client(conn, tlsConfig) + + // If crypto/tls is doing verification, there's no need to do + // our own. + if tlsConfig.InsecureSkipVerify == false { + return tlsConn, nil + } + + if err = tlsConn.Handshake(); err != nil { + tlsConn.Close() + return nil, err + } + + // The following is lightly-modified from the doFullHandshake + // method in crypto/tls's handshake_client.go. + opts := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + CurrentTime: time.Now(), + DNSName: "", + Intermediates: x509.NewCertPool(), + } + + certs := tlsConn.ConnectionState().PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + + _, err = certs[0].Verify(opts) + if err != nil { + tlsConn.Close() + return nil, err + } + + return tlsConn, err +} + +// IncomingTLSConfig generates a TLS configuration for incoming requests +func (c *Config) IncomingTLSConfig() (*tls.Config, error) { + // Create the tlsConfig + tlsConfig := &tls.Config{ + ServerName: c.ServerName, + ClientCAs: x509.NewCertPool(), + ClientAuth: tls.NoClientCert, + } + if tlsConfig.ServerName == "" { + tlsConfig.ServerName = c.NodeName + } + + // Parse the CA cert if any + err := c.AppendCA(tlsConfig.ClientCAs) + if err != nil { + return nil, err + } + + // Add cert/key + cert, err := c.KeyPair() + if err != nil { + return nil, err + } else if cert != nil { + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + // Check if we require verification + if c.VerifyIncoming { + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if c.CAFile == "" { + return nil, fmt.Errorf("VerifyIncoming set, and no CA certificate provided!") + } + if cert == nil { + return nil, fmt.Errorf("VerifyIncoming set, and no Cert/Key pair provided!") + } + } + return tlsConfig, nil +} diff --git a/consul/config_test.go b/tlsutil/config_test.go similarity index 98% rename from consul/config_test.go rename to tlsutil/config_test.go index 1007ffba7..150fddccd 100644 --- a/consul/config_test.go +++ b/tlsutil/config_test.go @@ -1,4 +1,4 @@ -package consul +package tlsutil import ( "crypto/tls" @@ -204,7 +204,7 @@ func TestConfig_wrapTLS_OK(t *testing.T) { t.Fatalf("OutgoingTLSConfig err: %v", err) } - tlsClient, err := wrapTLSClient(client, clientConfig) + tlsClient, err := WrapTLSClient(client, clientConfig) if err != nil { t.Fatalf("wrapTLS err: %v", err) } else { @@ -237,7 +237,7 @@ func TestConfig_wrapTLS_BadCert(t *testing.T) { t.Fatalf("OutgoingTLSConfig err: %v", err) } - tlsClient, err := wrapTLSClient(client, clientTLSConfig) + tlsClient, err := WrapTLSClient(client, clientTLSConfig) if err == nil { t.Fatalf("wrapTLS no err") } From e15262c8b7cd714ff2a19542474fc6aaaf947d06 Mon Sep 17 00:00:00 2001 From: Janne Paenkaelae Date: Tue, 18 Nov 2014 22:06:46 +0000 Subject: [PATCH 076/238] Make the 'consul version' to return value that is from 'git describe --tags' --- command/version.go | 2 +- commands.go | 2 +- scripts/build.sh | 3 ++- version.go | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/command/version.go b/command/version.go index 486b47e9d..fd8a5336c 100644 --- a/command/version.go +++ b/command/version.go @@ -21,7 +21,7 @@ func (c *VersionCommand) Help() string { func (c *VersionCommand) Run(_ []string) int { var versionString bytes.Buffer - fmt.Fprintf(&versionString, "Consul v%s", c.Version) + fmt.Fprintf(&versionString, "Consul %s", c.Version) if c.VersionPrerelease != "" { fmt.Fprintf(&versionString, ".%s", c.VersionPrerelease) diff --git a/commands.go b/commands.go index 820e64d1d..3a31abd73 100644 --- a/commands.go +++ b/commands.go @@ -90,7 +90,7 @@ func init() { "version": func() (cli.Command, error) { return &command.VersionCommand{ Revision: GitCommit, - Version: Version, + Version: GitDescribe, VersionPrerelease: VersionPrerelease, Ui: ui, }, nil diff --git a/scripts/build.sh b/scripts/build.sh index fa2388b0d..5f08688a1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -14,6 +14,7 @@ cd $DIR # Get the git commit GIT_COMMIT=$(git rev-parse HEAD) GIT_DIRTY=$(test -n "`git status --porcelain`" && echo "+CHANGES" || true) +GIT_DESCRIBE=$(git describe --tags) # If we're building on Windows, specify an extension EXTENSION="" @@ -45,7 +46,7 @@ go get \ # Build! echo "--> Building..." go build \ - -ldflags "${CGO_LDFLAGS} -X main.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \ + -ldflags "${CGO_LDFLAGS} -X main.GitCommit ${GIT_COMMIT}${GIT_DIRTY} -X main.GitDescribe ${GIT_DESCRIBE}" \ -v \ -o bin/consul${EXTENSION} cp bin/consul${EXTENSION} ${GOPATHSINGLE}/bin diff --git a/version.go b/version.go index c4f705a4a..e10a1ab82 100644 --- a/version.go +++ b/version.go @@ -2,6 +2,7 @@ package main // The git commit that was compiled. This will be filled in by the compiler. var GitCommit string +var GitDescribe string // The main version number that is being run at the moment. const Version = "0.4.1" From 2bd0e8c74568b4a84d04745448b8e2cc8fa91c2c Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Tue, 18 Nov 2014 17:56:48 -0500 Subject: [PATCH 077/238] consul.Config() helper to generate the tlsutil.Config{} struct, 30 second keepalive, use keepalive for HTTP and HTTPS --- command/agent/command.go | 7 +++---- command/agent/http.go | 6 ++++-- command/util_test.go | 2 +- consul/client.go | 12 +----------- consul/config.go | 14 ++++++++++++++ consul/server.go | 11 +---------- 6 files changed, 24 insertions(+), 28 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index de800e673..9236c23f2 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -466,10 +466,9 @@ func (c *Command) Run(args []string) int { if c.rpcServer != nil { defer c.rpcServer.Shutdown() } - if c.httpServers != nil { - for _, server := range c.httpServers { - defer server.Shutdown() - } + + for _, server := range c.httpServers { + defer server.Shutdown() } // Join startup nodes if specified diff --git a/command/agent/http.go b/command/agent/http.go index 79b847a34..e5804ae1d 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -93,11 +93,13 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS } // Create non-TLS listener - list, err = net.Listen("tcp", httpAddr.String()) + ln, err := net.Listen("tcp", httpAddr.String()) if err != nil { return nil, fmt.Errorf("Failed to get Listen on %s: %v", httpAddr.String(), err) } + list = tcpKeepAliveListener{ln.(*net.TCPListener)} + // Create the mux mux := http.NewServeMux() @@ -140,7 +142,7 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { return } tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) + tc.SetKeepAlivePeriod(30 * time.Second) return tc, nil } diff --git a/command/util_test.go b/command/util_test.go index bb0966473..cd201139b 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -98,7 +98,7 @@ func nextConfig() *agent.Config { conf.Server = true conf.Ports.HTTP = 10000 + 10*idx - conf.Ports.HTTPS = 10400 + 10*idx + conf.Ports.HTTPS = 10401 + 10*idx conf.Ports.RPC = 10100 + 10*idx conf.Ports.SerfLan = 10201 + 10*idx conf.Ports.SerfWan = 10202 + 10*idx diff --git a/consul/client.go b/consul/client.go index 54bd4056a..28838bf79 100644 --- a/consul/client.go +++ b/consul/client.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "fmt" "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/serf/serf" "log" "math/rand" @@ -94,16 +93,7 @@ func NewClient(config *Config) (*Client, error) { // Create the tlsConfig var tlsConfig *tls.Config var err error - tlsConf := &tlsutil.Config{ - VerifyIncoming: config.VerifyIncoming, - VerifyOutgoing: config.VerifyOutgoing, - CAFile: config.CAFile, - CertFile: config.CertFile, - KeyFile: config.KeyFile, - NodeName: config.NodeName, - ServerName: config.ServerName} - - if tlsConfig, err = tlsConf.OutgoingTLSConfig(); err != nil { + if tlsConfig, err = config.tlsConfig().OutgoingTLSConfig(); err != nil { return nil, err } diff --git a/consul/config.go b/consul/config.go index e623dcade..9cb1944cb 100644 --- a/consul/config.go +++ b/consul/config.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/memberlist" "github.com/hashicorp/raft" "github.com/hashicorp/serf/serf" @@ -234,3 +235,16 @@ func DefaultConfig() *Config { return conf } + +func (c *Config) tlsConfig() *tlsutil.Config { + tlsConf := &tlsutil.Config{ + VerifyIncoming: c.VerifyIncoming, + VerifyOutgoing: c.VerifyOutgoing, + CAFile: c.CAFile, + CertFile: c.CertFile, + KeyFile: c.KeyFile, + NodeName: c.NodeName, + ServerName: c.ServerName} + + return tlsConf +} diff --git a/consul/server.go b/consul/server.go index 4b6aa3a93..2adbc7edb 100644 --- a/consul/server.go +++ b/consul/server.go @@ -16,7 +16,6 @@ import ( "time" "github.com/hashicorp/consul/acl" - "github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/golang-lru" "github.com/hashicorp/raft" "github.com/hashicorp/raft-mdb" @@ -169,15 +168,7 @@ func NewServer(config *Config) (*Server, error) { } // Create the tlsConfig for outgoing connections - tlsConf := &tlsutil.Config{ - VerifyIncoming: config.VerifyIncoming, - VerifyOutgoing: config.VerifyOutgoing, - CAFile: config.CAFile, - CertFile: config.CertFile, - KeyFile: config.KeyFile, - NodeName: config.NodeName, - ServerName: config.ServerName} - + tlsConf := config.tlsConfig() tlsConfig, err := tlsConf.OutgoingTLSConfig() if err != nil { return nil, err From bd1e03428cd45c9b3b9b5d34f73940909a83ae4f Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Tue, 18 Nov 2014 18:46:43 -0800 Subject: [PATCH 078/238] consul: Increase maximum number of parallel readers --- consul/state_store.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/consul/state_store.go b/consul/state_store.go index 7ae9e4735..5bbadd423 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -25,6 +25,7 @@ const ( dbACLs = "acls" dbMaxMapSize32bit uint64 = 128 * 1024 * 1024 // 128MB maximum size dbMaxMapSize64bit uint64 = 32 * 1024 * 1024 * 1024 // 32GB maximum size + dbMaxReaders uint = 4096 // 4K, default is 126 ) // kvMode is used internally to control which type of set @@ -163,6 +164,12 @@ func (s *StateStore) initialize() error { return err } + // Increase the maximum number of concurrent readers + // TODO: Block transactions if we could exceed dbMaxReaders + if err := s.env.SetMaxReaders(dbMaxReaders); err != nil { + return err + } + // Optimize our flags for speed over safety, since the Raft log + snapshots // are durable. We treat this as an ephemeral in-memory DB, since we nuke // the data anyways. From de35a8e38f596bb3f7fb703411a802690ab6dfab Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 19 Nov 2014 11:51:25 -0800 Subject: [PATCH 079/238] agent: Fixing port collision in tests --- command/agent/agent_test.go | 17 +++++++++-------- command/agent/http.go | 13 ++----------- command/agent/http_test.go | 8 ++------ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 2901d67bc..a00d5cc11 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -2,15 +2,16 @@ package agent import ( "fmt" - "github.com/hashicorp/consul/consul" - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/consul/testutil" "io" "io/ioutil" "os" "sync/atomic" "testing" "time" + + "github.com/hashicorp/consul/consul" + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" ) var offset uint64 @@ -24,12 +25,12 @@ func nextConfig() *Config { conf.Datacenter = "dc1" conf.NodeName = fmt.Sprintf("Node %d", idx) conf.BindAddr = "127.0.0.1" - conf.Ports.DNS = 18600 + idx - conf.Ports.HTTP = 18500 + idx - conf.Ports.RPC = 18400 + idx + conf.Ports.DNS = 19000 + idx + conf.Ports.HTTP = 18800 + idx + conf.Ports.RPC = 18600 + idx conf.Ports.SerfLan = 18200 + idx - conf.Ports.SerfWan = 18300 + idx - conf.Ports.Server = 18100 + idx + conf.Ports.SerfWan = 18400 + idx + conf.Ports.Server = 18000 + idx conf.Server = true conf.ACLDatacenter = "dc1" conf.ACLMasterToken = "root" diff --git a/command/agent/http.go b/command/agent/http.go index e5804ae1d..a7a3c4d58 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -81,9 +81,7 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS // Start the server go http.Serve(list, mux) - - servers := make([]*HTTPServer, 1) - servers[0] = srv + servers = append(servers, srv) } if config.Ports.HTTP > 0 { @@ -116,14 +114,7 @@ func NewHTTPServers(agent *Agent, config *Config, logOutput io.Writer) ([]*HTTPS // Start the server go http.Serve(list, mux) - - if servers != nil { - // we already have the https server in servers, append - servers = append(servers, srv) - } else { - servers := make([]*HTTPServer, 1) - servers[0] = srv - } + servers = append(servers, srv) } return servers, nil diff --git a/command/agent/http_test.go b/command/agent/http_test.go index c52b822d7..d38b644ac 100644 --- a/command/agent/http_test.go +++ b/command/agent/http_test.go @@ -25,16 +25,12 @@ func makeHTTPServer(t *testing.T) (string, *HTTPServer) { if err := os.Mkdir(uiDir, 755); err != nil { t.Fatalf("err: %v", err) } - conf.Addresses.HTTP = "" - conf.Ports.HTTP = agent.config.Ports.HTTP - conf.Ports.HTTPS = -1 - addr, _ := agent.config.ClientListener("", agent.config.Ports.HTTP) servers, err := NewHTTPServers(agent, conf, agent.logOutput) if err != nil { t.Fatalf("err: %v", err) } - if servers == nil || len(servers) == 0 { - t.Fatalf(fmt.Sprintf("Could not create HTTP server to listen on: %s", addr.String())) + if len(servers) == 0 { + t.Fatalf(fmt.Sprintf("Failed to make HTTP server")) } return dir, servers[0] } From e831949380853f7d0c92b56bafe07487a77f0714 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 19 Nov 2014 13:29:15 -0800 Subject: [PATCH 080/238] agent: Fixing UiDir in test --- command/agent/http_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/agent/http_test.go b/command/agent/http_test.go index d38b644ac..a1570a9db 100644 --- a/command/agent/http_test.go +++ b/command/agent/http_test.go @@ -25,6 +25,7 @@ func makeHTTPServer(t *testing.T) (string, *HTTPServer) { if err := os.Mkdir(uiDir, 755); err != nil { t.Fatalf("err: %v", err) } + conf.UiDir = uiDir servers, err := NewHTTPServers(agent, conf, agent.logOutput) if err != nil { t.Fatalf("err: %v", err) From 5d486a6bd0f373159be25c5e86cf48817a624fcf Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 19 Nov 2014 13:38:58 -0800 Subject: [PATCH 081/238] agent: Fixing config merge test --- command/agent/config_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 64cb8b69b..13e8634ce 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -878,6 +878,7 @@ func TestMergeConfig(t *testing.T) { EnableDebug: false, CheckUpdateIntervalRaw: "8m", RetryIntervalRaw: "10s", + RetryIntervalWanRaw: "10s", } b := &Config{ @@ -964,7 +965,7 @@ func TestMergeConfig(t *testing.T) { c := MergeConfig(a, b) if !reflect.DeepEqual(c, b) { - t.Fatalf("should be equal %v %v", c, b) + t.Fatalf("should be equal %#v %#v", c, b) } } From dd8c0f12822432c426cbead489a57e9ca183a5fc Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Wed, 19 Nov 2014 13:53:17 -0800 Subject: [PATCH 082/238] agent: Fixing wan join tests --- command/agent/command_test.go | 69 +++++++---------------------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/command/agent/command_test.go b/command/agent/command_test.go index bd7f29a17..10557daa7 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -68,11 +68,19 @@ func TestRetryJoin(t *testing.T) { agent.config.BindAddr, agent.config.Ports.SerfLan) + serfWanAddr := fmt.Sprintf( + "%s:%d", + agent.config.BindAddr, + agent.config.Ports.SerfWan) + args := []string{ + "-server", "-data-dir", tmpDir, "-node", fmt.Sprintf(`"%s"`, conf2.NodeName), "-retry-join", serfAddr, "-retry-interval", "1s", + "-retry-join-wan", serfWanAddr, + "-retry-interval-wan", "1s", } go func() { @@ -87,6 +95,10 @@ func TestRetryJoin(t *testing.T) { if len(mem) != 2 { return false, fmt.Errorf("bad: %#v", mem) } + mem = agent.WANMembers() + if len(mem) != 2 { + return false, fmt.Errorf("bad (wan): %#v", mem) + } return true, nil }, func(err error) { t.Fatalf(err.Error()) @@ -121,60 +133,6 @@ func TestRetryJoinFail(t *testing.T) { t.Fatalf("bad: %d", code) } } -func TestRetryJoinWan(t *testing.T) { - dir, agent := makeAgent(t, nextConfig()) - defer os.RemoveAll(dir) - defer agent.Shutdown() - - conf2 := nextConfig() - tmpDir, err := ioutil.TempDir("", "consul") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(tmpDir) - - doneCh := make(chan struct{}) - shutdownCh := make(chan struct{}) - - defer func() { - close(shutdownCh) - <-doneCh - }() - - cmd := &Command{ - ShutdownCh: shutdownCh, - Ui: new(cli.MockUi), - } - - serfAddr := fmt.Sprintf( - "%s:%d", - agent.config.BindAddr, - agent.config.Ports.SerfLan) - - args := []string{ - "-data-dir", tmpDir, - "-node", fmt.Sprintf(`"%s"`, conf2.NodeName), - "-retry-join-wan", serfAddr, - "-retry-interval", "1s", - } - - go func() { - if code := cmd.Run(args); code != 0 { - log.Printf("bad: %d", code) - } - close(doneCh) - }() - - testutil.WaitForResult(func() (bool, error) { - mem := agent.WANMembers() - if len(mem) != 2 { - return false, fmt.Errorf("bad: %#v", mem) - } - return true, nil - }, func(err error) { - t.Fatalf(err.Error()) - }) -} func TestRetryJoinWanFail(t *testing.T) { conf := nextConfig() @@ -195,9 +153,10 @@ func TestRetryJoinWanFail(t *testing.T) { serfAddr := fmt.Sprintf("%s:%d", conf.BindAddr, conf.Ports.SerfWan) args := []string{ + "-server", "-data-dir", tmpDir, "-retry-join-wan", serfAddr, - "-retry-max", "1", + "-retry-max-wan", "1", } if code := cmd.Run(args); code == 0 { From 8a4ed8471144001ec92a8139e483a3503c31b59c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 5 Sep 2014 17:22:33 -0700 Subject: [PATCH 083/238] consul: first pass at keyring integration --- command/agent/agent.go | 85 +++++++++++++++++++++++++++++++++++++++- command/agent/command.go | 15 +++---- command/agent/config.go | 6 +++ consul/config.go | 5 +++ 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 9ef071137..f955d4a3a 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -1,16 +1,21 @@ package agent import ( + "encoding/base64" + "encoding/json" "fmt" "io" + "io/ioutil" "log" "net" "os" + "path/filepath" "strconv" "sync" "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/memberlist" "github.com/hashicorp/serf/serf" ) @@ -109,6 +114,33 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { // Initialize the local state agent.state.Init(config, agent.logger) + // Setup encryption keyring files + if !config.DisableKeyring && config.EncryptKey != "" { + keyringBytes, err := json.MarshalIndent([]string{config.EncryptKey}) + if err != nil { + return nil, err + } + paths := []string{ + filepath.Join(config.DataDir, "serf", "keyring_lan"), + filepath.Join(config.DataDir, "serf", "keyring_wan"), + } + for _, path := range paths { + if _, err := os.Stat(path); err == nil { + continue + } + fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return nil, err + } + defer fh.Close() + + if _, err := fh.Write(keyringBytes); err != nil { + os.Remove(path) + return nil, err + } + } + } + // Setup either the client or the server var err error if config.Server { @@ -160,11 +192,21 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.DataDir != "" { base.DataDir = a.config.DataDir } - if a.config.EncryptKey != "" { + if a.config.EncryptKey != "" && a.config.DisableKeyring { key, _ := a.config.EncryptBytes() base.SerfLANConfig.MemberlistConfig.SecretKey = key base.SerfWANConfig.MemberlistConfig.SecretKey = key } + if !a.config.DisableKeyring { + lanKeyring := filepath.Join(base.DataDir, "serf", "keyring_lan") + wanKeyring := filepath.Join(base.DataDir, "serf", "keyring_wan") + + base.SerfLANConfig.KeyringFile = lanKeyring + base.SerfWANConfig.KeyringFile = wanKeyring + + base.SerfLANConfig.MemberlistConfig.Keyring = loadKeyringFile(lanKeyring) + base.SerfWANConfig.MemberlistConfig.Keyring = loadKeyringFile(wanKeyring) + } if a.config.NodeName != "" { base.NodeName = a.config.NodeName } @@ -253,6 +295,9 @@ func (a *Agent) consulConfig() *consul.Config { } } + // Setup gossip keyring configuration + base.DisableKeyring = a.config.DisableKeyring + // Setup the loggers base.LogOutput = a.logOutput return base @@ -648,3 +693,41 @@ func (a *Agent) deletePid() error { } return nil } + +// loadKeyringFile will load a keyring out of a file +func loadKeyringFile(keyringFile string) *memberlist.Keyring { + if _, err := os.Stat(keyringFile); err != nil { + return nil + } + + // Read in the keyring file data + keyringData, err := ioutil.ReadFile(keyringFile) + if err != nil { + return nil + } + + // Decode keyring JSON + keys := make([]string, 0) + if err := json.Unmarshal(keyringData, &keys); err != nil { + return nil + } + + // Decode base64 values + keysDecoded := make([][]byte, len(keys)) + for i, key := range keys { + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil + } + keysDecoded[i] = keyBytes + } + + // Create the keyring + keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0]) + if err != nil { + return nil + } + + // Success! + return keyring +} diff --git a/command/agent/command.go b/command/agent/command.go index 6474402f9..91118fc63 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -67,6 +67,7 @@ func (c *Command) readConfig() *Config { cmdFlags.StringVar(&cmdConfig.UiDir, "ui-dir", "", "path to the web UI directory") cmdFlags.StringVar(&cmdConfig.PidFile, "pid-file", "", "path to file to store PID") cmdFlags.StringVar(&cmdConfig.EncryptKey, "encrypt", "", "gossip encryption key") + cmdFlags.BoolVar(&cmdConfig.DisableKeyring, "disable-keyring", false, "disable use of encryption keyring") cmdFlags.BoolVar(&cmdConfig.Server, "server", false, "run agent as server") cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode") @@ -143,13 +144,6 @@ func (c *Command) readConfig() *Config { config.NodeName = hostname } - if config.EncryptKey != "" { - if _, err := config.EncryptBytes(); err != nil { - c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) - return nil - } - } - // Ensure we have a data directory if config.DataDir == "" { c.Ui.Error("Must specify data directory using -data-dir") @@ -180,6 +174,13 @@ func (c *Command) readConfig() *Config { return nil } + if config.EncryptKey != "" { + if _, err := config.EncryptBytes(); err != nil { + c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) + return nil + } + } + // Compile all the watches for _, params := range config.Watches { // Parse the watches, excluding the handler diff --git a/command/agent/config.go b/command/agent/config.go index 36683ad16..e099c64b1 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -104,6 +104,9 @@ type Config struct { // recursors array. DNSRecursor string `mapstructure:"recursor"` + // Disable use of an encryption keyring. + DisableKeyring bool `mapstructure:"disable_keyring"` + // DNSRecursors can be set to allow the DNS servers to recursively // resolve non-consul domains DNSRecursors []string `mapstructure:"recursors"` @@ -676,6 +679,9 @@ func MergeConfig(a, b *Config) *Config { if b.EncryptKey != "" { result.EncryptKey = b.EncryptKey } + if b.DisableKeyring { + result.DisableKeyring = true + } if b.LogLevel != "" { result.LogLevel = b.LogLevel } diff --git a/consul/config.go b/consul/config.go index 9cb1944cb..10acaab2c 100644 --- a/consul/config.go +++ b/consul/config.go @@ -165,6 +165,11 @@ type Config struct { // UserEventHandler callback can be used to handle incoming // user events. This function should not block. UserEventHandler func(serf.UserEvent) + + // DisableKeyring is used to disable persisting the encryption keyring to + // filesystem. By default, if encryption is enabled, Consul will create a + // file inside of the DataDir to keep track of changes made to the ring. + DisableKeyring bool } // CheckVersion is used to check if the ProtocolVersion is valid From 4b2656653751a0c683fd3bffd69acea5b1798100 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 5 Sep 2014 22:01:49 -0700 Subject: [PATCH 084/238] consul: fix json marshaling --- command/agent/agent.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index f955d4a3a..97198c36c 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -116,7 +116,8 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { // Setup encryption keyring files if !config.DisableKeyring && config.EncryptKey != "" { - keyringBytes, err := json.MarshalIndent([]string{config.EncryptKey}) + keys := []string{config.EncryptKey} + keyringBytes, err := json.MarshalIndent(keys, "", " ") if err != nil { return nil, err } From 208b5ae58f703cca44f0ec5bc92aabf611622d60 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 5 Sep 2014 22:58:44 -0700 Subject: [PATCH 085/238] command: create serf dir if it doesn't exist, document -disable-keyring arg --- command/agent/agent.go | 11 +++++++++-- command/agent/command.go | 4 ++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 97198c36c..225c813a9 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -116,15 +116,22 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { // Setup encryption keyring files if !config.DisableKeyring && config.EncryptKey != "" { + serfDir := filepath.Join(config.DataDir, "serf") + if err := os.MkdirAll(serfDir, 0700); err != nil { + return nil, err + } + keys := []string{config.EncryptKey} keyringBytes, err := json.MarshalIndent(keys, "", " ") if err != nil { return nil, err } + paths := []string{ - filepath.Join(config.DataDir, "serf", "keyring_lan"), - filepath.Join(config.DataDir, "serf", "keyring_wan"), + filepath.Join(serfDir, "keyring_lan"), + filepath.Join(serfDir, "keyring_wan"), } + for _, path := range paths { if _, err := os.Stat(path); err == nil { continue diff --git a/command/agent/command.go b/command/agent/command.go index 91118fc63..4f31249bb 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -792,6 +792,10 @@ Options: -data-dir=path Path to a data directory to store agent state -dc=east-aws Datacenter of the agent -encrypt=key Provides the gossip encryption key + -disable-keyring Disables the use of an encryption keyring. The + Default behavior is to persist encryption keys using + a keyring file, and reload the keys on subsequent + starts. This argument disables keyring persistence. -join=1.2.3.4 Address of an agent to join at start time. Can be specified multiple times. -join-wan=1.2.3.4 Address of an agent to join -wan at start time. From 61e3647ac1a0889e0f779b5f3ae83bf532cf4e81 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 6 Sep 2014 09:41:57 -0700 Subject: [PATCH 086/238] command: warn when passing -encrypt when keyring already exists --- command/agent/command.go | 12 +++++++++++- command/agent/config.go | 12 ++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/command/agent/command.go b/command/agent/command.go index 4f31249bb..649cded48 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -219,6 +219,13 @@ func (c *Command) readConfig() *Config { c.Ui.Error("WARNING: Windows is not recommended as a Consul server. Do not use in production.") } + // Warn if an encryption key is passed while a keyring already exists + if config.EncryptKey != "" && config.CheckKeyringFiles() { + c.Ui.Error(fmt.Sprintf( + "WARNING: Keyring already exists, ignoring new key %s", + config.EncryptKey)) + } + // Set the version info config.Revision = c.Revision config.Version = c.Version @@ -586,6 +593,9 @@ func (c *Command) Run(args []string) int { }(wp) } + // Determine if gossip is encrypted + gossipEncrypted := (config.EncryptKey != "" || config.CheckKeyringFiles()) + // Let the agent know we've finished registration c.agent.StartSync() @@ -598,7 +608,7 @@ func (c *Command) Run(args []string) int { c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr, config.Ports.SerfLan, config.Ports.SerfWan)) c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v", - config.EncryptKey != "", config.VerifyOutgoing, config.VerifyIncoming)) + gossipEncrypted, config.VerifyOutgoing, config.VerifyIncoming)) // Enable log streaming c.Ui.Info("") diff --git a/command/agent/config.go b/command/agent/config.go index e099c64b1..2fad1db39 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -411,6 +411,18 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } +// CheckKeyringFiles checks for existence of the keyring files for Serf +func (c *Config) CheckKeyringFiles() bool { + serfDir := filepath.Join(c.DataDir, "serf") + if _, err := os.Stat(filepath.Join(serfDir, "keyring_lan")); err != nil { + return false + } + if _, err := os.Stat(filepath.Join(serfDir, "keyring_wan")); err != nil { + return false + } + return true +} + // DecodeConfig reads the configuration from the given reader in JSON // format and decodes it into a proper Config structure. func DecodeConfig(r io.Reader) (*Config, error) { From f25c2c1f06b7a9e1344c8083945f5119f2ab35f8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 6 Sep 2014 11:12:45 -0700 Subject: [PATCH 087/238] command: add skeletons for keys command --- command/keys.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ commands.go | 6 +++ 2 files changed, 103 insertions(+) create mode 100644 command/keys.go diff --git a/command/keys.go b/command/keys.go new file mode 100644 index 000000000..da8e743d1 --- /dev/null +++ b/command/keys.go @@ -0,0 +1,97 @@ +package command + +import ( + "flag" + "fmt" + "github.com/mitchellh/cli" + "strings" +) + +// KeysCommand is a Command implementation that handles querying, installing, +// and removing gossip encryption keys from a keyring. +type KeysCommand struct { + Ui cli.Ui +} + +func (c *KeysCommand) Run(args []string) int { + var installKey, useKey, removeKey string + var listKeys bool + + cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } + + cmdFlags.StringVar(&installKey, "install", "", "install key") + cmdFlags.StringVar(&useKey, "use", "", "use key") + cmdFlags.StringVar(&removeKey, "remove", "", "remove key") + cmdFlags.BoolVar(&listKeys, "list", false, "list keys") + + rpcAddr := RPCAddrFlag(cmdFlags) + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + // Only accept a single argument + found := listKeys + for _, arg := range []string{installKey, useKey, removeKey} { + if found && len(arg) > 0 { + c.Ui.Error("Only one of -list, -install, -use, or -remove allowed") + return 1 + } + found = found || len(arg) > 0 + } + + client, err := RPCClient(*rpcAddr) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + defer client.Close() + + if listKeys { + return 0 + } + + if installKey != "" { + return 0 + } + + if useKey != "" { + return 0 + } + + if removeKey != "" { + return 0 + } + + return 0 +} + +func (c *KeysCommand) Help() string { + helpText := ` +Usage: consul keys [options] + + Manages encryption keys used for gossip messages. Gossip encryption is + optional. When enabled, this command may be used to examine active encryption + keys in the cluster, add new keys, and remove old ones. When combined, this + functionality provides the ability to perform key rotation cluster-wide, + without disrupting the cluster. + +Options: + + -install= Install a new encryption key. This will broadcast + the new key to all members in the cluster. + -use= Change the primary encryption key, which is used to + encrypt messages. The key must already be installed + before this operation can succeed. + -remove= Remove the given key from the cluster. This + operation may only be performed on keys which are + not currently the primary key. + -list List all keys currently in use within the cluster. + -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. +` + return strings.TrimSpace(helpText) +} + +func (c *KeysCommand) Synopsis() string { + return "Manages gossip layer encryption keys" +} diff --git a/commands.go b/commands.go index 3a31abd73..8fab257ac 100644 --- a/commands.go +++ b/commands.go @@ -56,6 +56,12 @@ func init() { }, nil }, + "keys": func() (cli.Command, error) { + return &command.KeysCommand{ + Ui: ui, + }, nil + }, + "leave": func() (cli.Command, error) { return &command.LeaveCommand{ Ui: ui, From 96376212ffdae9a0efe0bf4d93a750e264652d1d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 18:09:51 -0700 Subject: [PATCH 088/238] consul: use rpc layer only for key management functions, add rpc commands --- command/agent/agent.go | 16 +++++++++ command/agent/rpc.go | 79 ++++++++++++++++++++++++++++++++++++------ command/keys.go | 19 +++++----- consul/client.go | 5 +++ consul/server.go | 10 ++++++ 5 files changed, 111 insertions(+), 18 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 225c813a9..16c1e33e5 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -739,3 +739,19 @@ func loadKeyringFile(keyringFile string) *memberlist.Keyring { // Success! return keyring } + +// ListKeysLAN returns the keys installed on the LAN gossip pool +func (a *Agent) ListKeysLAN() map[string]int { + if a.server != nil { + return a.server.ListKeysLAN() + } + return a.client.ListKeysLAN() +} + +// ListKeysWAN returns the keys installed on the WAN gossip pool +func (a *Agent) ListKeysWAN() map[string]int { + if a.server != nil { + return a.server.ListKeysWAN() + } + return nil +} diff --git a/command/agent/rpc.go b/command/agent/rpc.go index caf97cef1..e35615de5 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -41,16 +41,24 @@ const ( ) const ( - handshakeCommand = "handshake" - forceLeaveCommand = "force-leave" - joinCommand = "join" - membersLANCommand = "members-lan" - membersWANCommand = "members-wan" - stopCommand = "stop" - monitorCommand = "monitor" - leaveCommand = "leave" - statsCommand = "stats" - reloadCommand = "reload" + handshakeCommand = "handshake" + forceLeaveCommand = "force-leave" + joinCommand = "join" + membersLANCommand = "members-lan" + membersWANCommand = "members-wan" + stopCommand = "stop" + monitorCommand = "monitor" + leaveCommand = "leave" + statsCommand = "stats" + reloadCommand = "reload" + listKeysLANCommand = "list-keys-lan" + listKeysWANCommand = "list-keys-wan" + installKeyLANCommand = "install-key-lan" + installKeyWANCommand = "install-key-wan" + useKeyLANCommand = "use-key-lan" + useKeyWANCommand = "use-key-wan" + removeKeyLANCommand = "remove-key-lan" + removeKeyWANCommand = "remove-key-wan" ) const ( @@ -103,6 +111,13 @@ type joinResponse struct { Num int32 } +type keysResponse struct { + Messages map[string]string + NumNodes int + NumResp int + Keys map[string]int +} + type membersResponse struct { Members []Member } @@ -373,6 +388,32 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er case reloadCommand: return i.handleReload(client, seq) + case listKeysLANCommand: + return i.handleListKeysWAN(client, seq) + + case listKeysWANCommand: + return i.handleListKeysLAN(client, seq) + + /* + case installKeyLANCommand: + return i.handleInstallKeyLAN(client, seq) + + case installKeyWANCommand: + return i.handleInstallKeyWAN(client, seq) + + case useKeyLANCommand: + return i.handleUseKeyLAN(client, seq) + + case useKeyWANCommand: + return i.handleUseKeyWAN(client, seq) + + case removeKeyLANCommand: + return i.handleRemoveKeyLAN(client, seq) + + case removeKeyWANCommand: + return i.handleRemoveKeyWAN(client, seq) + */ + default: respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} client.Send(&respHeader, nil) @@ -583,6 +624,24 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { return client.Send(&resp, nil) } +func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { + header := responseHeader{ + Seq: seq, + Error: "", + } + resp := i.agent.ListKeysLAN() + return client.Send(&header, resp) +} + +func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { + header := responseHeader{ + Seq: seq, + Error: "", + } + resp := i.agent.ListKeysWAN() + return client.Send(&header, resp) +} + // Used to convert an error to a string representation func errToString(err error) string { if err == nil { diff --git a/command/keys.go b/command/keys.go index da8e743d1..e63b46015 100644 --- a/command/keys.go +++ b/command/keys.go @@ -3,8 +3,9 @@ package command import ( "flag" "fmt" - "github.com/mitchellh/cli" "strings" + + "github.com/mitchellh/cli" ) // KeysCommand is a Command implementation that handles querying, installing, @@ -30,6 +31,13 @@ func (c *KeysCommand) Run(args []string) int { return 1 } + client, err := RPCClient(*rpcAddr) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + defer client.Close() + // Only accept a single argument found := listKeys for _, arg := range []string{installKey, useKey, removeKey} { @@ -40,14 +48,9 @@ func (c *KeysCommand) Run(args []string) int { found = found || len(arg) > 0 } - client, err := RPCClient(*rpcAddr) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) - return 1 - } - defer client.Close() - if listKeys { + km := client.KeyManager() + fmt.Println(km.ListKeys()) return 0 } diff --git a/consul/client.go b/consul/client.go index 28838bf79..fb02cdcd0 100644 --- a/consul/client.go +++ b/consul/client.go @@ -206,6 +206,11 @@ func (c *Client) UserEvent(name string, payload []byte) error { return c.serf.UserEvent(userEventName(name), payload, false) } +// KeyManager returns the Serf keyring manager +func (c *Client) KeyManager() *serf.KeyManager { + return c.serf.KeyManager() +} + // lanEventHandler is used to handle events from the lan Serf cluster func (c *Client) lanEventHandler() { for { diff --git a/consul/server.go b/consul/server.go index 2adbc7edb..8f913ed46 100644 --- a/consul/server.go +++ b/consul/server.go @@ -551,6 +551,16 @@ func (s *Server) IsLeader() bool { return s.raft.State() == raft.Leader } +// KeyManagerLAN returns the LAN Serf keyring manager +func (s *Server) KeyManagerLAN() *serf.KeyManager { + return s.serfLAN.KeyManager() +} + +// KeyManagerWAN returns the WAN Serf keyring manager +func (s *Server) KeyManagerWAN() *serf.KeyManager { + return s.serfWAN.KeyManager() +} + // inmemCodec is used to do an RPC call without going over a network type inmemCodec struct { method string From 43a60f1424bb44b95ba004852482f618a9ae49a8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 18:36:54 -0700 Subject: [PATCH 089/238] command: basic rpc works for keys command --- command/agent/agent.go | 15 +++++++++------ command/agent/rpc.go | 13 +++++++++++-- command/agent/rpc_client.go | 22 ++++++++++++++++++++++ command/keys.go | 8 ++++++-- consul/client.go | 4 ++-- 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 16c1e33e5..b219f18bc 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -741,17 +741,20 @@ func loadKeyringFile(keyringFile string) *memberlist.Keyring { } // ListKeysLAN returns the keys installed on the LAN gossip pool -func (a *Agent) ListKeysLAN() map[string]int { +func (a *Agent) ListKeysLAN() (*serf.KeyResponse, error) { if a.server != nil { - return a.server.ListKeysLAN() + km := a.server.KeyManagerLAN() + return km.ListKeys() } - return a.client.ListKeysLAN() + km := a.client.KeyManagerLAN() + return km.ListKeys() } // ListKeysWAN returns the keys installed on the WAN gossip pool -func (a *Agent) ListKeysWAN() map[string]int { +func (a *Agent) ListKeysWAN() (*serf.KeyResponse, error) { if a.server != nil { - return a.server.ListKeysWAN() + km := a.server.KeyManagerWAN() + return km.ListKeys() } - return nil + return nil, nil } diff --git a/command/agent/rpc.go b/command/agent/rpc.go index e35615de5..e5934fc8a 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -629,7 +629,11 @@ func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { Seq: seq, Error: "", } - resp := i.agent.ListKeysLAN() + resp, err := i.agent.ListKeysLAN() + if err != nil { + return err + } + return client.Send(&header, resp) } @@ -638,7 +642,12 @@ func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { Seq: seq, Error: "", } - resp := i.agent.ListKeysWAN() + + resp, err := i.agent.ListKeysWAN() + if err != nil { + return err + } + return client.Send(&header, resp) } diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 6cd0fc19f..d5ff5894e 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -176,6 +176,28 @@ func (c *RPCClient) WANMembers() ([]Member, error) { return resp.Members, err } +func (c *RPCClient) ListKeysLAN() (map[string]int, error) { + header := requestHeader{ + Command: listKeysLANCommand, + Seq: c.getSeq(), + } + resp := make(map[string]int) + + err := c.genericRPC(&header, nil, &resp) + return resp, err +} + +func (c *RPCClient) ListKeysWAN() (map[string]int, error) { + header := requestHeader{ + Command: listKeysWANCommand, + Seq: c.getSeq(), + } + resp := make(map[string]int) + + err := c.genericRPC(&header, nil, &resp) + return resp, err +} + // Leave is used to trigger a graceful leave and shutdown func (c *RPCClient) Leave() error { header := requestHeader{ diff --git a/command/keys.go b/command/keys.go index e63b46015..4f063a2dd 100644 --- a/command/keys.go +++ b/command/keys.go @@ -49,8 +49,12 @@ func (c *KeysCommand) Run(args []string) int { } if listKeys { - km := client.KeyManager() - fmt.Println(km.ListKeys()) + keys, err := client.ListKeysLAN() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + fmt.Println(keys) return 0 } diff --git a/consul/client.go b/consul/client.go index fb02cdcd0..be1854101 100644 --- a/consul/client.go +++ b/consul/client.go @@ -206,8 +206,8 @@ func (c *Client) UserEvent(name string, payload []byte) error { return c.serf.UserEvent(userEventName(name), payload, false) } -// KeyManager returns the Serf keyring manager -func (c *Client) KeyManager() *serf.KeyManager { +// KeyManager returns the LAN Serf keyring manager +func (c *Client) KeyManagerLAN() *serf.KeyManager { return c.serf.KeyManager() } From b200332ae3b1cd485c588d2f88eb583deec474e4 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 19:38:30 -0700 Subject: [PATCH 090/238] command: add option for -wan to keys command --- command/.keys.go.swp | 3 +++ command/agent/rpc.go | 9 +++------ command/keys.go | 19 ++++++++++++++++--- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 command/.keys.go.swp diff --git a/command/.keys.go.swp b/command/.keys.go.swp new file mode 100644 index 000000000..dbc2250e1 --- /dev/null +++ b/command/.keys.go.swp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:384f1d9597d62c56a5d575eac062dfb02c3180d34992026ab58bf082df27237f +size 12288 diff --git a/command/agent/rpc.go b/command/agent/rpc.go index e5934fc8a..09bf68fb8 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -638,14 +638,11 @@ func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { } func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { + resp, err := i.agent.ListKeysWAN() + header := responseHeader{ Seq: seq, - Error: "", - } - - resp, err := i.agent.ListKeysWAN() - if err != nil { - return err + Error: errToString(err), } return client.Send(&header, resp) diff --git a/command/keys.go b/command/keys.go index 4f063a2dd..49bac1831 100644 --- a/command/keys.go +++ b/command/keys.go @@ -16,7 +16,7 @@ type KeysCommand struct { func (c *KeysCommand) Run(args []string) int { var installKey, useKey, removeKey string - var listKeys bool + var listKeys, wan bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -25,6 +25,7 @@ func (c *KeysCommand) Run(args []string) int { cmdFlags.StringVar(&useKey, "use", "", "use key") cmdFlags.StringVar(&removeKey, "remove", "", "remove key") cmdFlags.BoolVar(&listKeys, "list", false, "list keys") + cmdFlags.BoolVar(&wan, "wan", false, "operate on wan keys") rpcAddr := RPCAddrFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -49,11 +50,20 @@ func (c *KeysCommand) Run(args []string) int { } if listKeys { - keys, err := client.ListKeysLAN() + var keys map[string]int + var err error + + if wan { + keys, err = client.ListKeysWAN() + } else { + keys, err = client.ListKeysLAN() + } + if err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return 1 } + fmt.Println(keys) return 0 } @@ -70,7 +80,8 @@ func (c *KeysCommand) Run(args []string) int { return 0 } - return 0 + c.Ui.Output(c.Help()) + return 1 } func (c *KeysCommand) Help() string { @@ -94,6 +105,8 @@ Options: operation may only be performed on keys which are not currently the primary key. -list List all keys currently in use within the cluster. + -wan If talking with a server node, this flag can be used + to operate on the WAN gossip layer. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) From 83af160fc3434d6a4537e5fc77219e084b2504a2 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 20:09:35 -0700 Subject: [PATCH 091/238] command/keys: list keys working end-to-end --- command/.keys.go.swp | 3 --- command/agent/rpc.go | 47 ++++++++++++++++++++++++------------- command/agent/rpc_client.go | 16 ++++++------- command/keys.go | 24 +++++++++++++++---- 4 files changed, 59 insertions(+), 31 deletions(-) delete mode 100644 command/.keys.go.swp diff --git a/command/.keys.go.swp b/command/.keys.go.swp deleted file mode 100644 index dbc2250e1..000000000 --- a/command/.keys.go.swp +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:384f1d9597d62c56a5d575eac062dfb02c3180d34992026ab58bf082df27237f -size 12288 diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 09bf68fb8..e1007cb1d 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -111,10 +111,11 @@ type joinResponse struct { Num int32 } -type keysResponse struct { +type keyResponse struct { Messages map[string]string NumNodes int NumResp int + NumErr int Keys map[string]int } @@ -625,27 +626,41 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { } func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { - header := responseHeader{ - Seq: seq, - Error: "", - } - resp, err := i.agent.ListKeysLAN() - if err != nil { - return err - } - - return client.Send(&header, resp) -} - -func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { - resp, err := i.agent.ListKeysWAN() + queryResp, err := i.agent.ListKeysLAN() header := responseHeader{ Seq: seq, Error: errToString(err), } - return client.Send(&header, resp) + resp := keyResponse{ + Messages: queryResp.Messages, + Keys: queryResp.Keys, + NumResp: queryResp.NumResp, + NumErr: queryResp.NumErr, + NumNodes: queryResp.NumNodes, + } + + return client.Send(&header, &resp) +} + +func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { + queryResp, err := i.agent.ListKeysWAN() + + header := responseHeader{ + Seq: seq, + Error: errToString(err), + } + + resp := keyResponse{ + Messages: queryResp.Messages, + Keys: queryResp.Keys, + NumResp: queryResp.NumResp, + NumErr: queryResp.NumErr, + NumNodes: queryResp.NumNodes, + } + + return client.Send(&header, &resp) } // Used to convert an error to a string representation diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index d5ff5894e..daa72e1c0 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -176,26 +176,26 @@ func (c *RPCClient) WANMembers() ([]Member, error) { return resp.Members, err } -func (c *RPCClient) ListKeysLAN() (map[string]int, error) { +func (c *RPCClient) ListKeysLAN() (map[string]int, int, map[string]string, error) { header := requestHeader{ Command: listKeysLANCommand, Seq: c.getSeq(), } - resp := make(map[string]int) + resp := new(keyResponse) - err := c.genericRPC(&header, nil, &resp) - return resp, err + err := c.genericRPC(&header, nil, resp) + return resp.Keys, resp.NumNodes, resp.Messages, err } -func (c *RPCClient) ListKeysWAN() (map[string]int, error) { +func (c *RPCClient) ListKeysWAN() (map[string]int, int, map[string]string, error) { header := requestHeader{ Command: listKeysWANCommand, Seq: c.getSeq(), } - resp := make(map[string]int) + resp := new(keyResponse) - err := c.genericRPC(&header, nil, &resp) - return resp, err + err := c.genericRPC(&header, nil, resp) + return resp.Keys, resp.NumNodes, resp.Messages, err } // Leave is used to trigger a graceful leave and shutdown diff --git a/command/keys.go b/command/keys.go index 49bac1831..069af8566 100644 --- a/command/keys.go +++ b/command/keys.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/mitchellh/cli" + "github.com/ryanuber/columnize" ) // KeysCommand is a Command implementation that handles querying, installing, @@ -51,20 +52,35 @@ func (c *KeysCommand) Run(args []string) int { if listKeys { var keys map[string]int + var numNodes int + var messages map[string]string var err error + var out []string if wan { - keys, err = client.ListKeysWAN() + keys, numNodes, messages, err = client.ListKeysWAN() } else { - keys, err = client.ListKeysLAN() + keys, numNodes, messages, err = client.ListKeysLAN() } if err != nil { - c.Ui.Error(fmt.Sprintf("Error: %s", err)) + for node, msg := range messages { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) return 1 } - fmt.Println(keys) + c.Ui.Info("Keys gathered, listing cluster keys...") + c.Ui.Output("") + + for key, num := range keys { + out = append(out, fmt.Sprintf("%s | [%d/%d]", key, num, numNodes)) + } + c.Ui.Output(columnize.SimpleFormat(out)) + return 0 } From cae3f0fd0b8e94aaed7031c46601633a1c598ad0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 20:13:11 -0700 Subject: [PATCH 092/238] agent: fix inversed lan/wan key listing --- command/agent/rpc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index e1007cb1d..7030f6538 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -390,10 +390,10 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er return i.handleReload(client, seq) case listKeysLANCommand: - return i.handleListKeysWAN(client, seq) + return i.handleListKeysLAN(client, seq) case listKeysWANCommand: - return i.handleListKeysLAN(client, seq) + return i.handleListKeysWAN(client, seq) /* case installKeyLANCommand: From 164f2428ff96977a7e4c061757310136b204abc0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 21:45:15 -0700 Subject: [PATCH 093/238] command/keys: fail fast if no actionable args were passed --- command/keys.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/command/keys.go b/command/keys.go index 069af8566..0373804cf 100644 --- a/command/keys.go +++ b/command/keys.go @@ -33,13 +33,6 @@ func (c *KeysCommand) Run(args []string) int { return 1 } - client, err := RPCClient(*rpcAddr) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) - return 1 - } - defer client.Close() - // Only accept a single argument found := listKeys for _, arg := range []string{installKey, useKey, removeKey} { @@ -50,6 +43,19 @@ func (c *KeysCommand) Run(args []string) int { found = found || len(arg) > 0 } + // Fail fast if no actionable args were passed + if !found { + c.Ui.Error(c.Help()) + return 1 + } + + client, err := RPCClient(*rpcAddr) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + defer client.Close() + if listKeys { var keys map[string]int var numNodes int @@ -96,8 +102,8 @@ func (c *KeysCommand) Run(args []string) int { return 0 } - c.Ui.Output(c.Help()) - return 1 + // Should never make it here + return 0 } func (c *KeysCommand) Help() string { From b11afa33f30b07bdf8978bbcf09e2bd852520391 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 22:27:17 -0700 Subject: [PATCH 094/238] command/keys: use PrefixedUi for keys command --- command/keys.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/command/keys.go b/command/keys.go index 0373804cf..9be460063 100644 --- a/command/keys.go +++ b/command/keys.go @@ -33,6 +33,13 @@ func (c *KeysCommand) Run(args []string) int { return 1 } + c.Ui = &cli.PrefixedUi{ + OutputPrefix: "", + InfoPrefix: "==> ", + ErrorPrefix: "", + Ui: c.Ui, + } + // Only accept a single argument found := listKeys for _, arg := range []string{installKey, useKey, removeKey} { @@ -57,6 +64,8 @@ func (c *KeysCommand) Run(args []string) int { defer client.Close() if listKeys { + c.Ui.Info("Asking all members for installed keys...") + var keys map[string]int var numNodes int var messages map[string]string From d03ed1a9ba6a20af2e923a456c7ca9d5b025a3f2 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 23:25:38 -0700 Subject: [PATCH 095/238] agent: install key command implemented --- command/agent/agent.go | 21 +++++++++++++++- command/agent/rpc.go | 50 ++++++++++++++++++++++++++----------- command/agent/rpc_client.go | 26 +++++++++++++++++++ command/keys.go | 40 +++++++++++++++++++++++------ 4 files changed, 113 insertions(+), 24 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index b219f18bc..f1d52075f 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -756,5 +756,24 @@ func (a *Agent) ListKeysWAN() (*serf.KeyResponse, error) { km := a.server.KeyManagerWAN() return km.ListKeys() } - return nil, nil + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// InstallKeyWAN installs a new WAN gossip encryption key on server nodes +func (a *Agent) InstallKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.InstallKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// InstallKeyLAN installs a new LAN gossip encryption key on all nodes +func (a *Agent) InstallKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.InstallKey(key) + } + km := a.client.KeyManagerLAN() + return km.InstallKey(key) } diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 7030f6538..5124a6d65 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -111,6 +111,10 @@ type joinResponse struct { Num int32 } +type keyRequest struct { + Key string +} + type keyResponse struct { Messages map[string]string NumNodes int @@ -389,19 +393,13 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er case reloadCommand: return i.handleReload(client, seq) - case listKeysLANCommand: - return i.handleListKeysLAN(client, seq) + case listKeysLANCommand, listKeysWANCommand: + return i.handleListKeys(client, seq, command) - case listKeysWANCommand: - return i.handleListKeysWAN(client, seq) + case installKeyLANCommand, installKeyWANCommand: + return i.handleInstallKey(client, seq, command) /* - case installKeyLANCommand: - return i.handleInstallKeyLAN(client, seq) - - case installKeyWANCommand: - return i.handleInstallKeyWAN(client, seq) - case useKeyLANCommand: return i.handleUseKeyLAN(client, seq) @@ -625,8 +623,16 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { return client.Send(&resp, nil) } -func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { - queryResp, err := i.agent.ListKeysLAN() +func (i *AgentRPC) handleListKeys(client *rpcClient, seq uint64, cmd string) error { + var queryResp *serf.KeyResponse + var err error + + switch cmd { + case listKeysWANCommand: + queryResp, err = i.agent.ListKeysWAN() + default: + queryResp, err = i.agent.ListKeysLAN() + } header := responseHeader{ Seq: seq, @@ -644,15 +650,29 @@ func (i *AgentRPC) handleListKeysLAN(client *rpcClient, seq uint64) error { return client.Send(&header, &resp) } -func (i *AgentRPC) handleListKeysWAN(client *rpcClient, seq uint64) error { - queryResp, err := i.agent.ListKeysWAN() +func (i *AgentRPC) handleInstallKey(client *rpcClient, seq uint64, cmd string) error { + var req keyRequest + var resp keyResponse + var queryResp *serf.KeyResponse + var err error + + if err = client.dec.Decode(&req); err != nil { + return fmt.Errorf("decode failed: %v", err) + } + + switch cmd { + case installKeyWANCommand: + queryResp, err = i.agent.InstallKeyWAN(req.Key) + default: + queryResp, err = i.agent.InstallKeyLAN(req.Key) + } header := responseHeader{ Seq: seq, Error: errToString(err), } - resp := keyResponse{ + resp = keyResponse{ Messages: queryResp.Messages, Keys: queryResp.Keys, NumResp: queryResp.NumResp, diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index daa72e1c0..c6b442f77 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -198,6 +198,32 @@ func (c *RPCClient) ListKeysWAN() (map[string]int, int, map[string]string, error return resp.Keys, resp.NumNodes, resp.Messages, err } +func (c *RPCClient) InstallKeyWAN(key string) (map[string]string, error) { + header := requestHeader{ + Command: installKeyWANCommand, + Seq: c.getSeq(), + } + + req := keyRequest{key} + + resp := new(keyResponse) + err := c.genericRPC(&header, &req, resp) + return resp.Messages, err +} + +func (c *RPCClient) InstallKeyLAN(key string) (map[string]string, error) { + header := requestHeader{ + Command: installKeyLANCommand, + Seq: c.getSeq(), + } + + req := keyRequest{key} + + resp := new(keyResponse) + err := c.genericRPC(&header, &req, resp) + return resp.Messages, err +} + // Leave is used to trigger a graceful leave and shutdown func (c *RPCClient) Leave() error { header := requestHeader{ diff --git a/command/keys.go b/command/keys.go index 9be460063..2c0ce8f8e 100644 --- a/command/keys.go +++ b/command/keys.go @@ -40,6 +40,10 @@ func (c *KeysCommand) Run(args []string) int { Ui: c.Ui, } + var out []string + var failures map[string]string + var err error + // Only accept a single argument found := listKeys for _, arg := range []string{installKey, useKey, removeKey} { @@ -68,21 +72,20 @@ func (c *KeysCommand) Run(args []string) int { var keys map[string]int var numNodes int - var messages map[string]string - var err error - var out []string if wan { - keys, numNodes, messages, err = client.ListKeysWAN() + keys, numNodes, failures, err = client.ListKeysWAN() } else { - keys, numNodes, messages, err = client.ListKeysLAN() + keys, numNodes, failures, err = client.ListKeysLAN() } if err != nil { - for node, msg := range messages { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) } - c.Ui.Error(columnize.SimpleFormat(out)) c.Ui.Error("") c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) return 1 @@ -100,6 +103,27 @@ func (c *KeysCommand) Run(args []string) int { } if installKey != "" { + if wan { + c.Ui.Info("Installing new WAN gossip encryption key...") + failures, err = client.InstallKeyWAN(installKey) + } else { + c.Ui.Info("Installing new LAN gossip encryption key...") + failures, err = client.InstallKeyLAN(installKey) + } + + if err != nil { + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + } + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Error installing key: %s", err)) + return 1 + } + + c.Ui.Info("Successfully installed key!") return 0 } From d52163703eeabf07e48090c3cc0ba5f843b736ce Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 23:30:55 -0700 Subject: [PATCH 096/238] command/keys: customize info message when listing keys --- command/keys.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/keys.go b/command/keys.go index 2c0ce8f8e..c6464146c 100644 --- a/command/keys.go +++ b/command/keys.go @@ -68,14 +68,14 @@ func (c *KeysCommand) Run(args []string) int { defer client.Close() if listKeys { - c.Ui.Info("Asking all members for installed keys...") - var keys map[string]int var numNodes int if wan { + c.Ui.Info("Asking all WAN members for installed keys...") keys, numNodes, failures, err = client.ListKeysWAN() } else { + c.Ui.Info("Asking all LAN members for installed keys...") keys, numNodes, failures, err = client.ListKeysLAN() } From ccda799039df37b9bcd967ee769931875cc08450 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 8 Sep 2014 23:55:02 -0700 Subject: [PATCH 097/238] command/keys: use key command implemented --- command/agent/agent.go | 19 +++++++++++++++++++ command/agent/rpc.go | 19 ++++++++++--------- command/agent/rpc_client.go | 25 ++++++++++++++----------- command/keys.go | 22 ++++++++++++++++++++++ 4 files changed, 65 insertions(+), 20 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index f1d52075f..9caf81547 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -777,3 +777,22 @@ func (a *Agent) InstallKeyLAN(key string) (*serf.KeyResponse, error) { km := a.client.KeyManagerLAN() return km.InstallKey(key) } + +// UseKeyWAN changes the primary WAN gossip encryption key on server nodes +func (a *Agent) UseKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.UseKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// UseKeyLAN changes the primary LAN gossip encryption key on all nodes +func (a *Agent) UseKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.UseKey(key) + } + km := a.client.KeyManagerLAN() + return km.UseKey(key) +} diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 5124a6d65..6c33a13d6 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -397,15 +397,12 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er return i.handleListKeys(client, seq, command) case installKeyLANCommand, installKeyWANCommand: - return i.handleInstallKey(client, seq, command) + return i.handleGossipKeyChange(client, seq, command) + + case useKeyLANCommand, useKeyWANCommand: + return i.handleGossipKeyChange(client, seq, command) /* - case useKeyLANCommand: - return i.handleUseKeyLAN(client, seq) - - case useKeyWANCommand: - return i.handleUseKeyWAN(client, seq) - case removeKeyLANCommand: return i.handleRemoveKeyLAN(client, seq) @@ -650,7 +647,7 @@ func (i *AgentRPC) handleListKeys(client *rpcClient, seq uint64, cmd string) err return client.Send(&header, &resp) } -func (i *AgentRPC) handleInstallKey(client *rpcClient, seq uint64, cmd string) error { +func (i *AgentRPC) handleGossipKeyChange(client *rpcClient, seq uint64, cmd string) error { var req keyRequest var resp keyResponse var queryResp *serf.KeyResponse @@ -663,8 +660,12 @@ func (i *AgentRPC) handleInstallKey(client *rpcClient, seq uint64, cmd string) e switch cmd { case installKeyWANCommand: queryResp, err = i.agent.InstallKeyWAN(req.Key) - default: + case installKeyLANCommand: queryResp, err = i.agent.InstallKeyLAN(req.Key) + case useKeyWANCommand: + queryResp, err = i.agent.UseKeyWAN(req.Key) + case useKeyLANCommand: + queryResp, err = i.agent.UseKeyLAN(req.Key) } header := responseHeader{ diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index c6b442f77..5fe988b20 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -199,21 +199,24 @@ func (c *RPCClient) ListKeysWAN() (map[string]int, int, map[string]string, error } func (c *RPCClient) InstallKeyWAN(key string) (map[string]string, error) { - header := requestHeader{ - Command: installKeyWANCommand, - Seq: c.getSeq(), - } - - req := keyRequest{key} - - resp := new(keyResponse) - err := c.genericRPC(&header, &req, resp) - return resp.Messages, err + return c.changeGossipKey(key, installKeyWANCommand) } func (c *RPCClient) InstallKeyLAN(key string) (map[string]string, error) { + return c.changeGossipKey(key, installKeyLANCommand) +} + +func (c *RPCClient) UseKeyWAN(key string) (map[string]string, error) { + return c.changeGossipKey(key, useKeyWANCommand) +} + +func (c *RPCClient) UseKeyLAN(key string) (map[string]string, error) { + return c.changeGossipKey(key, useKeyLANCommand) +} + +func (c *RPCClient) changeGossipKey(key, cmd string) (map[string]string, error) { header := requestHeader{ - Command: installKeyLANCommand, + Command: cmd, Seq: c.getSeq(), } diff --git a/command/keys.go b/command/keys.go index c6464146c..e555517ea 100644 --- a/command/keys.go +++ b/command/keys.go @@ -128,6 +128,28 @@ func (c *KeysCommand) Run(args []string) int { } if useKey != "" { + if wan { + c.Ui.Info("Changing primary encryption key on WAN members...") + failures, err = client.UseKeyWAN(useKey) + } else { + c.Ui.Info("Changing primary encryption key on LAN members...") + failures, err = client.UseKeyLAN(useKey) + } + + if err != nil { + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + } + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Error changing primary key: %s", err)) + return 1 + } + + c.Ui.Info("Successfully changed primary key!") + return 0 } From 5f04ae277e285982a4224abd333ae7acd135524c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 9 Sep 2014 00:08:38 -0700 Subject: [PATCH 098/238] command/keys: remove key command implemented --- command/agent/agent.go | 19 +++++++++++++++++++ command/agent/rpc.go | 13 ++++++------- command/agent/rpc_client.go | 8 ++++++++ command/keys.go | 22 +++++++++++++++++++++- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 9caf81547..456415f96 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -796,3 +796,22 @@ func (a *Agent) UseKeyLAN(key string) (*serf.KeyResponse, error) { km := a.client.KeyManagerLAN() return km.UseKey(key) } + +// RemoveKeyWAN removes a WAN gossip encryption key on server nodes +func (a *Agent) RemoveKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.RemoveKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// RemoveKeyLAN removes a LAN gossip encryption key on all nodes +func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.RemoveKey(key) + } + km := a.client.KeyManagerLAN() + return km.RemoveKey(key) +} diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 6c33a13d6..2382bee48 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -402,13 +402,8 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er case useKeyLANCommand, useKeyWANCommand: return i.handleGossipKeyChange(client, seq, command) - /* - case removeKeyLANCommand: - return i.handleRemoveKeyLAN(client, seq) - - case removeKeyWANCommand: - return i.handleRemoveKeyWAN(client, seq) - */ + case removeKeyLANCommand, removeKeyWANCommand: + return i.handleGossipKeyChange(client, seq, command) default: respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} @@ -666,6 +661,10 @@ func (i *AgentRPC) handleGossipKeyChange(client *rpcClient, seq uint64, cmd stri queryResp, err = i.agent.UseKeyWAN(req.Key) case useKeyLANCommand: queryResp, err = i.agent.UseKeyLAN(req.Key) + case removeKeyWANCommand: + queryResp, err = i.agent.RemoveKeyWAN(req.Key) + case removeKeyLANCommand: + queryResp, err = i.agent.RemoveKeyLAN(req.Key) } header := responseHeader{ diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 5fe988b20..5d82dc8fa 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -214,6 +214,14 @@ func (c *RPCClient) UseKeyLAN(key string) (map[string]string, error) { return c.changeGossipKey(key, useKeyLANCommand) } +func (c *RPCClient) RemoveKeyWAN(key string) (map[string]string, error) { + return c.changeGossipKey(key, removeKeyWANCommand) +} + +func (c *RPCClient) RemoveKeyLAN(key string) (map[string]string, error) { + return c.changeGossipKey(key, removeKeyLANCommand) +} + func (c *RPCClient) changeGossipKey(key, cmd string) (map[string]string, error) { header := requestHeader{ Command: cmd, diff --git a/command/keys.go b/command/keys.go index e555517ea..b99853576 100644 --- a/command/keys.go +++ b/command/keys.go @@ -149,11 +149,31 @@ func (c *KeysCommand) Run(args []string) int { } c.Ui.Info("Successfully changed primary key!") - return 0 } if removeKey != "" { + if wan { + c.Ui.Info("Removing key from WAN members...") + failures, err = client.RemoveKeyWAN(removeKey) + } else { + c.Ui.Info("Removing key from LAN members...") + failures, err = client.RemoveKeyLAN(removeKey) + } + + if err != nil { + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + } + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Error removing key: %s", err)) + return 1 + } + + c.Ui.Info("Successfully removed key!") return 0 } From 109a3604da3e6c314aa4007d6d2041d4e83c219e Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 10 Sep 2014 08:49:16 -0700 Subject: [PATCH 099/238] command/keys: begin tests --- command/agent/rpc.go | 4 ++++ command/keys_test.go | 33 +++++++++++++++++++++++++++++++++ command/util_test.go | 7 +++++++ 3 files changed, 44 insertions(+) create mode 100644 command/keys_test.go diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 2382bee48..bf9b42d52 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -665,6 +665,10 @@ func (i *AgentRPC) handleGossipKeyChange(client *rpcClient, seq uint64, cmd stri queryResp, err = i.agent.RemoveKeyWAN(req.Key) case removeKeyLANCommand: queryResp, err = i.agent.RemoveKeyLAN(req.Key) + default: + respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} + client.Send(&respHeader, nil) + return fmt.Errorf("command '%s' not recognized", cmd) } header := responseHeader{ diff --git a/command/keys_test.go b/command/keys_test.go new file mode 100644 index 000000000..0d1c464b4 --- /dev/null +++ b/command/keys_test.go @@ -0,0 +1,33 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/command/agent" + "github.com/mitchellh/cli" +) + +func TestKeysCommand_implements(t *testing.T) { + var _ cli.Command = &KeysCommand{} +} + +func TestKeysCommand_list(t *testing.T) { + conf := agent.Config{EncryptKey: "HS5lJ+XuTlYKWaeGYyG+/A=="} + + a1 := testAgentWithConfig(&conf, t) + defer a1.Shutdown() + + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + args := []string{"-list", "-rpc-addr=" + a1.addr} + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + if !strings.Contains(ui.OutputWriter.String(), conf.EncryptKey) { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} diff --git a/command/util_test.go b/command/util_test.go index cd201139b..388c1e62b 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -39,6 +39,10 @@ func (a *agentWrapper) Shutdown() { } func testAgent(t *testing.T) *agentWrapper { + return testAgentWithConfig(nil, t) +} + +func testAgentWithConfig(c *agent.Config, t *testing.T) *agentWrapper { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("err: %s", err) @@ -48,6 +52,9 @@ func testAgent(t *testing.T) *agentWrapper { mult := io.MultiWriter(os.Stderr, lw) conf := nextConfig() + if c != nil { + conf = agent.MergeConfig(conf, c) + } dir, err := ioutil.TempDir("", "agent") if err != nil { From 04f2a53735d8239afdcf65e950235f6dc0703583 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 10 Sep 2014 10:11:11 -0700 Subject: [PATCH 100/238] command/keys: adding more tests --- command/keys_test.go | 157 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 5 deletions(-) diff --git a/command/keys_test.go b/command/keys_test.go index 0d1c464b4..b326e7b3d 100644 --- a/command/keys_test.go +++ b/command/keys_test.go @@ -12,22 +12,169 @@ func TestKeysCommand_implements(t *testing.T) { var _ cli.Command = &KeysCommand{} } -func TestKeysCommand_list(t *testing.T) { - conf := agent.Config{EncryptKey: "HS5lJ+XuTlYKWaeGYyG+/A=="} +func TestKeysCommandRun(t *testing.T) { + key1 := "HS5lJ+XuTlYKWaeGYyG+/A==" + key2 := "kZyFABeAmc64UMTrm9XuKA==" + key3 := "2k5VRlBIIKUPc1v77rsswg==" + // Begin with a single key + conf := agent.Config{EncryptKey: key1} a1 := testAgentWithConfig(&conf, t) defer a1.Shutdown() + // The keyring was initialized with only the provided key + out := listKeys(t, a1.addr, false) + if !strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } + if strings.Contains(out, key2) { + t.Fatalf("bad: %#v", out) + } + + // The key was installed on the WAN gossip layer also + out = listKeys(t, a1.addr, true) + if !strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } + if strings.Contains(out, key3) { + t.Fatalf("bad: %#v", out) + } + + // Install the second key onto the keyring + installKey(t, a1.addr, false, key2) + + // Both keys should be present + out = listKeys(t, a1.addr, false) + for _, key := range []string{key1, key2} { + if !strings.Contains(out, key) { + t.Fatalf("bad: %#v", out) + } + } + + // Second key should not be installed on WAN + out = listKeys(t, a1.addr, true) + if strings.Contains(out, key2) { + t.Fatalf("bad: %#v", out) + } + + // Change out the primary key + useKey(t, a1.addr, false, key2) + + // Remove the original key + removeKey(t, a1.addr, false, key1) + + // Make sure only the new key is present + out = listKeys(t, a1.addr, false) + if strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } + if !strings.Contains(out, key2) { + t.Fatalf("bad: %#v", out) + } + + // Original key still remains on WAN keyring + out = listKeys(t, a1.addr, true) + if !strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } + + // Install second key on WAN keyring + installKey(t, a1.addr, true, key3) + + // Two keys now present on WAN keyring + out = listKeys(t, a1.addr, true) + for _, key := range []string{key1, key3} { + if !strings.Contains(out, key) { + t.Fatalf("bad: %#v", out) + } + } + + // Change WAN primary key + useKey(t, a1.addr, true, key3) + + // Remove original key from WAN keyring + removeKey(t, a1.addr, true, key1) + + // Only new key should exist on WAN keyring + out = listKeys(t, a1.addr, true) + if !strings.Contains(out, key3) { + t.Fatalf("bad: %#v", out) + } + if strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } +} + +func TestKeysCommandRun_help(t *testing.T) { ui := new(cli.MockUi) c := &KeysCommand{Ui: ui} - args := []string{"-list", "-rpc-addr=" + a1.addr} + code := c.Run(nil) + if code != 1 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + if !strings.Contains(ui.ErrorWriter.String(), "Usage:") { + t.Fatalf("bad: %#v", ui.ErrorWriter.String()) + } +} + +func listKeys(t *testing.T, addr string, wan bool) string { + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + + args := []string{"-list", "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } - if !strings.Contains(ui.OutputWriter.String(), conf.EncryptKey) { - t.Fatalf("bad: %#v", ui.OutputWriter.String()) + return ui.OutputWriter.String() +} + +func installKey(t *testing.T, addr string, wan bool, key string) { + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + + args := []string{"-install=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } +} + +func useKey(t *testing.T, addr string, wan bool, key string) { + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + + args := []string{"-use=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } +} + +func removeKey(t *testing.T, addr string, wan bool, key string) { + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + + args := []string{"-remove=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } From b1b722dbff8c1d9e47b32a2ba8d32fa99e0c17f0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 10 Sep 2014 10:20:40 -0700 Subject: [PATCH 101/238] command/keys: test network connection failure --- command/keys_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/command/keys_test.go b/command/keys_test.go index b326e7b3d..903d13b19 100644 --- a/command/keys_test.go +++ b/command/keys_test.go @@ -112,11 +112,26 @@ func TestKeysCommandRun_help(t *testing.T) { if code != 1 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } + + // Test that we didn't actually try to dial the RPC server. if !strings.Contains(ui.ErrorWriter.String(), "Usage:") { t.Fatalf("bad: %#v", ui.ErrorWriter.String()) } } +func TestKeysCommandRun_failedConnection(t *testing.T) { + ui := new(cli.MockUi) + c := &KeysCommand{Ui: ui} + args := []string{"-list", "-rpc-addr=127.0.0.1:0"} + code := c.Run(args) + if code != 1 { + t.Fatalf("bad: %d, %#v", code, ui.ErrorWriter.String()) + } + if !strings.Contains(ui.ErrorWriter.String(), "dial") { + t.Fatalf("bad: %#v", ui.OutputWriter.String()) + } +} + func listKeys(t *testing.T, addr string, wan bool) string { ui := new(cli.MockUi) c := &KeysCommand{Ui: ui} From 0ad0805234cb8bf516f6c72606941e3b1b06f55b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 10 Sep 2014 21:56:31 -0700 Subject: [PATCH 102/238] agent: add rpc tests for listing lan/wan gossip keys --- command/agent/rpc_client_test.go | 84 ++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 18825613c..b290a6dee 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -30,6 +30,10 @@ func (r *rpcParts) Close() { // testRPCClient returns an RPCClient connected to an RPC server that // serves only this connection. func testRPCClient(t *testing.T) *rpcParts { + return testRPCClientWithConfig(t, nil) +} + +func testRPCClientWithConfig(t *testing.T, c *Config) *rpcParts { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("err: %s", err) @@ -39,6 +43,10 @@ func testRPCClient(t *testing.T) *rpcParts { mult := io.MultiWriter(os.Stderr, lw) conf := nextConfig() + if c != nil { + conf = MergeConfig(conf, c) + } + dir, agent := makeAgentLog(t, conf, mult) rpc := NewAgentRPC(agent, l, mult, lw) @@ -273,3 +281,79 @@ OUTER2: t.Fatalf("should log joining") } } + +func TestRPCClientListKeysLAN(t *testing.T) { + key1 := "tbLJg26ZJyJ9pK3qhc9jig==" + conf := Config{EncryptKey: key1} + p1 := testRPCClientWithConfig(t, &conf) + defer p1.Close() + + keys, numNodes, messages, err := p1.client.ListKeysLAN() + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, ok := keys[key1]; !ok { + t.Fatalf("bad: %#v", keys) + } + + if keys[key1] != 1 { + t.Fatalf("bad: %#v", keys) + } + + if numNodes != 1 { + t.Fatalf("bad: %d", numNodes) + } + + if len(messages) != 0 { + t.Fatalf("bad: %#v", messages) + } +} + +func TestRPCClientListKeysWAN(t *testing.T) { + key1 := "tbLJg26ZJyJ9pK3qhc9jig==" + conf := Config{EncryptKey: key1} + p1 := testRPCClientWithConfig(t, &conf) + defer p1.Close() + + keys, numNodes, messages, err := p1.client.ListKeysWAN() + if err != nil { + t.Fatalf("err: %s", err) + } + + if _, ok := keys[key1]; !ok { + t.Fatalf("bad: %#v", keys) + } + + if keys[key1] != 1 { + t.Fatalf("bad: %#v", keys) + } + + if numNodes != 1 { + t.Fatalf("bad: %d", numNodes) + } + + if len(messages) != 0 { + t.Fatalf("bad: %#v", messages) + } +} + +func TestRPCClientListKeysLAN_encryptionDisabled(t *testing.T) { + p1 := testRPCClient(t) + defer p1.Close() + + _, _, _, err := p1.client.ListKeysLAN() + if err == nil { + t.Fatalf("no error listing keys with encryption disabled") + } +} + +func TestRPCClientListKeysWAN_encryptionDisabled(t *testing.T) { + p1 := testRPCClient(t) + defer p1.Close() + + _, _, _, err := p1.client.ListKeysWAN() + if err == nil { + t.Fatalf("no error listing keys with encryption disabled") + } +} From ee50795850cc1ae143824f797854db5c9c7e0bc0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 10 Sep 2014 22:51:33 -0700 Subject: [PATCH 103/238] agent: install/use/remove key tests --- command/agent/rpc_client_test.go | 169 +++++++++++++++++++++++-------- 1 file changed, 129 insertions(+), 40 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index b290a6dee..a042d85e8 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -282,78 +282,167 @@ OUTER2: } } -func TestRPCClientListKeysLAN(t *testing.T) { +func TestRPCClientListKeys(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" conf := Config{EncryptKey: key1} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() - keys, numNodes, messages, err := p1.client.ListKeysLAN() - if err != nil { - t.Fatalf("err: %s", err) - } - + // Check WAN keys + keys := listKeys(t, p1.client, false) if _, ok := keys[key1]; !ok { t.Fatalf("bad: %#v", keys) } - if keys[key1] != 1 { + // Check LAN keys + keys = listKeys(t, p1.client, true) + if _, ok := keys[key1]; !ok { t.Fatalf("bad: %#v", keys) } - - if numNodes != 1 { - t.Fatalf("bad: %d", numNodes) - } - - if len(messages) != 0 { - t.Fatalf("bad: %#v", messages) - } } -func TestRPCClientListKeysWAN(t *testing.T) { +func TestRPCClientInstallKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" + key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" conf := Config{EncryptKey: key1} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() - keys, numNodes, messages, err := p1.client.ListKeysWAN() + // Test WAN keys + keys := listKeys(t, p1.client, true) + if _, ok := keys[key2]; ok { + t.Fatalf("bad: %#v", keys) + } + + installKey(t, p1.client, key2, true) + + keys = listKeys(t, p1.client, true) + if _, ok := keys[key2]; !ok { + t.Fatalf("bad: %#v", keys) + } + + // Test LAN keys + keys = listKeys(t, p1.client, false) + if _, ok := keys[key2]; ok { + t.Fatalf("bad: %#v", keys) + } + + installKey(t, p1.client, key2, false) + + keys = listKeys(t, p1.client, false) + if _, ok := keys[key2]; !ok { + t.Fatalf("bad: %#v", keys) + } +} + +func TestRPCClientRotateKey(t *testing.T) { + key1 := "tbLJg26ZJyJ9pK3qhc9jig==" + key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" + conf := Config{EncryptKey: key1} + p1 := testRPCClientWithConfig(t, &conf) + defer p1.Close() + + // Test WAN keys + keys := listKeys(t, p1.client, true) + if _, ok := keys[key2]; ok { + t.Fatalf("bad: %#v", keys) + } + + installKey(t, p1.client, key2, true) + useKey(t, p1.client, key2, true) + removeKey(t, p1.client, key1, true) + + keys = listKeys(t, p1.client, true) + if _, ok := keys[key1]; ok { + t.Fatalf("bad: %#v", keys) + } + if _, ok := keys[key2]; !ok { + t.Fatalf("bad: %#v", keys) + } + + // Test LAN keys + keys = listKeys(t, p1.client, false) + if _, ok := keys[key2]; ok { + t.Fatalf("bad: %#v", keys) + } + + installKey(t, p1.client, key2, false) + useKey(t, p1.client, key2, false) + removeKey(t, p1.client, key1, false) + + keys = listKeys(t, p1.client, false) + if _, ok := keys[key1]; ok { + t.Fatalf("bad: %#v", keys) + } + if _, ok := keys[key2]; !ok { + t.Fatalf("bad: %#v", keys) + } +} + +func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) { + p1 := testRPCClient(t) + defer p1.Close() + + _, _, failures, err := p1.client.ListKeysLAN() + if err == nil { + t.Fatalf("no error listing keys with encryption disabled") + } + + if len(failures) != 1 { + t.Fatalf("bad: %#v", failures) + } +} + +func listKeys(t *testing.T, c *RPCClient, wan bool) (keys map[string]int) { + var err error + + if wan { + keys, _, _, err = c.ListKeysWAN() + } else { + keys, _, _, err = c.ListKeysLAN() + } if err != nil { t.Fatalf("err: %s", err) } - if _, ok := keys[key1]; !ok { - t.Fatalf("bad: %#v", keys) - } + return +} - if keys[key1] != 1 { - t.Fatalf("bad: %#v", keys) - } +func installKey(t *testing.T, c *RPCClient, key string, wan bool) { + var err error - if numNodes != 1 { - t.Fatalf("bad: %d", numNodes) + if wan { + _, err = c.InstallKeyWAN(key) + } else { + _, err = c.InstallKeyLAN(key) } - - if len(messages) != 0 { - t.Fatalf("bad: %#v", messages) + if err != nil { + t.Fatalf("err: %s", err) } } -func TestRPCClientListKeysLAN_encryptionDisabled(t *testing.T) { - p1 := testRPCClient(t) - defer p1.Close() +func useKey(t *testing.T, c *RPCClient, key string, wan bool) { + var err error - _, _, _, err := p1.client.ListKeysLAN() - if err == nil { - t.Fatalf("no error listing keys with encryption disabled") + if wan { + _, err = c.UseKeyWAN(key) + } else { + _, err = c.UseKeyLAN(key) + } + if err != nil { + t.Fatalf("err: %s", err) } } -func TestRPCClientListKeysWAN_encryptionDisabled(t *testing.T) { - p1 := testRPCClient(t) - defer p1.Close() +func removeKey(t *testing.T, c *RPCClient, key string, wan bool) { + var err error - _, _, _, err := p1.client.ListKeysWAN() - if err == nil { - t.Fatalf("no error listing keys with encryption disabled") + if wan { + _, err = c.RemoveKeyWAN(key) + } else { + _, err = c.RemoveKeyLAN(key) + } + if err != nil { + t.Fatalf("err: %s", err) } } From c0f1b5f8c82e9d8d910146c83ef96ebbdd24d0b4 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 11 Sep 2014 11:19:48 -0700 Subject: [PATCH 104/238] website: document keys command --- .../source/docs/commands/keys.html.markdown | 58 +++++++++++++++++++ website/source/layouts/docs.erb | 4 ++ 2 files changed, 62 insertions(+) create mode 100644 website/source/docs/commands/keys.html.markdown diff --git a/website/source/docs/commands/keys.html.markdown b/website/source/docs/commands/keys.html.markdown new file mode 100644 index 000000000..785025b34 --- /dev/null +++ b/website/source/docs/commands/keys.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "docs" +page_title: "Commands: Keys" +sidebar_current: "docs-commands-keys" +--- + +# Consul Keys + +Command: `consul keys` + +The `keys` command is used to examine and modify the encryption keys used in +Consul's [Gossip Pools](/docs/internals/gossip.html). It is capable of +distributing new encryption keys to the cluster, revoking old encryption keys, +and changing the key used by the cluster to encrypt messages. + +Because Consul utilizes multiple gossip pools, this command will operate on only +a single pool at a time. The pool can be specified using the arguments +documented below. + +Consul allows multiple encryption keys to be in use simultaneously. This is +intended to provide a transition state while the cluster converges. It is the +responsibility of the operator to ensure that only the required encryption keys +are installed on the cluster. You can ensure that a key is not installed using +the `-list` and `-remove` options. + +By default, modifications made using this command will be persisted in the +Consul agent's data directory. This functionality can be altered via the +[Agent Configuration](/docs/agent/options.html). + +All variations of the keys command will return 0 if all nodes reply and there +are no errors. If any node fails to reply or reports failure, the exit code will +be 1. + +## Usage + +Usage: `consul keys [options]` + +Exactly one of `-list`, `-install`, `-remove`, or `-update` must be provided. + +The list of available flags are: + +* `-install` - Install a new encryption key. This will broadcast the new key to + all members in the cluster. + +* `-use` - Change the primary encryption key, which is used to encrypt messages. + The key must already be installed before this operation can succeed. + +* `-remove` - Remove the given key from the cluster. This operation may only be + performed on keys which are not currently the primary key. + +* `-list` - List all keys currently in use within the cluster. + +* `-wan` - If talking with a server node, this flag can be used to operate on + the WAN gossip layer. By default, this command operates on the LAN layer. More + information about the different gossip layers can be found on the + [gossip protocol](/docs/internals/gossip.html) page. + +* `-rpc-addr` - RPC address of the Consul agent. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 52d171e4d..6c48f1dbb 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -79,6 +79,10 @@ keygen + > + keys + + > leave From d8f513f6d6825c46e5c751d1f717922bb4044122 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 11 Sep 2014 11:34:58 -0700 Subject: [PATCH 105/238] website: update consul keys documentation --- website/source/docs/agent/options.html.markdown | 7 +++++++ website/source/docs/commands/keys.html.markdown | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 01149ef59..2a60a4309 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -85,10 +85,17 @@ The options below are all specified on the command-line. it relies on proper configuration. Nodes in the same datacenter should be on a single LAN. +* `-persist-keyring` - This flag enables persistence of changes to the + encryption keys used in the gossip pools. By default, any modifications to + the keyring via the [consul keys](/docs/command/keys.html) command will be + lost when the agent shuts down. + * `-encrypt` - Specifies the secret key to use for encryption of Consul network traffic. This key must be 16-bytes that are base64 encoded. The easiest way to create an encryption key is to use `consul keygen`. All nodes within a cluster must share the same encryption key to communicate. + If keyring persistence is enabled, the given key will only be used if there is + no pre-existing keyring. Otherwise, Consul will emit a warning and continue. * `-join` - Address of another agent to join upon starting up. This can be specified multiple times to specify multiple agents to join. If Consul is diff --git a/website/source/docs/commands/keys.html.markdown b/website/source/docs/commands/keys.html.markdown index 785025b34..beb9f3894 100644 --- a/website/source/docs/commands/keys.html.markdown +++ b/website/source/docs/commands/keys.html.markdown @@ -23,8 +23,9 @@ responsibility of the operator to ensure that only the required encryption keys are installed on the cluster. You can ensure that a key is not installed using the `-list` and `-remove` options. -By default, modifications made using this command will be persisted in the -Consul agent's data directory. This functionality can be altered via the +By default, modifications made using this command will **NOT** be persisted, and +will be lost when the agent shuts down. You can alter this behavior via the +`-persist-keyring` option in the [Agent Configuration](/docs/agent/options.html). All variations of the keys command will return 0 if all nodes reply and there From 2e92e19760f46709f9b1102b0d3658625bfbec6f Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 11 Sep 2014 19:52:16 -0700 Subject: [PATCH 106/238] agent: refactor keyring loader --- command/agent/agent.go | 44 ++++++++++++++++++++++++---------------- command/agent/command.go | 7 ++----- command/agent/config.go | 15 +++++++++----- consul/config.go | 5 ----- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 456415f96..5453f4ad9 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -115,7 +115,7 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { agent.state.Init(config, agent.logger) // Setup encryption keyring files - if !config.DisableKeyring && config.EncryptKey != "" { + if config.PersistKeyring && config.EncryptKey != "" { serfDir := filepath.Join(config.DataDir, "serf") if err := os.MkdirAll(serfDir, 0700); err != nil { return nil, err @@ -200,20 +200,17 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.DataDir != "" { base.DataDir = a.config.DataDir } - if a.config.EncryptKey != "" && a.config.DisableKeyring { + if a.config.EncryptKey != "" && !a.config.PersistKeyring { key, _ := a.config.EncryptBytes() base.SerfLANConfig.MemberlistConfig.SecretKey = key base.SerfWANConfig.MemberlistConfig.SecretKey = key } - if !a.config.DisableKeyring { + if a.config.PersistKeyring { lanKeyring := filepath.Join(base.DataDir, "serf", "keyring_lan") wanKeyring := filepath.Join(base.DataDir, "serf", "keyring_wan") base.SerfLANConfig.KeyringFile = lanKeyring base.SerfWANConfig.KeyringFile = wanKeyring - - base.SerfLANConfig.MemberlistConfig.Keyring = loadKeyringFile(lanKeyring) - base.SerfWANConfig.MemberlistConfig.Keyring = loadKeyringFile(wanKeyring) } if a.config.NodeName != "" { base.NodeName = a.config.NodeName @@ -303,9 +300,6 @@ func (a *Agent) consulConfig() *consul.Config { } } - // Setup gossip keyring configuration - base.DisableKeyring = a.config.DisableKeyring - // Setup the loggers base.LogOutput = a.logOutput return base @@ -313,6 +307,16 @@ func (a *Agent) consulConfig() *consul.Config { // setupServer is used to initialize the Consul server func (a *Agent) setupServer() error { + config := a.consulConfig() + + // Load a keyring file, if present + if err := loadKeyringFile(config.SerfLANConfig); err != nil { + return err + } + if err := loadKeyringFile(config.SerfWANConfig); err != nil { + return err + } + server, err := consul.NewServer(a.consulConfig()) if err != nil { return fmt.Errorf("Failed to start Consul server: %v", err) @@ -703,21 +707,25 @@ func (a *Agent) deletePid() error { } // loadKeyringFile will load a keyring out of a file -func loadKeyringFile(keyringFile string) *memberlist.Keyring { - if _, err := os.Stat(keyringFile); err != nil { +func loadKeyringFile(c *serf.Config) error { + if c.KeyringFile == "" { return nil } + if _, err := os.Stat(c.KeyringFile); err != nil { + return err + } + // Read in the keyring file data - keyringData, err := ioutil.ReadFile(keyringFile) + keyringData, err := ioutil.ReadFile(c.KeyringFile) if err != nil { - return nil + return err } // Decode keyring JSON keys := make([]string, 0) if err := json.Unmarshal(keyringData, &keys); err != nil { - return nil + return err } // Decode base64 values @@ -725,7 +733,7 @@ func loadKeyringFile(keyringFile string) *memberlist.Keyring { for i, key := range keys { keyBytes, err := base64.StdEncoding.DecodeString(key) if err != nil { - return nil + return err } keysDecoded[i] = keyBytes } @@ -733,11 +741,13 @@ func loadKeyringFile(keyringFile string) *memberlist.Keyring { // Create the keyring keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0]) if err != nil { - return nil + return err } + c.MemberlistConfig.Keyring = keyring + // Success! - return keyring + return nil } // ListKeysLAN returns the keys installed on the LAN gossip pool diff --git a/command/agent/command.go b/command/agent/command.go index 649cded48..6f1da6cf7 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -67,7 +67,7 @@ func (c *Command) readConfig() *Config { cmdFlags.StringVar(&cmdConfig.UiDir, "ui-dir", "", "path to the web UI directory") cmdFlags.StringVar(&cmdConfig.PidFile, "pid-file", "", "path to file to store PID") cmdFlags.StringVar(&cmdConfig.EncryptKey, "encrypt", "", "gossip encryption key") - cmdFlags.BoolVar(&cmdConfig.DisableKeyring, "disable-keyring", false, "disable use of encryption keyring") + cmdFlags.BoolVar(&cmdConfig.PersistKeyring, "persist-keyring", false, "persist keyring changes") cmdFlags.BoolVar(&cmdConfig.Server, "server", false, "run agent as server") cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode") @@ -802,10 +802,6 @@ Options: -data-dir=path Path to a data directory to store agent state -dc=east-aws Datacenter of the agent -encrypt=key Provides the gossip encryption key - -disable-keyring Disables the use of an encryption keyring. The - Default behavior is to persist encryption keys using - a keyring file, and reload the keys on subsequent - starts. This argument disables keyring persistence. -join=1.2.3.4 Address of an agent to join at start time. Can be specified multiple times. -join-wan=1.2.3.4 Address of an agent to join -wan at start time. @@ -823,6 +819,7 @@ Options: -log-level=info Log level of the agent. -node=hostname Name of this node. Must be unique in the cluster -protocol=N Sets the protocol version. Defaults to latest. + -persist-keyring Enable encryption keyring persistence. -rejoin Ignores a previous leave and attempts to rejoin the cluster. -server Switches agent to server mode. -syslog Enables logging to syslog diff --git a/command/agent/config.go b/command/agent/config.go index 2fad1db39..cb3596167 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -104,13 +104,13 @@ type Config struct { // recursors array. DNSRecursor string `mapstructure:"recursor"` - // Disable use of an encryption keyring. - DisableKeyring bool `mapstructure:"disable_keyring"` - // DNSRecursors can be set to allow the DNS servers to recursively // resolve non-consul domains DNSRecursors []string `mapstructure:"recursors"` + // Disable use of an encryption keyring. + DisableKeyring bool `mapstructure:"disable_keyring"` + // DNS configuration DNSConfig DNSConfig `mapstructure:"dns_config"` @@ -150,6 +150,11 @@ type Config struct { // the TERM signal. Defaults false. This can be changed on reload. LeaveOnTerm bool `mapstructure:"leave_on_terminate"` + // Enable keyring persistence. There are currently two keyrings; one for + // the LAN serf cluster and the other for the WAN. Each will maintain its + // own keyring file in the agent's data directory. + PersistKeyring bool `mapstructure:"persist_keyring"` + // SkipLeaveOnInt controls if Serf skips a graceful leave when receiving // the INT signal. Defaults false. This can be changed on reload. SkipLeaveOnInt bool `mapstructure:"skip_leave_on_interrupt"` @@ -691,8 +696,8 @@ func MergeConfig(a, b *Config) *Config { if b.EncryptKey != "" { result.EncryptKey = b.EncryptKey } - if b.DisableKeyring { - result.DisableKeyring = true + if b.PersistKeyring { + result.PersistKeyring = true } if b.LogLevel != "" { result.LogLevel = b.LogLevel diff --git a/consul/config.go b/consul/config.go index 10acaab2c..9cb1944cb 100644 --- a/consul/config.go +++ b/consul/config.go @@ -165,11 +165,6 @@ type Config struct { // UserEventHandler callback can be used to handle incoming // user events. This function should not block. UserEventHandler func(serf.UserEvent) - - // DisableKeyring is used to disable persisting the encryption keyring to - // filesystem. By default, if encryption is enabled, Consul will create a - // file inside of the DataDir to keep track of changes made to the ring. - DisableKeyring bool } // CheckVersion is used to check if the ProtocolVersion is valid From 3e64ed70df822d68a5e4104ce704532f1a10ab7f Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 11 Sep 2014 22:28:23 -0700 Subject: [PATCH 107/238] agent: clean up keyring file implementation --- command/agent/agent.go | 79 ++++++++++++++++++++++++---------------- command/agent/command.go | 7 +++- command/agent/config.go | 9 +++-- 3 files changed, 58 insertions(+), 37 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 5453f4ad9..781ecc90a 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -19,6 +19,11 @@ import ( "github.com/hashicorp/serf/serf" ) +const ( + serfLANKeyring = "serf/local.keyring" + serfWANKeyring = "serf/remote.keyring" +) + /* The agent is the long running process that is run on every machine. It exposes an RPC interface that is used by the CLI to control the @@ -116,37 +121,14 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { // Setup encryption keyring files if config.PersistKeyring && config.EncryptKey != "" { - serfDir := filepath.Join(config.DataDir, "serf") - if err := os.MkdirAll(serfDir, 0700); err != nil { - return nil, err - } - - keys := []string{config.EncryptKey} - keyringBytes, err := json.MarshalIndent(keys, "", " ") - if err != nil { - return nil, err - } - - paths := []string{ - filepath.Join(serfDir, "keyring_lan"), - filepath.Join(serfDir, "keyring_wan"), - } - - for _, path := range paths { - if _, err := os.Stat(path); err == nil { - continue - } - fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return nil, err - } - defer fh.Close() - - if _, err := fh.Write(keyringBytes); err != nil { - os.Remove(path) + if config.Server { + if err := agent.initKeyringFile(serfWANKeyring); err != nil { return nil, err } } + if err := agent.initKeyringFile(serfLANKeyring); err != nil { + return nil, err + } } // Setup either the client or the server @@ -206,9 +188,8 @@ func (a *Agent) consulConfig() *consul.Config { base.SerfWANConfig.MemberlistConfig.SecretKey = key } if a.config.PersistKeyring { - lanKeyring := filepath.Join(base.DataDir, "serf", "keyring_lan") - wanKeyring := filepath.Join(base.DataDir, "serf", "keyring_wan") - + lanKeyring := filepath.Join(base.DataDir, serfLANKeyring) + wanKeyring := filepath.Join(base.DataDir, serfWANKeyring) base.SerfLANConfig.KeyringFile = lanKeyring base.SerfWANConfig.KeyringFile = wanKeyring } @@ -825,3 +806,39 @@ func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { km := a.client.KeyManagerLAN() return km.RemoveKey(key) } + +// initKeyringFile is used to create and initialize a persistent keyring file +// for gossip encryption keys. It is used at agent startup to dump the initial +// encryption key into a keyfile if persistence is enabled. +func (a *Agent) initKeyringFile(path string) error { + serfDir := filepath.Join(a.config.DataDir, "serf") + if err := os.MkdirAll(serfDir, 0700); err != nil { + return err + } + + keys := []string{a.config.EncryptKey} + keyringBytes, err := json.MarshalIndent(keys, "", " ") + if err != nil { + return err + } + + keyringFile := filepath.Join(a.config.DataDir, path) + + // If the keyring file already exists, don't re-initialize + if _, err := os.Stat(keyringFile); err == nil { + return nil + } + + fh, err := os.OpenFile(keyringFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fh.Close() + + if _, err := fh.Write(keyringBytes); err != nil { + os.Remove(keyringFile) + return err + } + + return nil +} diff --git a/command/agent/command.go b/command/agent/command.go index 6f1da6cf7..94b6eacd3 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -220,7 +220,7 @@ func (c *Command) readConfig() *Config { } // Warn if an encryption key is passed while a keyring already exists - if config.EncryptKey != "" && config.CheckKeyringFiles() { + if config.EncryptKey != "" && (config.PersistKeyring && config.CheckKeyringFiles()) { c.Ui.Error(fmt.Sprintf( "WARNING: Keyring already exists, ignoring new key %s", config.EncryptKey)) @@ -594,7 +594,10 @@ func (c *Command) Run(args []string) int { } // Determine if gossip is encrypted - gossipEncrypted := (config.EncryptKey != "" || config.CheckKeyringFiles()) + gossipEncrypted := false + if config.EncryptKey != "" || (config.PersistKeyring && config.CheckKeyringFiles()) { + gossipEncrypted = true + } // Let the agent know we've finished registration c.agent.StartSync() diff --git a/command/agent/config.go b/command/agent/config.go index cb3596167..e4c1ee73a 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -418,12 +418,13 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { // CheckKeyringFiles checks for existence of the keyring files for Serf func (c *Config) CheckKeyringFiles() bool { - serfDir := filepath.Join(c.DataDir, "serf") - if _, err := os.Stat(filepath.Join(serfDir, "keyring_lan")); err != nil { + if _, err := os.Stat(filepath.Join(c.DataDir, serfLANKeyring)); err != nil { return false } - if _, err := os.Stat(filepath.Join(serfDir, "keyring_wan")); err != nil { - return false + if c.Server { + if _, err := os.Stat(filepath.Join(c.DataDir, serfWANKeyring)); err != nil { + return false + } } return true } From dfdd7c4ef79f7758b1fb6bc19fc1349318064f6b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 11 Sep 2014 22:46:57 -0700 Subject: [PATCH 108/238] agent: fix keyring loading when config is passed off --- command/agent/agent.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 781ecc90a..06aad6d43 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -298,7 +298,7 @@ func (a *Agent) setupServer() error { return err } - server, err := consul.NewServer(a.consulConfig()) + server, err := consul.NewServer(config) if err != nil { return fmt.Errorf("Failed to start Consul server: %v", err) } @@ -308,7 +308,14 @@ func (a *Agent) setupServer() error { // setupClient is used to initialize the Consul client func (a *Agent) setupClient() error { - client, err := consul.NewClient(a.consulConfig()) + config := a.consulConfig() + + // Load a keyring file, if present + if err := loadKeyringFile(config.SerfLANConfig); err != nil { + return err + } + + client, err := consul.NewClient(config) if err != nil { return fmt.Errorf("Failed to start Consul client: %v", err) } From a36ab53f25e2d04f4f97fef0e2bba07fbe3faad7 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 12 Sep 2014 17:18:10 -0700 Subject: [PATCH 109/238] agent: move keyring initialization out of agent, add -init option to keys command --- command/agent/agent.go | 64 ++++-------------------------- command/agent/command.go | 13 +++---- command/agent/config.go | 16 ++------ command/keys.go | 84 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 94 insertions(+), 83 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 06aad6d43..95fcd8bec 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -20,8 +20,8 @@ import ( ) const ( - serfLANKeyring = "serf/local.keyring" - serfWANKeyring = "serf/remote.keyring" + SerfLANKeyring = "serf/local.keyring" + SerfWANKeyring = "serf/remote.keyring" ) /* @@ -119,18 +119,6 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { // Initialize the local state agent.state.Init(config, agent.logger) - // Setup encryption keyring files - if config.PersistKeyring && config.EncryptKey != "" { - if config.Server { - if err := agent.initKeyringFile(serfWANKeyring); err != nil { - return nil, err - } - } - if err := agent.initKeyringFile(serfLANKeyring); err != nil { - return nil, err - } - } - // Setup either the client or the server var err error if config.Server { @@ -182,16 +170,16 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.DataDir != "" { base.DataDir = a.config.DataDir } - if a.config.EncryptKey != "" && !a.config.PersistKeyring { + if a.config.EncryptKey != "" { key, _ := a.config.EncryptBytes() base.SerfLANConfig.MemberlistConfig.SecretKey = key base.SerfWANConfig.MemberlistConfig.SecretKey = key } - if a.config.PersistKeyring { - lanKeyring := filepath.Join(base.DataDir, serfLANKeyring) - wanKeyring := filepath.Join(base.DataDir, serfWANKeyring) - base.SerfLANConfig.KeyringFile = lanKeyring - base.SerfWANConfig.KeyringFile = wanKeyring + if a.config.Server && a.config.keyringFilesExist() { + pathWAN := filepath.Join(base.DataDir, SerfWANKeyring) + pathLAN := filepath.Join(base.DataDir, SerfLANKeyring) + base.SerfWANConfig.KeyringFile = pathWAN + base.SerfLANConfig.KeyringFile = pathLAN } if a.config.NodeName != "" { base.NodeName = a.config.NodeName @@ -813,39 +801,3 @@ func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { km := a.client.KeyManagerLAN() return km.RemoveKey(key) } - -// initKeyringFile is used to create and initialize a persistent keyring file -// for gossip encryption keys. It is used at agent startup to dump the initial -// encryption key into a keyfile if persistence is enabled. -func (a *Agent) initKeyringFile(path string) error { - serfDir := filepath.Join(a.config.DataDir, "serf") - if err := os.MkdirAll(serfDir, 0700); err != nil { - return err - } - - keys := []string{a.config.EncryptKey} - keyringBytes, err := json.MarshalIndent(keys, "", " ") - if err != nil { - return err - } - - keyringFile := filepath.Join(a.config.DataDir, path) - - // If the keyring file already exists, don't re-initialize - if _, err := os.Stat(keyringFile); err == nil { - return nil - } - - fh, err := os.OpenFile(keyringFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer fh.Close() - - if _, err := fh.Write(keyringBytes); err != nil { - os.Remove(keyringFile) - return err - } - - return nil -} diff --git a/command/agent/command.go b/command/agent/command.go index 94b6eacd3..bacc099d5 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -67,8 +67,6 @@ func (c *Command) readConfig() *Config { cmdFlags.StringVar(&cmdConfig.UiDir, "ui-dir", "", "path to the web UI directory") cmdFlags.StringVar(&cmdConfig.PidFile, "pid-file", "", "path to file to store PID") cmdFlags.StringVar(&cmdConfig.EncryptKey, "encrypt", "", "gossip encryption key") - cmdFlags.BoolVar(&cmdConfig.PersistKeyring, "persist-keyring", false, "persist keyring changes") - cmdFlags.BoolVar(&cmdConfig.Server, "server", false, "run agent as server") cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode") cmdFlags.IntVar(&cmdConfig.BootstrapExpect, "bootstrap-expect", 0, "enable automatic bootstrap via expect mode") @@ -219,11 +217,10 @@ func (c *Command) readConfig() *Config { c.Ui.Error("WARNING: Windows is not recommended as a Consul server. Do not use in production.") } - // Warn if an encryption key is passed while a keyring already exists - if config.EncryptKey != "" && (config.PersistKeyring && config.CheckKeyringFiles()) { - c.Ui.Error(fmt.Sprintf( - "WARNING: Keyring already exists, ignoring new key %s", - config.EncryptKey)) + // Error if an encryption key is passed while a keyring already exists + if config.EncryptKey != "" && config.keyringFilesExist() { + c.Ui.Error(fmt.Sprintf("Error: -encrypt specified but keyring files exist")) + return nil } // Set the version info @@ -595,7 +592,7 @@ func (c *Command) Run(args []string) int { // Determine if gossip is encrypted gossipEncrypted := false - if config.EncryptKey != "" || (config.PersistKeyring && config.CheckKeyringFiles()) { + if config.EncryptKey != "" || config.keyringFilesExist() { gossipEncrypted = true } diff --git a/command/agent/config.go b/command/agent/config.go index e4c1ee73a..9334553d0 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -150,11 +150,6 @@ type Config struct { // the TERM signal. Defaults false. This can be changed on reload. LeaveOnTerm bool `mapstructure:"leave_on_terminate"` - // Enable keyring persistence. There are currently two keyrings; one for - // the LAN serf cluster and the other for the WAN. Each will maintain its - // own keyring file in the agent's data directory. - PersistKeyring bool `mapstructure:"persist_keyring"` - // SkipLeaveOnInt controls if Serf skips a graceful leave when receiving // the INT signal. Defaults false. This can be changed on reload. SkipLeaveOnInt bool `mapstructure:"skip_leave_on_interrupt"` @@ -416,13 +411,13 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } -// CheckKeyringFiles checks for existence of the keyring files for Serf -func (c *Config) CheckKeyringFiles() bool { - if _, err := os.Stat(filepath.Join(c.DataDir, serfLANKeyring)); err != nil { +// keyringFilesExist checks for existence of the keyring files for Serf +func (c *Config) keyringFilesExist() bool { + if _, err := os.Stat(filepath.Join(c.DataDir, SerfLANKeyring)); err != nil { return false } if c.Server { - if _, err := os.Stat(filepath.Join(c.DataDir, serfWANKeyring)); err != nil { + if _, err := os.Stat(filepath.Join(c.DataDir, SerfWANKeyring)); err != nil { return false } } @@ -697,9 +692,6 @@ func MergeConfig(a, b *Config) *Config { if b.EncryptKey != "" { result.EncryptKey = b.EncryptKey } - if b.PersistKeyring { - result.PersistKeyring = true - } if b.LogLevel != "" { result.LogLevel = b.LogLevel } diff --git a/command/keys.go b/command/keys.go index b99853576..8ca98bdf2 100644 --- a/command/keys.go +++ b/command/keys.go @@ -1,10 +1,15 @@ package command import ( + "encoding/base64" + "encoding/json" "flag" "fmt" + "os" + "path/filepath" "strings" + "github.com/hashicorp/consul/command/agent" "github.com/mitchellh/cli" "github.com/ryanuber/columnize" ) @@ -16,7 +21,7 @@ type KeysCommand struct { } func (c *KeysCommand) Run(args []string) int { - var installKey, useKey, removeKey string + var installKey, useKey, removeKey, init, dataDir string var listKeys, wan bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) @@ -27,6 +32,8 @@ func (c *KeysCommand) Run(args []string) int { cmdFlags.StringVar(&removeKey, "remove", "", "remove key") cmdFlags.BoolVar(&listKeys, "list", false, "list keys") cmdFlags.BoolVar(&wan, "wan", false, "operate on wan keys") + cmdFlags.StringVar(&init, "init", "", "initialize keyring") + cmdFlags.StringVar(&dataDir, "data-dir", "", "data directory") rpcAddr := RPCAddrFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -40,15 +47,11 @@ func (c *KeysCommand) Run(args []string) int { Ui: c.Ui, } - var out []string - var failures map[string]string - var err error - // Only accept a single argument found := listKeys - for _, arg := range []string{installKey, useKey, removeKey} { + for _, arg := range []string{installKey, useKey, removeKey, init} { if found && len(arg) > 0 { - c.Ui.Error("Only one of -list, -install, -use, or -remove allowed") + c.Ui.Error("Only a single action is allowed") return 1 } found = found || len(arg) > 0 @@ -60,6 +63,43 @@ func (c *KeysCommand) Run(args []string) int { return 1 } + var out, paths []string + var failures map[string]string + var err error + + if init != "" { + if dataDir == "" { + c.Ui.Error("Must provide -data-dir") + return 1 + } + if _, err := base64.StdEncoding.DecodeString(init); err != nil { + c.Ui.Error(fmt.Sprintf("Invalid key: %s", err)) + return 1 + } + + paths = append(paths, filepath.Join(dataDir, agent.SerfLANKeyring)) + if wan { + paths = append(paths, filepath.Join(dataDir, agent.SerfWANKeyring)) + } + + keys := []string{init} + keyringBytes, err := json.MarshalIndent(keys, "", " ") + if err != nil { + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + + for _, path := range paths { + if err := initializeKeyring(path, keyringBytes); err != nil { + c.Ui.Error("Error: %s", err) + return 1 + } + } + + return 0 + } + + // All other operations will require a client connection client, err := RPCClient(*rpcAddr) if err != nil { c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) @@ -181,6 +221,30 @@ func (c *KeysCommand) Run(args []string) int { return 0 } +// initializeKeyring will create a keyring file at a given path. +func initializeKeyring(path string, key []byte) error { + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + + if _, err := os.Stat(path); err == nil { + return fmt.Errorf("File already exists: %s", path) + } + + fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fh.Close() + + if _, err := fh.Write(keyringBytes); err != nil { + os.Remove(path) + return err + } + + return nil +} + func (c *KeysCommand) Help() string { helpText := ` Usage: consul keys [options] @@ -204,6 +268,12 @@ Options: -list List all keys currently in use within the cluster. -wan If talking with a server node, this flag can be used to operate on the WAN gossip layer. + -init= Create an initial keyring file for Consul to use + containing the provided key. By default, this option + will only initialize the LAN keyring. If the -wan + option is also passed, then the wan keyring will be + created as well. The -data-dir argument is required + with this option. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) From ee20698199047fee14a2ae697dbf5f2cf56586a7 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 12 Sep 2014 18:13:49 -0700 Subject: [PATCH 110/238] command/keys: refactor, restrict key operations to server nodes --- command/agent/agent.go | 9 +- command/agent/config.go | 7 +- command/keys.go | 208 +++++++++++++++++++--------------------- 3 files changed, 102 insertions(+), 122 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 95fcd8bec..d0f62fc3e 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -20,8 +20,7 @@ import ( ) const ( - SerfLANKeyring = "serf/local.keyring" - SerfWANKeyring = "serf/remote.keyring" + SerfKeyring = "serf/keyring" ) /* @@ -176,10 +175,8 @@ func (a *Agent) consulConfig() *consul.Config { base.SerfWANConfig.MemberlistConfig.SecretKey = key } if a.config.Server && a.config.keyringFilesExist() { - pathWAN := filepath.Join(base.DataDir, SerfWANKeyring) - pathLAN := filepath.Join(base.DataDir, SerfLANKeyring) - base.SerfWANConfig.KeyringFile = pathWAN - base.SerfLANConfig.KeyringFile = pathLAN + path := filepath.Join(base.DataDir, SerfKeyring) + base.SerfLANConfig.KeyringFile = path } if a.config.NodeName != "" { base.NodeName = a.config.NodeName diff --git a/command/agent/config.go b/command/agent/config.go index 9334553d0..60bd73032 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -413,14 +413,9 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { // keyringFilesExist checks for existence of the keyring files for Serf func (c *Config) keyringFilesExist() bool { - if _, err := os.Stat(filepath.Join(c.DataDir, SerfLANKeyring)); err != nil { + if _, err := os.Stat(filepath.Join(c.DataDir, SerfKeyring)); err != nil { return false } - if c.Server { - if _, err := os.Stat(filepath.Join(c.DataDir, SerfWANKeyring)); err != nil { - return false - } - } return true } diff --git a/command/keys.go b/command/keys.go index 8ca98bdf2..c603a3d53 100644 --- a/command/keys.go +++ b/command/keys.go @@ -22,7 +22,7 @@ type KeysCommand struct { func (c *KeysCommand) Run(args []string) int { var installKey, useKey, removeKey, init, dataDir string - var listKeys, wan bool + var listKeys bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -31,7 +31,6 @@ func (c *KeysCommand) Run(args []string) int { cmdFlags.StringVar(&useKey, "use", "", "use key") cmdFlags.StringVar(&removeKey, "remove", "", "remove key") cmdFlags.BoolVar(&listKeys, "list", false, "list keys") - cmdFlags.BoolVar(&wan, "wan", false, "operate on wan keys") cmdFlags.StringVar(&init, "init", "", "initialize keyring") cmdFlags.StringVar(&dataDir, "data-dir", "", "data directory") @@ -63,39 +62,17 @@ func (c *KeysCommand) Run(args []string) int { return 1 } - var out, paths []string - var failures map[string]string - var err error - if init != "" { if dataDir == "" { c.Ui.Error("Must provide -data-dir") return 1 } - if _, err := base64.StdEncoding.DecodeString(init); err != nil { - c.Ui.Error(fmt.Sprintf("Invalid key: %s", err)) - return 1 - } - - paths = append(paths, filepath.Join(dataDir, agent.SerfLANKeyring)) - if wan { - paths = append(paths, filepath.Join(dataDir, agent.SerfWANKeyring)) - } - - keys := []string{init} - keyringBytes, err := json.MarshalIndent(keys, "", " ") - if err != nil { + path := filepath.Join(dataDir, agent.SerfKeyring) + if err := initializeKeyring(path, init); err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return 1 } - for _, path := range paths { - if err := initializeKeyring(path, keyringBytes); err != nil { - c.Ui.Error("Error: %s", err) - return 1 - } - } - return 0 } @@ -107,112 +84,64 @@ func (c *KeysCommand) Run(args []string) int { } defer client.Close() + // For all key-related operations, we must be querying a server node. + s, err := client.Stats() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + if s["consul"]["server"] != "true" { + c.Ui.Error("Error: Key modification can only be handled by a server") + return 1 + } + if listKeys { - var keys map[string]int - var numNodes int - - if wan { - c.Ui.Info("Asking all WAN members for installed keys...") - keys, numNodes, failures, err = client.ListKeysWAN() - } else { - c.Ui.Info("Asking all LAN members for installed keys...") - keys, numNodes, failures, err = client.ListKeysLAN() + c.Ui.Info("Asking all WAN members for installed keys...") + if rval := c.listKeysOperation(client.ListKeysWAN); rval != 0 { + return rval } - - if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) - } - c.Ui.Error(columnize.SimpleFormat(out)) - } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) - return 1 + c.Ui.Info("Asking all LAN members for installed keys...") + if rval := c.listKeysOperation(client.ListKeysLAN); rval != 0 { + return rval } - - c.Ui.Info("Keys gathered, listing cluster keys...") - c.Ui.Output("") - - for key, num := range keys { - out = append(out, fmt.Sprintf("%s | [%d/%d]", key, num, numNodes)) - } - c.Ui.Output(columnize.SimpleFormat(out)) - return 0 } if installKey != "" { - if wan { - c.Ui.Info("Installing new WAN gossip encryption key...") - failures, err = client.InstallKeyWAN(installKey) - } else { - c.Ui.Info("Installing new LAN gossip encryption key...") - failures, err = client.InstallKeyLAN(installKey) + c.Ui.Info("Installing new WAN gossip encryption key...") + if rval := c.keyOperation(installKey, client.InstallKeyWAN); rval != 0 { + return rval } - - if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) - } - c.Ui.Error(columnize.SimpleFormat(out)) - } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Error installing key: %s", err)) - return 1 + c.Ui.Info("Installing new LAN gossip encryption key...") + if rval := c.keyOperation(installKey, client.InstallKeyLAN); rval != 0 { + return rval } - c.Ui.Info("Successfully installed key!") return 0 } if useKey != "" { - if wan { - c.Ui.Info("Changing primary encryption key on WAN members...") - failures, err = client.UseKeyWAN(useKey) - } else { - c.Ui.Info("Changing primary encryption key on LAN members...") - failures, err = client.UseKeyLAN(useKey) + c.Ui.Info("Changing primary WAN gossip encryption key...") + if rval := c.keyOperation(useKey, client.UseKeyWAN); rval != 0 { + return rval } - - if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) - } - c.Ui.Error(columnize.SimpleFormat(out)) - } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Error changing primary key: %s", err)) - return 1 + c.Ui.Info("Changing primary LAN gossip encryption key...") + if rval := c.keyOperation(useKey, client.UseKeyLAN); rval != 0 { + return rval } - c.Ui.Info("Successfully changed primary key!") return 0 } if removeKey != "" { - if wan { - c.Ui.Info("Removing key from WAN members...") - failures, err = client.RemoveKeyWAN(removeKey) - } else { - c.Ui.Info("Removing key from LAN members...") - failures, err = client.RemoveKeyLAN(removeKey) + c.Ui.Info("Removing WAN gossip encryption key...") + if rval := c.keyOperation(removeKey, client.RemoveKeyWAN); rval != 0 { + return rval } - - if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) - } - c.Ui.Error(columnize.SimpleFormat(out)) - } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Error removing key: %s", err)) - return 1 + c.Ui.Info("Removing LAN gossip encryption key...") + if rval := c.keyOperation(removeKey, client.RemoveKeyLAN); rval != 0 { + return rval } - c.Ui.Info("Successfully removed key!") return 0 } @@ -221,8 +150,67 @@ func (c *KeysCommand) Run(args []string) int { return 0 } +type keyFunc func(string) (map[string]string, error) + +func (c *KeysCommand) keyOperation(key string, fn keyFunc) int { + var out []string + + failures, err := fn(key) + + if err != nil { + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + } + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + + return 0 +} + +type listKeysFunc func() (map[string]int, int, map[string]string, error) + +func (c *KeysCommand) listKeysOperation(fn listKeysFunc) int { + var out []string + + keys, numNodes, failures, err := fn() + + if err != nil { + if len(failures) > 0 { + for node, msg := range failures { + out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + } + c.Ui.Error(columnize.SimpleFormat(out)) + } + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) + return 1 + } + for key, num := range keys { + out = append(out, fmt.Sprintf("%s | [%d/%d]", key, num, numNodes)) + } + c.Ui.Output(columnize.SimpleFormat(out)) + + c.Ui.Output("") + return 0 +} + // initializeKeyring will create a keyring file at a given path. -func initializeKeyring(path string, key []byte) error { +func initializeKeyring(path, key string) error { + if _, err := base64.StdEncoding.DecodeString(key); err != nil { + return fmt.Errorf("Invalid key: %s", err) + } + + keys := []string{key} + keyringBytes, err := json.MarshalIndent(keys, "", " ") + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return err } From 8dc53447a31e79ceb6540ba8f7adf5c1b1eb95d8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 12 Sep 2014 20:08:54 -0700 Subject: [PATCH 111/238] command: renamed keys to keyring to disambiguate usage --- command/{keys.go => keyring.go} | 34 +++--- command/{keys_test.go => keyring_test.go} | 105 ++++-------------- commands.go | 4 +- ...ys.html.markdown => keyring.html.markdown} | 39 ++++--- website/source/layouts/docs.erb | 4 +- 5 files changed, 63 insertions(+), 123 deletions(-) rename command/{keys.go => keyring.go} (86%) rename command/{keys_test.go => keyring_test.go} (50%) rename website/source/docs/commands/{keys.html.markdown => keyring.html.markdown} (60%) diff --git a/command/keys.go b/command/keyring.go similarity index 86% rename from command/keys.go rename to command/keyring.go index c603a3d53..17d1c5c4c 100644 --- a/command/keys.go +++ b/command/keyring.go @@ -14,13 +14,13 @@ import ( "github.com/ryanuber/columnize" ) -// KeysCommand is a Command implementation that handles querying, installing, +// KeyringCommand is a Command implementation that handles querying, installing, // and removing gossip encryption keys from a keyring. -type KeysCommand struct { +type KeyringCommand struct { Ui cli.Ui } -func (c *KeysCommand) Run(args []string) int { +func (c *KeyringCommand) Run(args []string) int { var installKey, useKey, removeKey, init, dataDir string var listKeys bool @@ -150,9 +150,12 @@ func (c *KeysCommand) Run(args []string) int { return 0 } +// keyFunc is a function which manipulates gossip encryption keyrings. This is +// used for key installation, removal, and primary key changes. type keyFunc func(string) (map[string]string, error) -func (c *KeysCommand) keyOperation(key string, fn keyFunc) int { +// keyOperation is a unified process for manipulating the gossip keyrings. +func (c *KeyringCommand) keyOperation(key string, fn keyFunc) int { var out []string failures, err := fn(key) @@ -172,9 +175,12 @@ func (c *KeysCommand) keyOperation(key string, fn keyFunc) int { return 0 } +// listKeysFunc is a function which handles querying lists of gossip keys type listKeysFunc func() (map[string]int, int, map[string]string, error) -func (c *KeysCommand) listKeysOperation(fn listKeysFunc) int { +// listKeysOperation is a unified process for querying and +// displaying gossip keys. +func (c *KeyringCommand) listKeysOperation(fn listKeysFunc) int { var out []string keys, numNodes, failures, err := fn() @@ -233,9 +239,9 @@ func initializeKeyring(path, key string) error { return nil } -func (c *KeysCommand) Help() string { +func (c *KeyringCommand) Help() string { helpText := ` -Usage: consul keys [options] +Usage: consul keyring [options] Manages encryption keys used for gossip messages. Gossip encryption is optional. When enabled, this command may be used to examine active encryption @@ -243,6 +249,9 @@ Usage: consul keys [options] functionality provides the ability to perform key rotation cluster-wide, without disrupting the cluster. + With the exception of the -init argument, all other operations performed by + this command can only be run against server nodes. + Options: -install= Install a new encryption key. This will broadcast @@ -254,19 +263,14 @@ Options: operation may only be performed on keys which are not currently the primary key. -list List all keys currently in use within the cluster. - -wan If talking with a server node, this flag can be used - to operate on the WAN gossip layer. -init= Create an initial keyring file for Consul to use - containing the provided key. By default, this option - will only initialize the LAN keyring. If the -wan - option is also passed, then the wan keyring will be - created as well. The -data-dir argument is required - with this option. + containing the provided key. The -data-dir argument + is required with this option. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) } -func (c *KeysCommand) Synopsis() string { +func (c *KeyringCommand) Synopsis() string { return "Manages gossip layer encryption keys" } diff --git a/command/keys_test.go b/command/keyring_test.go similarity index 50% rename from command/keys_test.go rename to command/keyring_test.go index 903d13b19..d3f01ade9 100644 --- a/command/keys_test.go +++ b/command/keyring_test.go @@ -8,14 +8,13 @@ import ( "github.com/mitchellh/cli" ) -func TestKeysCommand_implements(t *testing.T) { - var _ cli.Command = &KeysCommand{} +func TestKeyringCommand_implements(t *testing.T) { + var _ cli.Command = &KeyringCommand{} } -func TestKeysCommandRun(t *testing.T) { +func TestKeyringCommandRun(t *testing.T) { key1 := "HS5lJ+XuTlYKWaeGYyG+/A==" key2 := "kZyFABeAmc64UMTrm9XuKA==" - key3 := "2k5VRlBIIKUPc1v77rsswg==" // Begin with a single key conf := agent.Config{EncryptKey: key1} @@ -23,7 +22,7 @@ func TestKeysCommandRun(t *testing.T) { defer a1.Shutdown() // The keyring was initialized with only the provided key - out := listKeys(t, a1.addr, false) + out := listKeys(t, a1.addr) if !strings.Contains(out, key1) { t.Fatalf("bad: %#v", out) } @@ -31,83 +30,36 @@ func TestKeysCommandRun(t *testing.T) { t.Fatalf("bad: %#v", out) } - // The key was installed on the WAN gossip layer also - out = listKeys(t, a1.addr, true) - if !strings.Contains(out, key1) { - t.Fatalf("bad: %#v", out) - } - if strings.Contains(out, key3) { - t.Fatalf("bad: %#v", out) - } - // Install the second key onto the keyring - installKey(t, a1.addr, false, key2) + installKey(t, a1.addr, key2) // Both keys should be present - out = listKeys(t, a1.addr, false) + out = listKeys(t, a1.addr) for _, key := range []string{key1, key2} { if !strings.Contains(out, key) { t.Fatalf("bad: %#v", out) } } - // Second key should not be installed on WAN - out = listKeys(t, a1.addr, true) - if strings.Contains(out, key2) { - t.Fatalf("bad: %#v", out) - } - // Change out the primary key - useKey(t, a1.addr, false, key2) + useKey(t, a1.addr, key2) // Remove the original key - removeKey(t, a1.addr, false, key1) + removeKey(t, a1.addr, key1) // Make sure only the new key is present - out = listKeys(t, a1.addr, false) + out = listKeys(t, a1.addr) if strings.Contains(out, key1) { t.Fatalf("bad: %#v", out) } if !strings.Contains(out, key2) { t.Fatalf("bad: %#v", out) } - - // Original key still remains on WAN keyring - out = listKeys(t, a1.addr, true) - if !strings.Contains(out, key1) { - t.Fatalf("bad: %#v", out) - } - - // Install second key on WAN keyring - installKey(t, a1.addr, true, key3) - - // Two keys now present on WAN keyring - out = listKeys(t, a1.addr, true) - for _, key := range []string{key1, key3} { - if !strings.Contains(out, key) { - t.Fatalf("bad: %#v", out) - } - } - - // Change WAN primary key - useKey(t, a1.addr, true, key3) - - // Remove original key from WAN keyring - removeKey(t, a1.addr, true, key1) - - // Only new key should exist on WAN keyring - out = listKeys(t, a1.addr, true) - if !strings.Contains(out, key3) { - t.Fatalf("bad: %#v", out) - } - if strings.Contains(out, key1) { - t.Fatalf("bad: %#v", out) - } } -func TestKeysCommandRun_help(t *testing.T) { +func TestKeyringCommandRun_help(t *testing.T) { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} code := c.Run(nil) if code != 1 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) @@ -119,9 +71,9 @@ func TestKeysCommandRun_help(t *testing.T) { } } -func TestKeysCommandRun_failedConnection(t *testing.T) { +func TestKeyringCommandRun_failedConnection(t *testing.T) { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} args := []string{"-list", "-rpc-addr=127.0.0.1:0"} code := c.Run(args) if code != 1 { @@ -132,14 +84,11 @@ func TestKeysCommandRun_failedConnection(t *testing.T) { } } -func listKeys(t *testing.T, addr string, wan bool) string { +func listKeys(t *testing.T, addr string) string { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} args := []string{"-list", "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } code := c.Run(args) if code != 0 { @@ -149,45 +98,33 @@ func listKeys(t *testing.T, addr string, wan bool) string { return ui.OutputWriter.String() } -func installKey(t *testing.T, addr string, wan bool, key string) { +func installKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} args := []string{"-install=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func useKey(t *testing.T, addr string, wan bool, key string) { +func useKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} args := []string{"-use=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func removeKey(t *testing.T, addr string, wan bool, key string) { +func removeKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) - c := &KeysCommand{Ui: ui} + c := &KeyringCommand{Ui: ui} args := []string{"-remove=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) diff --git a/commands.go b/commands.go index 8fab257ac..f6cf50a91 100644 --- a/commands.go +++ b/commands.go @@ -56,8 +56,8 @@ func init() { }, nil }, - "keys": func() (cli.Command, error) { - return &command.KeysCommand{ + "keyring": func() (cli.Command, error) { + return &command.KeyringCommand{ Ui: ui, }, nil }, diff --git a/website/source/docs/commands/keys.html.markdown b/website/source/docs/commands/keyring.html.markdown similarity index 60% rename from website/source/docs/commands/keys.html.markdown rename to website/source/docs/commands/keyring.html.markdown index beb9f3894..2b65241e6 100644 --- a/website/source/docs/commands/keys.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -1,21 +1,21 @@ --- layout: "docs" -page_title: "Commands: Keys" -sidebar_current: "docs-commands-keys" +page_title: "Commands: Keyring" +sidebar_current: "docs-commands-keyring" --- -# Consul Keys +# Consul Keyring -Command: `consul keys` +Command: `consul keyring` -The `keys` command is used to examine and modify the encryption keys used in +The `keyring` command is used to examine and modify the encryption keys used in Consul's [Gossip Pools](/docs/internals/gossip.html). It is capable of distributing new encryption keys to the cluster, revoking old encryption keys, and changing the key used by the cluster to encrypt messages. -Because Consul utilizes multiple gossip pools, this command will operate on only -a single pool at a time. The pool can be specified using the arguments -documented below. +Because Consul utilizes multiple gossip pools, this command will only operate +against a server node for most operations. The only operation which may be used +on client machines is the `-init` argument for initial key configuration. Consul allows multiple encryption keys to be in use simultaneously. This is intended to provide a transition state while the cluster converges. It is the @@ -23,23 +23,27 @@ responsibility of the operator to ensure that only the required encryption keys are installed on the cluster. You can ensure that a key is not installed using the `-list` and `-remove` options. -By default, modifications made using this command will **NOT** be persisted, and -will be lost when the agent shuts down. You can alter this behavior via the -`-persist-keyring` option in the -[Agent Configuration](/docs/agent/options.html). - All variations of the keys command will return 0 if all nodes reply and there are no errors. If any node fails to reply or reports failure, the exit code will be 1. ## Usage -Usage: `consul keys [options]` +Usage: `consul keyring [options]` -Exactly one of `-list`, `-install`, `-remove`, or `-update` must be provided. +Only one actionable argument may be specified per run, including `-init`, +`-list`, `-install`, `-remove`, and `-update`. The list of available flags are: +* `-init` - Creates the keyring file(s). This is useful to configure initial + encryption keyrings, which can later be mutated using the other arguments in + this command. This argument accepts an ASCII key, which can be generated using + the [keygen command](/docs/commands/keygen.html). + + This operation can be run on both client and server nodes and requires no + network connectivity. + * `-install` - Install a new encryption key. This will broadcast the new key to all members in the cluster. @@ -51,9 +55,4 @@ The list of available flags are: * `-list` - List all keys currently in use within the cluster. -* `-wan` - If talking with a server node, this flag can be used to operate on - the WAN gossip layer. By default, this command operates on the LAN layer. More - information about the different gossip layers can be found on the - [gossip protocol](/docs/internals/gossip.html) page. - * `-rpc-addr` - RPC address of the Consul agent. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 6c48f1dbb..d6764e792 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -79,8 +79,8 @@ keygen - > - keys + > + keyring > From 6609cb680b37138dfd9d3b8ad4c4b34f890e1e26 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 13 Sep 2014 11:24:17 -0700 Subject: [PATCH 112/238] command: use separate key files for LAN/WAN --- command/agent/agent.go | 19 ++++++++++++++----- command/agent/command.go | 5 ++--- command/agent/config.go | 18 ++++++++++++++---- command/keyring.go | 17 +++++++++++++---- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index d0f62fc3e..b19645f16 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -20,7 +20,8 @@ import ( ) const ( - SerfKeyring = "serf/keyring" + SerfLANKeyring = "serf/local.keyring" + SerfWANKeyring = "serf/remote.keyring" ) /* @@ -174,10 +175,6 @@ func (a *Agent) consulConfig() *consul.Config { base.SerfLANConfig.MemberlistConfig.SecretKey = key base.SerfWANConfig.MemberlistConfig.SecretKey = key } - if a.config.Server && a.config.keyringFilesExist() { - path := filepath.Join(base.DataDir, SerfKeyring) - base.SerfLANConfig.KeyringFile = path - } if a.config.NodeName != "" { base.NodeName = a.config.NodeName } @@ -276,6 +273,14 @@ func (a *Agent) setupServer() error { config := a.consulConfig() // Load a keyring file, if present + keyfileLAN := filepath.Join(config.DataDir, SerfLANKeyring) + if _, err := os.Stat(keyfileLAN); err == nil { + config.SerfLANConfig.KeyringFile = keyfileLAN + } + keyfileWAN := filepath.Join(config.DataDir, SerfWANKeyring) + if _, err := os.Stat(keyfileWAN); err == nil { + config.SerfWANConfig.KeyringFile = keyfileWAN + } if err := loadKeyringFile(config.SerfLANConfig); err != nil { return err } @@ -296,6 +301,10 @@ func (a *Agent) setupClient() error { config := a.consulConfig() // Load a keyring file, if present + keyfileLAN := filepath.Join(config.DataDir, SerfLANKeyring) + if _, err := os.Stat(keyfileLAN); err == nil { + config.SerfLANConfig.KeyringFile = keyfileLAN + } if err := loadKeyringFile(config.SerfLANConfig); err != nil { return err } diff --git a/command/agent/command.go b/command/agent/command.go index bacc099d5..c02b8135c 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -218,7 +218,7 @@ func (c *Command) readConfig() *Config { } // Error if an encryption key is passed while a keyring already exists - if config.EncryptKey != "" && config.keyringFilesExist() { + if config.EncryptKey != "" && config.keyringFileExists() { c.Ui.Error(fmt.Sprintf("Error: -encrypt specified but keyring files exist")) return nil } @@ -592,7 +592,7 @@ func (c *Command) Run(args []string) int { // Determine if gossip is encrypted gossipEncrypted := false - if config.EncryptKey != "" || config.keyringFilesExist() { + if config.EncryptKey != "" || config.keyringFileExists() { gossipEncrypted = true } @@ -819,7 +819,6 @@ Options: -log-level=info Log level of the agent. -node=hostname Name of this node. Must be unique in the cluster -protocol=N Sets the protocol version. Defaults to latest. - -persist-keyring Enable encryption keyring persistence. -rejoin Ignores a previous leave and attempts to rejoin the cluster. -server Switches agent to server mode. -syslog Enables logging to syslog diff --git a/command/agent/config.go b/command/agent/config.go index 60bd73032..69097984d 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -411,12 +411,22 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } -// keyringFilesExist checks for existence of the keyring files for Serf -func (c *Config) keyringFilesExist() bool { - if _, err := os.Stat(filepath.Join(c.DataDir, SerfKeyring)); err != nil { +// keyringFileExists determines if there are encryption key files present +// in the data directory. +func (c *Config) keyringFileExists() bool { + fileLAN := filepath.Join(c.DataDir, SerfLANKeyring) + fileWAN := filepath.Join(c.DataDir, SerfWANKeyring) + + if _, err := os.Stat(fileLAN); err == nil { + return true + } + if !c.Server { return false } - return true + if _, err := os.Stat(fileWAN); err == nil { + return true + } + return false } // DecodeConfig reads the configuration from the given reader in JSON diff --git a/command/keyring.go b/command/keyring.go index 17d1c5c4c..aed55c039 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -67,8 +67,14 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error("Must provide -data-dir") return 1 } - path := filepath.Join(dataDir, agent.SerfKeyring) - if err := initializeKeyring(path, init); err != nil { + + fileLAN := filepath.Join(dataDir, agent.SerfLANKeyring) + if err := initializeKeyring(fileLAN, init); err != nil { + c.Ui.Error(fmt.Sprintf("Error: %s", err)) + return 1 + } + fileWAN := filepath.Join(dataDir, agent.SerfWANKeyring) + if err := initializeKeyring(fileWAN, init); err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return 1 } @@ -84,7 +90,10 @@ func (c *KeyringCommand) Run(args []string) int { } defer client.Close() - // For all key-related operations, we must be querying a server node. + // For all key-related operations, we must be querying a server node. It is + // probably better to enforce this even for LAN pool changes, because other- + // wise, the same exact command syntax will have different results depending + // on where it was run. s, err := client.Stats() if err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) @@ -263,7 +272,7 @@ Options: operation may only be performed on keys which are not currently the primary key. -list List all keys currently in use within the cluster. - -init= Create an initial keyring file for Consul to use + -init= Create the initial keyring files for Consul to use containing the provided key. The -data-dir argument is required with this option. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. From c88780fe1296c2a538dcadaf63443a5759971d7d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 13 Sep 2014 11:47:37 -0700 Subject: [PATCH 113/238] command/keyring: add tests for init --- command/keyring_test.go | 69 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/command/keyring_test.go b/command/keyring_test.go index d3f01ade9..b98c3c597 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -1,6 +1,9 @@ package command import ( + "io/ioutil" + "os" + "path/filepath" "strings" "testing" @@ -84,6 +87,72 @@ func TestKeyringCommandRun_failedConnection(t *testing.T) { } } +func TestKeyCommandRun_initKeyringFail(t *testing.T) { + ui := new(cli.MockUi) + c := &KeyringCommand{Ui: ui} + + // Should error if no data-dir given + args := []string{"-init=HS5lJ+XuTlYKWaeGYyG+/A=="} + code := c.Run(args) + if code != 1 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + // Errors on invalid key + args = []string{"-init=xyz", "-data-dir=/tmp"} + code = c.Run(args) + if code != 1 { + t.Fatalf("should have errored") + } +} + +func TestKeyCommandRun_initKeyring(t *testing.T) { + ui := new(cli.MockUi) + c := &KeyringCommand{Ui: ui} + + tempDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tempDir) + + args := []string{ + "-init=HS5lJ+XuTlYKWaeGYyG+/A==", + "-data-dir=" + tempDir, + } + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + fileLAN := filepath.Join(tempDir, agent.SerfLANKeyring) + fileWAN := filepath.Join(tempDir, agent.SerfWANKeyring) + if _, err := os.Stat(fileLAN); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(fileWAN); err != nil { + t.Fatalf("err: %s", err) + } + + expected := "[\n \"HS5lJ+XuTlYKWaeGYyG+/A==\"\n]" + + contentLAN, err := ioutil.ReadFile(fileLAN) + if err != nil { + t.Fatalf("err: %s", err) + } + if string(contentLAN) != expected { + t.Fatalf("bad: %#v", string(contentLAN)) + } + + contentWAN, err := ioutil.ReadFile(fileWAN) + if err != nil { + t.Fatalf("err: %s", err) + } + if string(contentWAN) != expected { + t.Fatalf("bad: %#v", string(contentWAN)) + } +} + func listKeys(t *testing.T, addr string) string { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} From 986eb0eefed21d41e4fb468993ac80c5ade353d0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 13 Sep 2014 12:12:24 -0700 Subject: [PATCH 114/238] agent: add tests for keyring presence checks --- command/agent/config_test.go | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 13e8634ce..19097e2c3 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -1030,3 +1030,56 @@ func TestReadConfigPaths_dir(t *testing.T) { t.Fatalf("bad: %#v", config) } } + +func TestKeyringFileExists(t *testing.T) { + tempDir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(tempDir) + + fileLAN := filepath.Join(tempDir, SerfLANKeyring) + fileWAN := filepath.Join(tempDir, SerfWANKeyring) + config := &Config{DataDir: tempDir, Server: true} + + // Returns false if we are a server and no keyring files present + if config.keyringFileExists() { + t.Fatalf("should return false") + } + + // Returns false if we are a client and no keyring files present + config.Server = false + if config.keyringFileExists() { + t.Fatalf("should return false") + } + + // Returns true if we are a client and the lan file exists + if err := ioutil.WriteFile(fileLAN, nil, 0600); err != nil { + t.Fatalf("err: %s", err) + } + if !config.keyringFileExists() { + t.Fatalf("should return true") + } + + // Returns true if we are a server and only the lan file exists + config.Server = true + if !config.keyringFileExists() { + t.Fatalf("should return true") + } + + // Returns true if we are a server and both files exist + if err := ioutil.WriteFile(fileWAN, nil, 0600); err != nil { + t.Fatalf("err: %s", err) + } + if !config.keyringFileExists() { + t.Fatalf("should return true") + } + + // Returns true if we are a server and only the wan file exists + if err := os.Remove(fileLAN); err != nil { + t.Fatalf("err: %s", err) + } + if !config.keyringFileExists() { + t.Fatalf("should return true") + } +} From bb06d5ccb80ee58a8e2aeece70a3c927446994b5 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 13 Sep 2014 12:19:21 -0700 Subject: [PATCH 115/238] website: remove keyring persistence options from agent page --- website/source/docs/agent/options.html.markdown | 7 ------- website/source/docs/commands/keyring.html.markdown | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 2a60a4309..01149ef59 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -85,17 +85,10 @@ The options below are all specified on the command-line. it relies on proper configuration. Nodes in the same datacenter should be on a single LAN. -* `-persist-keyring` - This flag enables persistence of changes to the - encryption keys used in the gossip pools. By default, any modifications to - the keyring via the [consul keys](/docs/command/keys.html) command will be - lost when the agent shuts down. - * `-encrypt` - Specifies the secret key to use for encryption of Consul network traffic. This key must be 16-bytes that are base64 encoded. The easiest way to create an encryption key is to use `consul keygen`. All nodes within a cluster must share the same encryption key to communicate. - If keyring persistence is enabled, the given key will only be used if there is - no pre-existing keyring. Otherwise, Consul will emit a warning and continue. * `-join` - Address of another agent to join upon starting up. This can be specified multiple times to specify multiple agents to join. If Consul is diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 2b65241e6..8481b588a 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -32,7 +32,7 @@ be 1. Usage: `consul keyring [options]` Only one actionable argument may be specified per run, including `-init`, -`-list`, `-install`, `-remove`, and `-update`. +`-list`, `-install`, `-remove`, and `-use`. The list of available flags are: From a9f3cbd7f054a275df29341aa5177c3f2eb8212c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 14 Sep 2014 17:31:44 -0700 Subject: [PATCH 116/238] command: various cleanup --- command/agent/agent.go | 3 ++- command/agent/command.go | 20 +++++++++----------- command/agent/config.go | 3 ++- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index b19645f16..169db3276 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -688,7 +688,8 @@ func (a *Agent) deletePid() error { return nil } -// loadKeyringFile will load a keyring out of a file +// loadKeyringFile will load a gossip encryption keyring out of a file. The file +// must be in JSON format and contain a list of encryption key strings. func loadKeyringFile(c *serf.Config) error { if c.KeyringFile == "" { return nil diff --git a/command/agent/command.go b/command/agent/command.go index c02b8135c..f15cea41f 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -67,6 +67,7 @@ func (c *Command) readConfig() *Config { cmdFlags.StringVar(&cmdConfig.UiDir, "ui-dir", "", "path to the web UI directory") cmdFlags.StringVar(&cmdConfig.PidFile, "pid-file", "", "path to file to store PID") cmdFlags.StringVar(&cmdConfig.EncryptKey, "encrypt", "", "gossip encryption key") + cmdFlags.BoolVar(&cmdConfig.Server, "server", false, "run agent as server") cmdFlags.BoolVar(&cmdConfig.Bootstrap, "bootstrap", false, "enable server bootstrap mode") cmdFlags.IntVar(&cmdConfig.BootstrapExpect, "bootstrap-expect", 0, "enable automatic bootstrap via expect mode") @@ -142,6 +143,13 @@ func (c *Command) readConfig() *Config { config.NodeName = hostname } + if config.EncryptKey != "" { + if _, err := config.EncryptBytes(); err != nil { + c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) + return nil + } + } + // Ensure we have a data directory if config.DataDir == "" { c.Ui.Error("Must specify data directory using -data-dir") @@ -172,13 +180,6 @@ func (c *Command) readConfig() *Config { return nil } - if config.EncryptKey != "" { - if _, err := config.EncryptBytes(); err != nil { - c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) - return nil - } - } - // Compile all the watches for _, params := range config.Watches { // Parse the watches, excluding the handler @@ -591,10 +592,7 @@ func (c *Command) Run(args []string) int { } // Determine if gossip is encrypted - gossipEncrypted := false - if config.EncryptKey != "" || config.keyringFileExists() { - gossipEncrypted = true - } + gossipEncrypted := config.EncryptKey != "" || config.keyringFileExists() // Let the agent know we've finished registration c.agent.StartSync() diff --git a/command/agent/config.go b/command/agent/config.go index 69097984d..1188c7be9 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -412,7 +412,8 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { } // keyringFileExists determines if there are encryption key files present -// in the data directory. +// in the data directory. On client nodes, this returns true if a LAN keyring +// is present. On server nodes, it returns true if either keyring file exists. func (c *Config) keyringFileExists() bool { fileLAN := filepath.Join(c.DataDir, SerfLANKeyring) fileWAN := filepath.Join(c.DataDir, SerfWANKeyring) From f9fd1f3f057e559c709a64d6ef070d7749ec849c Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 17 Sep 2014 22:31:32 -0700 Subject: [PATCH 117/238] agent: test loading keyring files for client and server --- command/agent/agent_test.go | 104 +++++++++++++++++++++++++++++++++++ command/agent/config_test.go | 8 +++ 2 files changed, 112 insertions(+) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index a00d5cc11..1e806a9ce 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -1,10 +1,12 @@ package agent import ( + "encoding/json" "fmt" "io" "io/ioutil" "os" + "path/filepath" "sync/atomic" "testing" "time" @@ -71,6 +73,43 @@ func makeAgentLog(t *testing.T, conf *Config, l io.Writer) (string, *Agent) { return dir, agent } +func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { + keyBytes, err := json.Marshal([]string{key}) + if err != nil { + t.Fatalf("err: %s", err) + } + + dir, err := ioutil.TempDir("", "agent") + if err != nil { + t.Fatalf("err: %v", err) + } + + conf.DataDir = dir + + fileLAN := filepath.Join(dir, SerfLANKeyring) + if err := os.MkdirAll(filepath.Dir(fileLAN), 0700); err != nil { + t.Fatalf("err: %s", err) + } + if err := ioutil.WriteFile(fileLAN, keyBytes, 0600); err != nil { + t.Fatalf("err: %s", err) + } + + fileWAN := filepath.Join(dir, SerfWANKeyring) + if err := os.MkdirAll(filepath.Dir(fileWAN), 0700); err != nil { + t.Fatalf("err: %s", err) + } + if err := ioutil.WriteFile(fileWAN, keyBytes, 0600); err != nil { + t.Fatalf("err: %s", err) + } + + agent, err := Create(conf, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + return dir, agent +} + func makeAgent(t *testing.T, conf *Config) (string, *Agent) { return makeAgentLog(t, conf, nil) } @@ -354,3 +393,68 @@ func TestAgent_ConsulService(t *testing.T) { t.Fatalf("%s service should be in sync", consul.ConsulServiceID) } } + +func TestAgent_LoadKeyrings(t *testing.T) { + key := "tbLJg26ZJyJ9pK3qhc9jig==" + + // Should be no configured keyring file by default + conf1 := nextConfig() + dir1, agent1 := makeAgent(t, conf1) + defer os.RemoveAll(dir1) + defer agent1.Shutdown() + + c := agent1.config.ConsulConfig + if c.SerfLANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfLANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } + if c.SerfWANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfWANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } + + // Server should auto-load LAN and WAN keyring files + conf2 := nextConfig() + dir2, agent2 := makeAgentKeyring(t, conf2, key) + defer os.RemoveAll(dir2) + defer agent2.Shutdown() + + c = agent2.config.ConsulConfig + if c.SerfLANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfLANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + if c.SerfWANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfWANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + + // Client should auto-load only the LAN keyring file + conf3 := nextConfig() + conf3.Server = false + dir3, agent3 := makeAgentKeyring(t, conf3, key) + defer os.RemoveAll(dir3) + defer agent3.Shutdown() + + c = agent3.config.ConsulConfig + if c.SerfLANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfLANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + if c.SerfWANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfWANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } +} diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 19097e2c3..52c0c58f7 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -1040,6 +1040,14 @@ func TestKeyringFileExists(t *testing.T) { fileLAN := filepath.Join(tempDir, SerfLANKeyring) fileWAN := filepath.Join(tempDir, SerfWANKeyring) + + if err := os.MkdirAll(filepath.Dir(fileLAN), 0700); err != nil { + t.Fatalf("err: %s", err) + } + if err := os.MkdirAll(filepath.Dir(fileWAN), 0700); err != nil { + t.Fatalf("err: %s", err) + } + config := &Config{DataDir: tempDir, Server: true} // Returns false if we are a server and no keyring files present From 355fc89f7fd57687766ffddc2cfa0700c3843d4a Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 17 Sep 2014 23:28:39 -0700 Subject: [PATCH 118/238] command: test generated keyring file content and conflicting args for agent --- command/agent/agent_test.go | 17 ++------------ command/agent/command_test.go | 36 +++++++++++++++++++++++++++++ command/keyring.go | 10 ++++---- command/keyring_test.go | 2 +- testutil/keyring.go | 43 +++++++++++++++++++++++++++++++++++ testutil/keyring_test.go | 42 ++++++++++++++++++++++++++++++++++ 6 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 testutil/keyring.go create mode 100644 testutil/keyring_test.go diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 1e806a9ce..8240b4854 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -1,7 +1,6 @@ package agent import ( - "encoding/json" "fmt" "io" "io/ioutil" @@ -74,11 +73,6 @@ func makeAgentLog(t *testing.T, conf *Config, l io.Writer) (string, *Agent) { } func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { - keyBytes, err := json.Marshal([]string{key}) - if err != nil { - t.Fatalf("err: %s", err) - } - dir, err := ioutil.TempDir("", "agent") if err != nil { t.Fatalf("err: %v", err) @@ -87,18 +81,11 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { conf.DataDir = dir fileLAN := filepath.Join(dir, SerfLANKeyring) - if err := os.MkdirAll(filepath.Dir(fileLAN), 0700); err != nil { + if err := testutil.InitKeyring(fileLAN, key); err != nil { t.Fatalf("err: %s", err) } - if err := ioutil.WriteFile(fileLAN, keyBytes, 0600); err != nil { - t.Fatalf("err: %s", err) - } - fileWAN := filepath.Join(dir, SerfWANKeyring) - if err := os.MkdirAll(filepath.Dir(fileWAN), 0700); err != nil { - t.Fatalf("err: %s", err) - } - if err := ioutil.WriteFile(fileWAN, keyBytes, 0600); err != nil { + if err := testutil.InitKeyring(fileWAN, key); err != nil { t.Fatalf("err: %s", err) } diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 10557daa7..db7d1c9a5 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -1,10 +1,17 @@ package agent import ( +<<<<<<< HEAD "fmt" "io/ioutil" "log" "os" +======= + "github.com/hashicorp/consul/testutil" + "github.com/mitchellh/cli" + "io/ioutil" + "path/filepath" +>>>>>>> command: test generated keyring file content and conflicting args for agent "testing" "github.com/hashicorp/consul/testutil" @@ -38,6 +45,7 @@ func TestValidDatacenter(t *testing.T) { } } +<<<<<<< HEAD func TestRetryJoin(t *testing.T) { dir, agent := makeAgent(t, nextConfig()) defer os.RemoveAll(dir) @@ -161,5 +169,33 @@ func TestRetryJoinWanFail(t *testing.T) { if code := cmd.Run(args); code == 0 { t.Fatalf("bad: %d", code) +======= +func TestArgConflict(t *testing.T) { + ui := new(cli.MockUi) + c := &Command{Ui: ui} + + dir, err := ioutil.TempDir("", "agent") + if err != nil { + t.Fatalf("err: %s", err) + } + + key := "HS5lJ+XuTlYKWaeGYyG+/A==" + + fileLAN := filepath.Join(dir, SerfLANKeyring) + if err := testutil.InitKeyring(fileLAN, key); err != nil { + t.Fatalf("err: %s", err) + } + + args := []string{ + "-encrypt=" + key, + "-data-dir=" + dir, + } + code := c.Run(args) + if code != 1 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + if !strings.Contains(ui.ErrorWriter.String(), "keyring files exist") { + t.Fatalf("bad: %#v", ui.ErrorWriter.String()) +>>>>>>> command: test generated keyring file content and conflicting args for agent } } diff --git a/command/keyring.go b/command/keyring.go index aed55c039..83293a644 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -69,12 +69,12 @@ func (c *KeyringCommand) Run(args []string) int { } fileLAN := filepath.Join(dataDir, agent.SerfLANKeyring) - if err := initializeKeyring(fileLAN, init); err != nil { + if err := initKeyring(fileLAN, init); err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return 1 } fileWAN := filepath.Join(dataDir, agent.SerfWANKeyring) - if err := initializeKeyring(fileWAN, init); err != nil { + if err := initKeyring(fileWAN, init); err != nil { c.Ui.Error(fmt.Sprintf("Error: %s", err)) return 1 } @@ -214,14 +214,14 @@ func (c *KeyringCommand) listKeysOperation(fn listKeysFunc) int { return 0 } -// initializeKeyring will create a keyring file at a given path. -func initializeKeyring(path, key string) error { +// initKeyring will create a keyring file at a given path. +func initKeyring(path, key string) error { if _, err := base64.StdEncoding.DecodeString(key); err != nil { return fmt.Errorf("Invalid key: %s", err) } keys := []string{key} - keyringBytes, err := json.MarshalIndent(keys, "", " ") + keyringBytes, err := json.Marshal(keys) if err != nil { return err } diff --git a/command/keyring_test.go b/command/keyring_test.go index b98c3c597..cc18ad799 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -134,7 +134,7 @@ func TestKeyCommandRun_initKeyring(t *testing.T) { t.Fatalf("err: %s", err) } - expected := "[\n \"HS5lJ+XuTlYKWaeGYyG+/A==\"\n]" + expected := `["HS5lJ+XuTlYKWaeGYyG+/A=="]` contentLAN, err := ioutil.ReadFile(fileLAN) if err != nil { diff --git a/testutil/keyring.go b/testutil/keyring.go new file mode 100644 index 000000000..60486bbb8 --- /dev/null +++ b/testutil/keyring.go @@ -0,0 +1,43 @@ +package testutil + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +// InitKeyring will create a keyring file at a given path. +func InitKeyring(path, key string) error { + if _, err := base64.StdEncoding.DecodeString(key); err != nil { + return fmt.Errorf("Invalid key: %s", err) + } + + keys := []string{key} + keyringBytes, err := json.Marshal(keys) + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + + if _, err := os.Stat(path); err == nil { + return fmt.Errorf("File already exists: %s", path) + } + + fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fh.Close() + + if _, err := fh.Write(keyringBytes); err != nil { + os.Remove(path) + return err + } + + return nil +} diff --git a/testutil/keyring_test.go b/testutil/keyring_test.go new file mode 100644 index 000000000..7e5b3e63f --- /dev/null +++ b/testutil/keyring_test.go @@ -0,0 +1,42 @@ +package testutil + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestAgent_InitKeyring(t *testing.T) { + key := "tbLJg26ZJyJ9pK3qhc9jig==" + + dir, err := ioutil.TempDir("", "agent") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(dir) + keyFile := filepath.Join(dir, "test/keyring") + + if err := InitKeyring(keyFile, key); err != nil { + t.Fatalf("err: %s", err) + } + + fi, err := os.Stat(filepath.Dir(keyFile)) + if err != nil { + t.Fatalf("err: %s", err) + } + + if !fi.IsDir() { + t.Fatalf("bad: %#v", fi) + } + + data, err := ioutil.ReadFile(keyFile) + if err != nil { + t.Fatalf("err: %s", err) + } + + expected := `["tbLJg26ZJyJ9pK3qhc9jig=="]` + if string(data) != expected { + t.Fatalf("bad: %#v", string(data)) + } +} From 9a7a7c10d4a71fcddb8d1f834061aace77972e14 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 18 Sep 2014 18:57:18 -0700 Subject: [PATCH 119/238] website: documentation updates for keyring command --- .../docs/commands/keyring.html.markdown | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 8481b588a..ff3285dc6 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -10,12 +10,14 @@ Command: `consul keyring` The `keyring` command is used to examine and modify the encryption keys used in Consul's [Gossip Pools](/docs/internals/gossip.html). It is capable of -distributing new encryption keys to the cluster, revoking old encryption keys, -and changing the key used by the cluster to encrypt messages. +distributing new encryption keys to the cluster, retiring old encryption keys, +and changing the keys used by the cluster to encrypt messages. Because Consul utilizes multiple gossip pools, this command will only operate -against a server node for most operations. The only operation which may be used -on client machines is the `-init` argument for initial key configuration. +against a server node for most operations. All members in a Consul cluster, +regardless of operational mode (client or server) or datacenter, will be +modified/queried each time this command is run. This helps maintain operational +simplicity by managing the multiple pools as a single unit. Consul allows multiple encryption keys to be in use simultaneously. This is intended to provide a transition state while the cluster converges. It is the @@ -23,9 +25,9 @@ responsibility of the operator to ensure that only the required encryption keys are installed on the cluster. You can ensure that a key is not installed using the `-list` and `-remove` options. -All variations of the keys command will return 0 if all nodes reply and there -are no errors. If any node fails to reply or reports failure, the exit code will -be 1. +All variations of the `keyring` command, unless otherwise specified below, will +return 0 if all nodes reply and there are no errors. If any node fails to reply +or reports failure, the exit code will be 1. ## Usage @@ -44,6 +46,9 @@ The list of available flags are: This operation can be run on both client and server nodes and requires no network connectivity. + Returns 0 if the key is successfully configured, or 1 if there were any + problems. + * `-install` - Install a new encryption key. This will broadcast the new key to all members in the cluster. From 72fc1ceeadbfd6fbb8438cfec09105c98a9382b0 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 21 Sep 2014 11:21:54 -0700 Subject: [PATCH 120/238] agent: split keyring functionality out of agent.go --- command/agent/agent.go | 130 -------------------------------- command/agent/keyring.go | 138 ++++++++++++++++++++++++++++++++++ command/agent/keyring_test.go | 71 +++++++++++++++++ 3 files changed, 209 insertions(+), 130 deletions(-) create mode 100644 command/agent/keyring.go create mode 100644 command/agent/keyring_test.go diff --git a/command/agent/agent.go b/command/agent/agent.go index 169db3276..f8b477147 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -1,11 +1,8 @@ package agent import ( - "encoding/base64" - "encoding/json" "fmt" "io" - "io/ioutil" "log" "net" "os" @@ -15,15 +12,9 @@ import ( "github.com/hashicorp/consul/consul" "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/memberlist" "github.com/hashicorp/serf/serf" ) -const ( - SerfLANKeyring = "serf/local.keyring" - SerfWANKeyring = "serf/remote.keyring" -) - /* The agent is the long running process that is run on every machine. It exposes an RPC interface that is used by the CLI to control the @@ -687,124 +678,3 @@ func (a *Agent) deletePid() error { } return nil } - -// loadKeyringFile will load a gossip encryption keyring out of a file. The file -// must be in JSON format and contain a list of encryption key strings. -func loadKeyringFile(c *serf.Config) error { - if c.KeyringFile == "" { - return nil - } - - if _, err := os.Stat(c.KeyringFile); err != nil { - return err - } - - // Read in the keyring file data - keyringData, err := ioutil.ReadFile(c.KeyringFile) - if err != nil { - return err - } - - // Decode keyring JSON - keys := make([]string, 0) - if err := json.Unmarshal(keyringData, &keys); err != nil { - return err - } - - // Decode base64 values - keysDecoded := make([][]byte, len(keys)) - for i, key := range keys { - keyBytes, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return err - } - keysDecoded[i] = keyBytes - } - - // Create the keyring - keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0]) - if err != nil { - return err - } - - c.MemberlistConfig.Keyring = keyring - - // Success! - return nil -} - -// ListKeysLAN returns the keys installed on the LAN gossip pool -func (a *Agent) ListKeysLAN() (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.ListKeys() - } - km := a.client.KeyManagerLAN() - return km.ListKeys() -} - -// ListKeysWAN returns the keys installed on the WAN gossip pool -func (a *Agent) ListKeysWAN() (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.ListKeys() - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// InstallKeyWAN installs a new WAN gossip encryption key on server nodes -func (a *Agent) InstallKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.InstallKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// InstallKeyLAN installs a new LAN gossip encryption key on all nodes -func (a *Agent) InstallKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.InstallKey(key) - } - km := a.client.KeyManagerLAN() - return km.InstallKey(key) -} - -// UseKeyWAN changes the primary WAN gossip encryption key on server nodes -func (a *Agent) UseKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.UseKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// UseKeyLAN changes the primary LAN gossip encryption key on all nodes -func (a *Agent) UseKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.UseKey(key) - } - km := a.client.KeyManagerLAN() - return km.UseKey(key) -} - -// RemoveKeyWAN removes a WAN gossip encryption key on server nodes -func (a *Agent) RemoveKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.RemoveKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// RemoveKeyLAN removes a LAN gossip encryption key on all nodes -func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.RemoveKey(key) - } - km := a.client.KeyManagerLAN() - return km.RemoveKey(key) -} diff --git a/command/agent/keyring.go b/command/agent/keyring.go new file mode 100644 index 000000000..dc63b4f59 --- /dev/null +++ b/command/agent/keyring.go @@ -0,0 +1,138 @@ +package agent + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/hashicorp/memberlist" + "github.com/hashicorp/serf/serf" +) + +const ( + SerfLANKeyring = "serf/local.keyring" + SerfWANKeyring = "serf/remote.keyring" +) + +// loadKeyringFile will load a gossip encryption keyring out of a file. The file +// must be in JSON format and contain a list of encryption key strings. +func loadKeyringFile(c *serf.Config) error { + if c.KeyringFile == "" { + return nil + } + + if _, err := os.Stat(c.KeyringFile); err != nil { + return err + } + + // Read in the keyring file data + keyringData, err := ioutil.ReadFile(c.KeyringFile) + if err != nil { + return err + } + + // Decode keyring JSON + keys := make([]string, 0) + if err := json.Unmarshal(keyringData, &keys); err != nil { + return err + } + + // Decode base64 values + keysDecoded := make([][]byte, len(keys)) + for i, key := range keys { + keyBytes, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + keysDecoded[i] = keyBytes + } + + // Create the keyring + keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0]) + if err != nil { + return err + } + + c.MemberlistConfig.Keyring = keyring + + // Success! + return nil +} + +// ListKeysLAN returns the keys installed on the LAN gossip pool +func (a *Agent) ListKeysLAN() (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.ListKeys() + } + km := a.client.KeyManagerLAN() + return km.ListKeys() +} + +// ListKeysWAN returns the keys installed on the WAN gossip pool +func (a *Agent) ListKeysWAN() (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.ListKeys() + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// InstallKeyWAN installs a new WAN gossip encryption key on server nodes +func (a *Agent) InstallKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.InstallKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// InstallKeyLAN installs a new LAN gossip encryption key on all nodes +func (a *Agent) InstallKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.InstallKey(key) + } + km := a.client.KeyManagerLAN() + return km.InstallKey(key) +} + +// UseKeyWAN changes the primary WAN gossip encryption key on server nodes +func (a *Agent) UseKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.UseKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// UseKeyLAN changes the primary LAN gossip encryption key on all nodes +func (a *Agent) UseKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.UseKey(key) + } + km := a.client.KeyManagerLAN() + return km.UseKey(key) +} + +// RemoveKeyWAN removes a WAN gossip encryption key on server nodes +func (a *Agent) RemoveKeyWAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerWAN() + return km.RemoveKey(key) + } + return nil, fmt.Errorf("WAN keyring not available on client node") +} + +// RemoveKeyLAN removes a LAN gossip encryption key on all nodes +func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { + if a.server != nil { + km := a.server.KeyManagerLAN() + return km.RemoveKey(key) + } + km := a.client.KeyManagerLAN() + return km.RemoveKey(key) +} diff --git a/command/agent/keyring_test.go b/command/agent/keyring_test.go new file mode 100644 index 000000000..7807e5376 --- /dev/null +++ b/command/agent/keyring_test.go @@ -0,0 +1,71 @@ +package agent + +import ( + "os" + "testing" +) + +func TestAgent_LoadKeyrings(t *testing.T) { + key := "tbLJg26ZJyJ9pK3qhc9jig==" + + // Should be no configured keyring file by default + conf1 := nextConfig() + dir1, agent1 := makeAgent(t, conf1) + defer os.RemoveAll(dir1) + defer agent1.Shutdown() + + c := agent1.config.ConsulConfig + if c.SerfLANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfLANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } + if c.SerfWANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfWANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } + + // Server should auto-load LAN and WAN keyring files + conf2 := nextConfig() + dir2, agent2 := makeAgentKeyring(t, conf2, key) + defer os.RemoveAll(dir2) + defer agent2.Shutdown() + + c = agent2.config.ConsulConfig + if c.SerfLANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfLANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + if c.SerfWANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfWANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + + // Client should auto-load only the LAN keyring file + conf3 := nextConfig() + conf3.Server = false + dir3, agent3 := makeAgentKeyring(t, conf3, key) + defer os.RemoveAll(dir3) + defer agent3.Shutdown() + + c = agent3.config.ConsulConfig + if c.SerfLANConfig.KeyringFile == "" { + t.Fatalf("should have keyring file") + } + if c.SerfLANConfig.MemberlistConfig.Keyring == nil { + t.Fatalf("keyring should be loaded") + } + if c.SerfWANConfig.KeyringFile != "" { + t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + } + if c.SerfWANConfig.MemberlistConfig.Keyring != nil { + t.Fatalf("keyring should not be loaded") + } +} From 03012e8ac652ce5d1201031d112142fef19e1482 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 21 Sep 2014 11:52:28 -0700 Subject: [PATCH 121/238] command: allow wan ring to be modified separately from lan pools --- command/agent/command_test.go | 13 ++-- command/keyring.go | 72 +++++++++++-------- command/keyring_test.go | 65 +++++++++++++---- .../docs/commands/keyring.html.markdown | 9 +-- 4 files changed, 103 insertions(+), 56 deletions(-) diff --git a/command/agent/command_test.go b/command/agent/command_test.go index db7d1c9a5..9c1bf4db5 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -1,17 +1,12 @@ package agent import ( -<<<<<<< HEAD "fmt" "io/ioutil" "log" "os" -======= - "github.com/hashicorp/consul/testutil" - "github.com/mitchellh/cli" - "io/ioutil" "path/filepath" ->>>>>>> command: test generated keyring file content and conflicting args for agent + "strings" "testing" "github.com/hashicorp/consul/testutil" @@ -45,7 +40,6 @@ func TestValidDatacenter(t *testing.T) { } } -<<<<<<< HEAD func TestRetryJoin(t *testing.T) { dir, agent := makeAgent(t, nextConfig()) defer os.RemoveAll(dir) @@ -169,7 +163,9 @@ func TestRetryJoinWanFail(t *testing.T) { if code := cmd.Run(args); code == 0 { t.Fatalf("bad: %d", code) -======= + } +} + func TestArgConflict(t *testing.T) { ui := new(cli.MockUi) c := &Command{Ui: ui} @@ -196,6 +192,5 @@ func TestArgConflict(t *testing.T) { } if !strings.Contains(ui.ErrorWriter.String(), "keyring files exist") { t.Fatalf("bad: %#v", ui.ErrorWriter.String()) ->>>>>>> command: test generated keyring file content and conflicting args for agent } } diff --git a/command/keyring.go b/command/keyring.go index 83293a644..630f0ba12 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -22,7 +22,7 @@ type KeyringCommand struct { func (c *KeyringCommand) Run(args []string) int { var installKey, useKey, removeKey, init, dataDir string - var listKeys bool + var listKeys, wan bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -33,6 +33,7 @@ func (c *KeyringCommand) Run(args []string) int { cmdFlags.BoolVar(&listKeys, "list", false, "list keys") cmdFlags.StringVar(&init, "init", "", "initialize keyring") cmdFlags.StringVar(&dataDir, "data-dir", "", "data directory") + cmdFlags.BoolVar(&wan, "wan", false, "operate on wan keyring") rpcAddr := RPCAddrFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -105,51 +106,57 @@ func (c *KeyringCommand) Run(args []string) int { } if listKeys { - c.Ui.Info("Asking all WAN members for installed keys...") - if rval := c.listKeysOperation(client.ListKeysWAN); rval != 0 { - return rval + if wan { + c.Ui.Info("Asking all WAN members for installed keys...") + return c.listKeysOperation(client.ListKeysWAN) } c.Ui.Info("Asking all LAN members for installed keys...") - if rval := c.listKeysOperation(client.ListKeysLAN); rval != 0 { - return rval - } - return 0 + return c.listKeysOperation(client.ListKeysLAN) } if installKey != "" { - c.Ui.Info("Installing new WAN gossip encryption key...") - if rval := c.keyOperation(installKey, client.InstallKeyWAN); rval != 0 { - return rval - } - c.Ui.Info("Installing new LAN gossip encryption key...") - if rval := c.keyOperation(installKey, client.InstallKeyLAN); rval != 0 { - return rval + if wan { + c.Ui.Info("Installing new WAN gossip encryption key...") + if rval := c.keyOperation(installKey, client.InstallKeyWAN); rval != 0 { + return rval + } + } else { + c.Ui.Info("Installing new LAN gossip encryption key...") + if rval := c.keyOperation(installKey, client.InstallKeyLAN); rval != 0 { + return rval + } } c.Ui.Info("Successfully installed key!") return 0 } if useKey != "" { - c.Ui.Info("Changing primary WAN gossip encryption key...") - if rval := c.keyOperation(useKey, client.UseKeyWAN); rval != 0 { - return rval - } - c.Ui.Info("Changing primary LAN gossip encryption key...") - if rval := c.keyOperation(useKey, client.UseKeyLAN); rval != 0 { - return rval + if wan { + c.Ui.Info("Changing primary WAN gossip encryption key...") + if rval := c.keyOperation(useKey, client.UseKeyWAN); rval != 0 { + return rval + } + } else { + c.Ui.Info("Changing primary LAN gossip encryption key...") + if rval := c.keyOperation(useKey, client.UseKeyLAN); rval != 0 { + return rval + } } c.Ui.Info("Successfully changed primary key!") return 0 } if removeKey != "" { - c.Ui.Info("Removing WAN gossip encryption key...") - if rval := c.keyOperation(removeKey, client.RemoveKeyWAN); rval != 0 { - return rval - } - c.Ui.Info("Removing LAN gossip encryption key...") - if rval := c.keyOperation(removeKey, client.RemoveKeyLAN); rval != 0 { - return rval + if wan { + c.Ui.Info("Removing WAN gossip encryption key...") + if rval := c.keyOperation(removeKey, client.RemoveKeyWAN); rval != 0 { + return rval + } + } else { + c.Ui.Info("Removing LAN gossip encryption key...") + if rval := c.keyOperation(removeKey, client.RemoveKeyLAN); rval != 0 { + return rval + } } c.Ui.Info("Successfully removed key!") return 0 @@ -258,8 +265,9 @@ Usage: consul keyring [options] functionality provides the ability to perform key rotation cluster-wide, without disrupting the cluster. - With the exception of the -init argument, all other operations performed by - this command can only be run against server nodes. + With the exception of the -init argument, all operations performed by this + command can only be run against server nodes. All operations default to the + LAN gossip pool. Options: @@ -275,6 +283,8 @@ Options: -init= Create the initial keyring files for Consul to use containing the provided key. The -data-dir argument is required with this option. + -wan Operate on the WAN keyring instead of the LAN + keyring (default). -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) diff --git a/command/keyring_test.go b/command/keyring_test.go index cc18ad799..c7c0847f4 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -25,7 +25,7 @@ func TestKeyringCommandRun(t *testing.T) { defer a1.Shutdown() // The keyring was initialized with only the provided key - out := listKeys(t, a1.addr) + out := listKeys(t, a1.addr, false) if !strings.Contains(out, key1) { t.Fatalf("bad: %#v", out) } @@ -34,30 +34,56 @@ func TestKeyringCommandRun(t *testing.T) { } // Install the second key onto the keyring - installKey(t, a1.addr, key2) + installKey(t, a1.addr, key2, false) // Both keys should be present - out = listKeys(t, a1.addr) + out = listKeys(t, a1.addr, false) for _, key := range []string{key1, key2} { if !strings.Contains(out, key) { t.Fatalf("bad: %#v", out) } } + // WAN keyring is untouched + out = listKeys(t, a1.addr, true) + if strings.Contains(out, key2) { + t.Fatalf("bad: %#v", out) + } + // Change out the primary key - useKey(t, a1.addr, key2) + useKey(t, a1.addr, key2, false) // Remove the original key - removeKey(t, a1.addr, key1) + removeKey(t, a1.addr, key1, false) // Make sure only the new key is present - out = listKeys(t, a1.addr) + out = listKeys(t, a1.addr, false) if strings.Contains(out, key1) { t.Fatalf("bad: %#v", out) } if !strings.Contains(out, key2) { t.Fatalf("bad: %#v", out) } + + // WAN keyring is still untouched + out = listKeys(t, a1.addr, true) + if !strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } + + // Rotate out the WAN key + installKey(t, a1.addr, key2, true) + useKey(t, a1.addr, key2, true) + removeKey(t, a1.addr, key1, true) + + // WAN keyring now has only the proper key + out = listKeys(t, a1.addr, true) + if !strings.Contains(out, key2) { + t.Fatalf("bad: %#v", out) + } + if strings.Contains(out, key1) { + t.Fatalf("bad: %#v", out) + } } func TestKeyringCommandRun_help(t *testing.T) { @@ -87,7 +113,7 @@ func TestKeyringCommandRun_failedConnection(t *testing.T) { } } -func TestKeyCommandRun_initKeyringFail(t *testing.T) { +func TestKeyringCommandRun_initKeyringFail(t *testing.T) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} @@ -106,7 +132,7 @@ func TestKeyCommandRun_initKeyringFail(t *testing.T) { } } -func TestKeyCommandRun_initKeyring(t *testing.T) { +func TestKeyringCommandRun_initKeyring(t *testing.T) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} @@ -153,11 +179,14 @@ func TestKeyCommandRun_initKeyring(t *testing.T) { } } -func listKeys(t *testing.T, addr string) string { +func listKeys(t *testing.T, addr string, wan bool) string { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-list", "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } code := c.Run(args) if code != 0 { @@ -167,33 +196,45 @@ func listKeys(t *testing.T, addr string) string { return ui.OutputWriter.String() } -func installKey(t *testing.T, addr string, key string) { +func installKey(t *testing.T, addr string, key string, wan bool) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-install=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func useKey(t *testing.T, addr string, key string) { +func useKey(t *testing.T, addr string, key string, wan bool) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-use=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func removeKey(t *testing.T, addr string, key string) { +func removeKey(t *testing.T, addr string, key string, wan bool) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-remove=" + key, "-rpc-addr=" + addr} + if wan { + args = append(args, "-wan") + } + code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index ff3285dc6..6a635746c 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -14,10 +14,9 @@ distributing new encryption keys to the cluster, retiring old encryption keys, and changing the keys used by the cluster to encrypt messages. Because Consul utilizes multiple gossip pools, this command will only operate -against a server node for most operations. All members in a Consul cluster, -regardless of operational mode (client or server) or datacenter, will be -modified/queried each time this command is run. This helps maintain operational -simplicity by managing the multiple pools as a single unit. +against a server node for most operations. By default, all operations carried +out by this command are run against the LAN gossip pool in the datacenter of the +agent. Consul allows multiple encryption keys to be in use simultaneously. This is intended to provide a transition state while the cluster converges. It is the @@ -60,4 +59,6 @@ The list of available flags are: * `-list` - List all keys currently in use within the cluster. +* `-wan` - Operate on the WAN keyring instead of the LAN keyring (default) + * `-rpc-addr` - RPC address of the Consul agent. From a551a6e4a0bf0df7a941f866b8d08d1153f8bd63 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 24 Sep 2014 16:39:14 -0700 Subject: [PATCH 122/238] consul: refactor keyring, repeat RPC calls to all DC's --- command/agent/keyring.go | 101 +++++++----------- command/agent/rpc.go | 128 ++++++++-------------- command/agent/rpc_client.go | 66 +++++------- command/keyring.go | 49 +++------ consul/internal_endpoint.go | 205 ++++++++++++++++++++++++++++++++++++ consul/rpc.go | 12 +++ consul/structs/structs.go | 19 ++++ 7 files changed, 354 insertions(+), 226 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index dc63b4f59..e816c3d78 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" + "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/memberlist" "github.com/hashicorp/serf/serf" ) @@ -61,78 +62,48 @@ func loadKeyringFile(c *serf.Config) error { return nil } -// ListKeysLAN returns the keys installed on the LAN gossip pool -func (a *Agent) ListKeysLAN() (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.ListKeys() +// keyringProcess is used to abstract away the semantic similarities in +// performing various operations on the encryption keyring. +func (a *Agent) keyringProcess( + method string, + args *structs.KeyringRequest) (*structs.KeyringResponse, error) { + + var reply structs.KeyringResponse + if a.server == nil { + return nil, fmt.Errorf("keyring operations must run against a server node") } - km := a.client.KeyManagerLAN() - return km.ListKeys() + if err := a.RPC(method, args, &reply); err != nil { + return &reply, err + } + + return &reply, nil } -// ListKeysWAN returns the keys installed on the WAN gossip pool -func (a *Agent) ListKeysWAN() (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.ListKeys() - } - return nil, fmt.Errorf("WAN keyring not available on client node") +// ListKeys lists out all keys installed on the collective Consul cluster. This +// includes both servers and clients in all DC's. +func (a *Agent) ListKeys() (*structs.KeyringResponse, error) { + args := structs.KeyringRequest{} + args.AllowStale = true + return a.keyringProcess("Internal.ListKeys", &args) } -// InstallKeyWAN installs a new WAN gossip encryption key on server nodes -func (a *Agent) InstallKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.InstallKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") +// InstallKey installs a new gossip encryption key +func (a *Agent) InstallKey(key string) (*structs.KeyringResponse, error) { + args := structs.KeyringRequest{Key: key} + args.AllowStale = true + return a.keyringProcess("Internal.InstallKey", &args) } -// InstallKeyLAN installs a new LAN gossip encryption key on all nodes -func (a *Agent) InstallKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.InstallKey(key) - } - km := a.client.KeyManagerLAN() - return km.InstallKey(key) +// UseKey changes the primary encryption key used to encrypt messages +func (a *Agent) UseKey(key string) (*structs.KeyringResponse, error) { + args := structs.KeyringRequest{Key: key} + args.AllowStale = true + return a.keyringProcess("Internal.UseKey", &args) } -// UseKeyWAN changes the primary WAN gossip encryption key on server nodes -func (a *Agent) UseKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.UseKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// UseKeyLAN changes the primary LAN gossip encryption key on all nodes -func (a *Agent) UseKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.UseKey(key) - } - km := a.client.KeyManagerLAN() - return km.UseKey(key) -} - -// RemoveKeyWAN removes a WAN gossip encryption key on server nodes -func (a *Agent) RemoveKeyWAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerWAN() - return km.RemoveKey(key) - } - return nil, fmt.Errorf("WAN keyring not available on client node") -} - -// RemoveKeyLAN removes a LAN gossip encryption key on all nodes -func (a *Agent) RemoveKeyLAN(key string) (*serf.KeyResponse, error) { - if a.server != nil { - km := a.server.KeyManagerLAN() - return km.RemoveKey(key) - } - km := a.client.KeyManagerLAN() - return km.RemoveKey(key) +// RemoveKey will remove a gossip encryption key from the keyring +func (a *Agent) RemoveKey(key string) (*structs.KeyringResponse, error) { + args := structs.KeyringRequest{Key: key} + args.AllowStale = true + return a.keyringProcess("Internal.RemoveKey", &args) } diff --git a/command/agent/rpc.go b/command/agent/rpc.go index bf9b42d52..983cc3481 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -24,15 +24,17 @@ package agent import ( "bufio" "fmt" - "github.com/hashicorp/go-msgpack/codec" - "github.com/hashicorp/logutils" - "github.com/hashicorp/serf/serf" "io" "log" "net" "os" "strings" "sync" + + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/go-msgpack/codec" + "github.com/hashicorp/logutils" + "github.com/hashicorp/serf/serf" ) const ( @@ -41,24 +43,20 @@ const ( ) const ( - handshakeCommand = "handshake" - forceLeaveCommand = "force-leave" - joinCommand = "join" - membersLANCommand = "members-lan" - membersWANCommand = "members-wan" - stopCommand = "stop" - monitorCommand = "monitor" - leaveCommand = "leave" - statsCommand = "stats" - reloadCommand = "reload" - listKeysLANCommand = "list-keys-lan" - listKeysWANCommand = "list-keys-wan" - installKeyLANCommand = "install-key-lan" - installKeyWANCommand = "install-key-wan" - useKeyLANCommand = "use-key-lan" - useKeyWANCommand = "use-key-wan" - removeKeyLANCommand = "remove-key-lan" - removeKeyWANCommand = "remove-key-wan" + handshakeCommand = "handshake" + forceLeaveCommand = "force-leave" + joinCommand = "join" + membersLANCommand = "members-lan" + membersWANCommand = "members-wan" + stopCommand = "stop" + monitorCommand = "monitor" + leaveCommand = "leave" + statsCommand = "stats" + reloadCommand = "reload" + installKeyCommand = "install-key" + useKeyCommand = "use-key" + removeKeyCommand = "remove-key" + listKeysCommand = "list-keys" ) const ( @@ -117,10 +115,10 @@ type keyRequest struct { type keyResponse struct { Messages map[string]string + Keys map[string]int NumNodes int NumResp int NumErr int - Keys map[string]int } type membersResponse struct { @@ -393,17 +391,8 @@ func (i *AgentRPC) handleRequest(client *rpcClient, reqHeader *requestHeader) er case reloadCommand: return i.handleReload(client, seq) - case listKeysLANCommand, listKeysWANCommand: - return i.handleListKeys(client, seq, command) - - case installKeyLANCommand, installKeyWANCommand: - return i.handleGossipKeyChange(client, seq, command) - - case useKeyLANCommand, useKeyWANCommand: - return i.handleGossipKeyChange(client, seq, command) - - case removeKeyLANCommand, removeKeyWANCommand: - return i.handleGossipKeyChange(client, seq, command) + case installKeyCommand, useKeyCommand, removeKeyCommand, listKeysCommand: + return i.handleKeyring(client, seq, command) default: respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} @@ -615,56 +604,27 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { return client.Send(&resp, nil) } -func (i *AgentRPC) handleListKeys(client *rpcClient, seq uint64, cmd string) error { - var queryResp *serf.KeyResponse - var err error - - switch cmd { - case listKeysWANCommand: - queryResp, err = i.agent.ListKeysWAN() - default: - queryResp, err = i.agent.ListKeysLAN() - } - - header := responseHeader{ - Seq: seq, - Error: errToString(err), - } - - resp := keyResponse{ - Messages: queryResp.Messages, - Keys: queryResp.Keys, - NumResp: queryResp.NumResp, - NumErr: queryResp.NumErr, - NumNodes: queryResp.NumNodes, - } - - return client.Send(&header, &resp) -} - -func (i *AgentRPC) handleGossipKeyChange(client *rpcClient, seq uint64, cmd string) error { +func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error { var req keyRequest + var queryResp *structs.KeyringResponse var resp keyResponse - var queryResp *serf.KeyResponse var err error - if err = client.dec.Decode(&req); err != nil { - return fmt.Errorf("decode failed: %v", err) + if cmd != listKeysCommand { + if err = client.dec.Decode(&req); err != nil { + return fmt.Errorf("decode failed: %v", err) + } } switch cmd { - case installKeyWANCommand: - queryResp, err = i.agent.InstallKeyWAN(req.Key) - case installKeyLANCommand: - queryResp, err = i.agent.InstallKeyLAN(req.Key) - case useKeyWANCommand: - queryResp, err = i.agent.UseKeyWAN(req.Key) - case useKeyLANCommand: - queryResp, err = i.agent.UseKeyLAN(req.Key) - case removeKeyWANCommand: - queryResp, err = i.agent.RemoveKeyWAN(req.Key) - case removeKeyLANCommand: - queryResp, err = i.agent.RemoveKeyLAN(req.Key) + case listKeysCommand: + queryResp, err = i.agent.ListKeys() + case installKeyCommand: + queryResp, err = i.agent.InstallKey(req.Key) + case useKeyCommand: + queryResp, err = i.agent.UseKey(req.Key) + case removeKeyCommand: + queryResp, err = i.agent.RemoveKey(req.Key) default: respHeader := responseHeader{Seq: seq, Error: unsupportedCommand} client.Send(&respHeader, nil) @@ -676,15 +636,17 @@ func (i *AgentRPC) handleGossipKeyChange(client *rpcClient, seq uint64, cmd stri Error: errToString(err), } - resp = keyResponse{ - Messages: queryResp.Messages, - Keys: queryResp.Keys, - NumResp: queryResp.NumResp, - NumErr: queryResp.NumErr, - NumNodes: queryResp.NumNodes, + if queryResp != nil { + resp = keyResponse{ + Messages: queryResp.Messages, + Keys: queryResp.Keys, + NumNodes: queryResp.NumNodes, + NumResp: queryResp.NumResp, + NumErr: queryResp.NumErr, + } } - return client.Send(&header, &resp) + return client.Send(&header, resp) } // Used to convert an error to a string representation diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 5d82dc8fa..36a54057e 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -176,60 +176,44 @@ func (c *RPCClient) WANMembers() ([]Member, error) { return resp.Members, err } -func (c *RPCClient) ListKeysLAN() (map[string]int, int, map[string]string, error) { +func (c *RPCClient) ListKeys() (map[string]int, int, map[string]string, error) { header := requestHeader{ - Command: listKeysLANCommand, + Command: listKeysCommand, Seq: c.getSeq(), } resp := new(keyResponse) - err := c.genericRPC(&header, nil, resp) return resp.Keys, resp.NumNodes, resp.Messages, err } -func (c *RPCClient) ListKeysWAN() (map[string]int, int, map[string]string, error) { +func (c *RPCClient) InstallKey(key string) (map[string]string, error) { header := requestHeader{ - Command: listKeysWANCommand, + Command: installKeyCommand, Seq: c.getSeq(), } - resp := new(keyResponse) - - err := c.genericRPC(&header, nil, resp) - return resp.Keys, resp.NumNodes, resp.Messages, err -} - -func (c *RPCClient) InstallKeyWAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, installKeyWANCommand) -} - -func (c *RPCClient) InstallKeyLAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, installKeyLANCommand) -} - -func (c *RPCClient) UseKeyWAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, useKeyWANCommand) -} - -func (c *RPCClient) UseKeyLAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, useKeyLANCommand) -} - -func (c *RPCClient) RemoveKeyWAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, removeKeyWANCommand) -} - -func (c *RPCClient) RemoveKeyLAN(key string) (map[string]string, error) { - return c.changeGossipKey(key, removeKeyLANCommand) -} - -func (c *RPCClient) changeGossipKey(key, cmd string) (map[string]string, error) { - header := requestHeader{ - Command: cmd, - Seq: c.getSeq(), - } - req := keyRequest{key} + resp := new(keyResponse) + err := c.genericRPC(&header, &req, resp) + return resp.Messages, err +} +func (c *RPCClient) UseKey(key string) (map[string]string, error) { + header := requestHeader{ + Command: useKeyCommand, + Seq: c.getSeq(), + } + req := keyRequest{key} + resp := new(keyResponse) + err := c.genericRPC(&header, &req, resp) + return resp.Messages, err +} + +func (c *RPCClient) RemoveKey(key string) (map[string]string, error) { + header := requestHeader{ + Command: removeKeyCommand, + Seq: c.getSeq(), + } + req := keyRequest{key} resp := new(keyResponse) err := c.genericRPC(&header, &req, resp) return resp.Messages, err diff --git a/command/keyring.go b/command/keyring.go index 630f0ba12..eaa298dcd 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -106,59 +106,34 @@ func (c *KeyringCommand) Run(args []string) int { } if listKeys { - if wan { - c.Ui.Info("Asking all WAN members for installed keys...") - return c.listKeysOperation(client.ListKeysWAN) - } - c.Ui.Info("Asking all LAN members for installed keys...") - return c.listKeysOperation(client.ListKeysLAN) + c.Ui.Info("Asking all members for installed keys...") + return c.listKeysOperation(client.ListKeys) } if installKey != "" { - if wan { - c.Ui.Info("Installing new WAN gossip encryption key...") - if rval := c.keyOperation(installKey, client.InstallKeyWAN); rval != 0 { - return rval - } - } else { - c.Ui.Info("Installing new LAN gossip encryption key...") - if rval := c.keyOperation(installKey, client.InstallKeyLAN); rval != 0 { - return rval - } + c.Ui.Info("Installing new gossip encryption key...") + if rval := c.keyOperation(installKey, client.InstallKey); rval != 0 { + return rval } c.Ui.Info("Successfully installed key!") return 0 } if useKey != "" { - if wan { - c.Ui.Info("Changing primary WAN gossip encryption key...") - if rval := c.keyOperation(useKey, client.UseKeyWAN); rval != 0 { - return rval - } - } else { - c.Ui.Info("Changing primary LAN gossip encryption key...") - if rval := c.keyOperation(useKey, client.UseKeyLAN); rval != 0 { - return rval - } + c.Ui.Info("Changing primary gossip encryption key...") + if rval := c.keyOperation(useKey, client.UseKey); rval != 0 { + return rval } c.Ui.Info("Successfully changed primary key!") return 0 } if removeKey != "" { - if wan { - c.Ui.Info("Removing WAN gossip encryption key...") - if rval := c.keyOperation(removeKey, client.RemoveKeyWAN); rval != 0 { - return rval - } - } else { - c.Ui.Info("Removing LAN gossip encryption key...") - if rval := c.keyOperation(removeKey, client.RemoveKeyLAN); rval != 0 { - return rval - } + c.Ui.Info("Removing gossip encryption key...") + if rval := c.keyOperation(removeKey, client.RemoveKey); rval != 0 { + return rval } - c.Ui.Info("Successfully removed key!") + c.Ui.Info("Successfully removed gossip encryption key!") return 0 } diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 5a38b31a2..7cc648747 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -1,7 +1,10 @@ package consul import ( + "fmt" + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/serf/serf" ) // Internal endpoint is used to query the miscellaneous info that @@ -62,3 +65,205 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, // Fire the event return m.srv.UserEvent(args.Name, args.Payload) } + +// TODO(ryanuber): Clean up all of these methods +func (m *Internal) InstallKey(args *structs.KeyringRequest, + reply *structs.KeyringResponse) error { + + var respLAN, respWAN *serf.KeyResponse + var err error + + if reply.Messages == nil { + reply.Messages = make(map[string]string) + } + if reply.Keys == nil { + reply.Keys = make(map[string]int) + } + + m.srv.setQueryMeta(&reply.QueryMeta) + + // Do a LAN key install. This will be invoked in each DC once the RPC call + // is forwarded below. + respLAN, err = m.srv.KeyManagerLAN().InstallKey(args.Key) + for node, msg := range respLAN.Messages { + reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg + } + reply.NumResp += respLAN.NumResp + reply.NumErr += respLAN.NumErr + reply.NumNodes += respLAN.NumNodes + if err != nil { + return fmt.Errorf("failed rotating LAN keyring in %s: %s", + m.srv.config.Datacenter, + err) + } + + if !args.Forwarded { + // Only perform WAN key rotation once. + respWAN, err = m.srv.KeyManagerWAN().InstallKey(args.Key) + if err != nil { + return err + } + for node, msg := range respWAN.Messages { + reply.Messages["server."+node] = msg + } + reply.NumResp += respWAN.NumResp + reply.NumErr += respWAN.NumErr + reply.NumNodes += respWAN.NumNodes + + // Mark key rotation as being already forwarded, then forward. + args.Forwarded = true + return m.srv.forwardAll("Internal.InstallKey", args, reply) + } + + return nil +} + +func (m *Internal) UseKey(args *structs.KeyringRequest, + reply *structs.KeyringResponse) error { + var respLAN, respWAN *serf.KeyResponse + var err error + + if reply.Messages == nil { + reply.Messages = make(map[string]string) + } + if reply.Keys == nil { + reply.Keys = make(map[string]int) + } + + m.srv.setQueryMeta(&reply.QueryMeta) + + // Do a LAN key install. This will be invoked in each DC once the RPC call + // is forwarded below. + respLAN, err = m.srv.KeyManagerLAN().UseKey(args.Key) + for node, msg := range respLAN.Messages { + reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg + } + reply.NumResp += respLAN.NumResp + reply.NumErr += respLAN.NumErr + reply.NumNodes += respLAN.NumNodes + if err != nil { + return fmt.Errorf("failed rotating LAN keyring in %s: %s", + m.srv.config.Datacenter, + err) + } + + if !args.Forwarded { + // Only perform WAN key rotation once. + respWAN, err = m.srv.KeyManagerWAN().UseKey(args.Key) + if err != nil { + return err + } + for node, msg := range respWAN.Messages { + reply.Messages["server."+node] = msg + } + reply.NumResp += respWAN.NumResp + reply.NumErr += respWAN.NumErr + reply.NumNodes += respWAN.NumNodes + + // Mark key rotation as being already forwarded, then forward. + args.Forwarded = true + return m.srv.forwardAll("Internal.UseKey", args, reply) + } + + return nil +} + +func (m *Internal) RemoveKey(args *structs.KeyringRequest, + reply *structs.KeyringResponse) error { + var respLAN, respWAN *serf.KeyResponse + var err error + + if reply.Messages == nil { + reply.Messages = make(map[string]string) + } + if reply.Keys == nil { + reply.Keys = make(map[string]int) + } + + m.srv.setQueryMeta(&reply.QueryMeta) + + // Do a LAN key install. This will be invoked in each DC once the RPC call + // is forwarded below. + respLAN, err = m.srv.KeyManagerLAN().RemoveKey(args.Key) + for node, msg := range respLAN.Messages { + reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg + } + reply.NumResp += respLAN.NumResp + reply.NumErr += respLAN.NumErr + reply.NumNodes += respLAN.NumNodes + if err != nil { + return fmt.Errorf("failed rotating LAN keyring in %s: %s", + m.srv.config.Datacenter, + err) + } + + if !args.Forwarded { + // Only perform WAN key rotation once. + respWAN, err = m.srv.KeyManagerWAN().RemoveKey(args.Key) + if err != nil { + return err + } + for node, msg := range respWAN.Messages { + reply.Messages["server."+node] = msg + } + reply.NumResp += respWAN.NumResp + reply.NumErr += respWAN.NumErr + reply.NumNodes += respWAN.NumNodes + + // Mark key rotation as being already forwarded, then forward. + args.Forwarded = true + return m.srv.forwardAll("Internal.RemoveKey", args, reply) + } + + return nil +} + +func (m *Internal) ListKeys(args *structs.KeyringRequest, + reply *structs.KeyringResponse) error { + var respLAN, respWAN *serf.KeyResponse + var err error + + if reply.Messages == nil { + reply.Messages = make(map[string]string) + } + if reply.Keys == nil { + reply.Keys = make(map[string]int) + } + + m.srv.setQueryMeta(&reply.QueryMeta) + + // Do a LAN key install. This will be invoked in each DC once the RPC call + // is forwarded below. + respLAN, err = m.srv.KeyManagerLAN().ListKeys() + for node, msg := range respLAN.Messages { + reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg + } + reply.NumResp += respLAN.NumResp + reply.NumErr += respLAN.NumErr + reply.NumNodes += respLAN.NumNodes + if err != nil { + return fmt.Errorf("failed rotating LAN keyring in %s: %s", + m.srv.config.Datacenter, + err) + } + + if !args.Forwarded { + // Only perform WAN key rotation once. + respWAN, err = m.srv.KeyManagerWAN().ListKeys() + if err != nil { + return err + } + for node, msg := range respWAN.Messages { + reply.Messages["server."+node] = msg + } + reply.NumResp += respWAN.NumResp + reply.NumErr += respWAN.NumErr + reply.NumNodes += respWAN.NumNodes + + // Mark key rotation as being already forwarded, then forward. + args.Forwarded = true + return m.srv.forwardAll("Internal.ListKeys", args, reply) + } + + return nil +} diff --git a/consul/rpc.go b/consul/rpc.go index cd5c36ebd..4526ca75b 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -223,6 +223,18 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ return s.connPool.RPC(server.Addr, server.Version, method, args, reply) } +// forwardAll forwards a single RPC call to every known datacenter. +func (s *Server) forwardAll(method string, args, reply interface{}) error { + for dc, _ := range s.remoteConsuls { + if dc != s.config.Datacenter { + if err := s.forwardDC(method, dc, args, reply); err != nil { + return err + } + } + } + return 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{}, error) { diff --git a/consul/structs/structs.go b/consul/structs/structs.go index c2585b132..31a6e319f 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -531,3 +531,22 @@ func Encode(t MessageType, msg interface{}) ([]byte, error) { err := codec.NewEncoder(&buf, msgpackHandle).Encode(msg) return buf.Bytes(), err } + +// KeyringRequest encapsulates a request to modify an encryption keyring. +// It can be used for install, remove, or use key type operations. +type KeyringRequest struct { + Key string + Forwarded bool + QueryOptions +} + +// KeyringResponse is a unified key response and can be used for install, +// remove, use, as well as listing key queries. +type KeyringResponse struct { + Messages map[string]string + Keys map[string]int + NumNodes int + NumResp int + NumErr int + QueryMeta +} From 71e9715c549a989668acef2d036d17f8ca9d52ef Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 24 Sep 2014 18:30:34 -0700 Subject: [PATCH 123/238] consul: restructuring --- command/agent/keyring.go | 12 +- command/agent/rpc.go | 29 ++-- consul/internal_endpoint.go | 261 ++++++++++++++---------------------- consul/structs/structs.go | 16 ++- 4 files changed, 134 insertions(+), 184 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index e816c3d78..827aa76bc 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -66,9 +66,9 @@ func loadKeyringFile(c *serf.Config) error { // performing various operations on the encryption keyring. func (a *Agent) keyringProcess( method string, - args *structs.KeyringRequest) (*structs.KeyringResponse, error) { + args *structs.KeyringRequest) (*structs.KeyringResponses, error) { - var reply structs.KeyringResponse + var reply structs.KeyringResponses if a.server == nil { return nil, fmt.Errorf("keyring operations must run against a server node") } @@ -81,28 +81,28 @@ func (a *Agent) keyringProcess( // ListKeys lists out all keys installed on the collective Consul cluster. This // includes both servers and clients in all DC's. -func (a *Agent) ListKeys() (*structs.KeyringResponse, error) { +func (a *Agent) ListKeys() (*structs.KeyringResponses, error) { args := structs.KeyringRequest{} args.AllowStale = true return a.keyringProcess("Internal.ListKeys", &args) } // InstallKey installs a new gossip encryption key -func (a *Agent) InstallKey(key string) (*structs.KeyringResponse, error) { +func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true return a.keyringProcess("Internal.InstallKey", &args) } // UseKey changes the primary encryption key used to encrypt messages -func (a *Agent) UseKey(key string) (*structs.KeyringResponse, error) { +func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true return a.keyringProcess("Internal.UseKey", &args) } // RemoveKey will remove a gossip encryption key from the keyring -func (a *Agent) RemoveKey(key string) (*structs.KeyringResponse, error) { +func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true return a.keyringProcess("Internal.RemoveKey", &args) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 983cc3481..5d6c5c8a7 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -606,7 +606,7 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error { var req keyRequest - var queryResp *structs.KeyringResponse + var queryResp *structs.KeyringResponses var resp keyResponse var err error @@ -636,14 +636,27 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro Error: errToString(err), } - if queryResp != nil { - resp = keyResponse{ - Messages: queryResp.Messages, - Keys: queryResp.Keys, - NumNodes: queryResp.NumNodes, - NumResp: queryResp.NumResp, - NumErr: queryResp.NumErr, + if resp.Messages == nil { + resp.Messages = make(map[string]string) + } + if resp.Keys == nil { + resp.Keys = make(map[string]int) + } + + for _, kr := range queryResp.Responses { + for node, msg := range kr.Messages { + resp.Messages[node+"."+kr.Datacenter] = msg } + for key, qty := range kr.Keys { + if _, ok := resp.Keys[key]; ok { + resp.Keys[key] += qty + } else { + resp.Keys[key] = qty + } + } + resp.NumNodes += kr.NumNodes + resp.NumResp += kr.NumResp + resp.NumErr += kr.NumErr } return client.Send(&header, resp) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 7cc648747..e7dafb319 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -1,10 +1,7 @@ package consul import ( - "fmt" - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/serf/serf" ) // Internal endpoint is used to query the miscellaneous info that @@ -66,49 +63,69 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } -// TODO(ryanuber): Clean up all of these methods -func (m *Internal) InstallKey(args *structs.KeyringRequest, - reply *structs.KeyringResponse) error { +func (m *Internal) ListKeys( + args *structs.KeyringRequest, + reply *structs.KeyringResponses) error { - var respLAN, respWAN *serf.KeyResponse - var err error - - if reply.Messages == nil { - reply.Messages = make(map[string]string) - } - if reply.Keys == nil { - reply.Keys = make(map[string]int) - } - - m.srv.setQueryMeta(&reply.QueryMeta) - - // Do a LAN key install. This will be invoked in each DC once the RPC call - // is forwarded below. - respLAN, err = m.srv.KeyManagerLAN().InstallKey(args.Key) - for node, msg := range respLAN.Messages { - reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg - } - reply.NumResp += respLAN.NumResp - reply.NumErr += respLAN.NumErr - reply.NumNodes += respLAN.NumNodes + respLAN, err := m.srv.KeyManagerLAN().ListKeys() if err != nil { - return fmt.Errorf("failed rotating LAN keyring in %s: %s", - m.srv.config.Datacenter, - err) + return err } + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respLAN.Messages, + Keys: respLAN.Keys, + NumResp: respLAN.NumResp, + NumNodes: respLAN.NumNodes, + NumErr: respLAN.NumErr, + }) if !args.Forwarded { - // Only perform WAN key rotation once. - respWAN, err = m.srv.KeyManagerWAN().InstallKey(args.Key) + respWAN, err := m.srv.KeyManagerWAN().ListKeys() if err != nil { return err } - for node, msg := range respWAN.Messages { - reply.Messages["server."+node] = msg - } - reply.NumResp += respWAN.NumResp - reply.NumErr += respWAN.NumErr - reply.NumNodes += respWAN.NumNodes + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respWAN.Messages, + Keys: respWAN.Keys, + NumResp: respWAN.NumResp, + NumNodes: respWAN.NumNodes, + NumErr: respWAN.NumErr, + }) + + // Mark key rotation as being already forwarded, then forward. + args.Forwarded = true + return m.srv.forwardAll("Internal.ListKeys", args, reply) + } + + return nil +} + +func (m *Internal) InstallKey( + args *structs.KeyringRequest, + reply *structs.KeyringResponses) error { + + respLAN, _ := m.srv.KeyManagerLAN().InstallKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respLAN.Messages, + Keys: respLAN.Keys, + NumResp: respLAN.NumResp, + NumNodes: respLAN.NumNodes, + NumErr: respLAN.NumErr, + }) + + if !args.Forwarded { + respWAN, _ := m.srv.KeyManagerWAN().InstallKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respWAN.Messages, + Keys: respWAN.Keys, + NumResp: respWAN.NumResp, + NumNodes: respWAN.NumNodes, + NumErr: respWAN.NumErr, + }) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true @@ -118,47 +135,30 @@ func (m *Internal) InstallKey(args *structs.KeyringRequest, return nil } -func (m *Internal) UseKey(args *structs.KeyringRequest, - reply *structs.KeyringResponse) error { - var respLAN, respWAN *serf.KeyResponse - var err error +func (m *Internal) UseKey( + args *structs.KeyringRequest, + reply *structs.KeyringResponses) error { - if reply.Messages == nil { - reply.Messages = make(map[string]string) - } - if reply.Keys == nil { - reply.Keys = make(map[string]int) - } - - m.srv.setQueryMeta(&reply.QueryMeta) - - // Do a LAN key install. This will be invoked in each DC once the RPC call - // is forwarded below. - respLAN, err = m.srv.KeyManagerLAN().UseKey(args.Key) - for node, msg := range respLAN.Messages { - reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg - } - reply.NumResp += respLAN.NumResp - reply.NumErr += respLAN.NumErr - reply.NumNodes += respLAN.NumNodes - if err != nil { - return fmt.Errorf("failed rotating LAN keyring in %s: %s", - m.srv.config.Datacenter, - err) - } + respLAN, _ := m.srv.KeyManagerLAN().UseKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respLAN.Messages, + Keys: respLAN.Keys, + NumResp: respLAN.NumResp, + NumNodes: respLAN.NumNodes, + NumErr: respLAN.NumErr, + }) if !args.Forwarded { - // Only perform WAN key rotation once. - respWAN, err = m.srv.KeyManagerWAN().UseKey(args.Key) - if err != nil { - return err - } - for node, msg := range respWAN.Messages { - reply.Messages["server."+node] = msg - } - reply.NumResp += respWAN.NumResp - reply.NumErr += respWAN.NumErr - reply.NumNodes += respWAN.NumNodes + respWAN, _ := m.srv.KeyManagerWAN().UseKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respWAN.Messages, + Keys: respWAN.Keys, + NumResp: respWAN.NumResp, + NumNodes: respWAN.NumNodes, + NumErr: respWAN.NumErr, + }) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true @@ -168,47 +168,30 @@ func (m *Internal) UseKey(args *structs.KeyringRequest, return nil } -func (m *Internal) RemoveKey(args *structs.KeyringRequest, - reply *structs.KeyringResponse) error { - var respLAN, respWAN *serf.KeyResponse - var err error +func (m *Internal) RemoveKey( + args *structs.KeyringRequest, + reply *structs.KeyringResponses) error { - if reply.Messages == nil { - reply.Messages = make(map[string]string) - } - if reply.Keys == nil { - reply.Keys = make(map[string]int) - } - - m.srv.setQueryMeta(&reply.QueryMeta) - - // Do a LAN key install. This will be invoked in each DC once the RPC call - // is forwarded below. - respLAN, err = m.srv.KeyManagerLAN().RemoveKey(args.Key) - for node, msg := range respLAN.Messages { - reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg - } - reply.NumResp += respLAN.NumResp - reply.NumErr += respLAN.NumErr - reply.NumNodes += respLAN.NumNodes - if err != nil { - return fmt.Errorf("failed rotating LAN keyring in %s: %s", - m.srv.config.Datacenter, - err) - } + respLAN, _ := m.srv.KeyManagerLAN().RemoveKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respLAN.Messages, + Keys: respLAN.Keys, + NumResp: respLAN.NumResp, + NumNodes: respLAN.NumNodes, + NumErr: respLAN.NumErr, + }) if !args.Forwarded { - // Only perform WAN key rotation once. - respWAN, err = m.srv.KeyManagerWAN().RemoveKey(args.Key) - if err != nil { - return err - } - for node, msg := range respWAN.Messages { - reply.Messages["server."+node] = msg - } - reply.NumResp += respWAN.NumResp - reply.NumErr += respWAN.NumErr - reply.NumNodes += respWAN.NumNodes + respWAN, _ := m.srv.KeyManagerWAN().RemoveKey(args.Key) + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: respWAN.Messages, + Keys: respWAN.Keys, + NumResp: respWAN.NumResp, + NumNodes: respWAN.NumNodes, + NumErr: respWAN.NumErr, + }) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true @@ -217,53 +200,3 @@ func (m *Internal) RemoveKey(args *structs.KeyringRequest, return nil } - -func (m *Internal) ListKeys(args *structs.KeyringRequest, - reply *structs.KeyringResponse) error { - var respLAN, respWAN *serf.KeyResponse - var err error - - if reply.Messages == nil { - reply.Messages = make(map[string]string) - } - if reply.Keys == nil { - reply.Keys = make(map[string]int) - } - - m.srv.setQueryMeta(&reply.QueryMeta) - - // Do a LAN key install. This will be invoked in each DC once the RPC call - // is forwarded below. - respLAN, err = m.srv.KeyManagerLAN().ListKeys() - for node, msg := range respLAN.Messages { - reply.Messages["client."+node+"."+m.srv.config.Datacenter] = msg - } - reply.NumResp += respLAN.NumResp - reply.NumErr += respLAN.NumErr - reply.NumNodes += respLAN.NumNodes - if err != nil { - return fmt.Errorf("failed rotating LAN keyring in %s: %s", - m.srv.config.Datacenter, - err) - } - - if !args.Forwarded { - // Only perform WAN key rotation once. - respWAN, err = m.srv.KeyManagerWAN().ListKeys() - if err != nil { - return err - } - for node, msg := range respWAN.Messages { - reply.Messages["server."+node] = msg - } - reply.NumResp += respWAN.NumResp - reply.NumErr += respWAN.NumErr - reply.NumNodes += respWAN.NumNodes - - // Mark key rotation as being already forwarded, then forward. - args.Forwarded = true - return m.srv.forwardAll("Internal.ListKeys", args, reply) - } - - return nil -} diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 31a6e319f..3f029ee81 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -543,10 +543,14 @@ type KeyringRequest struct { // KeyringResponse is a unified key response and can be used for install, // remove, use, as well as listing key queries. type KeyringResponse struct { - Messages map[string]string - Keys map[string]int - NumNodes int - NumResp int - NumErr int - QueryMeta + Datacenter string + Messages map[string]string + Keys map[string]int + NumNodes int + NumResp int + NumErr int +} + +type KeyringResponses struct { + Responses []*KeyringResponse } From f9b5b15a6b6a07c2c9fcc9b872d155b0a0ae85ab Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 25 Sep 2014 00:22:06 -0700 Subject: [PATCH 124/238] consul: use a function for ingesting responses --- consul/internal_endpoint.go | 127 +++++++++++++++--------------------- consul/rpc.go | 7 +- 2 files changed, 58 insertions(+), 76 deletions(-) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index e7dafb319..690707611 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -2,6 +2,7 @@ package consul import ( "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/serf/serf" ) // Internal endpoint is used to query the miscellaneous info that @@ -63,6 +64,22 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } +func (m *Internal) ingestKeyringResponse( + resp *serf.KeyResponse, + reply *structs.KeyringResponses) { + + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + Datacenter: m.srv.config.Datacenter, + Messages: resp.Messages, + Keys: resp.Keys, + NumResp: resp.NumResp, + NumNodes: resp.NumNodes, + NumErr: resp.NumErr, + }) +} + +// ListKeys will query the WAN and LAN gossip keyrings of all nodes, adding +// results into a collective response as we go. func (m *Internal) ListKeys( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { @@ -71,28 +88,14 @@ func (m *Internal) ListKeys( if err != nil { return err } - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respLAN.Messages, - Keys: respLAN.Keys, - NumResp: respLAN.NumResp, - NumNodes: respLAN.NumNodes, - NumErr: respLAN.NumErr, - }) + m.ingestKeyringResponse(respLAN, reply) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().ListKeys() if err != nil { return err } - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respWAN.Messages, - Keys: respWAN.Keys, - NumResp: respWAN.NumResp, - NumNodes: respWAN.NumNodes, - NumErr: respWAN.NumErr, - }) + m.ingestKeyringResponse(respWAN, reply) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true @@ -102,32 +105,25 @@ func (m *Internal) ListKeys( return nil } +// InstallKey broadcasts a new encryption key to all nodes. This involves +// installing a new key on every node across all datacenters. func (m *Internal) InstallKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - respLAN, _ := m.srv.KeyManagerLAN().InstallKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respLAN.Messages, - Keys: respLAN.Keys, - NumResp: respLAN.NumResp, - NumNodes: respLAN.NumNodes, - NumErr: respLAN.NumErr, - }) + respLAN, err := m.srv.KeyManagerLAN().InstallKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respLAN, reply) if !args.Forwarded { - respWAN, _ := m.srv.KeyManagerWAN().InstallKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respWAN.Messages, - Keys: respWAN.Keys, - NumResp: respWAN.NumResp, - NumNodes: respWAN.NumNodes, - NumErr: respWAN.NumErr, - }) + respWAN, err := m.srv.KeyManagerWAN().InstallKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respWAN, reply) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true return m.srv.forwardAll("Internal.InstallKey", args, reply) } @@ -135,32 +131,25 @@ func (m *Internal) InstallKey( return nil } +// UseKey instructs all nodes to change the key they are using to +// encrypt gossip messages. func (m *Internal) UseKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - respLAN, _ := m.srv.KeyManagerLAN().UseKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respLAN.Messages, - Keys: respLAN.Keys, - NumResp: respLAN.NumResp, - NumNodes: respLAN.NumNodes, - NumErr: respLAN.NumErr, - }) + respLAN, err := m.srv.KeyManagerLAN().UseKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respLAN, reply) if !args.Forwarded { - respWAN, _ := m.srv.KeyManagerWAN().UseKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respWAN.Messages, - Keys: respWAN.Keys, - NumResp: respWAN.NumResp, - NumNodes: respWAN.NumNodes, - NumErr: respWAN.NumErr, - }) + respWAN, err := m.srv.KeyManagerWAN().UseKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respWAN, reply) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true return m.srv.forwardAll("Internal.UseKey", args, reply) } @@ -168,32 +157,24 @@ func (m *Internal) UseKey( return nil } +// RemoveKey instructs all nodes to drop the specified key from the keyring. func (m *Internal) RemoveKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - respLAN, _ := m.srv.KeyManagerLAN().RemoveKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respLAN.Messages, - Keys: respLAN.Keys, - NumResp: respLAN.NumResp, - NumNodes: respLAN.NumNodes, - NumErr: respLAN.NumErr, - }) + respLAN, err := m.srv.KeyManagerLAN().RemoveKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respLAN, reply) if !args.Forwarded { - respWAN, _ := m.srv.KeyManagerWAN().RemoveKey(args.Key) - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - Datacenter: m.srv.config.Datacenter, - Messages: respWAN.Messages, - Keys: respWAN.Keys, - NumResp: respWAN.NumResp, - NumNodes: respWAN.NumNodes, - NumErr: respWAN.NumErr, - }) + respWAN, err := m.srv.KeyManagerWAN().RemoveKey(args.Key) + if err != nil { + return err + } + m.ingestKeyringResponse(respWAN, reply) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true return m.srv.forwardAll("Internal.RemoveKey", args, reply) } diff --git a/consul/rpc.go b/consul/rpc.go index 4526ca75b..e5b5be6c5 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -227,9 +227,10 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ func (s *Server) forwardAll(method string, args, reply interface{}) error { for dc, _ := range s.remoteConsuls { if dc != s.config.Datacenter { - if err := s.forwardDC(method, dc, args, reply); err != nil { - return err - } + // Forward the RPC call. Even if an error is returned here, we still + // want to continue broadcasting to the remaining DC's to avoid + // network partitions completely killing us. + go s.forwardDC(method, dc, args, reply) } } return nil From f6b5fc8c087257ac6e7a02c8529809a3f2df5d53 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 28 Sep 2014 12:35:51 -0700 Subject: [PATCH 125/238] consul: cross-dc key rotation works --- command/agent/rpc.go | 80 +++++++++++++++------- command/agent/rpc_client.go | 32 ++++----- command/keyring.go | 131 +++++++++++++++++++++++++----------- consul/internal_endpoint.go | 98 +++++++++++++++------------ consul/rpc.go | 13 ---- consul/structs/structs.go | 3 + 6 files changed, 222 insertions(+), 135 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 5d6c5c8a7..288a97cf3 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -113,12 +113,33 @@ type keyRequest struct { Key string } +type KeyringEntry struct { + Datacenter string + Pool string + Key string + Count int +} + +type KeyringMessage struct { + Datacenter string + Pool string + Node string + Message string +} + +type KeyringInfo struct { + Datacenter string + Pool string + NumNodes int + NumResp int + NumErr int + Error string +} + type keyResponse struct { - Messages map[string]string - Keys map[string]int - NumNodes int - NumResp int - NumErr int + Keys []KeyringEntry + Messages []KeyringMessage + Info []KeyringInfo } type membersResponse struct { @@ -607,7 +628,7 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error { var req keyRequest var queryResp *structs.KeyringResponses - var resp keyResponse + var r keyResponse var err error if cmd != listKeysCommand { @@ -636,30 +657,43 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro Error: errToString(err), } - if resp.Messages == nil { - resp.Messages = make(map[string]string) - } - if resp.Keys == nil { - resp.Keys = make(map[string]int) - } - for _, kr := range queryResp.Responses { - for node, msg := range kr.Messages { - resp.Messages[node+"."+kr.Datacenter] = msg + var pool string + if kr.WAN { + pool = "WAN" + } else { + pool = "LAN" + } + for node, message := range kr.Messages { + msg := KeyringMessage{ + Datacenter: kr.Datacenter, + Pool: pool, + Node: node, + Message: message, + } + r.Messages = append(r.Messages, msg) } for key, qty := range kr.Keys { - if _, ok := resp.Keys[key]; ok { - resp.Keys[key] += qty - } else { - resp.Keys[key] = qty + k := KeyringEntry{ + Datacenter: kr.Datacenter, + Pool: pool, + Key: key, + Count: qty, } + r.Keys = append(r.Keys, k) } - resp.NumNodes += kr.NumNodes - resp.NumResp += kr.NumResp - resp.NumErr += kr.NumErr + info := KeyringInfo{ + Datacenter: kr.Datacenter, + Pool: pool, + NumNodes: kr.NumNodes, + NumResp: kr.NumResp, + NumErr: kr.NumErr, + Error: kr.Error, + } + r.Info = append(r.Info, info) } - return client.Send(&header, resp) + return client.Send(&header, r) } // Used to convert an error to a string representation diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 36a54057e..454f427a8 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -176,47 +176,47 @@ func (c *RPCClient) WANMembers() ([]Member, error) { return resp.Members, err } -func (c *RPCClient) ListKeys() (map[string]int, int, map[string]string, error) { +func (c *RPCClient) ListKeys() (keyResponse, error) { header := requestHeader{ Command: listKeysCommand, Seq: c.getSeq(), } - resp := new(keyResponse) - err := c.genericRPC(&header, nil, resp) - return resp.Keys, resp.NumNodes, resp.Messages, err + var resp keyResponse + err := c.genericRPC(&header, nil, &resp) + return resp, err } -func (c *RPCClient) InstallKey(key string) (map[string]string, error) { +func (c *RPCClient) InstallKey(key string) (keyResponse, error) { header := requestHeader{ Command: installKeyCommand, Seq: c.getSeq(), } req := keyRequest{key} - resp := new(keyResponse) - err := c.genericRPC(&header, &req, resp) - return resp.Messages, err + var resp keyResponse + err := c.genericRPC(&header, &req, &resp) + return resp, err } -func (c *RPCClient) UseKey(key string) (map[string]string, error) { +func (c *RPCClient) UseKey(key string) (keyResponse, error) { header := requestHeader{ Command: useKeyCommand, Seq: c.getSeq(), } req := keyRequest{key} - resp := new(keyResponse) - err := c.genericRPC(&header, &req, resp) - return resp.Messages, err + var resp keyResponse + err := c.genericRPC(&header, &req, &resp) + return resp, err } -func (c *RPCClient) RemoveKey(key string) (map[string]string, error) { +func (c *RPCClient) RemoveKey(key string) (keyResponse, error) { header := requestHeader{ Command: removeKeyCommand, Seq: c.getSeq(), } req := keyRequest{key} - resp := new(keyResponse) - err := c.genericRPC(&header, &req, resp) - return resp.Messages, err + var resp keyResponse + err := c.genericRPC(&header, &req, &resp) + return resp, err } // Leave is used to trigger a graceful leave and shutdown diff --git a/command/keyring.go b/command/keyring.go index eaa298dcd..8d0dc9545 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -14,6 +14,12 @@ import ( "github.com/ryanuber/columnize" ) +const ( + installKeyCommand = "install" + useKeyCommand = "use" + removeKeyCommand = "remove" +) + // KeyringCommand is a Command implementation that handles querying, installing, // and removing gossip encryption keys from a keyring. type KeyringCommand struct { @@ -107,79 +113,106 @@ func (c *KeyringCommand) Run(args []string) int { if listKeys { c.Ui.Info("Asking all members for installed keys...") - return c.listKeysOperation(client.ListKeys) + return c.listKeysOperation(client) } if installKey != "" { c.Ui.Info("Installing new gossip encryption key...") - if rval := c.keyOperation(installKey, client.InstallKey); rval != 0 { - return rval + r, err := client.InstallKey(installKey) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 } - c.Ui.Info("Successfully installed key!") - return 0 + rval := c.handleResponse(r.Info, r.Messages, r.Keys) + if rval == 0 { + c.Ui.Info("Successfully installed new key!") + } + return rval } if useKey != "" { c.Ui.Info("Changing primary gossip encryption key...") - if rval := c.keyOperation(useKey, client.UseKey); rval != 0 { - return rval + r, err := client.UseKey(useKey) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 } - c.Ui.Info("Successfully changed primary key!") - return 0 + rval := c.handleResponse(r.Info, r.Messages, r.Keys) + if rval == 0 { + c.Ui.Info("Successfully changed primary encryption key!") + } + return rval } if removeKey != "" { c.Ui.Info("Removing gossip encryption key...") - if rval := c.keyOperation(removeKey, client.RemoveKey); rval != 0 { - return rval + r, err := client.RemoveKey(removeKey) + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 } - c.Ui.Info("Successfully removed gossip encryption key!") - return 0 + rval := c.handleResponse(r.Info, r.Messages, r.Keys) + if rval == 0 { + c.Ui.Info("Successfully removed encryption key!") + } + return rval } // Should never make it here return 0 } -// keyFunc is a function which manipulates gossip encryption keyrings. This is -// used for key installation, removal, and primary key changes. -type keyFunc func(string) (map[string]string, error) +func (c *KeyringCommand) handleResponse( + info []agent.KeyringInfo, + messages []agent.KeyringMessage, + entries []agent.KeyringEntry) int { -// keyOperation is a unified process for manipulating the gossip keyrings. -func (c *KeyringCommand) keyOperation(key string, fn keyFunc) int { - var out []string + var rval int - failures, err := fn(key) - - if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + for _, i := range info { + if i.Error != "" { + pool := i.Pool + if pool != "WAN" { + pool = i.Datacenter + " (LAN)" } - c.Ui.Error(columnize.SimpleFormat(out)) + + c.Ui.Error("") + c.Ui.Error(fmt.Sprintf("%s error: %s", pool, i.Error)) + + var errors []string + for _, msg := range messages { + if msg.Datacenter != i.Datacenter || msg.Pool != i.Pool { + continue + } + errors = append(errors, fmt.Sprintf( + "failed: %s | %s", + msg.Node, + msg.Message)) + } + c.Ui.Error(columnize.SimpleFormat(errors)) + rval = 1 } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Error: %s", err)) - return 1 } - return 0 + return rval } -// listKeysFunc is a function which handles querying lists of gossip keys -type listKeysFunc func() (map[string]int, int, map[string]string, error) - // listKeysOperation is a unified process for querying and // displaying gossip keys. -func (c *KeyringCommand) listKeysOperation(fn listKeysFunc) int { +func (c *KeyringCommand) listKeysOperation(client *agent.RPCClient) int { var out []string - keys, numNodes, failures, err := fn() + resp, err := client.ListKeys() if err != nil { - if len(failures) > 0 { - for node, msg := range failures { - out = append(out, fmt.Sprintf("failed: %s | %s", node, msg)) + if len(resp.Messages) > 0 { + for _, msg := range resp.Messages { + out = append(out, fmt.Sprintf( + "failed: %s | %s | %s | %s", + msg.Datacenter, + msg.Pool, + msg.Node, + msg.Message)) } c.Ui.Error(columnize.SimpleFormat(out)) } @@ -187,8 +220,26 @@ func (c *KeyringCommand) listKeysOperation(fn listKeysFunc) int { c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) return 1 } - for key, num := range keys { - out = append(out, fmt.Sprintf("%s | [%d/%d]", key, num, numNodes)) + + entries := make(map[string]map[string]int) + for _, key := range resp.Keys { + var dc string + if key.Pool == "WAN" { + dc = key.Pool + } else { + dc = key.Datacenter + } + if _, ok := entries[dc]; !ok { + entries[dc] = make(map[string]int) + } + entries[dc][key.Key] = key.Count + } + for dc, keys := range entries { + out = append(out, "") + out = append(out, dc) + for key, count := range keys { + out = append(out, fmt.Sprintf("%s|[%d/%d]", key, count, count)) + } } c.Ui.Output(columnize.SimpleFormat(out)) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 690707611..2cd08a2c6 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -64,42 +64,69 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } +// ingestKeyringResponse is a helper method to pick the relative information +// from a Serf message and stuff it into a KeyringResponse. func (m *Internal) ingestKeyringResponse( - resp *serf.KeyResponse, - reply *structs.KeyringResponses) { + serfResp *serf.KeyResponse, + reply *structs.KeyringResponses, + err error, wan bool) { + + errStr := "" + if err != nil { + errStr = err.Error() + } reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + WAN: wan, Datacenter: m.srv.config.Datacenter, - Messages: resp.Messages, - Keys: resp.Keys, - NumResp: resp.NumResp, - NumNodes: resp.NumNodes, - NumErr: resp.NumErr, + Messages: serfResp.Messages, + Keys: serfResp.Keys, + NumResp: serfResp.NumResp, + NumNodes: serfResp.NumNodes, + NumErr: serfResp.NumErr, + Error: errStr, }) } +func (m *Internal) forwardKeyring( + method string, + args *structs.KeyringRequest, + replies *structs.KeyringResponses) error { + + for dc, _ := range m.srv.remoteConsuls { + if dc == m.srv.config.Datacenter { + continue + } + rr := structs.KeyringResponses{} + if err := m.srv.forwardDC(method, dc, args, &rr); err != nil { + return err + } + for _, r := range rr.Responses { + replies.Responses = append(replies.Responses, r) + } + } + + return nil +} + // ListKeys will query the WAN and LAN gossip keyrings of all nodes, adding // results into a collective response as we go. func (m *Internal) ListKeys( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { + m.srv.setQueryMeta(&reply.QueryMeta) + respLAN, err := m.srv.KeyManagerLAN().ListKeys() - if err != nil { - return err - } - m.ingestKeyringResponse(respLAN, reply) + m.ingestKeyringResponse(respLAN, reply, err, false) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().ListKeys() - if err != nil { - return err - } - m.ingestKeyringResponse(respWAN, reply) + m.ingestKeyringResponse(respWAN, reply, err, true) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - return m.srv.forwardAll("Internal.ListKeys", args, reply) + m.forwardKeyring("Internal.ListKeys", args, reply) } return nil @@ -112,20 +139,15 @@ func (m *Internal) InstallKey( reply *structs.KeyringResponses) error { respLAN, err := m.srv.KeyManagerLAN().InstallKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respLAN, reply) + m.ingestKeyringResponse(respLAN, reply, err, false) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().InstallKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respWAN, reply) + m.ingestKeyringResponse(respWAN, reply, err, true) + // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - return m.srv.forwardAll("Internal.InstallKey", args, reply) + m.forwardKeyring("Internal.InstallKey", args, reply) } return nil @@ -138,20 +160,15 @@ func (m *Internal) UseKey( reply *structs.KeyringResponses) error { respLAN, err := m.srv.KeyManagerLAN().UseKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respLAN, reply) + m.ingestKeyringResponse(respLAN, reply, err, false) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().UseKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respWAN, reply) + m.ingestKeyringResponse(respWAN, reply, err, true) + // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - return m.srv.forwardAll("Internal.UseKey", args, reply) + m.forwardKeyring("Internal.UseKey", args, reply) } return nil @@ -163,20 +180,15 @@ func (m *Internal) RemoveKey( reply *structs.KeyringResponses) error { respLAN, err := m.srv.KeyManagerLAN().RemoveKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respLAN, reply) + m.ingestKeyringResponse(respLAN, reply, err, false) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().RemoveKey(args.Key) - if err != nil { - return err - } - m.ingestKeyringResponse(respWAN, reply) + m.ingestKeyringResponse(respWAN, reply, err, true) + // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - return m.srv.forwardAll("Internal.RemoveKey", args, reply) + m.forwardKeyring("Internal.RemoveKey", args, reply) } return nil diff --git a/consul/rpc.go b/consul/rpc.go index e5b5be6c5..cd5c36ebd 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -223,19 +223,6 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ return s.connPool.RPC(server.Addr, server.Version, method, args, reply) } -// forwardAll forwards a single RPC call to every known datacenter. -func (s *Server) forwardAll(method string, args, reply interface{}) error { - for dc, _ := range s.remoteConsuls { - if dc != s.config.Datacenter { - // Forward the RPC call. Even if an error is returned here, we still - // want to continue broadcasting to the remaining DC's to avoid - // network partitions completely killing us. - go s.forwardDC(method, dc, args, reply) - } - } - return 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{}, error) { diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 3f029ee81..6e752ea4f 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -543,14 +543,17 @@ type KeyringRequest struct { // KeyringResponse is a unified key response and can be used for install, // remove, use, as well as listing key queries. type KeyringResponse struct { + WAN bool Datacenter string Messages map[string]string Keys map[string]int NumNodes int NumResp int NumErr int + Error string } type KeyringResponses struct { Responses []*KeyringResponse + QueryMeta } From 30f5f06dfe33116ad289dceed138f7f144565c8d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 28 Sep 2014 13:33:37 -0700 Subject: [PATCH 126/238] command/keyring: clean up output --- command/keyring.go | 105 ++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/command/keyring.go b/command/keyring.go index 8d0dc9545..0e0bbc19b 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -113,7 +113,16 @@ func (c *KeyringCommand) Run(args []string) int { if listKeys { c.Ui.Info("Asking all members for installed keys...") - return c.listKeysOperation(client) + r, err := client.ListKeys() + if err != nil { + c.Ui.Error(fmt.Sprintf("error: %s", err)) + return 1 + } + if rval := c.handleResponse(r.Info, r.Messages, r.Keys); rval != 0 { + return rval + } + c.handleList(r.Info, r.Messages, r.Keys) + return 0 } if installKey != "" { @@ -123,11 +132,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - rval := c.handleResponse(r.Info, r.Messages, r.Keys) - if rval == 0 { - c.Ui.Info("Successfully installed new key!") - } - return rval + return c.handleResponse(r.Info, r.Messages, r.Keys) } if useKey != "" { @@ -137,11 +142,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - rval := c.handleResponse(r.Info, r.Messages, r.Keys) - if rval == 0 { - c.Ui.Info("Successfully changed primary encryption key!") - } - return rval + return c.handleResponse(r.Info, r.Messages, r.Keys) } if removeKey != "" { @@ -151,11 +152,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - rval := c.handleResponse(r.Info, r.Messages, r.Keys) - if rval == 0 { - c.Ui.Info("Successfully removed encryption key!") - } - return rval + return c.handleResponse(r.Info, r.Messages, r.Keys) } // Should never make it here @@ -165,7 +162,7 @@ func (c *KeyringCommand) Run(args []string) int { func (c *KeyringCommand) handleResponse( info []agent.KeyringInfo, messages []agent.KeyringMessage, - entries []agent.KeyringEntry) int { + keys []agent.KeyringEntry) int { var rval int @@ -194,57 +191,49 @@ func (c *KeyringCommand) handleResponse( } } + if rval == 0 { + c.Ui.Info("Done!") + } + return rval } -// listKeysOperation is a unified process for querying and -// displaying gossip keys. -func (c *KeyringCommand) listKeysOperation(client *agent.RPCClient) int { - var out []string +func (c *KeyringCommand) handleList( + info []agent.KeyringInfo, + messages []agent.KeyringMessage, + keys []agent.KeyringEntry) { - resp, err := client.ListKeys() - - if err != nil { - if len(resp.Messages) > 0 { - for _, msg := range resp.Messages { - out = append(out, fmt.Sprintf( - "failed: %s | %s | %s | %s", - msg.Datacenter, - msg.Pool, - msg.Node, - msg.Message)) + installed := make(map[string]map[string][]int) + for _, key := range keys { + var nodes int + for _, i := range info { + if i.Datacenter == key.Datacenter && i.Pool == key.Pool { + nodes = i.NumNodes } - c.Ui.Error(columnize.SimpleFormat(out)) } - c.Ui.Error("") - c.Ui.Error(fmt.Sprintf("Failed gathering member keys: %s", err)) - return 1 - } - entries := make(map[string]map[string]int) - for _, key := range resp.Keys { - var dc string - if key.Pool == "WAN" { - dc = key.Pool + pool := key.Pool + if pool != "WAN" { + pool = key.Datacenter + " (LAN)" + } + + if _, ok := installed[pool]; !ok { + installed[pool] = map[string][]int{key.Key: []int{key.Count, nodes}} } else { - dc = key.Datacenter - } - if _, ok := entries[dc]; !ok { - entries[dc] = make(map[string]int) - } - entries[dc][key.Key] = key.Count - } - for dc, keys := range entries { - out = append(out, "") - out = append(out, dc) - for key, count := range keys { - out = append(out, fmt.Sprintf("%s|[%d/%d]", key, count, count)) + installed[pool][key.Key] = []int{key.Count, nodes} } } - c.Ui.Output(columnize.SimpleFormat(out)) - - c.Ui.Output("") - return 0 + for pool, keys := range installed { + c.Ui.Output("") + c.Ui.Output(pool + ":") + var out []string + for key, num := range keys { + out = append(out, fmt.Sprintf( + "%s | [%d/%d]", + key, num[0], num[1])) + } + c.Ui.Output(columnize.SimpleFormat(out)) + } } // initKeyring will create a keyring file at a given path. From 52582e7365fe20cb60181249a8880e7a4f216b31 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 09:31:55 -0700 Subject: [PATCH 127/238] command: fixing test cases for keyring --- command/agent/rpc_client_test.go | 157 +++++++++++-------------------- 1 file changed, 53 insertions(+), 104 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index a042d85e8..94a9f07d3 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -284,19 +284,16 @@ OUTER2: func TestRPCClientListKeys(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" - conf := Config{EncryptKey: key1} + conf := Config{EncryptKey: key1, Datacenter: "dc1"} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() - // Check WAN keys - keys := listKeys(t, p1.client, false) - if _, ok := keys[key1]; !ok { + // Key is initially installed to both wan/lan + keys := listKeys(t, p1.client) + if _, ok := keys["dc1"][key1]; !ok { t.Fatalf("bad: %#v", keys) } - - // Check LAN keys - keys = listKeys(t, p1.client, true) - if _, ok := keys[key1]; !ok { + if _, ok := keys["wan"][key1]; !ok { t.Fatalf("bad: %#v", keys) } } @@ -308,74 +305,64 @@ func TestRPCClientInstallKey(t *testing.T) { p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() - // Test WAN keys - keys := listKeys(t, p1.client, true) - if _, ok := keys[key2]; ok { + // key2 is not installed yet + keys := listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; ok || num != 0 { + t.Fatalf("bad: %#v", keys) + } + if num, ok := keys["wan"][key2]; ok || num != 0 { t.Fatalf("bad: %#v", keys) } - installKey(t, p1.client, key2, true) - - keys = listKeys(t, p1.client, true) - if _, ok := keys[key2]; !ok { - t.Fatalf("bad: %#v", keys) + // install key2 + if _, err := p1.client.InstallKey(key2); err != nil { + t.Fatalf("err: %s", err) } - // Test LAN keys - keys = listKeys(t, p1.client, false) - if _, ok := keys[key2]; ok { + // key2 should now be installed + keys = listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; !ok || num != 1 { t.Fatalf("bad: %#v", keys) } - - installKey(t, p1.client, key2, false) - - keys = listKeys(t, p1.client, false) - if _, ok := keys[key2]; !ok { + if num, ok := keys["wan"][key2]; !ok || num != 1 { t.Fatalf("bad: %#v", keys) } } -func TestRPCClientRotateKey(t *testing.T) { +func TestRPCClientUseKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" conf := Config{EncryptKey: key1} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() - // Test WAN keys - keys := listKeys(t, p1.client, true) - if _, ok := keys[key2]; ok { + // add a second key to the ring + if _, err := p1.client.InstallKey(key2); err != nil { + t.Fatalf("err: %s", err) + } + + // key2 is installed + keys := listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; !ok || num != 1 { + t.Fatalf("bad: %#v", keys) + } + if num, ok := keys["wan"][key2]; !ok || num != 1 { t.Fatalf("bad: %#v", keys) } - installKey(t, p1.client, key2, true) - useKey(t, p1.client, key2, true) - removeKey(t, p1.client, key1, true) - - keys = listKeys(t, p1.client, true) - if _, ok := keys[key1]; ok { - t.Fatalf("bad: %#v", keys) - } - if _, ok := keys[key2]; !ok { - t.Fatalf("bad: %#v", keys) + // can't remove key1 yet + if _, err := p1.client.RemoveKey(key1); err == nil { + t.Fatalf("expected error removing primary key") } - // Test LAN keys - keys = listKeys(t, p1.client, false) - if _, ok := keys[key2]; ok { - t.Fatalf("bad: %#v", keys) + // change primary key + if _, err := p1.client.UseKey(key2); err != nil { + t.Fatalf("err: %s", err) } - installKey(t, p1.client, key2, false) - useKey(t, p1.client, key2, false) - removeKey(t, p1.client, key1, false) - - keys = listKeys(t, p1.client, false) - if _, ok := keys[key1]; ok { - t.Fatalf("bad: %#v", keys) - } - if _, ok := keys[key2]; !ok { - t.Fatalf("bad: %#v", keys) + // can remove key1 now + if _, err := p1.client.RemoveKey(key1); err != nil { + t.Fatalf("err: %s", err) } } @@ -383,66 +370,28 @@ func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) { p1 := testRPCClient(t) defer p1.Close() - _, _, failures, err := p1.client.ListKeysLAN() + r, err := p1.client.ListKeys() if err == nil { t.Fatalf("no error listing keys with encryption disabled") } - if len(failures) != 1 { - t.Fatalf("bad: %#v", failures) + if len(r.Messages) != 1 { + t.Fatalf("bad: %#v", r) } } -func listKeys(t *testing.T, c *RPCClient, wan bool) (keys map[string]int) { - var err error - - if wan { - keys, _, _, err = c.ListKeysWAN() - } else { - keys, _, _, err = c.ListKeysLAN() - } - if err != nil { - t.Fatalf("err: %s", err) - } - - return -} - -func installKey(t *testing.T, c *RPCClient, key string, wan bool) { - var err error - - if wan { - _, err = c.InstallKeyWAN(key) - } else { - _, err = c.InstallKeyLAN(key) - } - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func useKey(t *testing.T, c *RPCClient, key string, wan bool) { - var err error - - if wan { - _, err = c.UseKeyWAN(key) - } else { - _, err = c.UseKeyLAN(key) - } - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func removeKey(t *testing.T, c *RPCClient, key string, wan bool) { - var err error - - if wan { - _, err = c.RemoveKeyWAN(key) - } else { - _, err = c.RemoveKeyLAN(key) - } +func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int { + resp, err := c.ListKeys() if err != nil { t.Fatalf("err: %s", err) } + out := make(map[string]map[string]int) + for _, k := range resp.Keys { + respID := k.Datacenter + if k.Pool == "WAN" { + respID = "wan" + } + out[respID] = map[string]int{k.Key: k.Count} + } + return out } From 6277a76a9dd208f1cac0eabfe11ad4385d7a1c26 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 09:54:46 -0700 Subject: [PATCH 128/238] agent: adjust rpc client tests for keyring --- command/agent/rpc_client_test.go | 47 ++++++++++++++++++++++++-------- command/util_test.go | 2 +- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 94a9f07d3..3b08aa733 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -315,9 +315,11 @@ func TestRPCClientInstallKey(t *testing.T) { } // install key2 - if _, err := p1.client.InstallKey(key2); err != nil { + r, err := p1.client.InstallKey(key2) + if err != nil { t.Fatalf("err: %s", err) } + keyringSuccess(t, r) // key2 should now be installed keys = listKeys(t, p1.client) @@ -337,9 +339,11 @@ func TestRPCClientUseKey(t *testing.T) { defer p1.Close() // add a second key to the ring - if _, err := p1.client.InstallKey(key2); err != nil { + r, err := p1.client.InstallKey(key2) + if err != nil { t.Fatalf("err: %s", err) } + keyringSuccess(t, r) // key2 is installed keys := listKeys(t, p1.client) @@ -351,19 +355,25 @@ func TestRPCClientUseKey(t *testing.T) { } // can't remove key1 yet - if _, err := p1.client.RemoveKey(key1); err == nil { - t.Fatalf("expected error removing primary key") + r, err = p1.client.RemoveKey(key1) + if err != nil { + t.Fatalf("err: %s", err) } + keyringError(t, r) // change primary key - if _, err := p1.client.UseKey(key2); err != nil { + r, err = p1.client.UseKey(key2) + if err != nil { t.Fatalf("err: %s", err) } + keyringSuccess(t, r) // can remove key1 now - if _, err := p1.client.RemoveKey(key1); err != nil { + r, err = p1.client.RemoveKey(key1) + if err != nil { t.Fatalf("err: %s", err) } + keyringSuccess(t, r) } func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) { @@ -371,13 +381,10 @@ func TestRPCClientKeyOperation_encryptionDisabled(t *testing.T) { defer p1.Close() r, err := p1.client.ListKeys() - if err == nil { - t.Fatalf("no error listing keys with encryption disabled") - } - - if len(r.Messages) != 1 { - t.Fatalf("bad: %#v", r) + if err != nil { + t.Fatalf("err: %s", err) } + keyringError(t, r) } func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int { @@ -395,3 +402,19 @@ func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int { } return out } + +func keyringError(t *testing.T, r keyResponse) { + for _, i := range r.Info { + if i.Error == "" { + t.Fatalf("no error reported from %s (%s)", i.Datacenter, i.Pool) + } + } +} + +func keyringSuccess(t *testing.T, r keyResponse) { + for _, i := range r.Info { + if i.Error != "" { + t.Fatalf("error from %s (%s): %s", i.Datacenter, i.Pool, i.Error) + } + } +} diff --git a/command/util_test.go b/command/util_test.go index 388c1e62b..586489233 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -53,7 +53,7 @@ func testAgentWithConfig(c *agent.Config, t *testing.T) *agentWrapper { conf := nextConfig() if c != nil { - conf = agent.MergeConfig(conf, c) + conf = agent.MergeConfig(c, conf) } dir, err := ioutil.TempDir("", "agent") From c4a9291bb926383292526046c04efacbfdcb864d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 12:35:15 -0700 Subject: [PATCH 129/238] command/keyring: remove unneeded -wan arg, fix tests --- command/agent/rpc.go | 8 ++-- command/agent/rpc_client.go | 22 ++++----- command/agent/rpc_client_test.go | 4 +- command/keyring.go | 9 ++-- command/keyring_test.go | 78 ++++++++------------------------ 5 files changed, 40 insertions(+), 81 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 288a97cf3..81beff662 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -109,7 +109,7 @@ type joinResponse struct { Num int32 } -type keyRequest struct { +type keyringRequest struct { Key string } @@ -136,7 +136,7 @@ type KeyringInfo struct { Error string } -type keyResponse struct { +type keyringResponse struct { Keys []KeyringEntry Messages []KeyringMessage Info []KeyringInfo @@ -626,9 +626,9 @@ func (i *AgentRPC) handleReload(client *rpcClient, seq uint64) error { } func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) error { - var req keyRequest + var req keyringRequest var queryResp *structs.KeyringResponses - var r keyResponse + var r keyringResponse var err error if cmd != listKeysCommand { diff --git a/command/agent/rpc_client.go b/command/agent/rpc_client.go index 454f427a8..7ba1907b2 100644 --- a/command/agent/rpc_client.go +++ b/command/agent/rpc_client.go @@ -176,45 +176,45 @@ func (c *RPCClient) WANMembers() ([]Member, error) { return resp.Members, err } -func (c *RPCClient) ListKeys() (keyResponse, error) { +func (c *RPCClient) ListKeys() (keyringResponse, error) { header := requestHeader{ Command: listKeysCommand, Seq: c.getSeq(), } - var resp keyResponse + var resp keyringResponse err := c.genericRPC(&header, nil, &resp) return resp, err } -func (c *RPCClient) InstallKey(key string) (keyResponse, error) { +func (c *RPCClient) InstallKey(key string) (keyringResponse, error) { header := requestHeader{ Command: installKeyCommand, Seq: c.getSeq(), } - req := keyRequest{key} - var resp keyResponse + req := keyringRequest{key} + var resp keyringResponse err := c.genericRPC(&header, &req, &resp) return resp, err } -func (c *RPCClient) UseKey(key string) (keyResponse, error) { +func (c *RPCClient) UseKey(key string) (keyringResponse, error) { header := requestHeader{ Command: useKeyCommand, Seq: c.getSeq(), } - req := keyRequest{key} - var resp keyResponse + req := keyringRequest{key} + var resp keyringResponse err := c.genericRPC(&header, &req, &resp) return resp, err } -func (c *RPCClient) RemoveKey(key string) (keyResponse, error) { +func (c *RPCClient) RemoveKey(key string) (keyringResponse, error) { header := requestHeader{ Command: removeKeyCommand, Seq: c.getSeq(), } - req := keyRequest{key} - var resp keyResponse + req := keyringRequest{key} + var resp keyringResponse err := c.genericRPC(&header, &req, &resp) return resp, err } diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 3b08aa733..e5aed3898 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -403,7 +403,7 @@ func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int { return out } -func keyringError(t *testing.T, r keyResponse) { +func keyringError(t *testing.T, r keyringResponse) { for _, i := range r.Info { if i.Error == "" { t.Fatalf("no error reported from %s (%s)", i.Datacenter, i.Pool) @@ -411,7 +411,7 @@ func keyringError(t *testing.T, r keyResponse) { } } -func keyringSuccess(t *testing.T, r keyResponse) { +func keyringSuccess(t *testing.T, r keyringResponse) { for _, i := range r.Info { if i.Error != "" { t.Fatalf("error from %s (%s): %s", i.Datacenter, i.Pool, i.Error) diff --git a/command/keyring.go b/command/keyring.go index 0e0bbc19b..e06248d42 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -28,7 +28,7 @@ type KeyringCommand struct { func (c *KeyringCommand) Run(args []string) int { var installKey, useKey, removeKey, init, dataDir string - var listKeys, wan bool + var listKeys bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } @@ -39,7 +39,6 @@ func (c *KeyringCommand) Run(args []string) int { cmdFlags.BoolVar(&listKeys, "list", false, "list keys") cmdFlags.StringVar(&init, "init", "", "initialize keyring") cmdFlags.StringVar(&dataDir, "data-dir", "", "data directory") - cmdFlags.BoolVar(&wan, "wan", false, "operate on wan keyring") rpcAddr := RPCAddrFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -281,8 +280,8 @@ Usage: consul keyring [options] without disrupting the cluster. With the exception of the -init argument, all operations performed by this - command can only be run against server nodes. All operations default to the - LAN gossip pool. + command can only be run against server nodes, and affect both the LAN and + WAN keyrings in lock-step. Options: @@ -298,8 +297,6 @@ Options: -init= Create the initial keyring files for Consul to use containing the provided key. The -data-dir argument is required with this option. - -wan Operate on the WAN keyring instead of the LAN - keyring (default). -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) diff --git a/command/keyring_test.go b/command/keyring_test.go index c7c0847f4..98f8ba3dd 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -24,9 +24,12 @@ func TestKeyringCommandRun(t *testing.T) { a1 := testAgentWithConfig(&conf, t) defer a1.Shutdown() - // The keyring was initialized with only the provided key - out := listKeys(t, a1.addr, false) - if !strings.Contains(out, key1) { + // The LAN and WAN keyrings were initialized with key1 + out := listKeys(t, a1.addr) + if !strings.Contains(out, "dc1 (LAN):\n"+key1) { + t.Fatalf("bad: %#v", out) + } + if !strings.Contains(out, "WAN:\n"+key1) { t.Fatalf("bad: %#v", out) } if strings.Contains(out, key2) { @@ -34,51 +37,26 @@ func TestKeyringCommandRun(t *testing.T) { } // Install the second key onto the keyring - installKey(t, a1.addr, key2, false) + installKey(t, a1.addr, key2) // Both keys should be present - out = listKeys(t, a1.addr, false) + out = listKeys(t, a1.addr) for _, key := range []string{key1, key2} { if !strings.Contains(out, key) { t.Fatalf("bad: %#v", out) } } - // WAN keyring is untouched - out = listKeys(t, a1.addr, true) - if strings.Contains(out, key2) { + // Rotate to key2, remove key1 + useKey(t, a1.addr, key2) + removeKey(t, a1.addr, key1) + + // Only key2 is present now + out = listKeys(t, a1.addr) + if !strings.Contains(out, "dc1 (LAN):\n"+key2) { t.Fatalf("bad: %#v", out) } - - // Change out the primary key - useKey(t, a1.addr, key2, false) - - // Remove the original key - removeKey(t, a1.addr, key1, false) - - // Make sure only the new key is present - out = listKeys(t, a1.addr, false) - if strings.Contains(out, key1) { - t.Fatalf("bad: %#v", out) - } - if !strings.Contains(out, key2) { - t.Fatalf("bad: %#v", out) - } - - // WAN keyring is still untouched - out = listKeys(t, a1.addr, true) - if !strings.Contains(out, key1) { - t.Fatalf("bad: %#v", out) - } - - // Rotate out the WAN key - installKey(t, a1.addr, key2, true) - useKey(t, a1.addr, key2, true) - removeKey(t, a1.addr, key1, true) - - // WAN keyring now has only the proper key - out = listKeys(t, a1.addr, true) - if !strings.Contains(out, key2) { + if !strings.Contains(out, "WAN:\n"+key2) { t.Fatalf("bad: %#v", out) } if strings.Contains(out, key1) { @@ -179,15 +157,11 @@ func TestKeyringCommandRun_initKeyring(t *testing.T) { } } -func listKeys(t *testing.T, addr string, wan bool) string { +func listKeys(t *testing.T, addr string) string { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-list", "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) @@ -196,45 +170,33 @@ func listKeys(t *testing.T, addr string, wan bool) string { return ui.OutputWriter.String() } -func installKey(t *testing.T, addr string, key string, wan bool) { +func installKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-install=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func useKey(t *testing.T, addr string, key string, wan bool) { +func useKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-use=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } } -func removeKey(t *testing.T, addr string, key string, wan bool) { +func removeKey(t *testing.T, addr string, key string) { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} args := []string{"-remove=" + key, "-rpc-addr=" + addr} - if wan { - args = append(args, "-wan") - } - code := c.Run(args) if code != 0 { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) From 1ec111bbfc12d4aed9c473a11d9addd5196f8d4a Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 13:01:26 -0700 Subject: [PATCH 130/238] consul: kill unused struct fields --- command/agent/rpc.go | 4 ---- consul/internal_endpoint.go | 2 -- consul/structs/structs.go | 2 -- website/source/docs/commands/keyring.html.markdown | 4 +--- 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index 81beff662..e64084d17 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -131,8 +131,6 @@ type KeyringInfo struct { Datacenter string Pool string NumNodes int - NumResp int - NumErr int Error string } @@ -686,8 +684,6 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro Datacenter: kr.Datacenter, Pool: pool, NumNodes: kr.NumNodes, - NumResp: kr.NumResp, - NumErr: kr.NumErr, Error: kr.Error, } r.Info = append(r.Info, info) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 2cd08a2c6..a1a170f77 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -81,9 +81,7 @@ func (m *Internal) ingestKeyringResponse( Datacenter: m.srv.config.Datacenter, Messages: serfResp.Messages, Keys: serfResp.Keys, - NumResp: serfResp.NumResp, NumNodes: serfResp.NumNodes, - NumErr: serfResp.NumErr, Error: errStr, }) } diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 6e752ea4f..2557bfc39 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -548,8 +548,6 @@ type KeyringResponse struct { Messages map[string]string Keys map[string]int NumNodes int - NumResp int - NumErr int Error string } diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 6a635746c..03b1d6c25 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -14,9 +14,7 @@ distributing new encryption keys to the cluster, retiring old encryption keys, and changing the keys used by the cluster to encrypt messages. Because Consul utilizes multiple gossip pools, this command will only operate -against a server node for most operations. By default, all operations carried -out by this command are run against the LAN gossip pool in the datacenter of the -agent. +against a server node for most operations. Consul allows multiple encryption keys to be in use simultaneously. This is intended to provide a transition state while the cluster converges. It is the From c1ea2911127bbff2e72c4a688a62719ca8fa68a1 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 13:44:51 -0700 Subject: [PATCH 131/238] command: fix panic when client RPC is asked for a keyring operation --- command/agent/rpc.go | 5 +++++ command/keyring.go | 14 -------------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/command/agent/rpc.go b/command/agent/rpc.go index e64084d17..c4fe71f70 100644 --- a/command/agent/rpc.go +++ b/command/agent/rpc.go @@ -655,6 +655,10 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro Error: errToString(err), } + if queryResp == nil { + goto SEND + } + for _, kr := range queryResp.Responses { var pool string if kr.WAN { @@ -689,6 +693,7 @@ func (i *AgentRPC) handleKeyring(client *rpcClient, seq uint64, cmd string) erro r.Info = append(r.Info, info) } +SEND: return client.Send(&header, r) } diff --git a/command/keyring.go b/command/keyring.go index e06248d42..cc6d211a8 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -96,20 +96,6 @@ func (c *KeyringCommand) Run(args []string) int { } defer client.Close() - // For all key-related operations, we must be querying a server node. It is - // probably better to enforce this even for LAN pool changes, because other- - // wise, the same exact command syntax will have different results depending - // on where it was run. - s, err := client.Stats() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error: %s", err)) - return 1 - } - if s["consul"]["server"] != "true" { - c.Ui.Error("Error: Key modification can only be handled by a server") - return 1 - } - if listKeys { c.Ui.Info("Asking all members for installed keys...") r, err := client.ListKeys() From bea19b51358c043902e56796f1aa3e922400f320 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 15:49:47 -0700 Subject: [PATCH 132/238] command/keyring: refactor, adjust tests --- command/keyring.go | 17 ++++------------- command/keyring_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/command/keyring.go b/command/keyring.go index cc6d211a8..cde37f3e6 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/consul/command/agent" "github.com/mitchellh/cli" - "github.com/ryanuber/columnize" ) const ( @@ -97,7 +96,7 @@ func (c *KeyringCommand) Run(args []string) int { defer client.Close() if listKeys { - c.Ui.Info("Asking all members for installed keys...") + c.Ui.Info("Gathering installed encryption keys...") r, err := client.ListKeys() if err != nil { c.Ui.Error(fmt.Sprintf("error: %s", err)) @@ -161,17 +160,12 @@ func (c *KeyringCommand) handleResponse( c.Ui.Error("") c.Ui.Error(fmt.Sprintf("%s error: %s", pool, i.Error)) - var errors []string for _, msg := range messages { if msg.Datacenter != i.Datacenter || msg.Pool != i.Pool { continue } - errors = append(errors, fmt.Sprintf( - "failed: %s | %s", - msg.Node, - msg.Message)) + c.Ui.Error(fmt.Sprintf(" %s: %s", msg.Node, msg.Message)) } - c.Ui.Error(columnize.SimpleFormat(errors)) rval = 1 } } @@ -208,16 +202,13 @@ func (c *KeyringCommand) handleList( installed[pool][key.Key] = []int{key.Count, nodes} } } + for pool, keys := range installed { c.Ui.Output("") c.Ui.Output(pool + ":") - var out []string for key, num := range keys { - out = append(out, fmt.Sprintf( - "%s | [%d/%d]", - key, num[0], num[1])) + c.Ui.Output(fmt.Sprintf(" %s [%d/%d]", key, num[0], num[1])) } - c.Ui.Output(columnize.SimpleFormat(out)) } } diff --git a/command/keyring_test.go b/command/keyring_test.go index 98f8ba3dd..7e975b6cc 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -26,10 +26,10 @@ func TestKeyringCommandRun(t *testing.T) { // The LAN and WAN keyrings were initialized with key1 out := listKeys(t, a1.addr) - if !strings.Contains(out, "dc1 (LAN):\n"+key1) { + if !strings.Contains(out, "dc1 (LAN):\n "+key1) { t.Fatalf("bad: %#v", out) } - if !strings.Contains(out, "WAN:\n"+key1) { + if !strings.Contains(out, "WAN:\n "+key1) { t.Fatalf("bad: %#v", out) } if strings.Contains(out, key2) { @@ -53,10 +53,10 @@ func TestKeyringCommandRun(t *testing.T) { // Only key2 is present now out = listKeys(t, a1.addr) - if !strings.Contains(out, "dc1 (LAN):\n"+key2) { + if !strings.Contains(out, "dc1 (LAN):\n "+key2) { t.Fatalf("bad: %#v", out) } - if !strings.Contains(out, "WAN:\n"+key2) { + if !strings.Contains(out, "WAN:\n "+key2) { t.Fatalf("bad: %#v", out) } if strings.Contains(out, key1) { From d7edc1c51ca3894fd28660039d8e7073c4be515d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 29 Sep 2014 23:19:53 -0700 Subject: [PATCH 133/238] consul: break rpc forwarding and response ingestion out of internal endpoints --- consul/internal_endpoint.go | 77 ++++++++----------------------------- consul/keyring.go | 55 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 61 deletions(-) create mode 100644 consul/keyring.go diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index a1a170f77..9d8d000e9 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -2,7 +2,6 @@ package consul import ( "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/serf/serf" ) // Internal endpoint is used to query the miscellaneous info that @@ -64,67 +63,23 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } -// ingestKeyringResponse is a helper method to pick the relative information -// from a Serf message and stuff it into a KeyringResponse. -func (m *Internal) ingestKeyringResponse( - serfResp *serf.KeyResponse, - reply *structs.KeyringResponses, - err error, wan bool) { - - errStr := "" - if err != nil { - errStr = err.Error() - } - - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - WAN: wan, - Datacenter: m.srv.config.Datacenter, - Messages: serfResp.Messages, - Keys: serfResp.Keys, - NumNodes: serfResp.NumNodes, - Error: errStr, - }) -} - -func (m *Internal) forwardKeyring( - method string, - args *structs.KeyringRequest, - replies *structs.KeyringResponses) error { - - for dc, _ := range m.srv.remoteConsuls { - if dc == m.srv.config.Datacenter { - continue - } - rr := structs.KeyringResponses{} - if err := m.srv.forwardDC(method, dc, args, &rr); err != nil { - return err - } - for _, r := range rr.Responses { - replies.Responses = append(replies.Responses, r) - } - } - - return nil -} - // ListKeys will query the WAN and LAN gossip keyrings of all nodes, adding // results into a collective response as we go. func (m *Internal) ListKeys( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - m.srv.setQueryMeta(&reply.QueryMeta) - + dc := m.srv.config.Datacenter respLAN, err := m.srv.KeyManagerLAN().ListKeys() - m.ingestKeyringResponse(respLAN, reply, err, false) + ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().ListKeys() - m.ingestKeyringResponse(respWAN, reply, err, true) + ingestKeyringResponse(respWAN, reply, dc, true, err) // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.forwardKeyring("Internal.ListKeys", args, reply) + m.srv.forwardKeyringRPC("Internal.ListKeys", args, reply) } return nil @@ -136,16 +91,16 @@ func (m *Internal) InstallKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { + dc := m.srv.config.Datacenter respLAN, err := m.srv.KeyManagerLAN().InstallKey(args.Key) - m.ingestKeyringResponse(respLAN, reply, err, false) + ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().InstallKey(args.Key) - m.ingestKeyringResponse(respWAN, reply, err, true) + ingestKeyringResponse(respWAN, reply, dc, true, err) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.forwardKeyring("Internal.InstallKey", args, reply) + m.srv.forwardKeyringRPC("Internal.InstallKey", args, reply) } return nil @@ -157,16 +112,16 @@ func (m *Internal) UseKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { + dc := m.srv.config.Datacenter respLAN, err := m.srv.KeyManagerLAN().UseKey(args.Key) - m.ingestKeyringResponse(respLAN, reply, err, false) + ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().UseKey(args.Key) - m.ingestKeyringResponse(respWAN, reply, err, true) + ingestKeyringResponse(respWAN, reply, dc, true, err) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.forwardKeyring("Internal.UseKey", args, reply) + m.srv.forwardKeyringRPC("Internal.UseKey", args, reply) } return nil @@ -177,16 +132,16 @@ func (m *Internal) RemoveKey( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { + dc := m.srv.config.Datacenter respLAN, err := m.srv.KeyManagerLAN().RemoveKey(args.Key) - m.ingestKeyringResponse(respLAN, reply, err, false) + ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { respWAN, err := m.srv.KeyManagerWAN().RemoveKey(args.Key) - m.ingestKeyringResponse(respWAN, reply, err, true) + ingestKeyringResponse(respWAN, reply, dc, true, err) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.forwardKeyring("Internal.RemoveKey", args, reply) + m.srv.forwardKeyringRPC("Internal.RemoveKey", args, reply) } return nil diff --git a/consul/keyring.go b/consul/keyring.go new file mode 100644 index 000000000..373523118 --- /dev/null +++ b/consul/keyring.go @@ -0,0 +1,55 @@ +package consul + +import ( + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/serf/serf" +) + +// ingestKeyringResponse is a helper method to pick the relative information +// from a Serf message and stuff it into a KeyringResponse. +func ingestKeyringResponse( + serfResp *serf.KeyResponse, reply *structs.KeyringResponses, + dc string, wan bool, err error) { + + errStr := "" + if err != nil { + errStr = err.Error() + } + + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + WAN: wan, + Datacenter: dc, + Messages: serfResp.Messages, + Keys: serfResp.Keys, + NumNodes: serfResp.NumNodes, + Error: errStr, + }) +} + +// forwardKeyringRPC is used to forward a keyring-related RPC request to one +// server in each datacenter. Since the net/rpc package writes replies in-place, +// we use this specialized method for dealing with keyring-related replies +// specifically by appending them to a wrapper response struct. +// +// This will only error for RPC-related errors. Otherwise, application-level +// errors are returned inside of the inner response objects. +func (s *Server) forwardKeyringRPC( + method string, + args *structs.KeyringRequest, + replies *structs.KeyringResponses) error { + + for dc, _ := range s.remoteConsuls { + if dc == s.config.Datacenter { + continue + } + rr := structs.KeyringResponses{} + if err := s.forwardDC(method, dc, args, &rr); err != nil { + return err + } + for _, r := range rr.Responses { + replies.Responses = append(replies.Responses, r) + } + } + + return nil +} From a1943afddcadb80b5bf63a327832898b5ec45f8d Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 30 Sep 2014 10:03:47 -0700 Subject: [PATCH 134/238] consul: make forwarding to multiple datacenters parallel --- consul/internal_endpoint.go | 8 ++-- consul/keyring.go | 55 -------------------------- consul/serf.go | 79 +++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 59 deletions(-) delete mode 100644 consul/keyring.go diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 9d8d000e9..ee2270700 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -79,7 +79,7 @@ func (m *Internal) ListKeys( // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.srv.forwardKeyringRPC("Internal.ListKeys", args, reply) + m.srv.keyringRPC("Internal.ListKeys", args, reply) } return nil @@ -100,7 +100,7 @@ func (m *Internal) InstallKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.forwardKeyringRPC("Internal.InstallKey", args, reply) + m.srv.keyringRPC("Internal.InstallKey", args, reply) } return nil @@ -121,7 +121,7 @@ func (m *Internal) UseKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.forwardKeyringRPC("Internal.UseKey", args, reply) + m.srv.keyringRPC("Internal.UseKey", args, reply) } return nil @@ -141,7 +141,7 @@ func (m *Internal) RemoveKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.forwardKeyringRPC("Internal.RemoveKey", args, reply) + m.srv.keyringRPC("Internal.RemoveKey", args, reply) } return nil diff --git a/consul/keyring.go b/consul/keyring.go deleted file mode 100644 index 373523118..000000000 --- a/consul/keyring.go +++ /dev/null @@ -1,55 +0,0 @@ -package consul - -import ( - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/serf/serf" -) - -// ingestKeyringResponse is a helper method to pick the relative information -// from a Serf message and stuff it into a KeyringResponse. -func ingestKeyringResponse( - serfResp *serf.KeyResponse, reply *structs.KeyringResponses, - dc string, wan bool, err error) { - - errStr := "" - if err != nil { - errStr = err.Error() - } - - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - WAN: wan, - Datacenter: dc, - Messages: serfResp.Messages, - Keys: serfResp.Keys, - NumNodes: serfResp.NumNodes, - Error: errStr, - }) -} - -// forwardKeyringRPC is used to forward a keyring-related RPC request to one -// server in each datacenter. Since the net/rpc package writes replies in-place, -// we use this specialized method for dealing with keyring-related replies -// specifically by appending them to a wrapper response struct. -// -// This will only error for RPC-related errors. Otherwise, application-level -// errors are returned inside of the inner response objects. -func (s *Server) forwardKeyringRPC( - method string, - args *structs.KeyringRequest, - replies *structs.KeyringResponses) error { - - for dc, _ := range s.remoteConsuls { - if dc == s.config.Datacenter { - continue - } - rr := structs.KeyringResponses{} - if err := s.forwardDC(method, dc, args, &rr); err != nil { - return err - } - for _, r := range rr.Responses { - replies.Responses = append(replies.Responses, r) - } - } - - return nil -} diff --git a/consul/serf.go b/consul/serf.go index ae1dacc33..bfdf5668e 100644 --- a/consul/serf.go +++ b/consul/serf.go @@ -4,6 +4,7 @@ import ( "net" "strings" + "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/serf/serf" ) @@ -276,3 +277,81 @@ func (s *Server) nodeFailed(me serf.MemberEvent, wan bool) { } } } + +// ingestKeyringResponse is a helper method to pick the relative information +// from a Serf message and stuff it into a KeyringResponse. +func ingestKeyringResponse( + serfResp *serf.KeyResponse, reply *structs.KeyringResponses, + dc string, wan bool, err error) { + + errStr := "" + if err != nil { + errStr = err.Error() + } + + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + WAN: wan, + Datacenter: dc, + Messages: serfResp.Messages, + Keys: serfResp.Keys, + NumNodes: serfResp.NumNodes, + Error: errStr, + }) +} + +// forwardKeyring handles sending an RPC request to a remote datacenter and +// funneling any errors or responses back through the provided channels. +func (s *Server) forwardKeyringRPC( + method, dc string, + args *structs.KeyringRequest, + errorCh chan<- error, + respCh chan<- *structs.KeyringResponses) { + + rr := structs.KeyringResponses{} + if err := s.forwardDC(method, dc, args, &rr); err != nil { + errorCh <- err + return + } + respCh <- &rr + return +} + +// keyringRPC is used to forward a keyring-related RPC request to one +// server in each datacenter. This will only error for RPC-related errors. +// Otherwise, application-level errors are returned inside of the inner +// response objects. +func (s *Server) keyringRPC( + method string, + args *structs.KeyringRequest, + replies *structs.KeyringResponses) error { + + errorCh := make(chan error) + respCh := make(chan *structs.KeyringResponses) + + for dc, _ := range s.remoteConsuls { + if dc == s.config.Datacenter { + continue + } + go s.forwardKeyringRPC(method, dc, args, errorCh, respCh) + } + + rlen := len(s.remoteConsuls) - 1 + done := 0 + for { + select { + case err := <-errorCh: + return err + case rr := <-respCh: + for _, r := range rr.Responses { + replies.Responses = append(replies.Responses, r) + } + done++ + } + + if done == rlen { + break + } + } + + return nil +} From cb795199d12a6165c19ba33ecc5df93a3d7e96a8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 30 Sep 2014 14:19:25 -0700 Subject: [PATCH 135/238] consul: test rpc errors returned from remote datacenters --- consul/serf.go | 6 +++++- consul/serf_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/consul/serf.go b/consul/serf.go index bfdf5668e..02d068374 100644 --- a/consul/serf.go +++ b/consul/serf.go @@ -325,6 +325,11 @@ func (s *Server) keyringRPC( args *structs.KeyringRequest, replies *structs.KeyringResponses) error { + rlen := len(s.remoteConsuls) - 1 + if rlen == 0 { + return nil + } + errorCh := make(chan error) respCh := make(chan *structs.KeyringResponses) @@ -335,7 +340,6 @@ func (s *Server) keyringRPC( go s.forwardKeyringRPC(method, dc, args, errorCh, respCh) } - rlen := len(s.remoteConsuls) - 1 done := 0 for { select { diff --git a/consul/serf_test.go b/consul/serf_test.go index 07225a729..b30b4fe82 100644 --- a/consul/serf_test.go +++ b/consul/serf_test.go @@ -1,6 +1,8 @@ package consul import ( + "fmt" + "os" "testing" ) @@ -19,3 +21,25 @@ func TestUserEventNames(t *testing.T) { t.Fatalf("bad: %v", raw) } } + +func TestKeyringRPCError(t *testing.T) { + dir1, s1 := testServerDC(t, "dc1") + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + dir2, s2 := testServerDC(t, "dc2") + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + // RPC error from remote datacenter is returned + if err := s1.keyringRPC("Bad.Method", nil, nil); err == nil { + t.Fatalf("bad") + } +} From 001a579d479b8d702d10a335cff1194da42f5942 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 30 Sep 2014 15:31:07 -0700 Subject: [PATCH 136/238] command/keyring: cleanup --- command/keyring.go | 22 +++++++------------ consul/structs/structs.go | 3 +++ .../docs/commands/keyring.html.markdown | 13 +++++++---- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/command/keyring.go b/command/keyring.go index cde37f3e6..78c56fd2b 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -13,12 +13,6 @@ import ( "github.com/mitchellh/cli" ) -const ( - installKeyCommand = "install" - useKeyCommand = "use" - removeKeyCommand = "remove" -) - // KeyringCommand is a Command implementation that handles querying, installing, // and removing gossip encryption keys from a keyring. type KeyringCommand struct { @@ -102,10 +96,10 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - if rval := c.handleResponse(r.Info, r.Messages, r.Keys); rval != 0 { + if rval := c.handleResponse(r.Info, r.Messages); rval != 0 { return rval } - c.handleList(r.Info, r.Messages, r.Keys) + c.handleList(r.Info, r.Keys) return 0 } @@ -116,7 +110,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - return c.handleResponse(r.Info, r.Messages, r.Keys) + return c.handleResponse(r.Info, r.Messages) } if useKey != "" { @@ -126,7 +120,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - return c.handleResponse(r.Info, r.Messages, r.Keys) + return c.handleResponse(r.Info, r.Messages) } if removeKey != "" { @@ -136,7 +130,7 @@ func (c *KeyringCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("error: %s", err)) return 1 } - return c.handleResponse(r.Info, r.Messages, r.Keys) + return c.handleResponse(r.Info, r.Messages) } // Should never make it here @@ -145,8 +139,7 @@ func (c *KeyringCommand) Run(args []string) int { func (c *KeyringCommand) handleResponse( info []agent.KeyringInfo, - messages []agent.KeyringMessage, - keys []agent.KeyringEntry) int { + messages []agent.KeyringMessage) int { var rval int @@ -179,7 +172,6 @@ func (c *KeyringCommand) handleResponse( func (c *KeyringCommand) handleList( info []agent.KeyringInfo, - messages []agent.KeyringMessage, keys []agent.KeyringEntry) { installed := make(map[string]map[string][]int) @@ -274,6 +266,8 @@ Options: -init= Create the initial keyring files for Consul to use containing the provided key. The -data-dir argument is required with this option. + -data-dir= The path to the Consul agent's data directory. This + argument is only needed for keyring initialization. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 2557bfc39..dfaadbaa7 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -551,6 +551,9 @@ type KeyringResponse struct { Error string } +// KeyringResponses holds multiple responses to keyring queries. Each +// datacenter replies independently, and KeyringResponses is used as a +// container for the set of all responses. type KeyringResponses struct { Responses []*KeyringResponse QueryMeta diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 03b1d6c25..518ccefc9 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -22,6 +22,10 @@ responsibility of the operator to ensure that only the required encryption keys are installed on the cluster. You can ensure that a key is not installed using the `-list` and `-remove` options. +With the exception of the `-init` argument, all operations performed by this +command can only be run against server nodes, and affect both the LAN and +WAN keyrings in lock-step. + All variations of the `keyring` command, unless otherwise specified below, will return 0 if all nodes reply and there are no errors. If any node fails to reply or reports failure, the exit code will be 1. @@ -38,13 +42,14 @@ The list of available flags are: * `-init` - Creates the keyring file(s). This is useful to configure initial encryption keyrings, which can later be mutated using the other arguments in this command. This argument accepts an ASCII key, which can be generated using - the [keygen command](/docs/commands/keygen.html). + the [keygen command](/docs/commands/keygen.html). Requires the `-data-dir` + argument. This operation can be run on both client and server nodes and requires no network connectivity. - Returns 0 if the key is successfully configured, or 1 if there were any - problems. + Returns 0 if the key is successfully configured, or 1 if there were any + problems. * `-install` - Install a new encryption key. This will broadcast the new key to all members in the cluster. @@ -57,6 +62,6 @@ The list of available flags are: * `-list` - List all keys currently in use within the cluster. -* `-wan` - Operate on the WAN keyring instead of the LAN keyring (default) +* `-data-dir` - The path to Consul's data directory. Used with `-init` only. * `-rpc-addr` - RPC address of the Consul agent. From 08f1605159bf6b009347dd63635bf924ccc3f4fd Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 30 Sep 2014 17:31:16 -0700 Subject: [PATCH 137/238] website: clean up keyring command docs and add output examples --- .../docs/commands/keyring.html.markdown | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 518ccefc9..6caa172dd 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -13,14 +13,11 @@ Consul's [Gossip Pools](/docs/internals/gossip.html). It is capable of distributing new encryption keys to the cluster, retiring old encryption keys, and changing the keys used by the cluster to encrypt messages. -Because Consul utilizes multiple gossip pools, this command will only operate -against a server node for most operations. - Consul allows multiple encryption keys to be in use simultaneously. This is intended to provide a transition state while the cluster converges. It is the responsibility of the operator to ensure that only the required encryption keys -are installed on the cluster. You can ensure that a key is not installed using -the `-list` and `-remove` options. +are installed on the cluster. You can review the installed keys using the +`-list` argument, and remove unneeded keys with `-remove`. With the exception of the `-init` argument, all operations performed by this command can only be run against server nodes, and affect both the LAN and @@ -65,3 +62,55 @@ The list of available flags are: * `-data-dir` - The path to Consul's data directory. Used with `-init` only. * `-rpc-addr` - RPC address of the Consul agent. + +## Output + +The output of the `consul keyring -list` command consolidates information from +all nodes and all datacenters to provide a simple and easy to understand view of +the cluster. The following is some example output from a cluster with two +datacenters, each which consist of one server and one client: + +``` +==> Gathering installed encryption keys... +==> Done! + +WAN: + a1i101sMY8rxB+0eAKD/gw== [2/2] + +dc2 (LAN): + a1i101sMY8rxB+0eAKD/gw== [2/2] + +dc1 (LAN): + a1i101sMY8rxB+0eAKD/gw== [2/2] +``` + +As you can see, the output above is divided first by gossip pool, and then by +encryption key. The indicator to the right of each key displays the number of +nodes the key is installed on over the total number of nodes in the pool. + +## Errors + +If any errors are encountered while performing a keyring operation, no key +information is displayed, but instead only error information. The error +information is arranged in a similar fashion, organized first by datacenter, +followed by a simple list of nodes which had errors, and the actual text of the +error. Below is sample output from the same cluster as above, if we try to do +something that causes an error; in this case, trying to remove the primary key: + +``` +==> Removing gossip encryption key... + +dc1 (LAN) error: 2/2 nodes reported failure + server1: Removing the primary key is not allowed + client1: Removing the primary key is not allowed + +WAN error: 2/2 nodes reported failure + server1.dc1: Removing the primary key is not allowed + server2.dc2: Removing the primary key is not allowed + +dc2 (LAN) error: 2/2 nodes reported failure + server2: Removing the primary key is not allowed + client2: Removing the primary key is not allowed +``` + +As you can see, each node with a failure reported what went wrong. From 33dea16567490e51d9d4b3c90a5fd60b7751de39 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 30 Sep 2014 18:23:55 -0700 Subject: [PATCH 138/238] agent: fix install key test --- command/agent/rpc_client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index e5aed3898..518423a69 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -301,7 +301,7 @@ func TestRPCClientListKeys(t *testing.T) { func TestRPCClientInstallKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1} + conf := Config{EncryptKey: key1, Server: true} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() From 057c22db100f3977c743b7d526e8a164fcac8642 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 1 Oct 2014 23:09:00 -0700 Subject: [PATCH 139/238] consul: generalize multi-DC RPC call broadcasts --- consul/internal_endpoint.go | 31 +++++++++++--- consul/rpc.go | 43 +++++++++++++++++++ consul/serf.go | 83 ------------------------------------- consul/structs/structs.go | 40 +++++++++++++++++- 4 files changed, 107 insertions(+), 90 deletions(-) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index ee2270700..223b8eeff 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -2,6 +2,7 @@ package consul import ( "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/serf/serf" ) // Internal endpoint is used to query the miscellaneous info that @@ -77,9 +78,8 @@ func (m *Internal) ListKeys( respWAN, err := m.srv.KeyManagerWAN().ListKeys() ingestKeyringResponse(respWAN, reply, dc, true, err) - // Mark key rotation as being already forwarded, then forward. args.Forwarded = true - m.srv.keyringRPC("Internal.ListKeys", args, reply) + m.srv.globalRPC("Internal.ListKeys", args, reply) } return nil @@ -100,7 +100,7 @@ func (m *Internal) InstallKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.keyringRPC("Internal.InstallKey", args, reply) + m.srv.globalRPC("Internal.InstallKey", args, reply) } return nil @@ -121,7 +121,7 @@ func (m *Internal) UseKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.keyringRPC("Internal.UseKey", args, reply) + m.srv.globalRPC("Internal.UseKey", args, reply) } return nil @@ -141,8 +141,29 @@ func (m *Internal) RemoveKey( ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.keyringRPC("Internal.RemoveKey", args, reply) + m.srv.globalRPC("Internal.RemoveKey", args, reply) } return nil } + +// ingestKeyringResponse is a helper method to pick the relative information +// from a Serf message and stuff it into a KeyringResponse. +func ingestKeyringResponse( + serfResp *serf.KeyResponse, reply *structs.KeyringResponses, + dc string, wan bool, err error) { + + errStr := "" + if err != nil { + errStr = err.Error() + } + + reply.Responses = append(reply.Responses, &structs.KeyringResponse{ + WAN: wan, + Datacenter: dc, + Messages: serfResp.Messages, + Keys: serfResp.Keys, + NumNodes: serfResp.NumNodes, + Error: errStr, + }) +} diff --git a/consul/rpc.go b/consul/rpc.go index cd5c36ebd..6fcb07d82 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -223,6 +223,49 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ return s.connPool.RPC(server.Addr, server.Version, method, args, reply) } +// globalRPC is used to forward an RPC request to one server in each datacenter. +// This will only error for RPC-related errors. Otherwise, application-level +// errors are returned inside of the inner response objects. +func (s *Server) globalRPC(method string, args interface{}, + reply structs.CompoundResponse) error { + + rlen := len(s.remoteConsuls) + if rlen < 2 { + return nil + } + + errorCh := make(chan error) + respCh := make(chan interface{}) + + // Make a new request into each datacenter + for dc, _ := range s.remoteConsuls { + info := &structs.GenericRPC{Datacenter: dc} + go func() { + rr := reply.New() + if _, err := s.forward(method, info, args, &rr); err != nil { + errorCh <- err + return + } + respCh <- rr + }() + } + + done := 0 + for { + select { + case err := <-errorCh: + return err + case rr := <-respCh: + reply.Add(rr) + done++ + } + if done == rlen { + break + } + } + return 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{}, error) { diff --git a/consul/serf.go b/consul/serf.go index 02d068374..ae1dacc33 100644 --- a/consul/serf.go +++ b/consul/serf.go @@ -4,7 +4,6 @@ import ( "net" "strings" - "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/serf/serf" ) @@ -277,85 +276,3 @@ func (s *Server) nodeFailed(me serf.MemberEvent, wan bool) { } } } - -// ingestKeyringResponse is a helper method to pick the relative information -// from a Serf message and stuff it into a KeyringResponse. -func ingestKeyringResponse( - serfResp *serf.KeyResponse, reply *structs.KeyringResponses, - dc string, wan bool, err error) { - - errStr := "" - if err != nil { - errStr = err.Error() - } - - reply.Responses = append(reply.Responses, &structs.KeyringResponse{ - WAN: wan, - Datacenter: dc, - Messages: serfResp.Messages, - Keys: serfResp.Keys, - NumNodes: serfResp.NumNodes, - Error: errStr, - }) -} - -// forwardKeyring handles sending an RPC request to a remote datacenter and -// funneling any errors or responses back through the provided channels. -func (s *Server) forwardKeyringRPC( - method, dc string, - args *structs.KeyringRequest, - errorCh chan<- error, - respCh chan<- *structs.KeyringResponses) { - - rr := structs.KeyringResponses{} - if err := s.forwardDC(method, dc, args, &rr); err != nil { - errorCh <- err - return - } - respCh <- &rr - return -} - -// keyringRPC is used to forward a keyring-related RPC request to one -// server in each datacenter. This will only error for RPC-related errors. -// Otherwise, application-level errors are returned inside of the inner -// response objects. -func (s *Server) keyringRPC( - method string, - args *structs.KeyringRequest, - replies *structs.KeyringResponses) error { - - rlen := len(s.remoteConsuls) - 1 - if rlen == 0 { - return nil - } - - errorCh := make(chan error) - respCh := make(chan *structs.KeyringResponses) - - for dc, _ := range s.remoteConsuls { - if dc == s.config.Datacenter { - continue - } - go s.forwardKeyringRPC(method, dc, args, errorCh, respCh) - } - - done := 0 - for { - select { - case err := <-errorCh: - return err - case rr := <-respCh: - for _, r := range rr.Responses { - replies.Responses = append(replies.Responses, r) - } - done++ - } - - if done == rlen { - break - } - } - - return nil -} diff --git a/consul/structs/structs.go b/consul/structs/structs.go index dfaadbaa7..1b3ead960 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -127,6 +127,16 @@ type QueryMeta struct { KnownLeader bool } +// GenericRPC is the simplest possible RPCInfo implementation +type GenericRPC struct { + Datacenter string + QueryOptions +} + +func (r *GenericRPC) RequestDatacenter() string { + return r.Datacenter +} + // RegisterRequest is used for the Catalog.Register endpoint // to register a node as providing a service. If no service // is provided, the node is registered. @@ -532,14 +542,31 @@ func Encode(t MessageType, msg interface{}) ([]byte, error) { return buf.Bytes(), err } +// CompoundResponse is an interface for gathering multiple responses. It is +// used in cross-datacenter RPC calls where more than 1 datacenter is +// expected to reply. +type CompoundResponse interface { + // Add adds a new response to the compound response + Add(interface{}) + + // New returns an empty response object which can be passed around by + // reference, and then passed to Add() later on. + New() interface{} +} + // KeyringRequest encapsulates a request to modify an encryption keyring. // It can be used for install, remove, or use key type operations. type KeyringRequest struct { - Key string - Forwarded bool + Key string + Datacenter string + Forwarded bool QueryOptions } +func (r *KeyringRequest) RequestDatacenter() string { + return r.Datacenter +} + // KeyringResponse is a unified key response and can be used for install, // remove, use, as well as listing key queries. type KeyringResponse struct { @@ -558,3 +585,12 @@ type KeyringResponses struct { Responses []*KeyringResponse QueryMeta } + +func (r *KeyringResponses) Add(v interface{}) { + val := v.(*KeyringResponses) + r.Responses = append(r.Responses, val.Responses...) +} + +func (r *KeyringResponses) New() interface{} { + return new(KeyringResponses) +} From db0084ccd018466ed12e6f55b5c5f06d0c2ee467 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 2 Oct 2014 17:10:54 -0700 Subject: [PATCH 140/238] consul: use keyring operation type to cut out duplicated logic --- command/agent/keyring.go | 12 ++++-- consul/internal_endpoint.go | 86 +++++++++++-------------------------- consul/structs/structs.go | 10 +++++ 3 files changed, 44 insertions(+), 64 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 827aa76bc..f6d80a9dc 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -84,26 +84,30 @@ func (a *Agent) keyringProcess( func (a *Agent) ListKeys() (*structs.KeyringResponses, error) { args := structs.KeyringRequest{} args.AllowStale = true - return a.keyringProcess("Internal.ListKeys", &args) + args.Operation = structs.KeyringList + return a.keyringProcess("Internal.KeyringOperation", &args) } // InstallKey installs a new gossip encryption key func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true - return a.keyringProcess("Internal.InstallKey", &args) + args.Operation = structs.KeyringInstall + return a.keyringProcess("Internal.KeyringOperation", &args) } // UseKey changes the primary encryption key used to encrypt messages func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true - return a.keyringProcess("Internal.UseKey", &args) + args.Operation = structs.KeyringUse + return a.keyringProcess("Internal.KeyringOperation", &args) } // RemoveKey will remove a gossip encryption key from the keyring func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key} args.AllowStale = true - return a.keyringProcess("Internal.RemoveKey", &args) + args.Operation = structs.KeyringRemove + return a.keyringProcess("Internal.KeyringOperation", &args) } diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 223b8eeff..f9bed288c 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -12,6 +12,15 @@ type Internal struct { srv *Server } +type KeyringOperation uint8 + +const ( + listKeysOperation KeyringOperation = iota + installKeyOperation + useKeyOperation + removeKeyOperation +) + // ChecksInState is used to get all the checks in a given state func (m *Internal) NodeInfo(args *structs.NodeSpecificRequest, reply *structs.IndexedNodeDump) error { @@ -66,85 +75,42 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, // ListKeys will query the WAN and LAN gossip keyrings of all nodes, adding // results into a collective response as we go. -func (m *Internal) ListKeys( +func (m *Internal) KeyringOperation( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { dc := m.srv.config.Datacenter - respLAN, err := m.srv.KeyManagerLAN().ListKeys() + + respLAN, err := m.doKeyringOperation(args, m.srv.KeyManagerLAN()) ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { - respWAN, err := m.srv.KeyManagerWAN().ListKeys() + respWAN, err := m.doKeyringOperation(args, m.srv.KeyManagerWAN()) ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true - m.srv.globalRPC("Internal.ListKeys", args, reply) + return m.srv.globalRPC("Internal.KeyringOperation", args, reply) } return nil } -// InstallKey broadcasts a new encryption key to all nodes. This involves -// installing a new key on every node across all datacenters. -func (m *Internal) InstallKey( +func (m *Internal) doKeyringOperation( args *structs.KeyringRequest, - reply *structs.KeyringResponses) error { + mgr *serf.KeyManager) (r *serf.KeyResponse, err error) { - dc := m.srv.config.Datacenter - respLAN, err := m.srv.KeyManagerLAN().InstallKey(args.Key) - ingestKeyringResponse(respLAN, reply, dc, false, err) - - if !args.Forwarded { - respWAN, err := m.srv.KeyManagerWAN().InstallKey(args.Key) - ingestKeyringResponse(respWAN, reply, dc, true, err) - - args.Forwarded = true - m.srv.globalRPC("Internal.InstallKey", args, reply) + switch args.Operation { + case structs.KeyringList: + r, err = mgr.ListKeys() + case structs.KeyringInstall: + r, err = mgr.InstallKey(args.Key) + case structs.KeyringUse: + r, err = mgr.UseKey(args.Key) + case structs.KeyringRemove: + r, err = mgr.RemoveKey(args.Key) } - return nil -} - -// UseKey instructs all nodes to change the key they are using to -// encrypt gossip messages. -func (m *Internal) UseKey( - args *structs.KeyringRequest, - reply *structs.KeyringResponses) error { - - dc := m.srv.config.Datacenter - respLAN, err := m.srv.KeyManagerLAN().UseKey(args.Key) - ingestKeyringResponse(respLAN, reply, dc, false, err) - - if !args.Forwarded { - respWAN, err := m.srv.KeyManagerWAN().UseKey(args.Key) - ingestKeyringResponse(respWAN, reply, dc, true, err) - - args.Forwarded = true - m.srv.globalRPC("Internal.UseKey", args, reply) - } - - return nil -} - -// RemoveKey instructs all nodes to drop the specified key from the keyring. -func (m *Internal) RemoveKey( - args *structs.KeyringRequest, - reply *structs.KeyringResponses) error { - - dc := m.srv.config.Datacenter - respLAN, err := m.srv.KeyManagerLAN().RemoveKey(args.Key) - ingestKeyringResponse(respLAN, reply, dc, false, err) - - if !args.Forwarded { - respWAN, err := m.srv.KeyManagerWAN().RemoveKey(args.Key) - ingestKeyringResponse(respWAN, reply, dc, true, err) - - args.Forwarded = true - m.srv.globalRPC("Internal.RemoveKey", args, reply) - } - - return nil + return r, err } // ingestKeyringResponse is a helper method to pick the relative information diff --git a/consul/structs/structs.go b/consul/structs/structs.go index 1b3ead960..d655adf79 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -554,9 +554,19 @@ type CompoundResponse interface { New() interface{} } +type KeyringOp string + +const ( + KeyringList KeyringOp = "list" + KeyringInstall = "install" + KeyringUse = "use" + KeyringRemove = "remove" +) + // KeyringRequest encapsulates a request to modify an encryption keyring. // It can be used for install, remove, or use key type operations. type KeyringRequest struct { + Operation KeyringOp Key string Datacenter string Forwarded bool From daebf399469c892bcb0fee7a4ff76fda4e2cb483 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 2 Oct 2014 17:14:52 -0700 Subject: [PATCH 141/238] agent: guard against empty keyring files --- command/agent/keyring.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index f6d80a9dc..6df94d3c9 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -50,6 +50,11 @@ func loadKeyringFile(c *serf.Config) error { keysDecoded[i] = keyBytes } + // Guard against empty keyring + if len(keysDecoded) == 0 { + return fmt.Errorf("no keys present in keyring file: %s", c.KeyringFile) + } + // Create the keyring keyring, err := memberlist.NewKeyring(keysDecoded, keysDecoded[0]) if err != nil { From 4e8f53fa5d55d6e986f4f101673fbcd7e72f1d53 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 2 Oct 2014 18:03:05 -0700 Subject: [PATCH 142/238] consul: detach executeKeyringOp() from *Internal --- consul/internal_endpoint.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index f9bed288c..50109f88d 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -81,11 +81,11 @@ func (m *Internal) KeyringOperation( dc := m.srv.config.Datacenter - respLAN, err := m.doKeyringOperation(args, m.srv.KeyManagerLAN()) + respLAN, err := executeKeyringOp(args, m.srv.KeyManagerLAN()) ingestKeyringResponse(respLAN, reply, dc, false, err) if !args.Forwarded { - respWAN, err := m.doKeyringOperation(args, m.srv.KeyManagerWAN()) + respWAN, err := executeKeyringOp(args, m.srv.KeyManagerWAN()) ingestKeyringResponse(respWAN, reply, dc, true, err) args.Forwarded = true @@ -95,7 +95,10 @@ func (m *Internal) KeyringOperation( return nil } -func (m *Internal) doKeyringOperation( +// executeKeyringOp executes the appropriate keyring-related function based on +// the type of keyring operation in the request. It takes the KeyManager as an +// argument, so it can handle any operation for either LAN or WAN pools. +func executeKeyringOp( args *structs.KeyringRequest, mgr *serf.KeyManager) (r *serf.KeyResponse, err error) { From 7f85c708dc102bd570ae006d4748fdf34eeb3c5e Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 2 Oct 2014 18:12:01 -0700 Subject: [PATCH 143/238] agent: squash some more common keyring semantics --- command/agent/keyring.go | 20 ++++++++------------ consul/internal_endpoint.go | 9 --------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 6df94d3c9..2f1835d63 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -73,6 +73,10 @@ func (a *Agent) keyringProcess( method string, args *structs.KeyringRequest) (*structs.KeyringResponses, error) { + // Allow any server to handle the request, since this is + // done over the gossip protocol. + args.AllowStale = true + var reply structs.KeyringResponses if a.server == nil { return nil, fmt.Errorf("keyring operations must run against a server node") @@ -87,32 +91,24 @@ func (a *Agent) keyringProcess( // ListKeys lists out all keys installed on the collective Consul cluster. This // includes both servers and clients in all DC's. func (a *Agent) ListKeys() (*structs.KeyringResponses, error) { - args := structs.KeyringRequest{} - args.AllowStale = true - args.Operation = structs.KeyringList + args := structs.KeyringRequest{Operation: structs.KeyringList} return a.keyringProcess("Internal.KeyringOperation", &args) } // InstallKey installs a new gossip encryption key func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) { - args := structs.KeyringRequest{Key: key} - args.AllowStale = true - args.Operation = structs.KeyringInstall + args := structs.KeyringRequest{Key: key, Operation: structs.KeyringInstall} return a.keyringProcess("Internal.KeyringOperation", &args) } // UseKey changes the primary encryption key used to encrypt messages func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) { - args := structs.KeyringRequest{Key: key} - args.AllowStale = true - args.Operation = structs.KeyringUse + args := structs.KeyringRequest{Key: key, Operation: structs.KeyringUse} return a.keyringProcess("Internal.KeyringOperation", &args) } // RemoveKey will remove a gossip encryption key from the keyring func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) { - args := structs.KeyringRequest{Key: key} - args.AllowStale = true - args.Operation = structs.KeyringRemove + args := structs.KeyringRequest{Key: key, Operation: structs.KeyringRemove} return a.keyringProcess("Internal.KeyringOperation", &args) } diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 50109f88d..055e450c7 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -12,15 +12,6 @@ type Internal struct { srv *Server } -type KeyringOperation uint8 - -const ( - listKeysOperation KeyringOperation = iota - installKeyOperation - useKeyOperation - removeKeyOperation -) - // ChecksInState is used to get all the checks in a given state func (m *Internal) NodeInfo(args *structs.NodeSpecificRequest, reply *structs.IndexedNodeDump) error { From c59107f08efeaf9d34f25501d1f87573e191eae4 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 2 Oct 2014 22:05:00 -0700 Subject: [PATCH 144/238] command: remove -init argument from keyring, auto-persist keyrings when using agent -encrypt --- command/agent/agent.go | 13 ++-- command/agent/command.go | 55 +++++++++++---- command/agent/config.go | 19 ----- command/agent/keyring.go | 39 ++++++++++- command/keyring.go | 69 +------------------ .../docs/commands/keyring.html.markdown | 31 +++------ 6 files changed, 94 insertions(+), 132 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index f8b477147..b6f3a299a 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -264,17 +264,18 @@ func (a *Agent) setupServer() error { config := a.consulConfig() // Load a keyring file, if present - keyfileLAN := filepath.Join(config.DataDir, SerfLANKeyring) + keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring) if _, err := os.Stat(keyfileLAN); err == nil { config.SerfLANConfig.KeyringFile = keyfileLAN } - keyfileWAN := filepath.Join(config.DataDir, SerfWANKeyring) - if _, err := os.Stat(keyfileWAN); err == nil { - config.SerfWANConfig.KeyringFile = keyfileWAN - } if err := loadKeyringFile(config.SerfLANConfig); err != nil { return err } + + keyfileWAN := filepath.Join(config.DataDir, serfWANKeyring) + if _, err := os.Stat(keyfileWAN); err == nil { + config.SerfWANConfig.KeyringFile = keyfileWAN + } if err := loadKeyringFile(config.SerfWANConfig); err != nil { return err } @@ -292,7 +293,7 @@ func (a *Agent) setupClient() error { config := a.consulConfig() // Load a keyring file, if present - keyfileLAN := filepath.Join(config.DataDir, SerfLANKeyring) + keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring) if _, err := os.Stat(keyfileLAN); err == nil { config.SerfLANConfig.KeyringFile = keyfileLAN } diff --git a/command/agent/command.go b/command/agent/command.go index f15cea41f..089f0a88a 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -143,17 +143,35 @@ func (c *Command) readConfig() *Config { config.NodeName = hostname } + // Ensure we have a data directory + if config.DataDir == "" { + c.Ui.Error("Must specify data directory using -data-dir") + return nil + } + if config.EncryptKey != "" { if _, err := config.EncryptBytes(); err != nil { c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) return nil } - } - // Ensure we have a data directory - if config.DataDir == "" { - c.Ui.Error("Must specify data directory using -data-dir") - return nil + fileLAN := filepath.Join(config.DataDir, serfLANKeyring) + if _, err := os.Stat(fileLAN); err != nil { + initKeyring(fileLAN, config.EncryptKey) + } else { + c.Ui.Error(fmt.Sprintf("WARNING: %s exists, not using key: %s", + fileLAN, config.EncryptKey)) + } + + if config.Server { + fileWAN := filepath.Join(config.DataDir, serfWANKeyring) + if _, err := os.Stat(fileWAN); err != nil { + initKeyring(fileWAN, config.EncryptKey) + } else { + c.Ui.Error(fmt.Sprintf("WARNING: %s exists, not using key: %s", + fileWAN, config.EncryptKey)) + } + } } // Verify data center is valid @@ -218,12 +236,6 @@ func (c *Command) readConfig() *Config { c.Ui.Error("WARNING: Windows is not recommended as a Consul server. Do not use in production.") } - // Error if an encryption key is passed while a keyring already exists - if config.EncryptKey != "" && config.keyringFileExists() { - c.Ui.Error(fmt.Sprintf("Error: -encrypt specified but keyring files exist")) - return nil - } - // Set the version info config.Revision = c.Revision config.Version = c.Version @@ -465,6 +477,22 @@ func (c *Command) retryJoinWan(config *Config, errCh chan<- struct{}) { } } +// gossipEncrypted determines if the consul instance is using symmetric +// encryption keys to protect gossip protocol messages. +func (c *Command) gossipEncrypted() bool { + if c.agent.config.EncryptKey != "" { + return true + } + + server := c.agent.server + if server != nil { + return server.KeyManagerLAN() != nil || server.KeyManagerWAN() != nil + } + + client := c.agent.client + return client != nil && client.KeyManagerLAN() != nil +} + func (c *Command) Run(args []string) int { c.Ui = &cli.PrefixedUi{ OutputPrefix: "==> ", @@ -591,9 +619,6 @@ func (c *Command) Run(args []string) int { }(wp) } - // Determine if gossip is encrypted - gossipEncrypted := config.EncryptKey != "" || config.keyringFileExists() - // Let the agent know we've finished registration c.agent.StartSync() @@ -606,7 +631,7 @@ func (c *Command) Run(args []string) int { c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr, config.Ports.SerfLan, config.Ports.SerfWan)) c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v", - gossipEncrypted, config.VerifyOutgoing, config.VerifyIncoming)) + c.gossipEncrypted(), config.VerifyOutgoing, config.VerifyIncoming)) // Enable log streaming c.Ui.Info("") diff --git a/command/agent/config.go b/command/agent/config.go index 1188c7be9..dce299eac 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -411,25 +411,6 @@ func (c *Config) ClientListenerAddr(override string, port int) (string, error) { return addr.String(), nil } -// keyringFileExists determines if there are encryption key files present -// in the data directory. On client nodes, this returns true if a LAN keyring -// is present. On server nodes, it returns true if either keyring file exists. -func (c *Config) keyringFileExists() bool { - fileLAN := filepath.Join(c.DataDir, SerfLANKeyring) - fileWAN := filepath.Join(c.DataDir, SerfWANKeyring) - - if _, err := os.Stat(fileLAN); err == nil { - return true - } - if !c.Server { - return false - } - if _, err := os.Stat(fileWAN); err == nil { - return true - } - return false -} - // DecodeConfig reads the configuration from the given reader in JSON // format and decodes it into a proper Config structure. func DecodeConfig(r io.Reader) (*Config, error) { diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 2f1835d63..d4253b5e1 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "os" + "path/filepath" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/memberlist" @@ -13,10 +14,44 @@ import ( ) const ( - SerfLANKeyring = "serf/local.keyring" - SerfWANKeyring = "serf/remote.keyring" + serfLANKeyring = "serf/local.keyring" + serfWANKeyring = "serf/remote.keyring" ) +// initKeyring will create a keyring file at a given path. +func initKeyring(path, key string) error { + if _, err := base64.StdEncoding.DecodeString(key); err != nil { + return fmt.Errorf("Invalid key: %s", err) + } + + keys := []string{key} + keyringBytes, err := json.Marshal(keys) + if err != nil { + return err + } + + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return err + } + + if _, err := os.Stat(path); err == nil { + return fmt.Errorf("File already exists: %s", path) + } + + fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer fh.Close() + + if _, err := fh.Write(keyringBytes); err != nil { + os.Remove(path) + return err + } + + return nil +} + // loadKeyringFile will load a gossip encryption keyring out of a file. The file // must be in JSON format and contain a list of encryption key strings. func loadKeyringFile(c *serf.Config) error { diff --git a/command/keyring.go b/command/keyring.go index 78c56fd2b..41c0b6ff7 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -1,12 +1,8 @@ package command import ( - "encoding/base64" - "encoding/json" "flag" "fmt" - "os" - "path/filepath" "strings" "github.com/hashicorp/consul/command/agent" @@ -20,7 +16,7 @@ type KeyringCommand struct { } func (c *KeyringCommand) Run(args []string) int { - var installKey, useKey, removeKey, init, dataDir string + var installKey, useKey, removeKey string var listKeys bool cmdFlags := flag.NewFlagSet("keys", flag.ContinueOnError) @@ -30,8 +26,6 @@ func (c *KeyringCommand) Run(args []string) int { cmdFlags.StringVar(&useKey, "use", "", "use key") cmdFlags.StringVar(&removeKey, "remove", "", "remove key") cmdFlags.BoolVar(&listKeys, "list", false, "list keys") - cmdFlags.StringVar(&init, "init", "", "initialize keyring") - cmdFlags.StringVar(&dataDir, "data-dir", "", "data directory") rpcAddr := RPCAddrFlag(cmdFlags) if err := cmdFlags.Parse(args); err != nil { @@ -47,7 +41,7 @@ func (c *KeyringCommand) Run(args []string) int { // Only accept a single argument found := listKeys - for _, arg := range []string{installKey, useKey, removeKey, init} { + for _, arg := range []string{installKey, useKey, removeKey} { if found && len(arg) > 0 { c.Ui.Error("Only a single action is allowed") return 1 @@ -61,26 +55,6 @@ func (c *KeyringCommand) Run(args []string) int { return 1 } - if init != "" { - if dataDir == "" { - c.Ui.Error("Must provide -data-dir") - return 1 - } - - fileLAN := filepath.Join(dataDir, agent.SerfLANKeyring) - if err := initKeyring(fileLAN, init); err != nil { - c.Ui.Error(fmt.Sprintf("Error: %s", err)) - return 1 - } - fileWAN := filepath.Join(dataDir, agent.SerfWANKeyring) - if err := initKeyring(fileWAN, init); err != nil { - c.Ui.Error(fmt.Sprintf("Error: %s", err)) - return 1 - } - - return 0 - } - // All other operations will require a client connection client, err := RPCClient(*rpcAddr) if err != nil { @@ -204,40 +178,6 @@ func (c *KeyringCommand) handleList( } } -// initKeyring will create a keyring file at a given path. -func initKeyring(path, key string) error { - if _, err := base64.StdEncoding.DecodeString(key); err != nil { - return fmt.Errorf("Invalid key: %s", err) - } - - keys := []string{key} - keyringBytes, err := json.Marshal(keys) - if err != nil { - return err - } - - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return err - } - - if _, err := os.Stat(path); err == nil { - return fmt.Errorf("File already exists: %s", path) - } - - fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer fh.Close() - - if _, err := fh.Write(keyringBytes); err != nil { - os.Remove(path) - return err - } - - return nil -} - func (c *KeyringCommand) Help() string { helpText := ` Usage: consul keyring [options] @@ -263,11 +203,6 @@ Options: operation may only be performed on keys which are not currently the primary key. -list List all keys currently in use within the cluster. - -init= Create the initial keyring files for Consul to use - containing the provided key. The -data-dir argument - is required with this option. - -data-dir= The path to the Consul agent's data directory. This - argument is only needed for keyring initialization. -rpc-addr=127.0.0.1:8400 RPC address of the Consul agent. ` return strings.TrimSpace(helpText) diff --git a/website/source/docs/commands/keyring.html.markdown b/website/source/docs/commands/keyring.html.markdown index 6caa172dd..3ecbaebc9 100644 --- a/website/source/docs/commands/keyring.html.markdown +++ b/website/source/docs/commands/keyring.html.markdown @@ -19,34 +19,23 @@ responsibility of the operator to ensure that only the required encryption keys are installed on the cluster. You can review the installed keys using the `-list` argument, and remove unneeded keys with `-remove`. -With the exception of the `-init` argument, all operations performed by this -command can only be run against server nodes, and affect both the LAN and -WAN keyrings in lock-step. +All operations performed by this command can only be run against server nodes, +and affect both the LAN and WAN keyrings in lock-step. -All variations of the `keyring` command, unless otherwise specified below, will -return 0 if all nodes reply and there are no errors. If any node fails to reply -or reports failure, the exit code will be 1. +All variations of the `keyring` command return 0 if all nodes reply and there +are no errors. If any node fails to reply or reports failure, the exit code +will be 1. ## Usage Usage: `consul keyring [options]` -Only one actionable argument may be specified per run, including `-init`, -`-list`, `-install`, `-remove`, and `-use`. +Only one actionable argument may be specified per run, including `-list`, +`-install`, `-remove`, and `-use`. The list of available flags are: -* `-init` - Creates the keyring file(s). This is useful to configure initial - encryption keyrings, which can later be mutated using the other arguments in - this command. This argument accepts an ASCII key, which can be generated using - the [keygen command](/docs/commands/keygen.html). Requires the `-data-dir` - argument. - - This operation can be run on both client and server nodes and requires no - network connectivity. - - Returns 0 if the key is successfully configured, or 1 if there were any - problems. +* `-list` - List all keys currently in use within the cluster. * `-install` - Install a new encryption key. This will broadcast the new key to all members in the cluster. @@ -57,10 +46,6 @@ The list of available flags are: * `-remove` - Remove the given key from the cluster. This operation may only be performed on keys which are not currently the primary key. -* `-list` - List all keys currently in use within the cluster. - -* `-data-dir` - The path to Consul's data directory. Used with `-init` only. - * `-rpc-addr` - RPC address of the Consul agent. ## Output From 295f876923eeecd26f1794a1a7ce87ea0658faba Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 3 Oct 2014 19:20:58 -0700 Subject: [PATCH 145/238] command/agent: fix up gossip encryption indicator --- command/agent/command.go | 7 ++++++- consul/client.go | 5 +++++ consul/server.go | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/command/agent/command.go b/command/agent/command.go index 089f0a88a..620389767 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -619,6 +619,11 @@ func (c *Command) Run(args []string) int { }(wp) } + // Figure out if gossip is encrypted + gossipEncrypted := (config.Server && + c.agent.server.Encrypted() || + c.agent.client.Encrypted()) + // Let the agent know we've finished registration c.agent.StartSync() @@ -631,7 +636,7 @@ func (c *Command) Run(args []string) int { c.Ui.Info(fmt.Sprintf(" Cluster Addr: %v (LAN: %d, WAN: %d)", config.AdvertiseAddr, config.Ports.SerfLan, config.Ports.SerfWan)) c.Ui.Info(fmt.Sprintf("Gossip encrypt: %v, RPC-TLS: %v, TLS-Incoming: %v", - c.gossipEncrypted(), config.VerifyOutgoing, config.VerifyIncoming)) + gossipEncrypted, config.VerifyOutgoing, config.VerifyIncoming)) // Enable log streaming c.Ui.Info("") diff --git a/consul/client.go b/consul/client.go index be1854101..2c053513a 100644 --- a/consul/client.go +++ b/consul/client.go @@ -211,6 +211,11 @@ func (c *Client) KeyManagerLAN() *serf.KeyManager { return c.serf.KeyManager() } +// Encrypted determines if gossip is encrypted +func (c *Client) Encrypted() bool { + return c.serf.EncryptionEnabled() +} + // lanEventHandler is used to handle events from the lan Serf cluster func (c *Client) lanEventHandler() { for { diff --git a/consul/server.go b/consul/server.go index 8f913ed46..cba7f11ba 100644 --- a/consul/server.go +++ b/consul/server.go @@ -561,6 +561,11 @@ func (s *Server) KeyManagerWAN() *serf.KeyManager { return s.serfWAN.KeyManager() } +// Encrypted determines if gossip is encrypted +func (s *Server) Encrypted() bool { + return s.serfLAN.EncryptionEnabled() && s.serfWAN.EncryptionEnabled() +} + // inmemCodec is used to do an RPC call without going over a network type inmemCodec struct { method string From d02afd42fbd2685e0267ab0424966769adaceb35 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 4 Oct 2014 13:43:10 -0700 Subject: [PATCH 146/238] agent: -encrypt appends to keyring if one exists --- command/agent/agent_test.go | 4 +- command/agent/command.go | 16 +++---- command/agent/command_test.go | 4 ++ command/agent/config_test.go | 61 --------------------------- command/agent/keyring.go | 23 +++++++--- command/agent/keyring_test.go | 72 ++++++++++++++++++++++++++++++++ command/agent/rpc_client_test.go | 2 +- command/keyring_test.go | 4 +- consul/rpc.go | 3 ++ consul/serf_test.go | 24 ----------- consul/server_test.go | 42 ++++++++++++++++++- 11 files changed, 149 insertions(+), 106 deletions(-) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 8240b4854..10ee331b8 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -80,11 +80,11 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { conf.DataDir = dir - fileLAN := filepath.Join(dir, SerfLANKeyring) + fileLAN := filepath.Join(dir, serfLANKeyring) if err := testutil.InitKeyring(fileLAN, key); err != nil { t.Fatalf("err: %s", err) } - fileWAN := filepath.Join(dir, SerfWANKeyring) + fileWAN := filepath.Join(dir, serfWANKeyring) if err := testutil.InitKeyring(fileWAN, key); err != nil { t.Fatalf("err: %s", err) } diff --git a/command/agent/command.go b/command/agent/command.go index 620389767..7bb949618 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -156,20 +156,16 @@ func (c *Command) readConfig() *Config { } fileLAN := filepath.Join(config.DataDir, serfLANKeyring) - if _, err := os.Stat(fileLAN); err != nil { - initKeyring(fileLAN, config.EncryptKey) - } else { - c.Ui.Error(fmt.Sprintf("WARNING: %s exists, not using key: %s", - fileLAN, config.EncryptKey)) + if err := initKeyring(fileLAN, config.EncryptKey); err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) + return nil } if config.Server { fileWAN := filepath.Join(config.DataDir, serfWANKeyring) - if _, err := os.Stat(fileWAN); err != nil { - initKeyring(fileWAN, config.EncryptKey) - } else { - c.Ui.Error(fmt.Sprintf("WARNING: %s exists, not using key: %s", - fileWAN, config.EncryptKey)) + if err := initKeyring(fileWAN, config.EncryptKey); err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) + return nil } } } diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 9c1bf4db5..703d476ac 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -1,12 +1,16 @@ package agent import ( +<<<<<<< HEAD "fmt" "io/ioutil" "log" "os" "path/filepath" "strings" +======= + "github.com/mitchellh/cli" +>>>>>>> agent: -encrypt appends to keyring if one exists "testing" "github.com/hashicorp/consul/testutil" diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 52c0c58f7..13e8634ce 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -1030,64 +1030,3 @@ func TestReadConfigPaths_dir(t *testing.T) { t.Fatalf("bad: %#v", config) } } - -func TestKeyringFileExists(t *testing.T) { - tempDir, err := ioutil.TempDir("", "consul") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(tempDir) - - fileLAN := filepath.Join(tempDir, SerfLANKeyring) - fileWAN := filepath.Join(tempDir, SerfWANKeyring) - - if err := os.MkdirAll(filepath.Dir(fileLAN), 0700); err != nil { - t.Fatalf("err: %s", err) - } - if err := os.MkdirAll(filepath.Dir(fileWAN), 0700); err != nil { - t.Fatalf("err: %s", err) - } - - config := &Config{DataDir: tempDir, Server: true} - - // Returns false if we are a server and no keyring files present - if config.keyringFileExists() { - t.Fatalf("should return false") - } - - // Returns false if we are a client and no keyring files present - config.Server = false - if config.keyringFileExists() { - t.Fatalf("should return false") - } - - // Returns true if we are a client and the lan file exists - if err := ioutil.WriteFile(fileLAN, nil, 0600); err != nil { - t.Fatalf("err: %s", err) - } - if !config.keyringFileExists() { - t.Fatalf("should return true") - } - - // Returns true if we are a server and only the lan file exists - config.Server = true - if !config.keyringFileExists() { - t.Fatalf("should return true") - } - - // Returns true if we are a server and both files exist - if err := ioutil.WriteFile(fileWAN, nil, 0600); err != nil { - t.Fatalf("err: %s", err) - } - if !config.keyringFileExists() { - t.Fatalf("should return true") - } - - // Returns true if we are a server and only the wan file exists - if err := os.Remove(fileLAN); err != nil { - t.Fatalf("err: %s", err) - } - if !config.keyringFileExists() { - t.Fatalf("should return true") - } -} diff --git a/command/agent/keyring.go b/command/agent/keyring.go index d4253b5e1..524968605 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -20,11 +20,28 @@ const ( // initKeyring will create a keyring file at a given path. func initKeyring(path, key string) error { + var keys []string + if _, err := base64.StdEncoding.DecodeString(key); err != nil { return fmt.Errorf("Invalid key: %s", err) } - keys := []string{key} + if _, err := os.Stat(path); err == nil { + content, err := ioutil.ReadFile(path) + if err != nil { + return err + } + if err := json.Unmarshal(content, &keys); err != nil { + return err + } + for _, existing := range keys { + if key == existing { + return nil + } + } + } + + keys = append(keys, key) keyringBytes, err := json.Marshal(keys) if err != nil { return err @@ -34,10 +51,6 @@ func initKeyring(path, key string) error { return err } - if _, err := os.Stat(path); err == nil { - return fmt.Errorf("File already exists: %s", path) - } - fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { return err diff --git a/command/agent/keyring_test.go b/command/agent/keyring_test.go index 7807e5376..734a67dc1 100644 --- a/command/agent/keyring_test.go +++ b/command/agent/keyring_test.go @@ -1,7 +1,12 @@ package agent import ( + "bytes" + "encoding/json" + "io/ioutil" "os" + "path/filepath" + "strings" "testing" ) @@ -69,3 +74,70 @@ func TestAgent_LoadKeyrings(t *testing.T) { t.Fatalf("keyring should not be loaded") } } + +func TestAgent_InitKeyring(t *testing.T) { + key1 := "tbLJg26ZJyJ9pK3qhc9jig==" + key2 := "4leC33rgtXKIVUr9Nr0snQ==" + + dir, err := ioutil.TempDir("", "consul") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.RemoveAll(dir) + + file := filepath.Join(dir, "keyring") + + // First initialize the keyring + if err := initKeyring(file, key1); err != nil { + t.Fatalf("err: %s", err) + } + + content1, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("err: %s", err) + } + if !strings.Contains(string(content1), key1) { + t.Fatalf("bad: %s", content1) + } + if strings.Contains(string(content1), key2) { + t.Fatalf("bad: %s", content1) + } + + // Now initialize again with the same key + if err := initKeyring(file, key1); err != nil { + t.Fatalf("err: %s", err) + } + + content2, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("err: %s", err) + } + if !bytes.Equal(content1, content2) { + t.Fatalf("bad: %s", content2) + } + + // Initialize an existing keyring with a new key + if err := initKeyring(file, key2); err != nil { + t.Fatalf("err: %s", err) + } + + content3, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("err: %s", err) + } + if !strings.Contains(string(content3), key1) { + t.Fatalf("bad: %s", content3) + } + if !strings.Contains(string(content3), key2) { + t.Fatalf("bad: %s", content3) + } + + // Unmarshal and make sure that key1 is still primary + var keys []string + if err := json.Unmarshal(content3, &keys); err != nil { + t.Fatalf("err: %s", err) + } + if keys[0] != key1 { + t.Fatalf("bad: %#v", keys) + } +} diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 518423a69..2eed8f8a8 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -334,7 +334,7 @@ func TestRPCClientInstallKey(t *testing.T) { func TestRPCClientUseKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1} + conf := Config{EncryptKey: key1, Server: true} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() diff --git a/command/keyring_test.go b/command/keyring_test.go index 7e975b6cc..cc75d9797 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -129,8 +129,8 @@ func TestKeyringCommandRun_initKeyring(t *testing.T) { t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) } - fileLAN := filepath.Join(tempDir, agent.SerfLANKeyring) - fileWAN := filepath.Join(tempDir, agent.SerfWANKeyring) + fileLAN := filepath.Join(tempDir, agent.serfLANKeyring) + fileWAN := filepath.Join(tempDir, agent.serfWANKeyring) if _, err := os.Stat(fileLAN); err != nil { t.Fatalf("err: %s", err) } diff --git a/consul/rpc.go b/consul/rpc.go index 6fcb07d82..f400b2be9 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -229,6 +229,9 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ func (s *Server) globalRPC(method string, args interface{}, reply structs.CompoundResponse) error { + if reply == nil { + return fmt.Errorf("nil reply struct") + } rlen := len(s.remoteConsuls) if rlen < 2 { return nil diff --git a/consul/serf_test.go b/consul/serf_test.go index b30b4fe82..07225a729 100644 --- a/consul/serf_test.go +++ b/consul/serf_test.go @@ -1,8 +1,6 @@ package consul import ( - "fmt" - "os" "testing" ) @@ -21,25 +19,3 @@ func TestUserEventNames(t *testing.T) { t.Fatalf("bad: %v", raw) } } - -func TestKeyringRPCError(t *testing.T) { - dir1, s1 := testServerDC(t, "dc1") - defer os.RemoveAll(dir1) - defer s1.Shutdown() - - dir2, s2 := testServerDC(t, "dc2") - defer os.RemoveAll(dir2) - defer s2.Shutdown() - - // Try to join - addr := fmt.Sprintf("127.0.0.1:%d", - s1.config.SerfWANConfig.MemberlistConfig.BindPort) - if _, err := s2.JoinWAN([]string{addr}); err != nil { - t.Fatalf("err: %v", err) - } - - // RPC error from remote datacenter is returned - if err := s1.keyringRPC("Bad.Method", nil, nil); err == nil { - t.Fatalf("bad") - } -} diff --git a/consul/server_test.go b/consul/server_test.go index 76b7d4ed4..50627837e 100644 --- a/consul/server_test.go +++ b/consul/server_test.go @@ -6,9 +6,11 @@ import ( "io/ioutil" "net" "os" + "strings" "testing" "time" + "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/testutil" ) @@ -471,5 +473,43 @@ func TestServer_BadExpect(t *testing.T) { }, func(err error) { t.Fatalf("should have 0 peers: %v", err) }) - +} + +func TestServer_globalRPC(t *testing.T) { + dir1, s1 := testServerDC(t, "dc1") + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + dir2, s2 := testServerDC(t, "dc2") + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfLANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinLAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForLeader(t, s1.RPC, "dc1") + + // Check that replies from each DC come in + resp := &structs.KeyringResponses{} + args := &structs.KeyringRequest{Operation: structs.KeyringList} + if err := s1.globalRPC("Internal.KeyringOperation", args, resp); err != nil { + t.Fatalf("err: %s", err) + } + if len(resp.Responses) != 3 { + t.Fatalf("bad: %#v", resp.Responses) + } + + // Check that error from remote DC is returned + resp = &structs.KeyringResponses{} + err := s1.globalRPC("Bad.Method", nil, resp) + if err == nil { + t.Fatalf("should have errored") + } + if !strings.Contains(err.Error(), "Bad.Method") { + t.Fatalf("unexpcted error: %s", err) + } } From 9217e371bcab783860ac3d1b3ad096c0148010e8 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 4 Oct 2014 14:10:57 -0700 Subject: [PATCH 147/238] website: document new behavior of the -encrypt option --- website/source/docs/agent/options.html.markdown | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 01149ef59..9046b03d1 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -89,6 +89,13 @@ The options below are all specified on the command-line. network traffic. This key must be 16-bytes that are base64 encoded. The easiest way to create an encryption key is to use `consul keygen`. All nodes within a cluster must share the same encryption key to communicate. + The provided key is automatically persisted to the data directory, and loaded + automatically whenever the agent is restarted. This means that to encrypt + Consul's gossip protocol, this option only needs to be provided once on each + agent's initial startup sequence. If it is provided after Consul has been + initialized with an encryption key, then the provided key is simply added + as a secondary encryption key. More information on how keys can be changed + is available on the [keyring command](/docs/commands/keyring.html) page. * `-join` - Address of another agent to join upon starting up. This can be specified multiple times to specify multiple agents to join. If Consul is From bb9617642adc561610c1d46ed6eba10ae8026d45 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 4 Oct 2014 15:35:18 -0700 Subject: [PATCH 148/238] agent: make rpc tests more reliable --- command/agent/rpc_client_test.go | 65 ++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index 2eed8f8a8..ef082838a 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -293,7 +293,7 @@ func TestRPCClientListKeys(t *testing.T) { if _, ok := keys["dc1"][key1]; !ok { t.Fatalf("bad: %#v", keys) } - if _, ok := keys["wan"][key1]; !ok { + if _, ok := keys["WAN"][key1]; !ok { t.Fatalf("bad: %#v", keys) } } @@ -301,18 +301,23 @@ func TestRPCClientListKeys(t *testing.T) { func TestRPCClientInstallKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1, Server: true} + conf := Config{EncryptKey: key1} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() // key2 is not installed yet - keys := listKeys(t, p1.client) - if num, ok := keys["dc1"][key2]; ok || num != 0 { - t.Fatalf("bad: %#v", keys) - } - if num, ok := keys["wan"][key2]; ok || num != 0 { - t.Fatalf("bad: %#v", keys) - } + testutil.WaitForResult(func() (bool, error) { + keys := listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; ok || num != 0 { + return false, fmt.Errorf("bad: %#v", keys) + } + if num, ok := keys["WAN"][key2]; ok || num != 0 { + return false, fmt.Errorf("bad: %#v", keys) + } + return true, nil + }, func(err error) { + t.Fatal(err.Error()) + }) // install key2 r, err := p1.client.InstallKey(key2) @@ -322,19 +327,24 @@ func TestRPCClientInstallKey(t *testing.T) { keyringSuccess(t, r) // key2 should now be installed - keys = listKeys(t, p1.client) - if num, ok := keys["dc1"][key2]; !ok || num != 1 { - t.Fatalf("bad: %#v", keys) - } - if num, ok := keys["wan"][key2]; !ok || num != 1 { - t.Fatalf("bad: %#v", keys) - } + testutil.WaitForResult(func() (bool, error) { + keys := listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; !ok || num != 1 { + return false, fmt.Errorf("bad: %#v", keys) + } + if num, ok := keys["WAN"][key2]; !ok || num != 1 { + return false, fmt.Errorf("bad: %#v", keys) + } + return true, nil + }, func(err error) { + t.Fatal(err.Error()) + }) } func TestRPCClientUseKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1, Server: true} + conf := Config{EncryptKey: key1} p1 := testRPCClientWithConfig(t, &conf) defer p1.Close() @@ -346,13 +356,18 @@ func TestRPCClientUseKey(t *testing.T) { keyringSuccess(t, r) // key2 is installed - keys := listKeys(t, p1.client) - if num, ok := keys["dc1"][key2]; !ok || num != 1 { - t.Fatalf("bad: %#v", keys) - } - if num, ok := keys["wan"][key2]; !ok || num != 1 { - t.Fatalf("bad: %#v", keys) - } + testutil.WaitForResult(func() (bool, error) { + keys := listKeys(t, p1.client) + if num, ok := keys["dc1"][key2]; !ok || num != 1 { + return false, fmt.Errorf("bad: %#v", keys) + } + if num, ok := keys["WAN"][key2]; !ok || num != 1 { + return false, fmt.Errorf("bad: %#v", keys) + } + return true, nil + }, func(err error) { + t.Fatal(err.Error()) + }) // can't remove key1 yet r, err = p1.client.RemoveKey(key1) @@ -396,7 +411,7 @@ func listKeys(t *testing.T, c *RPCClient) map[string]map[string]int { for _, k := range resp.Keys { respID := k.Datacenter if k.Pool == "WAN" { - respID = "wan" + respID = k.Pool } out[respID] = map[string]int{k.Key: k.Count} } From f24ac482e387245f2c58db59d6c5f866655be715 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 4 Oct 2014 15:45:26 -0700 Subject: [PATCH 149/238] agent: fix test cases --- command/agent/agent_test.go | 4 +-- command/keyring_test.go | 50 ------------------------------------- testutil/keyring.go | 43 ------------------------------- testutil/keyring_test.go | 42 ------------------------------- 4 files changed, 2 insertions(+), 137 deletions(-) delete mode 100644 testutil/keyring.go delete mode 100644 testutil/keyring_test.go diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 10ee331b8..1e4aec000 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -81,11 +81,11 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { conf.DataDir = dir fileLAN := filepath.Join(dir, serfLANKeyring) - if err := testutil.InitKeyring(fileLAN, key); err != nil { + if err := initKeyring(fileLAN, key); err != nil { t.Fatalf("err: %s", err) } fileWAN := filepath.Join(dir, serfWANKeyring) - if err := testutil.InitKeyring(fileWAN, key); err != nil { + if err := initKeyring(fileWAN, key); err != nil { t.Fatalf("err: %s", err) } diff --git a/command/keyring_test.go b/command/keyring_test.go index cc75d9797..7f2d4c074 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -1,9 +1,6 @@ package command import ( - "io/ioutil" - "os" - "path/filepath" "strings" "testing" @@ -110,53 +107,6 @@ func TestKeyringCommandRun_initKeyringFail(t *testing.T) { } } -func TestKeyringCommandRun_initKeyring(t *testing.T) { - ui := new(cli.MockUi) - c := &KeyringCommand{Ui: ui} - - tempDir, err := ioutil.TempDir("", "consul") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(tempDir) - - args := []string{ - "-init=HS5lJ+XuTlYKWaeGYyG+/A==", - "-data-dir=" + tempDir, - } - code := c.Run(args) - if code != 0 { - t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) - } - - fileLAN := filepath.Join(tempDir, agent.serfLANKeyring) - fileWAN := filepath.Join(tempDir, agent.serfWANKeyring) - if _, err := os.Stat(fileLAN); err != nil { - t.Fatalf("err: %s", err) - } - if _, err := os.Stat(fileWAN); err != nil { - t.Fatalf("err: %s", err) - } - - expected := `["HS5lJ+XuTlYKWaeGYyG+/A=="]` - - contentLAN, err := ioutil.ReadFile(fileLAN) - if err != nil { - t.Fatalf("err: %s", err) - } - if string(contentLAN) != expected { - t.Fatalf("bad: %#v", string(contentLAN)) - } - - contentWAN, err := ioutil.ReadFile(fileWAN) - if err != nil { - t.Fatalf("err: %s", err) - } - if string(contentWAN) != expected { - t.Fatalf("bad: %#v", string(contentWAN)) - } -} - func listKeys(t *testing.T, addr string) string { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} diff --git a/testutil/keyring.go b/testutil/keyring.go deleted file mode 100644 index 60486bbb8..000000000 --- a/testutil/keyring.go +++ /dev/null @@ -1,43 +0,0 @@ -package testutil - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "os" - "path/filepath" -) - -// InitKeyring will create a keyring file at a given path. -func InitKeyring(path, key string) error { - if _, err := base64.StdEncoding.DecodeString(key); err != nil { - return fmt.Errorf("Invalid key: %s", err) - } - - keys := []string{key} - keyringBytes, err := json.Marshal(keys) - if err != nil { - return err - } - - if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return err - } - - if _, err := os.Stat(path); err == nil { - return fmt.Errorf("File already exists: %s", path) - } - - fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) - if err != nil { - return err - } - defer fh.Close() - - if _, err := fh.Write(keyringBytes); err != nil { - os.Remove(path) - return err - } - - return nil -} diff --git a/testutil/keyring_test.go b/testutil/keyring_test.go deleted file mode 100644 index 7e5b3e63f..000000000 --- a/testutil/keyring_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package testutil - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" -) - -func TestAgent_InitKeyring(t *testing.T) { - key := "tbLJg26ZJyJ9pK3qhc9jig==" - - dir, err := ioutil.TempDir("", "agent") - if err != nil { - t.Fatalf("err: %s", err) - } - defer os.RemoveAll(dir) - keyFile := filepath.Join(dir, "test/keyring") - - if err := InitKeyring(keyFile, key); err != nil { - t.Fatalf("err: %s", err) - } - - fi, err := os.Stat(filepath.Dir(keyFile)) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !fi.IsDir() { - t.Fatalf("bad: %#v", fi) - } - - data, err := ioutil.ReadFile(keyFile) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := `["tbLJg26ZJyJ9pK3qhc9jig=="]` - if string(data) != expected { - t.Fatalf("bad: %#v", string(data)) - } -} From b3f251de9c184a42dcb199ec163a091830f12008 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 5 Oct 2014 13:15:59 -0700 Subject: [PATCH 150/238] command/keyring: clean up tests --- command/keyring_test.go | 19 ------------------- consul/internal_endpoint.go | 5 +++-- consul/rpc.go | 2 +- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/command/keyring_test.go b/command/keyring_test.go index 7f2d4c074..25e20599c 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -88,25 +88,6 @@ func TestKeyringCommandRun_failedConnection(t *testing.T) { } } -func TestKeyringCommandRun_initKeyringFail(t *testing.T) { - ui := new(cli.MockUi) - c := &KeyringCommand{Ui: ui} - - // Should error if no data-dir given - args := []string{"-init=HS5lJ+XuTlYKWaeGYyG+/A=="} - code := c.Run(args) - if code != 1 { - t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) - } - - // Errors on invalid key - args = []string{"-init=xyz", "-data-dir=/tmp"} - code = c.Run(args) - if code != 1 { - t.Fatalf("should have errored") - } -} - func listKeys(t *testing.T, addr string) string { ui := new(cli.MockUi) c := &KeyringCommand{Ui: ui} diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 055e450c7..fe3c68162 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -64,8 +64,9 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } -// ListKeys will query the WAN and LAN gossip keyrings of all nodes, adding -// results into a collective response as we go. +// KeyringOperation will query the WAN and LAN gossip keyrings of all nodes, +// adding results into a collective response as we go. It can describe requests +// for all keyring-related operations. func (m *Internal) KeyringOperation( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { diff --git a/consul/rpc.go b/consul/rpc.go index f400b2be9..fbb73ce7b 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -225,7 +225,7 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ // globalRPC is used to forward an RPC request to one server in each datacenter. // This will only error for RPC-related errors. Otherwise, application-level -// errors are returned inside of the inner response objects. +// errors can be sent in the response objects. func (s *Server) globalRPC(method string, args interface{}, reply structs.CompoundResponse) error { From 00fc7fa1ddf07e40613fc47974332d07277db9dd Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 5 Oct 2014 13:43:56 -0700 Subject: [PATCH 151/238] command/keyring: adjust command help --- command/keyring.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/command/keyring.go b/command/keyring.go index 41c0b6ff7..ee072b879 100644 --- a/command/keyring.go +++ b/command/keyring.go @@ -188,9 +188,12 @@ Usage: consul keyring [options] functionality provides the ability to perform key rotation cluster-wide, without disrupting the cluster. - With the exception of the -init argument, all operations performed by this - command can only be run against server nodes, and affect both the LAN and - WAN keyrings in lock-step. + All operations performed by this command can only be run against server nodes, + and affect both the LAN and WAN keyrings in lock-step. + + All variations of the keyring command return 0 if all nodes reply and there + are no errors. If any node fails to reply or reports failure, the exit code + will be 1. Options: From 344b63b9db2588838e41ae52d99c771dfcd955ea Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 5 Oct 2014 13:59:27 -0700 Subject: [PATCH 152/238] consul: simplify keyring operations --- consul/internal_endpoint.go | 43 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index fe3c68162..967d10706 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -71,15 +71,10 @@ func (m *Internal) KeyringOperation( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - dc := m.srv.config.Datacenter - - respLAN, err := executeKeyringOp(args, m.srv.KeyManagerLAN()) - ingestKeyringResponse(respLAN, reply, dc, false, err) + m.executeKeyringOp(args, reply, false) if !args.Forwarded { - respWAN, err := executeKeyringOp(args, m.srv.KeyManagerWAN()) - ingestKeyringResponse(respWAN, reply, dc, true, err) - + m.executeKeyringOp(args, reply, true) args.Forwarded = true return m.srv.globalRPC("Internal.KeyringOperation", args, reply) } @@ -90,30 +85,34 @@ func (m *Internal) KeyringOperation( // executeKeyringOp executes the appropriate keyring-related function based on // the type of keyring operation in the request. It takes the KeyManager as an // argument, so it can handle any operation for either LAN or WAN pools. -func executeKeyringOp( +func (m *Internal) executeKeyringOp( args *structs.KeyringRequest, - mgr *serf.KeyManager) (r *serf.KeyResponse, err error) { + reply *structs.KeyringResponses, + wan bool) { + + var serfResp *serf.KeyResponse + var err error + + dc := m.srv.config.Datacenter + + var mgr *serf.KeyManager + if wan { + mgr = m.srv.KeyManagerWAN() + } else { + mgr = m.srv.KeyManagerLAN() + } switch args.Operation { case structs.KeyringList: - r, err = mgr.ListKeys() + serfResp, err = mgr.ListKeys() case structs.KeyringInstall: - r, err = mgr.InstallKey(args.Key) + serfResp, err = mgr.InstallKey(args.Key) case structs.KeyringUse: - r, err = mgr.UseKey(args.Key) + serfResp, err = mgr.UseKey(args.Key) case structs.KeyringRemove: - r, err = mgr.RemoveKey(args.Key) + serfResp, err = mgr.RemoveKey(args.Key) } - return r, err -} - -// ingestKeyringResponse is a helper method to pick the relative information -// from a Serf message and stuff it into a KeyringResponse. -func ingestKeyringResponse( - serfResp *serf.KeyResponse, reply *structs.KeyringResponses, - dc string, wan bool, err error) { - errStr := "" if err != nil { errStr = err.Error() From 66ad81ef13176aff82bda25c4ac9b7664d5ee56b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 5 Oct 2014 18:33:12 -0700 Subject: [PATCH 153/238] consul: add test for internal keyring rpc endpoint --- consul/internal_endpoint_test.go | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/consul/internal_endpoint_test.go b/consul/internal_endpoint_test.go index e3c33fe92..45b57f21c 100644 --- a/consul/internal_endpoint_test.go +++ b/consul/internal_endpoint_test.go @@ -1,6 +1,8 @@ package consul import ( + "encoding/base64" + "fmt" "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/testutil" "os" @@ -150,3 +152,89 @@ func TestInternal_NodeDump(t *testing.T) { t.Fatalf("missing foo or bar") } } + +func TestInternal_KeyringOperation(t *testing.T) { + key1 := "H1dfkSZOVnP/JUnaBfTzXg==" + keyBytes1, err := base64.StdEncoding.DecodeString(key1) + if err != nil { + t.Fatalf("err: %s", err) + } + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.SerfLANConfig.MemberlistConfig.SecretKey = keyBytes1 + c.SerfWANConfig.MemberlistConfig.SecretKey = keyBytes1 + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + var out structs.KeyringResponses + req := structs.KeyringRequest{ + Operation: structs.KeyringList, + Datacenter: "dc1", + } + if err := client.Call("Internal.KeyringOperation", &req, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Two responses (local lan/wan pools) from single-node cluster + if len(out.Responses) != 2 { + t.Fatalf("bad: %#v", out) + } + if _, ok := out.Responses[0].Keys[key1]; !ok { + t.Fatalf("bad: %#v", out) + } + wanResp, lanResp := 0, 0 + for _, resp := range out.Responses { + if resp.WAN { + wanResp++ + } else { + lanResp++ + } + } + if lanResp != 1 || wanResp != 1 { + t.Fatalf("should have one lan and one wan response") + } + + // Start a second agent to test cross-dc queries + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.SerfLANConfig.MemberlistConfig.SecretKey = keyBytes1 + c.SerfWANConfig.MemberlistConfig.SecretKey = keyBytes1 + c.Datacenter = "dc2" + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + // Try to join + addr := fmt.Sprintf("127.0.0.1:%d", + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { + t.Fatalf("err: %v", err) + } + + var out2 structs.KeyringResponses + req2 := structs.KeyringRequest{ + Operation: structs.KeyringList, + } + if err := client.Call("Internal.KeyringOperation", &req2, &out2); err != nil { + t.Fatalf("err: %v", err) + } + + // 3 responses (one from each DC LAN, one from WAN) in two-node cluster + if len(out2.Responses) != 3 { + t.Fatalf("bad: %#v", out) + } + wanResp, lanResp = 0, 0 + for _, resp := range out2.Responses { + if resp.WAN { + wanResp++ + } else { + lanResp++ + } + } + if lanResp != 2 || wanResp != 1 { + t.Fatalf("should have two lan and one wan response") + } +} From fcacee723b8157430a526a81e3c37a3e77198d14 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 6 Oct 2014 15:14:30 -0700 Subject: [PATCH 154/238] consul: simplify keyring operations --- command/agent/keyring.go | 15 ++++++--------- consul/internal_endpoint.go | 9 +++------ consul/rpc.go | 16 +++++----------- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 524968605..66ca7e223 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -117,10 +117,7 @@ func loadKeyringFile(c *serf.Config) error { // keyringProcess is used to abstract away the semantic similarities in // performing various operations on the encryption keyring. -func (a *Agent) keyringProcess( - method string, - args *structs.KeyringRequest) (*structs.KeyringResponses, error) { - +func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringResponses, error) { // Allow any server to handle the request, since this is // done over the gossip protocol. args.AllowStale = true @@ -129,7 +126,7 @@ func (a *Agent) keyringProcess( if a.server == nil { return nil, fmt.Errorf("keyring operations must run against a server node") } - if err := a.RPC(method, args, &reply); err != nil { + if err := a.RPC("Internal.KeyringOperation", args, &reply); err != nil { return &reply, err } @@ -140,23 +137,23 @@ func (a *Agent) keyringProcess( // includes both servers and clients in all DC's. func (a *Agent) ListKeys() (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Operation: structs.KeyringList} - return a.keyringProcess("Internal.KeyringOperation", &args) + return a.keyringProcess(&args) } // InstallKey installs a new gossip encryption key func (a *Agent) InstallKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key, Operation: structs.KeyringInstall} - return a.keyringProcess("Internal.KeyringOperation", &args) + return a.keyringProcess(&args) } // UseKey changes the primary encryption key used to encrypt messages func (a *Agent) UseKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key, Operation: structs.KeyringUse} - return a.keyringProcess("Internal.KeyringOperation", &args) + return a.keyringProcess(&args) } // RemoveKey will remove a gossip encryption key from the keyring func (a *Agent) RemoveKey(key string) (*structs.KeyringResponses, error) { args := structs.KeyringRequest{Key: key, Operation: structs.KeyringRemove} - return a.keyringProcess("Internal.KeyringOperation", &args) + return a.keyringProcess(&args) } diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index 967d10706..e20e315da 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -72,10 +72,9 @@ func (m *Internal) KeyringOperation( reply *structs.KeyringResponses) error { m.executeKeyringOp(args, reply, false) - if !args.Forwarded { - m.executeKeyringOp(args, reply, true) args.Forwarded = true + m.executeKeyringOp(args, reply, true) return m.srv.globalRPC("Internal.KeyringOperation", args, reply) } @@ -92,10 +91,8 @@ func (m *Internal) executeKeyringOp( var serfResp *serf.KeyResponse var err error - - dc := m.srv.config.Datacenter - var mgr *serf.KeyManager + if wan { mgr = m.srv.KeyManagerWAN() } else { @@ -120,7 +117,7 @@ func (m *Internal) executeKeyringOp( reply.Responses = append(reply.Responses, &structs.KeyringResponse{ WAN: wan, - Datacenter: dc, + Datacenter: m.srv.config.Datacenter, Messages: serfResp.Messages, Keys: serfResp.Keys, NumNodes: serfResp.NumNodes, diff --git a/consul/rpc.go b/consul/rpc.go index fbb73ce7b..8fca85ff4 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -229,11 +229,8 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ func (s *Server) globalRPC(method string, args interface{}, reply structs.CompoundResponse) error { - if reply == nil { - return fmt.Errorf("nil reply struct") - } - rlen := len(s.remoteConsuls) - if rlen < 2 { + totalDC := len(s.remoteConsuls) + if totalDC == 1 { return nil } @@ -253,17 +250,14 @@ func (s *Server) globalRPC(method string, args interface{}, }() } - done := 0 - for { + replies := 0 + for replies < totalDC { select { case err := <-errorCh: return err case rr := <-respCh: reply.Add(rr) - done++ - } - if done == rlen { - break + replies++ } } return nil From 2661bbfa270e0af398e2a6fbbc81161e2791d259 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 7 Oct 2014 11:05:31 -0700 Subject: [PATCH 155/238] consul: more tests, remove unused KeyManager() method --- command/agent/rpc_client_test.go | 24 +++++++++++--------- command/keyring_test.go | 5 +++-- command/util_test.go | 8 +++---- consul/client.go | 5 ----- consul/client_test.go | 20 +++++++++++++++++ consul/server_test.go | 38 ++++++++++++++++++++++++++++---- consul/structs/structs_test.go | 21 ++++++++++++++++++ 7 files changed, 94 insertions(+), 27 deletions(-) diff --git a/command/agent/rpc_client_test.go b/command/agent/rpc_client_test.go index ef082838a..3bf03d6dc 100644 --- a/command/agent/rpc_client_test.go +++ b/command/agent/rpc_client_test.go @@ -30,10 +30,10 @@ func (r *rpcParts) Close() { // testRPCClient returns an RPCClient connected to an RPC server that // serves only this connection. func testRPCClient(t *testing.T) *rpcParts { - return testRPCClientWithConfig(t, nil) + return testRPCClientWithConfig(t, func(c *Config) {}) } -func testRPCClientWithConfig(t *testing.T, c *Config) *rpcParts { +func testRPCClientWithConfig(t *testing.T, cb func(c *Config)) *rpcParts { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("err: %s", err) @@ -43,9 +43,7 @@ func testRPCClientWithConfig(t *testing.T, c *Config) *rpcParts { mult := io.MultiWriter(os.Stderr, lw) conf := nextConfig() - if c != nil { - conf = MergeConfig(conf, c) - } + cb(conf) dir, agent := makeAgentLog(t, conf, mult) rpc := NewAgentRPC(agent, l, mult, lw) @@ -284,8 +282,10 @@ OUTER2: func TestRPCClientListKeys(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" - conf := Config{EncryptKey: key1, Datacenter: "dc1"} - p1 := testRPCClientWithConfig(t, &conf) + p1 := testRPCClientWithConfig(t, func(c *Config) { + c.EncryptKey = key1 + c.Datacenter = "dc1" + }) defer p1.Close() // Key is initially installed to both wan/lan @@ -301,8 +301,9 @@ func TestRPCClientListKeys(t *testing.T) { func TestRPCClientInstallKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1} - p1 := testRPCClientWithConfig(t, &conf) + p1 := testRPCClientWithConfig(t, func(c *Config) { + c.EncryptKey = key1 + }) defer p1.Close() // key2 is not installed yet @@ -344,8 +345,9 @@ func TestRPCClientInstallKey(t *testing.T) { func TestRPCClientUseKey(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "xAEZ3uVHRMZD9GcYMZaRQw==" - conf := Config{EncryptKey: key1} - p1 := testRPCClientWithConfig(t, &conf) + p1 := testRPCClientWithConfig(t, func(c *Config) { + c.EncryptKey = key1 + }) defer p1.Close() // add a second key to the ring diff --git a/command/keyring_test.go b/command/keyring_test.go index 25e20599c..bb8691ebb 100644 --- a/command/keyring_test.go +++ b/command/keyring_test.go @@ -17,8 +17,9 @@ func TestKeyringCommandRun(t *testing.T) { key2 := "kZyFABeAmc64UMTrm9XuKA==" // Begin with a single key - conf := agent.Config{EncryptKey: key1} - a1 := testAgentWithConfig(&conf, t) + a1 := testAgentWithConfig(t, func(c *agent.Config) { + c.EncryptKey = key1 + }) defer a1.Shutdown() // The LAN and WAN keyrings were initialized with key1 diff --git a/command/util_test.go b/command/util_test.go index 586489233..a48f33cb0 100644 --- a/command/util_test.go +++ b/command/util_test.go @@ -39,10 +39,10 @@ func (a *agentWrapper) Shutdown() { } func testAgent(t *testing.T) *agentWrapper { - return testAgentWithConfig(nil, t) + return testAgentWithConfig(t, func(c *agent.Config) {}) } -func testAgentWithConfig(c *agent.Config, t *testing.T) *agentWrapper { +func testAgentWithConfig(t *testing.T, cb func(c *agent.Config)) *agentWrapper { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("err: %s", err) @@ -52,9 +52,7 @@ func testAgentWithConfig(c *agent.Config, t *testing.T) *agentWrapper { mult := io.MultiWriter(os.Stderr, lw) conf := nextConfig() - if c != nil { - conf = agent.MergeConfig(c, conf) - } + cb(conf) dir, err := ioutil.TempDir("", "agent") if err != nil { diff --git a/consul/client.go b/consul/client.go index 2c053513a..cf0ddca0c 100644 --- a/consul/client.go +++ b/consul/client.go @@ -206,11 +206,6 @@ func (c *Client) UserEvent(name string, payload []byte) error { return c.serf.UserEvent(userEventName(name), payload, false) } -// KeyManager returns the LAN Serf keyring manager -func (c *Client) KeyManagerLAN() *serf.KeyManager { - return c.serf.KeyManager() -} - // Encrypted determines if gossip is encrypted func (c *Client) Encrypted() bool { return c.serf.EncryptionEnabled() diff --git a/consul/client_test.go b/consul/client_test.go index a783c3a52..33425cdf7 100644 --- a/consul/client_test.go +++ b/consul/client_test.go @@ -269,3 +269,23 @@ func TestClientServer_UserEvent(t *testing.T) { t.Fatalf("missing events") } } + +func TestClient_Encrypted(t *testing.T) { + dir1, c1 := testClient(t) + defer os.RemoveAll(dir1) + defer c1.Shutdown() + + key := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + dir2, c2 := testClientWithConfig(t, func(c *Config) { + c.SerfLANConfig.MemberlistConfig.SecretKey = key + }) + defer os.RemoveAll(dir2) + defer c2.Shutdown() + + if c1.Encrypted() { + t.Fatalf("should not be encrypted") + } + if !c2.Encrypted() { + t.Fatalf("should be encrypted") + } +} diff --git a/consul/server_test.go b/consul/server_test.go index 50627837e..9a6c31123 100644 --- a/consul/server_test.go +++ b/consul/server_test.go @@ -486,14 +486,23 @@ func TestServer_globalRPC(t *testing.T) { // Try to join addr := fmt.Sprintf("127.0.0.1:%d", - s1.config.SerfLANConfig.MemberlistConfig.BindPort) - if _, err := s2.JoinLAN([]string{addr}); err != nil { + s1.config.SerfWANConfig.MemberlistConfig.BindPort) + if _, err := s2.JoinWAN([]string{addr}); err != nil { t.Fatalf("err: %v", err) } + // Check the members + testutil.WaitForResult(func() (bool, error) { + members := len(s1.WANMembers()) + return members == 2, fmt.Errorf("expected 2 members, got %d", members) + }, func(err error) { + t.Fatalf(err.Error()) + }) + + // Wait for leader election testutil.WaitForLeader(t, s1.RPC, "dc1") - // Check that replies from each DC come in + // Check that replies from each gossip pool come in resp := &structs.KeyringResponses{} args := &structs.KeyringRequest{Operation: structs.KeyringList} if err := s1.globalRPC("Internal.KeyringOperation", args, resp); err != nil { @@ -503,7 +512,7 @@ func TestServer_globalRPC(t *testing.T) { t.Fatalf("bad: %#v", resp.Responses) } - // Check that error from remote DC is returned + // Check that an error from a remote DC is returned resp = &structs.KeyringResponses{} err := s1.globalRPC("Bad.Method", nil, resp) if err == nil { @@ -513,3 +522,24 @@ func TestServer_globalRPC(t *testing.T) { t.Fatalf("unexpcted error: %s", err) } } + +func TestServer_Encrypted(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + + key := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + dir2, s2 := testServerWithConfig(t, func(c *Config) { + c.SerfLANConfig.MemberlistConfig.SecretKey = key + c.SerfWANConfig.MemberlistConfig.SecretKey = key + }) + defer os.RemoveAll(dir2) + defer s2.Shutdown() + + if s1.Encrypted() { + t.Fatalf("should not be encrypted") + } + if !s2.Encrypted() { + t.Fatalf("should be encrypted") + } +} diff --git a/consul/structs/structs_test.go b/consul/structs/structs_test.go index e5944cbe4..abf8ebb74 100644 --- a/consul/structs/structs_test.go +++ b/consul/structs/structs_test.go @@ -32,3 +32,24 @@ func TestEncodeDecode(t *testing.T) { t.Fatalf("bad: %#v %#v", arg, out) } } + +func TestStructs_Implements(t *testing.T) { + var ( + _ RPCInfo = &GenericRPC{} + _ RPCInfo = &RegisterRequest{} + _ RPCInfo = &DeregisterRequest{} + _ RPCInfo = &DCSpecificRequest{} + _ RPCInfo = &ServiceSpecificRequest{} + _ RPCInfo = &NodeSpecificRequest{} + _ RPCInfo = &ChecksInStateRequest{} + _ RPCInfo = &KVSRequest{} + _ RPCInfo = &KeyRequest{} + _ RPCInfo = &KeyListRequest{} + _ RPCInfo = &SessionRequest{} + _ RPCInfo = &SessionSpecificRequest{} + _ RPCInfo = &EventFireRequest{} + _ RPCInfo = &ACLPolicyRequest{} + _ RPCInfo = &KeyringRequest{} + _ CompoundResponse = &KeyringResponses{} + ) +} From 4a8249db00fabca02c0ea1ca92ffcffd44eec2f7 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 8 Oct 2014 13:28:59 -0700 Subject: [PATCH 156/238] consul: fix obscure bug when launching goroutines from for loop --- consul/internal_endpoint.go | 2 +- consul/rpc.go | 16 +++++----------- consul/structs/structs.go | 10 ---------- consul/structs/structs_test.go | 1 - 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index e20e315da..c27a07837 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -71,13 +71,13 @@ func (m *Internal) KeyringOperation( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { - m.executeKeyringOp(args, reply, false) if !args.Forwarded { args.Forwarded = true m.executeKeyringOp(args, reply, true) return m.srv.globalRPC("Internal.KeyringOperation", args, reply) } + m.executeKeyringOp(args, reply, false) return nil } diff --git a/consul/rpc.go b/consul/rpc.go index 8fca85ff4..8956b0d0f 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -229,29 +229,23 @@ func (s *Server) forwardDC(method, dc string, args interface{}, reply interface{ func (s *Server) globalRPC(method string, args interface{}, reply structs.CompoundResponse) error { - totalDC := len(s.remoteConsuls) - if totalDC == 1 { - return nil - } - errorCh := make(chan error) respCh := make(chan interface{}) // Make a new request into each datacenter for dc, _ := range s.remoteConsuls { - info := &structs.GenericRPC{Datacenter: dc} - go func() { + go func(dc string) { rr := reply.New() - if _, err := s.forward(method, info, args, &rr); err != nil { + if err := s.forwardDC(method, dc, args, &rr); err != nil { errorCh <- err return } respCh <- rr - }() + }(dc) } - replies := 0 - for replies < totalDC { + replies, total := 0, len(s.remoteConsuls) + for replies < total { select { case err := <-errorCh: return err diff --git a/consul/structs/structs.go b/consul/structs/structs.go index d655adf79..b1f315271 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -127,16 +127,6 @@ type QueryMeta struct { KnownLeader bool } -// GenericRPC is the simplest possible RPCInfo implementation -type GenericRPC struct { - Datacenter string - QueryOptions -} - -func (r *GenericRPC) RequestDatacenter() string { - return r.Datacenter -} - // RegisterRequest is used for the Catalog.Register endpoint // to register a node as providing a service. If no service // is provided, the node is registered. diff --git a/consul/structs/structs_test.go b/consul/structs/structs_test.go index abf8ebb74..cb7808731 100644 --- a/consul/structs/structs_test.go +++ b/consul/structs/structs_test.go @@ -35,7 +35,6 @@ func TestEncodeDecode(t *testing.T) { func TestStructs_Implements(t *testing.T) { var ( - _ RPCInfo = &GenericRPC{} _ RPCInfo = &RegisterRequest{} _ RPCInfo = &DeregisterRequest{} _ RPCInfo = &DCSpecificRequest{} From 3b2ab70c4d4eb5b79e6f71d2b2a84cb8c9db1919 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 9 Oct 2014 10:25:53 -0700 Subject: [PATCH 157/238] consul: clean up comments, fix globalRPC tests --- command/agent/keyring.go | 4 ---- consul/internal_endpoint.go | 6 ++--- consul/server_test.go | 48 ++++++++++--------------------------- 3 files changed, 15 insertions(+), 43 deletions(-) diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 66ca7e223..51a466653 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -118,10 +118,6 @@ func loadKeyringFile(c *serf.Config) error { // keyringProcess is used to abstract away the semantic similarities in // performing various operations on the encryption keyring. func (a *Agent) keyringProcess(args *structs.KeyringRequest) (*structs.KeyringResponses, error) { - // Allow any server to handle the request, since this is - // done over the gossip protocol. - args.AllowStale = true - var reply structs.KeyringResponses if a.server == nil { return nil, fmt.Errorf("keyring operations must run against a server node") diff --git a/consul/internal_endpoint.go b/consul/internal_endpoint.go index c27a07837..3032e0b03 100644 --- a/consul/internal_endpoint.go +++ b/consul/internal_endpoint.go @@ -64,19 +64,19 @@ func (m *Internal) EventFire(args *structs.EventFireRequest, return m.srv.UserEvent(args.Name, args.Payload) } -// KeyringOperation will query the WAN and LAN gossip keyrings of all nodes, -// adding results into a collective response as we go. It can describe requests -// for all keyring-related operations. +// KeyringOperation will query the WAN and LAN gossip keyrings of all nodes. func (m *Internal) KeyringOperation( args *structs.KeyringRequest, reply *structs.KeyringResponses) error { + // Only perform WAN keyring querying and RPC forwarding once if !args.Forwarded { args.Forwarded = true m.executeKeyringOp(args, reply, true) return m.srv.globalRPC("Internal.KeyringOperation", args, reply) } + // Query the LAN keyring of this node's DC m.executeKeyringOp(args, reply, false) return nil } diff --git a/consul/server_test.go b/consul/server_test.go index 9a6c31123..0d0d1d588 100644 --- a/consul/server_test.go +++ b/consul/server_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/hashicorp/consul/consul/structs" "github.com/hashicorp/consul/testutil" ) @@ -475,46 +474,23 @@ func TestServer_BadExpect(t *testing.T) { }) } -func TestServer_globalRPC(t *testing.T) { +type fakeGlobalResp struct{} + +func (r *fakeGlobalResp) Add(interface{}) { + return +} + +func (r *fakeGlobalResp) New() interface{} { + return struct{}{} +} + +func TestServer_globalRPCErrors(t *testing.T) { dir1, s1 := testServerDC(t, "dc1") defer os.RemoveAll(dir1) defer s1.Shutdown() - dir2, s2 := testServerDC(t, "dc2") - defer os.RemoveAll(dir2) - defer s2.Shutdown() - - // Try to join - addr := fmt.Sprintf("127.0.0.1:%d", - s1.config.SerfWANConfig.MemberlistConfig.BindPort) - if _, err := s2.JoinWAN([]string{addr}); err != nil { - t.Fatalf("err: %v", err) - } - - // Check the members - testutil.WaitForResult(func() (bool, error) { - members := len(s1.WANMembers()) - return members == 2, fmt.Errorf("expected 2 members, got %d", members) - }, func(err error) { - t.Fatalf(err.Error()) - }) - - // Wait for leader election - testutil.WaitForLeader(t, s1.RPC, "dc1") - - // Check that replies from each gossip pool come in - resp := &structs.KeyringResponses{} - args := &structs.KeyringRequest{Operation: structs.KeyringList} - if err := s1.globalRPC("Internal.KeyringOperation", args, resp); err != nil { - t.Fatalf("err: %s", err) - } - if len(resp.Responses) != 3 { - t.Fatalf("bad: %#v", resp.Responses) - } - // Check that an error from a remote DC is returned - resp = &structs.KeyringResponses{} - err := s1.globalRPC("Bad.Method", nil, resp) + err := s1.globalRPC("Bad.Method", nil, &fakeGlobalResp{}) if err == nil { t.Fatalf("should have errored") } From 196cbd27b2e8511edb10ca253b23a2c92e6c3342 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 9 Oct 2014 15:28:38 -0700 Subject: [PATCH 158/238] agent: ignore -encrypt if provided when keyring exists --- command/agent/agent_test.go | 4 +- command/agent/command.go | 16 +++++- command/agent/keyring.go | 31 ++++------- command/agent/keyring_test.go | 53 ++++++------------- .../source/docs/agent/options.html.markdown | 5 +- 5 files changed, 45 insertions(+), 64 deletions(-) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 1e4aec000..3f398016f 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -81,11 +81,11 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { conf.DataDir = dir fileLAN := filepath.Join(dir, serfLANKeyring) - if err := initKeyring(fileLAN, key); err != nil { + if _, err := initKeyring(fileLAN, key); err != nil { t.Fatalf("err: %s", err) } fileWAN := filepath.Join(dir, serfWANKeyring) - if err := initKeyring(fileWAN, key); err != nil { + if _, err := initKeyring(fileWAN, key); err != nil { t.Fatalf("err: %s", err) } diff --git a/command/agent/command.go b/command/agent/command.go index 7bb949618..8c0455c43 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -156,17 +156,29 @@ func (c *Command) readConfig() *Config { } fileLAN := filepath.Join(config.DataDir, serfLANKeyring) - if err := initKeyring(fileLAN, config.EncryptKey); err != nil { + done, err := initKeyring(fileLAN, config.EncryptKey) + if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) return nil } + if !done { + c.Ui.Error(fmt.Sprintf( + "WARNING: keyring file %s already exists, not overwriting", + fileLAN)) + } if config.Server { fileWAN := filepath.Join(config.DataDir, serfWANKeyring) - if err := initKeyring(fileWAN, config.EncryptKey); err != nil { + done, err := initKeyring(fileWAN, config.EncryptKey) + if err != nil { c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) return nil } + if !done { + c.Ui.Error(fmt.Sprintf( + "WARNING: keyring file %s already exists, not overwriting", + fileWAN)) + } } } diff --git a/command/agent/keyring.go b/command/agent/keyring.go index 51a466653..f0644982e 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -18,51 +18,42 @@ const ( serfWANKeyring = "serf/remote.keyring" ) -// initKeyring will create a keyring file at a given path. -func initKeyring(path, key string) error { +// initKeyring will create a keyring file at a given path. Returns whether any +// action was taken and any applicable error. +func initKeyring(path, key string) (bool, error) { var keys []string if _, err := base64.StdEncoding.DecodeString(key); err != nil { - return fmt.Errorf("Invalid key: %s", err) + return false, fmt.Errorf("Invalid key: %s", err) } + // Just exit if the file already exists. if _, err := os.Stat(path); err == nil { - content, err := ioutil.ReadFile(path) - if err != nil { - return err - } - if err := json.Unmarshal(content, &keys); err != nil { - return err - } - for _, existing := range keys { - if key == existing { - return nil - } - } + return false, nil } keys = append(keys, key) keyringBytes, err := json.Marshal(keys) if err != nil { - return err + return false, err } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return err + return false, err } fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { - return err + return false, err } defer fh.Close() if _, err := fh.Write(keyringBytes); err != nil { os.Remove(path) - return err + return false, err } - return nil + return true, nil } // loadKeyringFile will load a gossip encryption keyring out of a file. The file diff --git a/command/agent/keyring_test.go b/command/agent/keyring_test.go index 734a67dc1..50fb1e1b4 100644 --- a/command/agent/keyring_test.go +++ b/command/agent/keyring_test.go @@ -1,12 +1,10 @@ package agent import ( - "bytes" - "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" - "strings" "testing" ) @@ -78,6 +76,7 @@ func TestAgent_LoadKeyrings(t *testing.T) { func TestAgent_InitKeyring(t *testing.T) { key1 := "tbLJg26ZJyJ9pK3qhc9jig==" key2 := "4leC33rgtXKIVUr9Nr0snQ==" + expected := fmt.Sprintf(`["%s"]`, key1) dir, err := ioutil.TempDir("", "consul") if err != nil { @@ -88,56 +87,36 @@ func TestAgent_InitKeyring(t *testing.T) { file := filepath.Join(dir, "keyring") // First initialize the keyring - if err := initKeyring(file, key1); err != nil { - t.Fatalf("err: %s", err) - } - - content1, err := ioutil.ReadFile(file) + done, err := initKeyring(file, key1) if err != nil { t.Fatalf("err: %s", err) } - if !strings.Contains(string(content1), key1) { - t.Fatalf("bad: %s", content1) - } - if strings.Contains(string(content1), key2) { - t.Fatalf("bad: %s", content1) + if !done { + t.Fatalf("should have modified keyring") } - // Now initialize again with the same key - if err := initKeyring(file, key1); err != nil { - t.Fatalf("err: %s", err) - } - - content2, err := ioutil.ReadFile(file) + content, err := ioutil.ReadFile(file) if err != nil { t.Fatalf("err: %s", err) } - if !bytes.Equal(content1, content2) { - t.Fatalf("bad: %s", content2) + if string(content) != expected { + t.Fatalf("bad: %s", content) } - // Initialize an existing keyring with a new key - if err := initKeyring(file, key2); err != nil { - t.Fatalf("err: %s", err) - } - - content3, err := ioutil.ReadFile(file) + // Try initializing again with a different key + done, err = initKeyring(file, key2) if err != nil { t.Fatalf("err: %s", err) } - if !strings.Contains(string(content3), key1) { - t.Fatalf("bad: %s", content3) - } - if !strings.Contains(string(content3), key2) { - t.Fatalf("bad: %s", content3) + if done { + t.Fatalf("should not have modified keyring") } - // Unmarshal and make sure that key1 is still primary - var keys []string - if err := json.Unmarshal(content3, &keys); err != nil { + content, err = ioutil.ReadFile(file) + if err != nil { t.Fatalf("err: %s", err) } - if keys[0] != key1 { - t.Fatalf("bad: %#v", keys) + if string(content) != expected { + t.Fatalf("bad: %s", content) } } diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 9046b03d1..ece3157a3 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -93,9 +93,8 @@ The options below are all specified on the command-line. automatically whenever the agent is restarted. This means that to encrypt Consul's gossip protocol, this option only needs to be provided once on each agent's initial startup sequence. If it is provided after Consul has been - initialized with an encryption key, then the provided key is simply added - as a secondary encryption key. More information on how keys can be changed - is available on the [keyring command](/docs/commands/keyring.html) page. + initialized with an encryption key, then the provided key is ignored and + a warning will be displayed. * `-join` - Address of another agent to join upon starting up. This can be specified multiple times to specify multiple agents to join. If Consul is From c9118b53bf5c892143098630bbf0279dc968d04e Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Fri, 10 Oct 2014 11:13:30 -0700 Subject: [PATCH 159/238] agent: fix loading keyring on agent start --- command/agent/agent.go | 72 ++++++++++++++++++++++------------- command/agent/agent_test.go | 4 +- command/agent/command.go | 28 +++----------- command/agent/keyring.go | 19 +++++---- command/agent/keyring_test.go | 15 ++------ 5 files changed, 66 insertions(+), 72 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index b6f3a299a..bdd2197e6 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -161,11 +161,6 @@ func (a *Agent) consulConfig() *consul.Config { if a.config.DataDir != "" { base.DataDir = a.config.DataDir } - if a.config.EncryptKey != "" { - key, _ := a.config.EncryptBytes() - base.SerfLANConfig.MemberlistConfig.SecretKey = key - base.SerfWANConfig.MemberlistConfig.SecretKey = key - } if a.config.NodeName != "" { base.NodeName = a.config.NodeName } @@ -263,21 +258,8 @@ func (a *Agent) consulConfig() *consul.Config { func (a *Agent) setupServer() error { config := a.consulConfig() - // Load a keyring file, if present - keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring) - if _, err := os.Stat(keyfileLAN); err == nil { - config.SerfLANConfig.KeyringFile = keyfileLAN - } - if err := loadKeyringFile(config.SerfLANConfig); err != nil { - return err - } - - keyfileWAN := filepath.Join(config.DataDir, serfWANKeyring) - if _, err := os.Stat(keyfileWAN); err == nil { - config.SerfWANConfig.KeyringFile = keyfileWAN - } - if err := loadKeyringFile(config.SerfWANConfig); err != nil { - return err + if err := a.setupKeyrings(config); err != nil { + return fmt.Errorf("Failed to configure keyring: %v", err) } server, err := consul.NewServer(config) @@ -292,13 +274,8 @@ func (a *Agent) setupServer() error { func (a *Agent) setupClient() error { config := a.consulConfig() - // Load a keyring file, if present - keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring) - if _, err := os.Stat(keyfileLAN); err == nil { - config.SerfLANConfig.KeyringFile = keyfileLAN - } - if err := loadKeyringFile(config.SerfLANConfig); err != nil { - return err + if err := a.setupKeyrings(config); err != nil { + return fmt.Errorf("Failed to configure keyring: %v", err) } client, err := consul.NewClient(config) @@ -309,6 +286,47 @@ func (a *Agent) setupClient() error { return nil } +// setupKeyrings is used to initialize and load keyrings during agent startup +func (a *Agent) setupKeyrings(config *consul.Config) error { + fileLAN := filepath.Join(a.config.DataDir, serfLANKeyring) + fileWAN := filepath.Join(a.config.DataDir, serfWANKeyring) + + if a.config.EncryptKey == "" { + goto LOAD + } + if _, err := os.Stat(fileLAN); err != nil { + if err := initKeyring(fileLAN, a.config.EncryptKey); err != nil { + return err + } + } + if a.config.Server { + if _, err := os.Stat(fileWAN); err != nil { + if err := initKeyring(fileWAN, a.config.EncryptKey); err != nil { + return err + } + } + } + +LOAD: + if _, err := os.Stat(fileLAN); err == nil { + config.SerfLANConfig.KeyringFile = fileLAN + } + if err := loadKeyringFile(config.SerfLANConfig); err != nil { + return err + } + if a.config.Server { + if _, err := os.Stat(fileWAN); err == nil { + config.SerfWANConfig.KeyringFile = fileWAN + } + if err := loadKeyringFile(config.SerfWANConfig); err != nil { + return err + } + } + + // Success! + return nil +} + // RPC is used to make an RPC call to the Consul servers // This allows the agent to implement the Consul.Interface func (a *Agent) RPC(method string, args interface{}, reply interface{}) error { diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 3f398016f..1e4aec000 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -81,11 +81,11 @@ func makeAgentKeyring(t *testing.T, conf *Config, key string) (string, *Agent) { conf.DataDir = dir fileLAN := filepath.Join(dir, serfLANKeyring) - if _, err := initKeyring(fileLAN, key); err != nil { + if err := initKeyring(fileLAN, key); err != nil { t.Fatalf("err: %s", err) } fileWAN := filepath.Join(dir, serfWANKeyring) - if _, err := initKeyring(fileWAN, key); err != nil { + if err := initKeyring(fileWAN, key); err != nil { t.Fatalf("err: %s", err) } diff --git a/command/agent/command.go b/command/agent/command.go index 8c0455c43..6d9da74da 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -154,30 +154,14 @@ func (c *Command) readConfig() *Config { c.Ui.Error(fmt.Sprintf("Invalid encryption key: %s", err)) return nil } - - fileLAN := filepath.Join(config.DataDir, serfLANKeyring) - done, err := initKeyring(fileLAN, config.EncryptKey) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) - return nil + keyfileLAN := filepath.Join(config.DataDir, serfLANKeyring) + if _, err := os.Stat(keyfileLAN); err == nil { + c.Ui.Error("WARNING: LAN keyring exists but -encrypt given, ignoring") } - if !done { - c.Ui.Error(fmt.Sprintf( - "WARNING: keyring file %s already exists, not overwriting", - fileLAN)) - } - if config.Server { - fileWAN := filepath.Join(config.DataDir, serfWANKeyring) - done, err := initKeyring(fileWAN, config.EncryptKey) - if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing keyring: %s", err)) - return nil - } - if !done { - c.Ui.Error(fmt.Sprintf( - "WARNING: keyring file %s already exists, not overwriting", - fileWAN)) + keyfileWAN := filepath.Join(config.DataDir, serfWANKeyring) + if _, err := os.Stat(keyfileWAN); err == nil { + c.Ui.Error("WARNING: WAN keyring exists but -encrypt given, ignoring") } } } diff --git a/command/agent/keyring.go b/command/agent/keyring.go index f0644982e..07bd19b0c 100644 --- a/command/agent/keyring.go +++ b/command/agent/keyring.go @@ -18,42 +18,41 @@ const ( serfWANKeyring = "serf/remote.keyring" ) -// initKeyring will create a keyring file at a given path. Returns whether any -// action was taken and any applicable error. -func initKeyring(path, key string) (bool, error) { +// initKeyring will create a keyring file at a given path. +func initKeyring(path, key string) error { var keys []string if _, err := base64.StdEncoding.DecodeString(key); err != nil { - return false, fmt.Errorf("Invalid key: %s", err) + return fmt.Errorf("Invalid key: %s", err) } // Just exit if the file already exists. if _, err := os.Stat(path); err == nil { - return false, nil + return nil } keys = append(keys, key) keyringBytes, err := json.Marshal(keys) if err != nil { - return false, err + return err } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { - return false, err + return err } fh, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { - return false, err + return err } defer fh.Close() if _, err := fh.Write(keyringBytes); err != nil { os.Remove(path) - return false, err + return err } - return true, nil + return nil } // loadKeyringFile will load a gossip encryption keyring out of a file. The file diff --git a/command/agent/keyring_test.go b/command/agent/keyring_test.go index 50fb1e1b4..558c71f5d 100644 --- a/command/agent/keyring_test.go +++ b/command/agent/keyring_test.go @@ -66,7 +66,7 @@ func TestAgent_LoadKeyrings(t *testing.T) { t.Fatalf("keyring should be loaded") } if c.SerfWANConfig.KeyringFile != "" { - t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) + t.Fatalf("bad: %#v", c.SerfWANConfig.KeyringFile) } if c.SerfWANConfig.MemberlistConfig.Keyring != nil { t.Fatalf("keyring should not be loaded") @@ -87,13 +87,9 @@ func TestAgent_InitKeyring(t *testing.T) { file := filepath.Join(dir, "keyring") // First initialize the keyring - done, err := initKeyring(file, key1) - if err != nil { + if err := initKeyring(file, key1); err != nil { t.Fatalf("err: %s", err) } - if !done { - t.Fatalf("should have modified keyring") - } content, err := ioutil.ReadFile(file) if err != nil { @@ -104,14 +100,11 @@ func TestAgent_InitKeyring(t *testing.T) { } // Try initializing again with a different key - done, err = initKeyring(file, key2) - if err != nil { + if err := initKeyring(file, key2); err != nil { t.Fatalf("err: %s", err) } - if done { - t.Fatalf("should not have modified keyring") - } + // Content should still be the same content, err = ioutil.ReadFile(file) if err != nil { t.Fatalf("err: %s", err) From a675b5faae92cee1779f68b8f04d74c2e5cec9ae Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 11 Oct 2014 17:29:24 -0700 Subject: [PATCH 160/238] agent: fix gossip encryption detection --- command/agent/command.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/command/agent/command.go b/command/agent/command.go index 6d9da74da..a62dbdd67 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -612,9 +612,12 @@ func (c *Command) Run(args []string) int { } // Figure out if gossip is encrypted - gossipEncrypted := (config.Server && - c.agent.server.Encrypted() || - c.agent.client.Encrypted()) + var gossipEncrypted bool + if config.Server { + gossipEncrypted = c.agent.server.Encrypted() + } else { + gossipEncrypted = c.agent.client.Encrypted() + } // Let the agent know we've finished registration c.agent.StartSync() From 4cd89a9113480ad7c3cf4f57c29316cfdc90bca9 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 19 Nov 2014 16:45:49 -0800 Subject: [PATCH 161/238] Rebase against upstream --- command/agent/agent_test.go | 65 ----------------------------------- command/agent/command_test.go | 35 ------------------- consul/client.go | 5 +++ 3 files changed, 5 insertions(+), 100 deletions(-) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 1e4aec000..547232431 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -380,68 +380,3 @@ func TestAgent_ConsulService(t *testing.T) { t.Fatalf("%s service should be in sync", consul.ConsulServiceID) } } - -func TestAgent_LoadKeyrings(t *testing.T) { - key := "tbLJg26ZJyJ9pK3qhc9jig==" - - // Should be no configured keyring file by default - conf1 := nextConfig() - dir1, agent1 := makeAgent(t, conf1) - defer os.RemoveAll(dir1) - defer agent1.Shutdown() - - c := agent1.config.ConsulConfig - if c.SerfLANConfig.KeyringFile != "" { - t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) - } - if c.SerfLANConfig.MemberlistConfig.Keyring != nil { - t.Fatalf("keyring should not be loaded") - } - if c.SerfWANConfig.KeyringFile != "" { - t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) - } - if c.SerfWANConfig.MemberlistConfig.Keyring != nil { - t.Fatalf("keyring should not be loaded") - } - - // Server should auto-load LAN and WAN keyring files - conf2 := nextConfig() - dir2, agent2 := makeAgentKeyring(t, conf2, key) - defer os.RemoveAll(dir2) - defer agent2.Shutdown() - - c = agent2.config.ConsulConfig - if c.SerfLANConfig.KeyringFile == "" { - t.Fatalf("should have keyring file") - } - if c.SerfLANConfig.MemberlistConfig.Keyring == nil { - t.Fatalf("keyring should be loaded") - } - if c.SerfWANConfig.KeyringFile == "" { - t.Fatalf("should have keyring file") - } - if c.SerfWANConfig.MemberlistConfig.Keyring == nil { - t.Fatalf("keyring should be loaded") - } - - // Client should auto-load only the LAN keyring file - conf3 := nextConfig() - conf3.Server = false - dir3, agent3 := makeAgentKeyring(t, conf3, key) - defer os.RemoveAll(dir3) - defer agent3.Shutdown() - - c = agent3.config.ConsulConfig - if c.SerfLANConfig.KeyringFile == "" { - t.Fatalf("should have keyring file") - } - if c.SerfLANConfig.MemberlistConfig.Keyring == nil { - t.Fatalf("keyring should be loaded") - } - if c.SerfWANConfig.KeyringFile != "" { - t.Fatalf("bad: %#v", c.SerfLANConfig.KeyringFile) - } - if c.SerfWANConfig.MemberlistConfig.Keyring != nil { - t.Fatalf("keyring should not be loaded") - } -} diff --git a/command/agent/command_test.go b/command/agent/command_test.go index 703d476ac..10557daa7 100644 --- a/command/agent/command_test.go +++ b/command/agent/command_test.go @@ -1,16 +1,10 @@ package agent import ( -<<<<<<< HEAD "fmt" "io/ioutil" "log" "os" - "path/filepath" - "strings" -======= - "github.com/mitchellh/cli" ->>>>>>> agent: -encrypt appends to keyring if one exists "testing" "github.com/hashicorp/consul/testutil" @@ -169,32 +163,3 @@ func TestRetryJoinWanFail(t *testing.T) { t.Fatalf("bad: %d", code) } } - -func TestArgConflict(t *testing.T) { - ui := new(cli.MockUi) - c := &Command{Ui: ui} - - dir, err := ioutil.TempDir("", "agent") - if err != nil { - t.Fatalf("err: %s", err) - } - - key := "HS5lJ+XuTlYKWaeGYyG+/A==" - - fileLAN := filepath.Join(dir, SerfLANKeyring) - if err := testutil.InitKeyring(fileLAN, key); err != nil { - t.Fatalf("err: %s", err) - } - - args := []string{ - "-encrypt=" + key, - "-data-dir=" + dir, - } - code := c.Run(args) - if code != 1 { - t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) - } - if !strings.Contains(ui.ErrorWriter.String(), "keyring files exist") { - t.Fatalf("bad: %#v", ui.ErrorWriter.String()) - } -} diff --git a/consul/client.go b/consul/client.go index cf0ddca0c..d10db8550 100644 --- a/consul/client.go +++ b/consul/client.go @@ -206,6 +206,11 @@ func (c *Client) UserEvent(name string, payload []byte) error { return c.serf.UserEvent(userEventName(name), payload, false) } +// KeyManagerLAN returns the LAN Serf keyring manager +func (c *Client) KeyManagerLAN() *serf.KeyManager { + return c.serf.KeyManager() +} + // Encrypted determines if gossip is encrypted func (c *Client) Encrypted() bool { return c.serf.EncryptionEnabled() From accf2bbb59bbbf85bcec371147749ef54c6325e2 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Wed, 19 Nov 2014 23:18:12 -0800 Subject: [PATCH 162/238] agent: remove unused config variable --- command/agent/config.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index dce299eac..36683ad16 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -108,9 +108,6 @@ type Config struct { // resolve non-consul domains DNSRecursors []string `mapstructure:"recursors"` - // Disable use of an encryption keyring. - DisableKeyring bool `mapstructure:"disable_keyring"` - // DNS configuration DNSConfig DNSConfig `mapstructure:"dns_config"` From aa0cecd04eab591fa77027b0687663ef3ae8370a Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Wed, 19 Nov 2014 21:11:41 -0500 Subject: [PATCH 163/238] Ephemeral Nodes for via Session behavior settings. Added a "delete" behavior for session invalidation, in addition to the default "release" behavior. On session invalidation, the sessions Behavior field is checked and if it is set to "delete", all nodes owned by the session are deleted. If it is "release", then just the locks are released as default. --- command/agent/session_endpoint.go | 3 +- command/agent/session_endpoint_test.go | 181 +++++++++++++++++++++ consul/session_endpoint.go | 3 + consul/session_endpoint_test.go | 217 ++++++++++++++++++++++++- consul/state_store.go | 52 ++++-- consul/state_store_test.go | 47 ++++++ consul/structs/structs.go | 8 + 7 files changed, 498 insertions(+), 13 deletions(-) diff --git a/command/agent/session_endpoint.go b/command/agent/session_endpoint.go index 760255f22..cd9fa7ecd 100644 --- a/command/agent/session_endpoint.go +++ b/command/agent/session_endpoint.go @@ -32,13 +32,14 @@ func (s *HTTPServer) SessionCreate(resp http.ResponseWriter, req *http.Request) return nil, nil } - // Default the session to our node + serf check + // Default the session to our node + serf check + release session invalidate behavior args := structs.SessionRequest{ Op: structs.SessionCreate, Session: structs.Session{ Node: s.agent.config.NodeName, Checks: []string{consul.SerfCheckID}, LockDelay: 15 * time.Second, + Behavior: structs.SessionKeysRelease, }, } s.parseDC(req, &args.Datacenter) diff --git a/command/agent/session_endpoint_test.go b/command/agent/session_endpoint_test.go index b5a93eea3..5f32bc173 100644 --- a/command/agent/session_endpoint_test.go +++ b/command/agent/session_endpoint_test.go @@ -59,6 +59,55 @@ func TestSessionCreate(t *testing.T) { }) } +func TestSessionCreateDelete(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + // Create a health check + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: srv.agent.config.NodeName, + Address: "127.0.0.1", + Check: &structs.HealthCheck{ + CheckID: "consul", + Node: srv.agent.config.NodeName, + Name: "consul", + ServiceID: "consul", + Status: structs.HealthPassing, + }, + } + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Associate session with node and 2 health checks, and make it delete on session destroy + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Name": "my-cool-session", + "Node": srv.agent.config.NodeName, + "Checks": []string{consul.SerfCheckID, "consul"}, + "LockDelay": "20s", + "Behavior": "delete", + } + enc.Encode(raw) + + req, err := http.NewRequest("PUT", "/v1/session/create", body) + if err != nil { + t.Fatalf("err: %v", err) + } + + resp := httptest.NewRecorder() + obj, err := srv.SessionCreate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + if _, ok := obj.(sessionCreateResponse); !ok { + t.Fatalf("should work") + } + }) +} + func TestFixupLockDelay(t *testing.T) { inp := map[string]interface{}{ "lockdelay": float64(15), @@ -105,6 +154,28 @@ func makeTestSession(t *testing.T, srv *HTTPServer) string { return sessResp.ID } +func makeTestSessionDelete(t *testing.T, srv *HTTPServer) string { + // Create Session with delete behavior + body := bytes.NewBuffer(nil) + enc := json.NewEncoder(body) + raw := map[string]interface{}{ + "Behavior": "delete", + } + enc.Encode(raw) + + req, err := http.NewRequest("PUT", "/v1/session/create", body) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := httptest.NewRecorder() + obj, err := srv.SessionCreate(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + sessResp := obj.(sessionCreateResponse) + return sessResp.ID +} + func TestSessionDestroy(t *testing.T) { httpTest(t, func(srv *HTTPServer) { id := makeTestSession(t, srv) @@ -188,3 +259,113 @@ func TestSessionsForNode(t *testing.T) { } }) } + +func TestSessionDeleteDestroy(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestSessionDelete(t, srv) + + // now create a new key for the session and acquire it + buf := bytes.NewBuffer([]byte("test")) + req, err := http.NewRequest("PUT", "/v1/kv/ephemeral?acquire="+id, buf) + if err != nil { + t.Fatalf("err: %v", err) + } + resp := httptest.NewRecorder() + obj, err := srv.KVSEndpoint(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + + if res := obj.(bool); !res { + t.Fatalf("should work") + } + + // now destroy the session, this should delete the key create above + req, err = http.NewRequest("PUT", "/v1/session/destroy/"+id, nil) + resp = httptest.NewRecorder() + obj, err = srv.SessionDestroy(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + if resp := obj.(bool); !resp { + t.Fatalf("should work") + } + + // Verify that the key is gone + req, _ = http.NewRequest("GET", "/v1/kv/ephemeral", nil) + resp = httptest.NewRecorder() + obj, _ = srv.KVSEndpoint(resp, req) + res, found := obj.(structs.DirEntries) + if found || len(res) != 0 { + t.Fatalf("bad: %v found, should be nothing", res) + } + }) +} + +func TestSessionDeleteGet(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + id := makeTestSessionDelete(t, srv) + + req, err := http.NewRequest("GET", + "/v1/session/info/"+id, nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionGet(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 1 { + t.Fatalf("bad: %v", respObj) + } + }) +} + +func TestSessionDeleteList(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + var ids []string + for i := 0; i < 10; i++ { + ids = append(ids, makeTestSessionDelete(t, srv)) + } + + req, err := http.NewRequest("GET", "/v1/session/list", nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionList(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 10 { + t.Fatalf("bad: %v", respObj) + } + }) +} + +func TestSessionsDeleteForNode(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + var ids []string + for i := 0; i < 10; i++ { + ids = append(ids, makeTestSessionDelete(t, srv)) + } + + req, err := http.NewRequest("GET", + "/v1/session/node/"+srv.agent.config.NodeName, nil) + resp := httptest.NewRecorder() + obj, err := srv.SessionsForNode(resp, req) + if err != nil { + t.Fatalf("err: %v", err) + } + respObj, ok := obj.(structs.Sessions) + if !ok { + t.Fatalf("should work") + } + if len(respObj) != 10 { + t.Fatalf("bad: %v", respObj) + } + }) +} diff --git a/consul/session_endpoint.go b/consul/session_endpoint.go index c08bef39e..297e32f32 100644 --- a/consul/session_endpoint.go +++ b/consul/session_endpoint.go @@ -28,6 +28,9 @@ func (s *Session) Apply(args *structs.SessionRequest, reply *string) error { if args.Session.Node == "" && args.Op == structs.SessionCreate { return fmt.Errorf("Must provide Node") } + if args.Session.Behavior == "" { + args.Session.Behavior = structs.SessionKeysRelease // force default behavior + } // If this is a create, we must generate the Session ID. This must // be done prior to appending to the raft log, because the ID is not diff --git a/consul/session_endpoint_test.go b/consul/session_endpoint_test.go index ab3bfb843..5e2b03caa 100644 --- a/consul/session_endpoint_test.go +++ b/consul/session_endpoint_test.go @@ -66,6 +66,66 @@ func TestSessionEndpoint_Apply(t *testing.T) { } } +func TestSessionEndpoint_DeleteApply(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Just add a node + s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) + + arg := structs.SessionRequest{ + Datacenter: "dc1", + Op: structs.SessionCreate, + Session: structs.Session{ + Node: "foo", + Name: "my-session", + Behavior: structs.SessionKeysDelete, + }, + } + var out string + if err := client.Call("Session.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + // Verify + state := s1.fsm.State() + _, s, err := state.SessionGet(out) + if err != nil { + t.Fatalf("err: %v", err) + } + if s == nil { + t.Fatalf("should not be nil") + } + if s.Node != "foo" { + t.Fatalf("bad: %v", s) + } + if s.Name != "my-session" { + t.Fatalf("bad: %v", s) + } + + // Do a delete + arg.Op = structs.SessionDestroy + arg.Session.ID = out + if err := client.Call("Session.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Verify + _, s, err = state.SessionGet(id) + if err != nil { + t.Fatalf("err: %v", err) + } + if s != nil { + t.Fatalf("bad: %v", s) + } +} + func TestSessionEndpoint_Get(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -80,7 +140,51 @@ func TestSessionEndpoint_Get(t *testing.T) { Datacenter: "dc1", Op: structs.SessionCreate, Session: structs.Session{ - Node: "foo", + Node: "foo", + }, + } + var out string + if err := client.Call("Session.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + getR := structs.SessionSpecificRequest{ + Datacenter: "dc1", + Session: out, + } + var sessions structs.IndexedSessions + if err := client.Call("Session.Get", &getR, &sessions); err != nil { + t.Fatalf("err: %v", err) + } + + if sessions.Index == 0 { + t.Fatalf("Bad: %v", sessions) + } + if len(sessions.Sessions) != 1 { + t.Fatalf("Bad: %v", sessions) + } + s := sessions.Sessions[0] + if s.ID != out { + t.Fatalf("bad: %v", s) + } +} + +func TestSessionEndpoint_DeleteGet(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) + arg := structs.SessionRequest{ + Datacenter: "dc1", + Op: structs.SessionCreate, + Session: structs.Session{ + Node: "foo", + Behavior: structs.SessionKeysDelete, }, } var out string @@ -160,6 +264,58 @@ func TestSessionEndpoint_List(t *testing.T) { } } +func TestSessionEndpoint_DeleteList(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) + ids := []string{} + for i := 0; i < 5; i++ { + arg := structs.SessionRequest{ + Datacenter: "dc1", + Op: structs.SessionCreate, + Session: structs.Session{ + Node: "foo", + Behavior: structs.SessionKeysDelete, + }, + } + var out string + if err := client.Call("Session.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + ids = append(ids, out) + } + + getR := structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var sessions structs.IndexedSessions + if err := client.Call("Session.List", &getR, &sessions); err != nil { + t.Fatalf("err: %v", err) + } + + if sessions.Index == 0 { + t.Fatalf("Bad: %v", sessions) + } + if len(sessions.Sessions) != 5 { + t.Fatalf("Bad: %v", sessions.Sessions) + } + for i := 0; i < len(sessions.Sessions); i++ { + s := sessions.Sessions[i] + if !strContains(ids, s.ID) { + t.Fatalf("bad: %v", s) + } + if s.Node != "foo" { + t.Fatalf("bad: %v", s) + } + } +} + func TestSessionEndpoint_NodeSessions(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -217,3 +373,62 @@ func TestSessionEndpoint_NodeSessions(t *testing.T) { } } } + +func TestSessionEndpoint_DeleteNodeSessions(t *testing.T) { + dir1, s1 := testServer(t) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) + s1.fsm.State().EnsureNode(1, structs.Node{"bar", "127.0.0.1"}) + ids := []string{} + for i := 0; i < 10; i++ { + arg := structs.SessionRequest{ + Datacenter: "dc1", + Op: structs.SessionCreate, + Session: structs.Session{ + Node: "bar", + Behavior: structs.SessionKeysDelete, + }, + } + if i < 5 { + arg.Session.Node = "foo" + } + var out string + if err := client.Call("Session.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + if i < 5 { + ids = append(ids, out) + } + } + + getR := structs.NodeSpecificRequest{ + Datacenter: "dc1", + Node: "foo", + } + var sessions structs.IndexedSessions + if err := client.Call("Session.NodeSessions", &getR, &sessions); err != nil { + t.Fatalf("err: %v", err) + } + + if sessions.Index == 0 { + t.Fatalf("Bad: %v", sessions) + } + if len(sessions.Sessions) != 5 { + t.Fatalf("Bad: %v", sessions.Sessions) + } + for i := 0; i < len(sessions.Sessions); i++ { + s := sessions.Sessions[i] + if !strContains(ids, s.ID) { + t.Fatalf("bad: %v", s) + } + if s.Node != "foo" { + t.Fatalf("bad: %v", s) + } + } +} diff --git a/consul/state_store.go b/consul/state_store.go index 5bbadd423..bc0da3639 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -1327,6 +1327,11 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error return fmt.Errorf("Missing Session ID") } + // make sure we have a default set for session.Behavior + if session.Behavior == "" { + session.Behavior = structs.SessionKeysRelease + } + // Assign the create index session.CreateIndex = index @@ -1454,7 +1459,7 @@ func (s *StateStore) SessionDestroy(index uint64, id string) error { } defer tx.Abort() - log.Printf("[DEBUG] consul.state: Invalidating session %s due to session destroy", + s.logger.Printf("[DEBUG] consul.state: Invalidating session %s due to session destroy", id) if err := s.invalidateSession(index, tx, id); err != nil { return err @@ -1471,7 +1476,7 @@ func (s *StateStore) invalidateNode(index uint64, tx *MDBTxn, node string) error } for _, sess := range sessions { session := sess.(*structs.Session).ID - log.Printf("[DEBUG] consul.state: Invalidating session %s due to node '%s' invalidation", + s.logger.Printf("[DEBUG] consul.state: Invalidating session %s due to node '%s' invalidation", session, node) if err := s.invalidateSession(index, tx, session); err != nil { return err @@ -1489,7 +1494,7 @@ func (s *StateStore) invalidateCheck(index uint64, tx *MDBTxn, node, check strin } for _, sc := range sessionChecks { session := sc.(*sessionCheck).Session - log.Printf("[DEBUG] consul.state: Invalidating session %s due to check '%s' invalidation", + s.logger.Printf("[DEBUG] consul.state: Invalidating session %s due to check '%s' invalidation", session, check) if err := s.invalidateSession(index, tx, session); err != nil { return err @@ -1513,15 +1518,23 @@ func (s *StateStore) invalidateSession(index uint64, tx *MDBTxn, id string) erro } session := res[0].(*structs.Session) - // Enforce the MaxLockDelay - delay := session.LockDelay - if delay > structs.MaxLockDelay { - delay = structs.MaxLockDelay - } + if session.Behavior == structs.SessionKeysDelete { + // delete the keys held by the session + if err := s.deleteKeys(index, tx, id); err != nil { + return err + } - // Invalidate any held locks - if err := s.invalidateLocks(index, tx, delay, id); err != nil { - return err + } else { // default to release + // Enforce the MaxLockDelay + delay := session.LockDelay + if delay > structs.MaxLockDelay { + delay = structs.MaxLockDelay + } + + // Invalidate any held locks + if err := s.invalidateLocks(index, tx, delay, id); err != nil { + return err + } } // Nuke the session @@ -1588,6 +1601,23 @@ func (s *StateStore) invalidateLocks(index uint64, tx *MDBTxn, return nil } +// deleteKeys is used to delete all the keys created by a session +// within a given txn. All tables should be locked in the tx. +func (s *StateStore) deleteKeys(index uint64, tx *MDBTxn, id string) error { + num, err := s.kvsTable.DeleteTxn(tx, "session", id) + if err != nil { + return err + } + + if num > 0 { + if err := s.kvsTable.SetLastIndexTxn(tx, index); err != nil { + return err + } + tx.Defer(func() { s.watch[s.kvsTable].Notify() }) + } + return nil +} + // ACLSet is used to create or update an ACL entry func (s *StateStore) ACLSet(index uint64, acl *structs.ACL) error { // Check for an ID diff --git a/consul/state_store_test.go b/consul/state_store_test.go index 9480cf1b1..0dba19abd 100644 --- a/consul/state_store_test.go +++ b/consul/state_store_test.go @@ -2279,6 +2279,53 @@ func TestSessionInvalidate_KeyUnlock(t *testing.T) { } } +func TestSessionInvalidate_KeyDelete(t *testing.T) { + store, err := testStateStore() + if err != nil { + t.Fatalf("err: %v", err) + } + defer store.Close() + + if err := store.EnsureNode(3, structs.Node{"foo", "127.0.0.1"}); err != nil { + t.Fatalf("err: %v", err) + } + session := &structs.Session{ + ID: generateUUID(), + Node: "foo", + LockDelay: 50 * time.Millisecond, + Behavior: structs.SessionKeysDelete, + } + if err := store.SessionCreate(4, session); err != nil { + t.Fatalf("err: %v", err) + } + + // Lock a key with the session + d := &structs.DirEntry{ + Key: "/bar", + Flags: 42, + Value: []byte("test"), + Session: session.ID, + } + ok, err := store.KVSLock(5, d) + if err != nil { + t.Fatalf("err: %v", err) + } + if !ok { + t.Fatalf("unexpected fail") + } + + // Delete the node + if err := store.DeleteNode(6, "foo"); err != nil { + t.Fatalf("err: %v", err) + } + + // Key should be deleted + _, d2, err := store.KVSGet("/bar") + if d2 != nil { + t.Fatalf("unexpected undeleted key") + } +} + func TestACLSet_Get(t *testing.T) { store, err := testStateStore() if err != nil { diff --git a/consul/structs/structs.go b/consul/structs/structs.go index b1f315271..ced8567d2 100644 --- a/consul/structs/structs.go +++ b/consul/structs/structs.go @@ -378,6 +378,13 @@ type IndexedKeyList struct { QueryMeta } +type SessionBehavior string + +const ( + SessionKeysRelease SessionBehavior = "release" + SessionKeysDelete = "delete" +) + // Session is used to represent an open session in the KV store. // This issued to associate node checks with acquired locks. type Session struct { @@ -387,6 +394,7 @@ type Session struct { Node string Checks []string LockDelay time.Duration + Behavior SessionBehavior // What to do when session is invalidated } type Sessions []*Session From 3aabda02b30b56e269b253db973f008878280302 Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Thu, 20 Nov 2014 14:29:18 -0500 Subject: [PATCH 164/238] Clean up tests, use switch to default session.Behavior value if unspecified, unrecognized --- command/agent/session_endpoint_test.go | 72 +---------- consul/session_endpoint.go | 6 +- consul/session_endpoint_test.go | 160 +------------------------ consul/state_store.go | 7 +- 4 files changed, 16 insertions(+), 229 deletions(-) diff --git a/command/agent/session_endpoint_test.go b/command/agent/session_endpoint_test.go index 5f32bc173..74ec20ebf 100644 --- a/command/agent/session_endpoint_test.go +++ b/command/agent/session_endpoint_test.go @@ -87,7 +87,7 @@ func TestSessionCreateDelete(t *testing.T) { "Node": srv.agent.config.NodeName, "Checks": []string{consul.SerfCheckID, "consul"}, "LockDelay": "20s", - "Behavior": "delete", + "Behavior": structs.SessionKeysDelete, } enc.Encode(raw) @@ -280,7 +280,7 @@ func TestSessionDeleteDestroy(t *testing.T) { t.Fatalf("should work") } - // now destroy the session, this should delete the key create above + // now destroy the session, this should delete the key created above req, err = http.NewRequest("PUT", "/v1/session/destroy/"+id, nil) resp = httptest.NewRecorder() obj, err = srv.SessionDestroy(resp, req) @@ -301,71 +301,3 @@ func TestSessionDeleteDestroy(t *testing.T) { } }) } - -func TestSessionDeleteGet(t *testing.T) { - httpTest(t, func(srv *HTTPServer) { - id := makeTestSessionDelete(t, srv) - - req, err := http.NewRequest("GET", - "/v1/session/info/"+id, nil) - resp := httptest.NewRecorder() - obj, err := srv.SessionGet(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - respObj, ok := obj.(structs.Sessions) - if !ok { - t.Fatalf("should work") - } - if len(respObj) != 1 { - t.Fatalf("bad: %v", respObj) - } - }) -} - -func TestSessionDeleteList(t *testing.T) { - httpTest(t, func(srv *HTTPServer) { - var ids []string - for i := 0; i < 10; i++ { - ids = append(ids, makeTestSessionDelete(t, srv)) - } - - req, err := http.NewRequest("GET", "/v1/session/list", nil) - resp := httptest.NewRecorder() - obj, err := srv.SessionList(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - respObj, ok := obj.(structs.Sessions) - if !ok { - t.Fatalf("should work") - } - if len(respObj) != 10 { - t.Fatalf("bad: %v", respObj) - } - }) -} - -func TestSessionsDeleteForNode(t *testing.T) { - httpTest(t, func(srv *HTTPServer) { - var ids []string - for i := 0; i < 10; i++ { - ids = append(ids, makeTestSessionDelete(t, srv)) - } - - req, err := http.NewRequest("GET", - "/v1/session/node/"+srv.agent.config.NodeName, nil) - resp := httptest.NewRecorder() - obj, err := srv.SessionsForNode(resp, req) - if err != nil { - t.Fatalf("err: %v", err) - } - respObj, ok := obj.(structs.Sessions) - if !ok { - t.Fatalf("should work") - } - if len(respObj) != 10 { - t.Fatalf("bad: %v", respObj) - } - }) -} diff --git a/consul/session_endpoint.go b/consul/session_endpoint.go index 297e32f32..f7e46636f 100644 --- a/consul/session_endpoint.go +++ b/consul/session_endpoint.go @@ -28,7 +28,11 @@ func (s *Session) Apply(args *structs.SessionRequest, reply *string) error { if args.Session.Node == "" && args.Op == structs.SessionCreate { return fmt.Errorf("Must provide Node") } - if args.Session.Behavior == "" { + switch args.Session.Behavior { + case structs.SessionKeysRelease, structs.SessionKeysDelete: + // we like it, use it + + default: args.Session.Behavior = structs.SessionKeysRelease // force default behavior } diff --git a/consul/session_endpoint_test.go b/consul/session_endpoint_test.go index 5e2b03caa..794975294 100644 --- a/consul/session_endpoint_test.go +++ b/consul/session_endpoint_test.go @@ -108,6 +108,9 @@ func TestSessionEndpoint_DeleteApply(t *testing.T) { if s.Name != "my-session" { t.Fatalf("bad: %v", s) } + if s.Behavior != structs.SessionKeysDelete { + t.Fatalf("bad: %v", s) + } // Do a delete arg.Op = structs.SessionDestroy @@ -140,51 +143,7 @@ func TestSessionEndpoint_Get(t *testing.T) { Datacenter: "dc1", Op: structs.SessionCreate, Session: structs.Session{ - Node: "foo", - }, - } - var out string - if err := client.Call("Session.Apply", &arg, &out); err != nil { - t.Fatalf("err: %v", err) - } - - getR := structs.SessionSpecificRequest{ - Datacenter: "dc1", - Session: out, - } - var sessions structs.IndexedSessions - if err := client.Call("Session.Get", &getR, &sessions); err != nil { - t.Fatalf("err: %v", err) - } - - if sessions.Index == 0 { - t.Fatalf("Bad: %v", sessions) - } - if len(sessions.Sessions) != 1 { - t.Fatalf("Bad: %v", sessions) - } - s := sessions.Sessions[0] - if s.ID != out { - t.Fatalf("bad: %v", s) - } -} - -func TestSessionEndpoint_DeleteGet(t *testing.T) { - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() - client := rpcClient(t, s1) - defer client.Close() - - testutil.WaitForLeader(t, client.Call, "dc1") - - s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) - arg := structs.SessionRequest{ - Datacenter: "dc1", - Op: structs.SessionCreate, - Session: structs.Session{ - Node: "foo", - Behavior: structs.SessionKeysDelete, + Node: "foo", }, } var out string @@ -264,58 +223,6 @@ func TestSessionEndpoint_List(t *testing.T) { } } -func TestSessionEndpoint_DeleteList(t *testing.T) { - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() - client := rpcClient(t, s1) - defer client.Close() - - testutil.WaitForLeader(t, client.Call, "dc1") - - s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) - ids := []string{} - for i := 0; i < 5; i++ { - arg := structs.SessionRequest{ - Datacenter: "dc1", - Op: structs.SessionCreate, - Session: structs.Session{ - Node: "foo", - Behavior: structs.SessionKeysDelete, - }, - } - var out string - if err := client.Call("Session.Apply", &arg, &out); err != nil { - t.Fatalf("err: %v", err) - } - ids = append(ids, out) - } - - getR := structs.DCSpecificRequest{ - Datacenter: "dc1", - } - var sessions structs.IndexedSessions - if err := client.Call("Session.List", &getR, &sessions); err != nil { - t.Fatalf("err: %v", err) - } - - if sessions.Index == 0 { - t.Fatalf("Bad: %v", sessions) - } - if len(sessions.Sessions) != 5 { - t.Fatalf("Bad: %v", sessions.Sessions) - } - for i := 0; i < len(sessions.Sessions); i++ { - s := sessions.Sessions[i] - if !strContains(ids, s.ID) { - t.Fatalf("bad: %v", s) - } - if s.Node != "foo" { - t.Fatalf("bad: %v", s) - } - } -} - func TestSessionEndpoint_NodeSessions(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -373,62 +280,3 @@ func TestSessionEndpoint_NodeSessions(t *testing.T) { } } } - -func TestSessionEndpoint_DeleteNodeSessions(t *testing.T) { - dir1, s1 := testServer(t) - defer os.RemoveAll(dir1) - defer s1.Shutdown() - client := rpcClient(t, s1) - defer client.Close() - - testutil.WaitForLeader(t, client.Call, "dc1") - - s1.fsm.State().EnsureNode(1, structs.Node{"foo", "127.0.0.1"}) - s1.fsm.State().EnsureNode(1, structs.Node{"bar", "127.0.0.1"}) - ids := []string{} - for i := 0; i < 10; i++ { - arg := structs.SessionRequest{ - Datacenter: "dc1", - Op: structs.SessionCreate, - Session: structs.Session{ - Node: "bar", - Behavior: structs.SessionKeysDelete, - }, - } - if i < 5 { - arg.Session.Node = "foo" - } - var out string - if err := client.Call("Session.Apply", &arg, &out); err != nil { - t.Fatalf("err: %v", err) - } - if i < 5 { - ids = append(ids, out) - } - } - - getR := structs.NodeSpecificRequest{ - Datacenter: "dc1", - Node: "foo", - } - var sessions structs.IndexedSessions - if err := client.Call("Session.NodeSessions", &getR, &sessions); err != nil { - t.Fatalf("err: %v", err) - } - - if sessions.Index == 0 { - t.Fatalf("Bad: %v", sessions) - } - if len(sessions.Sessions) != 5 { - t.Fatalf("Bad: %v", sessions.Sessions) - } - for i := 0; i < len(sessions.Sessions); i++ { - s := sessions.Sessions[i] - if !strContains(ids, s.ID) { - t.Fatalf("bad: %v", s) - } - if s.Node != "foo" { - t.Fatalf("bad: %v", s) - } - } -} diff --git a/consul/state_store.go b/consul/state_store.go index bc0da3639..2bd0396ef 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -1327,8 +1327,11 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error return fmt.Errorf("Missing Session ID") } - // make sure we have a default set for session.Behavior - if session.Behavior == "" { + switch session.Behavior { + case structs.SessionKeysRelease, structs.SessionKeysDelete: + // we like + default: + // force SessionKeysRelease session.Behavior = structs.SessionKeysRelease } From d7e09d57bab7b6dc1b750c2a5d6c45733d91b26c Mon Sep 17 00:00:00 2001 From: Atin Malaviya Date: Thu, 20 Nov 2014 19:16:07 -0500 Subject: [PATCH 165/238] Set empty Behavior setting into SessionKeysRelease and flag error for unrecognized values --- consul/session_endpoint.go | 5 +++-- consul/state_store.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/consul/session_endpoint.go b/consul/session_endpoint.go index f7e46636f..cfe60ea57 100644 --- a/consul/session_endpoint.go +++ b/consul/session_endpoint.go @@ -31,9 +31,10 @@ func (s *Session) Apply(args *structs.SessionRequest, reply *string) error { switch args.Session.Behavior { case structs.SessionKeysRelease, structs.SessionKeysDelete: // we like it, use it - - default: + case "": args.Session.Behavior = structs.SessionKeysRelease // force default behavior + default: + return fmt.Errorf("Invalid Behavior setting '%s'", args.Session.Behavior) } // If this is a create, we must generate the Session ID. This must diff --git a/consul/state_store.go b/consul/state_store.go index 2bd0396ef..0173cff69 100644 --- a/consul/state_store.go +++ b/consul/state_store.go @@ -1330,9 +1330,10 @@ func (s *StateStore) SessionCreate(index uint64, session *structs.Session) error switch session.Behavior { case structs.SessionKeysRelease, structs.SessionKeysDelete: // we like + case "": + session.Behavior = structs.SessionKeysRelease // force default behavior default: - // force SessionKeysRelease - session.Behavior = structs.SessionKeysRelease + return fmt.Errorf("Invalid Session Behavior setting '%s'", session.Behavior) } // Assign the create index From 5badc695b822497887ff3cc40174434ba40fe0c9 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 20 Nov 2014 19:51:08 -0800 Subject: [PATCH 166/238] agent: Preserve ordering of event buffer. Fixes #479 --- command/agent/event_endpoint.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/command/agent/event_endpoint.go b/command/agent/event_endpoint.go index f1aa39e3a..b93af5ce9 100644 --- a/command/agent/event_endpoint.go +++ b/command/agent/event_endpoint.go @@ -118,16 +118,12 @@ RUN_QUERY: // Filter the events if necessary if nameFilter != "" { - n := len(events) - for i := 0; i < n; i++ { - if events[i].Name == nameFilter { - continue + for i := 0; i < len(events); i++ { + if events[i].Name != nameFilter { + events = append(events[:i], events[i+1:]...) + i-- } - events[i], events[n-1] = events[n-1], nil - i-- - n-- } - events = events[:n] } // Determine the index From 3f5e0d8ec07831fa3831e26f6219365eed42146b Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 20 Nov 2014 20:35:52 -0800 Subject: [PATCH 167/238] agent: Test event order preservation for watches --- command/agent/event_endpoint_test.go | 51 ++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/command/agent/event_endpoint_test.go b/command/agent/event_endpoint_test.go index 6740a3c57..0af98e538 100644 --- a/command/agent/event_endpoint_test.go +++ b/command/agent/event_endpoint_test.go @@ -130,14 +130,14 @@ func TestEventList_Filter(t *testing.T) { func TestEventList_Blocking(t *testing.T) { httpTest(t, func(srv *HTTPServer) { - p := &UserEvent{Name: "test"} + p := &UserEvent{Name: "foo"} if err := srv.agent.UserEvent("", p); err != nil { t.Fatalf("err: %v", err) } var index string testutil.WaitForResult(func() (bool, error) { - req, err := http.NewRequest("GET", "/v1/event/list", nil) + req, err := http.NewRequest("GET", "/v1/event/list?name=foo", nil) if err != nil { return false, err } @@ -158,14 +158,14 @@ func TestEventList_Blocking(t *testing.T) { go func() { time.Sleep(50 * time.Millisecond) - p := &UserEvent{Name: "second"} + p := &UserEvent{Name: "bar"} if err := srv.agent.UserEvent("", p); err != nil { t.Fatalf("err: %v", err) } }() testutil.WaitForResult(func() (bool, error) { - url := "/v1/event/list?index=" + index + url := "/v1/event/list?name=bar&index=" + index req, err := http.NewRequest("GET", url, nil) if err != nil { return false, err @@ -175,14 +175,53 @@ func TestEventList_Blocking(t *testing.T) { if err != nil { return false, err } - + header := resp.Header().Get("X-Consul-Index") + if header == "" || header == "0" { + return false, fmt.Errorf("bad: %#v", header) + } list, ok := obj.([]*UserEvent) if !ok { return false, fmt.Errorf("bad: %#v", obj) } - if len(list) != 2 || list[1].Name != "second" { + if len(list) != 1 || list[0].Name != "bar" { return false, fmt.Errorf("bad: %#v", list) } + index = header + return true, nil + }, func(err error) { + t.Fatalf("err: %v", err) + }) + + // Test again to make sure that the event order is preserved + // when name filtering on a list of > 1 matching event. + p = &UserEvent{Name: "bar"} + if err := srv.agent.UserEvent("", p); err != nil { + t.Fatalf("err: %v", err) + } + + testutil.WaitForResult(func() (bool, error) { + url := "/v1/event/list?name=bar&index=" + index + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + resp := httptest.NewRecorder() + obj, err := srv.EventList(resp, req) + if err != nil { + return false, err + } + header := resp.Header().Get("X-Consul-Index") + if header == "" || header == "0" { + return false, fmt.Errorf("bad: %#v", header) + } + list, ok := obj.([]*UserEvent) + if !ok { + return false, fmt.Errorf("bad: %#v", obj) + } + if len(list) != 2 || list[1].Name != "bar" || list[1].ID != p.ID { + return false, fmt.Errorf("bad: %#v", list) + } + index = header return true, nil }, func(err error) { t.Fatalf("err: %v", err) From 3d4ea8142c310e7807703ceb428f5efbcf2ccbaa Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Thu, 20 Nov 2014 20:48:51 -0800 Subject: [PATCH 168/238] agent: make event buffer test non-sequential --- command/agent/event_endpoint_test.go | 49 +++++++++++++++------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/command/agent/event_endpoint_test.go b/command/agent/event_endpoint_test.go index 0af98e538..81b384695 100644 --- a/command/agent/event_endpoint_test.go +++ b/command/agent/event_endpoint_test.go @@ -130,14 +130,14 @@ func TestEventList_Filter(t *testing.T) { func TestEventList_Blocking(t *testing.T) { httpTest(t, func(srv *HTTPServer) { - p := &UserEvent{Name: "foo"} + p := &UserEvent{Name: "test"} if err := srv.agent.UserEvent("", p); err != nil { t.Fatalf("err: %v", err) } var index string testutil.WaitForResult(func() (bool, error) { - req, err := http.NewRequest("GET", "/v1/event/list?name=foo", nil) + req, err := http.NewRequest("GET", "/v1/event/list", nil) if err != nil { return false, err } @@ -158,14 +158,14 @@ func TestEventList_Blocking(t *testing.T) { go func() { time.Sleep(50 * time.Millisecond) - p := &UserEvent{Name: "bar"} + p := &UserEvent{Name: "second"} if err := srv.agent.UserEvent("", p); err != nil { t.Fatalf("err: %v", err) } }() testutil.WaitForResult(func() (bool, error) { - url := "/v1/event/list?name=bar&index=" + index + url := "/v1/event/list?index=" + index req, err := http.NewRequest("GET", url, nil) if err != nil { return false, err @@ -175,32 +175,42 @@ func TestEventList_Blocking(t *testing.T) { if err != nil { return false, err } - header := resp.Header().Get("X-Consul-Index") - if header == "" || header == "0" { - return false, fmt.Errorf("bad: %#v", header) - } + list, ok := obj.([]*UserEvent) if !ok { return false, fmt.Errorf("bad: %#v", obj) } - if len(list) != 1 || list[0].Name != "bar" { + if len(list) != 2 || list[1].Name != "second" { return false, fmt.Errorf("bad: %#v", list) } - index = header return true, nil }, func(err error) { t.Fatalf("err: %v", err) }) + }) +} - // Test again to make sure that the event order is preserved - // when name filtering on a list of > 1 matching event. - p = &UserEvent{Name: "bar"} - if err := srv.agent.UserEvent("", p); err != nil { - t.Fatalf("err: %v", err) +func TestEventList_EventBufOrder(t *testing.T) { + httpTest(t, func(srv *HTTPServer) { + // Fire some events in a non-sequential order + expected := &UserEvent{Name: "foo"} + + for _, e := range []*UserEvent{ + &UserEvent{Name: "foo"}, + &UserEvent{Name: "bar"}, + &UserEvent{Name: "foo"}, + expected, + &UserEvent{Name: "bar"}, + } { + if err := srv.agent.UserEvent("", e); err != nil { + t.Fatalf("err: %v", err) + } } + // Test that the event order is preserved when name + // filtering on a list of > 1 matching event. testutil.WaitForResult(func() (bool, error) { - url := "/v1/event/list?name=bar&index=" + index + url := "/v1/event/list?name=foo" req, err := http.NewRequest("GET", url, nil) if err != nil { return false, err @@ -210,18 +220,13 @@ func TestEventList_Blocking(t *testing.T) { if err != nil { return false, err } - header := resp.Header().Get("X-Consul-Index") - if header == "" || header == "0" { - return false, fmt.Errorf("bad: %#v", header) - } list, ok := obj.([]*UserEvent) if !ok { return false, fmt.Errorf("bad: %#v", obj) } - if len(list) != 2 || list[1].Name != "bar" || list[1].ID != p.ID { + if len(list) != 3 || list[2].ID != expected.ID { return false, fmt.Errorf("bad: %#v", list) } - index = header return true, nil }, func(err error) { t.Fatalf("err: %v", err) From 22ad8172b8b1b14788453ab7bb505f8af66c5494 Mon Sep 17 00:00:00 2001 From: Brandon Wilson Date: Fri, 21 Nov 2014 12:27:48 -0800 Subject: [PATCH 169/238] correct the doc for obtaining formatted json was "?pretty" should be "?pretty=1" --- website/source/docs/agent/http.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/agent/http.html.markdown b/website/source/docs/agent/http.html.markdown index 0211ebae7..48f25bab7 100644 --- a/website/source/docs/agent/http.html.markdown +++ b/website/source/docs/agent/http.html.markdown @@ -86,7 +86,7 @@ leader. These can be used to gauge if a stale read should be used. ## Formatted JSON Output By default, the output of all HTTP API requests return minimized JSON with all -whitespace removed. By adding "?pretty" to the HTTP request URL, +whitespace removed. By adding "?pretty=1" to the HTTP request URL, formatted JSON will be returned. ## ACLs From 6cb25cf5737e11dfbf547a2453958ddc868de7bf Mon Sep 17 00:00:00 2001 From: Dan Sosedoff Date: Sat, 22 Nov 2014 11:44:23 -0600 Subject: [PATCH 170/238] Fix comment for commands map --- commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands.go b/commands.go index f6cf50a91..bdcb2de59 100644 --- a/commands.go +++ b/commands.go @@ -8,7 +8,7 @@ import ( "os/signal" ) -// Commands is the mapping of all the available Serf commands. +// Commands is the mapping of all the available Consul commands. var Commands map[string]cli.CommandFactory func init() { From adc5d0a522d0226c9974fe2e9432d33cd8162721 Mon Sep 17 00:00:00 2001 From: lalyos Date: Sun, 23 Nov 2014 09:16:37 +0100 Subject: [PATCH 171/238] agent: implementing reverse dns lookup for ipv4 and ipv6 --- command/agent/dns.go | 52 ++++++++++++++++++++++++ command/agent/dns_test.go | 83 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/command/agent/dns.go b/command/agent/dns.go index 63810913c..15378f58c 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -66,6 +66,9 @@ func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain s logger: log.New(logOutput, "", log.LstdFlags), } + // Register mux handler, for reverse lookup + mux.HandleFunc("arpa.", srv.handlePtr) + // Register mux handlers, always handle "consul." mux.HandleFunc(domain, srv.handleQuery) if domain != consulDomain { @@ -162,6 +165,55 @@ START: return addr.String(), nil } +// handlePtr is used to handle "reverse" DNS queries +func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { + q := req.Question[0] + defer func(s time.Time) { + d.logger.Printf("[DEBUG] dns: request for %v (%v)", q, time.Now().Sub(s)) + }(time.Now()) + + // Setup the message response + m := new(dns.Msg) + m.SetReply(req) + m.Authoritative = true + m.RecursionAvailable = (len(d.recursors) > 0) + + // Only add the SOA if requested + if req.Question[0].Qtype == dns.TypeSOA { + d.addSOA(d.domain, m) + } + + datacenter := d.agent.config.Datacenter + + // Get the QName without the domain suffix + qName := strings.ToLower(dns.Fqdn(req.Question[0].Name)) + + args := structs.DCSpecificRequest{ + Datacenter: datacenter, + QueryOptions: structs.QueryOptions{AllowStale: d.config.AllowStale}, + } + var out structs.IndexedNodes + + if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil { + for _, n := range out.Nodes { + arpa, _ := dns.ReverseAddr(n.Address) + if arpa == qName { + ptr := &dns.PTR{ + Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, + Ptr: fmt.Sprintf("%s.node.%s.consul.", n.Node, datacenter), + } + m.Answer = append(m.Answer, ptr) + break + } + } + } + + // Write out the complete response + if err := resp.WriteMsg(m); err != nil { + d.logger.Printf("[WARN] dns: failed to respond: %v", err) + } +} + // handleQUery is used to handle DNS queries in the configured domain func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { q := req.Question[0] diff --git a/command/agent/dns_test.go b/command/agent/dns_test.go index e25329497..6d1ca4f71 100644 --- a/command/agent/dns_test.go +++ b/command/agent/dns_test.go @@ -304,6 +304,89 @@ func TestDNS_NodeLookup_CNAME(t *testing.T) { } } +func TestDNS_ReverseLookup(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register node + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo2", + Address: "127.0.0.2", + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + m := new(dns.Msg) + m.SetQuestion("2.0.0.127.in-addr.arpa.", dns.TypeANY) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + ptrRec, ok := in.Answer[0].(*dns.PTR) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if ptrRec.Ptr != "foo2.node.dc1.consul." { + t.Fatalf("Bad: %#v", ptrRec) + } +} + +func TestDNS_ReverseLookup_IPV6(t *testing.T) { + dir, srv := makeDNSServer(t) + defer os.RemoveAll(dir) + defer srv.agent.Shutdown() + + testutil.WaitForLeader(t, srv.agent.RPC, "dc1") + + // Register node + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "bar", + Address: "::4242:4242", + } + + var out struct{} + if err := srv.agent.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + + m := new(dns.Msg) + m.SetQuestion("2.4.2.4.2.4.2.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.", dns.TypeANY) + + c := new(dns.Client) + addr, _ := srv.agent.config.ClientListener("", srv.agent.config.Ports.DNS) + in, _, err := c.Exchange(m, addr.String()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + ptrRec, ok := in.Answer[0].(*dns.PTR) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if ptrRec.Ptr != "bar.node.dc1.consul." { + t.Fatalf("Bad: %#v", ptrRec) + } +} func TestDNS_ServiceLookup(t *testing.T) { dir, srv := makeDNSServer(t) defer os.RemoveAll(dir) From b733df334bba6014484b30e01b2c6f95c2d85988 Mon Sep 17 00:00:00 2001 From: Janne Paenkaelae Date: Sun, 23 Nov 2014 15:42:08 +0000 Subject: [PATCH 172/238] Update to 'consul version' behaviour After e6b6f181728b88c2e430fc7ad71fe1f84db3418c if consul was built with just running "go build" the GitDescribe would be empty and consul version would be empty. This change alters the behaviour so that if consul is build without proper ldflags the version will be postfixed with "dev" prerelease to indicate that it is self compiled in a wrong way. Should someone have a bug in such a binary at least devs should easily see from the version number that binary has not been created by recommended means. --- commands.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/commands.go b/commands.go index 3a31abd73..2991cbae7 100644 --- a/commands.go +++ b/commands.go @@ -88,10 +88,20 @@ func init() { }, "version": func() (cli.Command, error) { + ver := "" + rel := "" + if len(GitDescribe) == 0 { + ver = Version + rel = "dev" + } else { + ver = GitDescribe + rel = VersionPrerelease + } + return &command.VersionCommand{ Revision: GitCommit, - Version: GitDescribe, - VersionPrerelease: VersionPrerelease, + Version: ver, + VersionPrerelease: rel, Ui: ui, }, nil }, From e3c2075a9d88f524e076b42b56b659ad9f1b6010 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 24 Nov 2014 00:36:03 -0800 Subject: [PATCH 173/238] agent: first pass at local service and check persistence --- command/agent/agent.go | 165 +++++++++++++++++++++++++++++++++++- command/agent/agent_test.go | 112 ++++++++++++++++++++++++ 2 files changed, 275 insertions(+), 2 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index bdd2197e6..54b3f9545 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -1,6 +1,7 @@ package agent import ( + "encoding/json" "fmt" "io" "log" @@ -15,6 +16,14 @@ import ( "github.com/hashicorp/serf/serf" ) +const ( + // Path to save agent service definitions + servicesDir = "services" + + // Path to save local agent checks + checksDir = "checks" +) + /* The agent is the long running process that is run on every machine. It exposes an RPC interface that is used by the CLI to control the @@ -132,6 +141,14 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { return nil, err } + // Load any persisted services and services + if err := agent.restoreServices(); err != nil { + return nil, err + } + if err := agent.restoreChecks(); err != nil { + return nil, err + } + // Start handling events go agent.handleEvents() @@ -472,6 +489,140 @@ func (a *Agent) ResumeSync() { a.state.Resume() } +// persistService saves a service definition to a JSON file in the data dir +func (a *Agent) persistService(service *structs.NodeService) error { + svcPath := filepath.Join(a.config.DataDir, servicesDir, service.ID) + if _, err := os.Stat(svcPath); os.IsNotExist(err) { + encoded, err := json.Marshal(service) + if err != nil { + return nil + } + if err := os.MkdirAll(filepath.Dir(svcPath), 0700); err != nil { + return err + } + fh, err := os.OpenFile(svcPath, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer fh.Close() + if _, err := fh.Write(encoded); err != nil { + return err + } + } + return nil +} + +// purgeService removes a persisted service definition file from the data dir +func (a *Agent) purgeService(serviceID string) error { + svcPath := filepath.Join(a.config.DataDir, servicesDir, serviceID) + if _, err := os.Stat(svcPath); err == nil { + return os.Remove(svcPath) + } + return nil +} + +// restoreServices is used to load previously persisted service definitions +// into the agent during startup. +func (a *Agent) restoreServices() error { + svcDir := filepath.Join(a.config.DataDir, servicesDir) + if _, err := os.Stat(svcDir); os.IsNotExist(err) { + return nil + } + + err := filepath.Walk(svcDir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.Name() == servicesDir { + return nil + } + fh, err := os.Open(filepath.Join(svcDir, fi.Name())) + if err != nil { + return err + } + content := make([]byte, fi.Size()) + if _, err := fh.Read(content); err != nil { + return err + } + + var svc *structs.NodeService + if err := json.Unmarshal(content, &svc); err != nil { + return err + } + + a.logger.Printf("[DEBUG] Restored service definition: %s", svc.ID) + return a.AddService(svc, nil) + }) + return err +} + +// persistCheck saves a check definition to the local agent's state directory +func (a *Agent) persistCheck(check *structs.HealthCheck) error { + checkPath := filepath.Join(a.config.DataDir, checksDir, check.CheckID) + if _, err := os.Stat(checkPath); os.IsNotExist(err) { + encoded, err := json.Marshal(check) + if err != nil { + return nil + } + if err := os.MkdirAll(filepath.Dir(checkPath), 0700); err != nil { + return err + } + fh, err := os.OpenFile(checkPath, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer fh.Close() + if _, err := fh.Write(encoded); err != nil { + return err + } + } + return nil +} + +// purgeCheck removes a persisted check definition file from the data dir +func (a *Agent) purgeCheck(checkID string) error { + checkPath := filepath.Join(a.config.DataDir, checksDir, checkID) + if _, err := os.Stat(checkPath); err == nil { + return os.Remove(checkPath) + } + return nil +} + +// restoreChecks is used to load previously persisted health check definitions +// into the agent during startup. +func (a *Agent) restoreChecks() error { + checkDir := filepath.Join(a.config.DataDir, checksDir) + if _, err := os.Stat(checkDir); os.IsNotExist(err) { + return nil + } + + err := filepath.Walk(checkDir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if fi.Name() == checksDir { + return nil + } + fh, err := os.Open(filepath.Join(checkDir, fi.Name())) + if err != nil { + return err + } + content := make([]byte, fi.Size()) + if _, err := fh.Read(content); err != nil { + return err + } + + var check *structs.HealthCheck + if err := json.Unmarshal(content, &check); err != nil { + return err + } + + a.logger.Printf("[DEBUG] Restored health check: %s", check.CheckID) + return a.AddCheck(check, nil) + }) + return err +} + // AddService is used to add a service entry. // This entry is persistent and the agent will make a best effort to // ensure it is registered @@ -489,6 +640,9 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) err // Add the service a.state.AddService(service) + // Persist the service to a file + a.persistService(service) + // Create an associated health check if chkType != nil { check := &structs.HealthCheck{ @@ -520,6 +674,11 @@ func (a *Agent) RemoveService(serviceID string) error { // Remove service immeidately a.state.RemoveService(serviceID) + // Remove the service from the data dir + if err := a.purgeService(serviceID); err != nil { + return err + } + // Deregister any associated health checks checkID := fmt.Sprintf("service:%s", serviceID) return a.RemoveCheck(checkID) @@ -580,7 +739,9 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType) error { // Add to the local state for anti-entropy a.state.AddCheck(check) - return nil + + // Persist the check + return a.persistCheck(check) } // RemoveCheck is used to remove a health check. @@ -601,7 +762,7 @@ func (a *Agent) RemoveCheck(checkID string) error { check.Stop() delete(a.checkTTLs, checkID) } - return nil + return a.purgeCheck(checkID) } // UpdateCheck is used to update the status of a check. diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 547232431..9829fedda 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -1,6 +1,8 @@ package agent import ( + "bytes" + "encoding/json" "fmt" "io" "io/ioutil" @@ -380,3 +382,113 @@ func TestAgent_ConsulService(t *testing.T) { t.Fatalf("%s service should be in sync", consul.ConsulServiceID) } } + +func TestAgent_PersistService(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + svc := &structs.NodeService{ + ID: "redis", + Service: "redis", + Tags: []string{"foo"}, + Port: 8000, + } + + if err := agent.AddService(svc, nil); err != nil { + t.Fatalf("err: %v", err) + } + + file := filepath.Join(agent.config.DataDir, servicesDir, svc.ID) + if _, err := os.Stat(file); err != nil { + t.Fatalf("err: %s", err) + } + + expected, err := json.Marshal(svc) + if err != nil { + t.Fatalf("err: %s", err) + } + content, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("err: %s", err) + } + if !bytes.Equal(expected, content) { + t.Fatalf("bad: %s", string(content)) + } + agent.Shutdown() + + // Should load it back during later start + agent2, err := Create(config, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + defer agent2.Shutdown() + + if _, ok := agent2.state.services[svc.ID]; !ok { + t.Fatalf("bad: %#v", agent2.state.services) + } + + // Should remove the service file + if err := agent2.RemoveService(svc.ID); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(file); !os.IsNotExist(err) { + t.Fatalf("err: %s", err) + } +} + +func TestAgent_PersistCheck(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + check := &structs.HealthCheck{ + Node: config.NodeName, + CheckID: "service:redis1", + Name: "redischeck", + Status: structs.HealthPassing, + ServiceID: "redis", + ServiceName: "redis", + } + + if err := agent.AddCheck(check, nil); err != nil { + t.Fatalf("err: %v", err) + } + + file := filepath.Join(agent.config.DataDir, checksDir, check.CheckID) + if _, err := os.Stat(file); err != nil { + t.Fatalf("err: %s", err) + } + + expected, err := json.Marshal(check) + if err != nil { + t.Fatalf("err: %s", err) + } + content, err := ioutil.ReadFile(file) + if err != nil { + t.Fatalf("err: %s", err) + } + if !bytes.Equal(expected, content) { + t.Fatalf("bad: %s", string(content)) + } + agent.Shutdown() + + // Should load it back during later start + agent2, err := Create(config, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + defer agent2.Shutdown() + + if _, ok := agent2.state.checks[check.CheckID]; !ok { + t.Fatalf("bad: %#v", agent2.state.checks) + } + + // Should remove the service file + if err := agent2.RemoveCheck(check.CheckID); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(file); !os.IsNotExist(err) { + t.Fatalf("err: %s", err) + } +} From ce0d27cabaaa8f8394fe51e664f103db8490b497 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 24 Nov 2014 01:15:18 -0800 Subject: [PATCH 174/238] agent: default restored checks to critical status --- command/agent/agent.go | 4 ++++ command/agent/agent_test.go | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 54b3f9545..e3e1cddb3 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -617,6 +617,10 @@ func (a *Agent) restoreChecks() error { return err } + // Default check to critical to avoid placing potentially unhealthy + // services into the active pool + check.Status = structs.HealthCritical + a.logger.Printf("[DEBUG] Restored health check: %s", check.CheckID) return a.AddCheck(check, nil) }) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 9829fedda..c875de916 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -480,9 +480,13 @@ func TestAgent_PersistCheck(t *testing.T) { } defer agent2.Shutdown() - if _, ok := agent2.state.checks[check.CheckID]; !ok { + result, ok := agent2.state.checks[check.CheckID] + if !ok { t.Fatalf("bad: %#v", agent2.state.checks) } + if result.Status != structs.HealthCritical { + t.Fatalf("bad: %#v", result) + } // Should remove the service file if err := agent2.RemoveCheck(check.CheckID); err != nil { From 1f5af52e0b1ec635ccd47cccc270726eb75fea81 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 24 Nov 2014 01:58:39 -0800 Subject: [PATCH 175/238] agent: pass error through when writing state files --- command/agent/agent.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index e3e1cddb3..82cc62f78 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -645,7 +645,9 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) err a.state.AddService(service) // Persist the service to a file - a.persistService(service) + if err := a.persistService(service); err != nil { + return err + } // Create an associated health check if chkType != nil { From 61eb2da69def078dc20841272a6c8da8b1e1dbe2 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 24 Nov 2014 11:05:11 -0800 Subject: [PATCH 176/238] Only override version pre-release if blank --- commands.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/commands.go b/commands.go index f204bd572..8f39afa10 100644 --- a/commands.go +++ b/commands.go @@ -1,11 +1,12 @@ package main import ( + "os" + "os/signal" + "github.com/hashicorp/consul/command" "github.com/hashicorp/consul/command/agent" "github.com/mitchellh/cli" - "os" - "os/signal" ) // Commands is the mapping of all the available Consul commands. @@ -94,14 +95,13 @@ func init() { }, "version": func() (cli.Command, error) { - ver := "" - rel := "" - if len(GitDescribe) == 0 { - ver = Version - rel = "dev" - } else { + ver := Version + rel := VersionPrerelease + if GitDescribe != "" { ver = GitDescribe - rel = VersionPrerelease + } + if GitDescribe == "" && rel == "" { + rel = "dev" } return &command.VersionCommand{ From 521414f09a12cbe3c04d183cbbce3e26a5185cd6 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 24 Nov 2014 11:09:04 -0800 Subject: [PATCH 177/238] agent: Adding TODO for future optimization --- command/agent/dns.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/command/agent/dns.go b/command/agent/dns.go index 15378f58c..4244f63f8 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -194,6 +194,8 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { } var out structs.IndexedNodes + // TODO: Replace ListNodes with an internal RPC that can do the filter + // server side to avoid transfering the entire node list. if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil { for _, n := range out.Nodes { arpa, _ := dns.ReverseAddr(n.Address) From fe119a1bd29cf2e80d38327f030cd8cbab822d75 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Mon, 24 Nov 2014 19:24:32 -0800 Subject: [PATCH 178/238] agent: prefer config over persisted services/checks (#497) --- command/agent/agent.go | 68 ++++++++++--- command/agent/agent_endpoint.go | 4 +- command/agent/agent_endpoint_test.go | 10 +- command/agent/agent_test.go | 140 +++++++++++++++++++++++++-- command/agent/command.go | 24 +---- 5 files changed, 193 insertions(+), 53 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 82cc62f78..0b5405da5 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -141,7 +141,27 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { return nil, err } - // Load any persisted services and services + // Register the services from config + for _, service := range config.Services { + ns := service.NodeService() + chkType := service.CheckType() + if err := agent.AddService(ns, chkType, false); err != nil { + return nil, fmt.Errorf( + "Failed to register service '%s': %v", service.Name, err) + } + } + + // Register the checks from config + for _, check := range config.Checks { + health := check.HealthCheck(config.NodeName) + chkType := &check.CheckType + if err := agent.AddCheck(health, chkType, false); err != nil { + return nil, fmt.Errorf( + "Failed to register check '%s': %v %v", check.Name, err, check) + } + } + + // Load any persisted services or checks if err := agent.restoreServices(); err != nil { return nil, err } @@ -550,8 +570,15 @@ func (a *Agent) restoreServices() error { return err } - a.logger.Printf("[DEBUG] Restored service definition: %s", svc.ID) - return a.AddService(svc, nil) + if _, ok := a.state.services[svc.ID]; ok { + // Purge previously persisted service. This allows config to be + // preferred over services persisted from the API. + a.logger.Printf("[DEBUG] Service %s exists, not restoring", svc.ID) + return a.purgeService(svc.ID) + } else { + a.logger.Printf("[DEBUG] Restored service definition: %s", svc.ID) + return a.AddService(svc, nil, false) + } }) return err } @@ -617,12 +644,19 @@ func (a *Agent) restoreChecks() error { return err } - // Default check to critical to avoid placing potentially unhealthy - // services into the active pool - check.Status = structs.HealthCritical + if _, ok := a.state.checks[check.CheckID]; ok { + // Purge previously persisted check. This allows config to be + // preferred over persisted checks from the API. + a.logger.Printf("[DEBUG] Check %s exists, not restoring", check.CheckID) + return a.purgeCheck(check.CheckID) + } else { + // Default check to critical to avoid placing potentially unhealthy + // services into the active pool + check.Status = structs.HealthCritical - a.logger.Printf("[DEBUG] Restored health check: %s", check.CheckID) - return a.AddCheck(check, nil) + a.logger.Printf("[DEBUG] Restored health check: %s", check.CheckID) + return a.AddCheck(check, nil, false) + } }) return err } @@ -630,7 +664,7 @@ func (a *Agent) restoreChecks() error { // AddService is used to add a service entry. // This entry is persistent and the agent will make a best effort to // ensure it is registered -func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) error { +func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType, persist bool) error { if service.Service == "" { return fmt.Errorf("Service name missing") } @@ -645,8 +679,10 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) err a.state.AddService(service) // Persist the service to a file - if err := a.persistService(service); err != nil { - return err + if persist { + if err := a.persistService(service); err != nil { + return err + } } // Create an associated health check @@ -660,7 +696,7 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType) err ServiceID: service.ID, ServiceName: service.Service, } - if err := a.AddCheck(check, chkType); err != nil { + if err := a.AddCheck(check, chkType, persist); err != nil { return err } } @@ -694,7 +730,7 @@ func (a *Agent) RemoveService(serviceID string) error { // This entry is persistent and the agent will make a best effort to // ensure it is registered. The Check may include a CheckType which // is used to automatically update the check status -func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType) error { +func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType, persist bool) error { if check.CheckID == "" { return fmt.Errorf("CheckID missing") } @@ -747,7 +783,11 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType) error { a.state.AddCheck(check) // Persist the check - return a.persistCheck(check) + if persist { + return a.persistCheck(check) + } + + return nil } // RemoveCheck is used to remove a health check. diff --git a/command/agent/agent_endpoint.go b/command/agent/agent_endpoint.go index 2edee0c09..de3c24157 100644 --- a/command/agent/agent_endpoint.go +++ b/command/agent/agent_endpoint.go @@ -97,7 +97,7 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ } // Add the check - return nil, s.agent.AddCheck(health, chkType) + return nil, s.agent.AddCheck(health, chkType, true) } func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -169,7 +169,7 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re } // Add the check - return nil, s.agent.AddService(ns, chkType) + return nil, s.agent.AddService(ns, chkType, true) } func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { diff --git a/command/agent/agent_endpoint_test.go b/command/agent/agent_endpoint_test.go index 798fd4a1d..1b266347e 100644 --- a/command/agent/agent_endpoint_test.go +++ b/command/agent/agent_endpoint_test.go @@ -288,7 +288,7 @@ func TestHTTPAgentDeregisterCheck(t *testing.T) { defer srv.agent.Shutdown() chk := &structs.HealthCheck{Name: "test", CheckID: "test"} - if err := srv.agent.AddCheck(chk, nil); err != nil { + if err := srv.agent.AddCheck(chk, nil, false); err != nil { t.Fatalf("err: %v", err) } @@ -320,7 +320,7 @@ func TestHTTPAgentPassCheck(t *testing.T) { chk := &structs.HealthCheck{Name: "test", CheckID: "test"} chkType := &CheckType{TTL: 15 * time.Second} - if err := srv.agent.AddCheck(chk, chkType); err != nil { + if err := srv.agent.AddCheck(chk, chkType, false); err != nil { t.Fatalf("err: %v", err) } @@ -353,7 +353,7 @@ func TestHTTPAgentWarnCheck(t *testing.T) { chk := &structs.HealthCheck{Name: "test", CheckID: "test"} chkType := &CheckType{TTL: 15 * time.Second} - if err := srv.agent.AddCheck(chk, chkType); err != nil { + if err := srv.agent.AddCheck(chk, chkType, false); err != nil { t.Fatalf("err: %v", err) } @@ -386,7 +386,7 @@ func TestHTTPAgentFailCheck(t *testing.T) { chk := &structs.HealthCheck{Name: "test", CheckID: "test"} chkType := &CheckType{TTL: 15 * time.Second} - if err := srv.agent.AddCheck(chk, chkType); err != nil { + if err := srv.agent.AddCheck(chk, chkType, false); err != nil { t.Fatalf("err: %v", err) } @@ -465,7 +465,7 @@ func TestHTTPAgentDeregisterService(t *testing.T) { ID: "test", Service: "test", } - if err := srv.agent.AddService(service, nil); err != nil { + if err := srv.agent.AddService(service, nil, false); err != nil { t.Fatalf("err: %v", err) } diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index c875de916..5200df3a8 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "os" "path/filepath" + "reflect" "sync/atomic" "testing" "time" @@ -148,7 +149,7 @@ func TestAgent_AddService(t *testing.T) { TTL: time.Minute, Notes: "redis health check", } - err := agent.AddService(srv, chk) + err := agent.AddService(srv, chk, false) if err != nil { t.Fatalf("err: %v", err) } @@ -195,7 +196,7 @@ func TestAgent_RemoveService(t *testing.T) { Port: 8000, } chk := &CheckType{TTL: time.Minute} - if err := agent.AddService(srv, chk); err != nil { + if err := agent.AddService(srv, chk, false); err != nil { t.Fatalf("err: %v", err) } @@ -235,7 +236,7 @@ func TestAgent_AddCheck(t *testing.T) { Script: "exit 0", Interval: 15 * time.Second, } - err := agent.AddCheck(health, chk) + err := agent.AddCheck(health, chk, false) if err != nil { t.Fatalf("err: %v", err) } @@ -266,7 +267,7 @@ func TestAgent_AddCheck_MinInterval(t *testing.T) { Script: "exit 0", Interval: time.Microsecond, } - err := agent.AddCheck(health, chk) + err := agent.AddCheck(health, chk, false) if err != nil { t.Fatalf("err: %v", err) } @@ -304,7 +305,7 @@ func TestAgent_RemoveCheck(t *testing.T) { Script: "exit 0", Interval: 15 * time.Second, } - err := agent.AddCheck(health, chk) + err := agent.AddCheck(health, chk, false) if err != nil { t.Fatalf("err: %v", err) } @@ -339,7 +340,7 @@ func TestAgent_UpdateCheck(t *testing.T) { chk := &CheckType{ TTL: 15 * time.Second, } - err := agent.AddCheck(health, chk) + err := agent.AddCheck(health, chk, false) if err != nil { t.Fatalf("err: %v", err) } @@ -395,11 +396,21 @@ func TestAgent_PersistService(t *testing.T) { Port: 8000, } - if err := agent.AddService(svc, nil); err != nil { + file := filepath.Join(agent.config.DataDir, servicesDir, svc.ID) + + // Check is not persisted unless requested + if err := agent.AddService(svc, nil, false); err != nil { + t.Fatalf("err: %v", err) + } + if _, err := os.Stat(file); err == nil { + t.Fatalf("should not persist") + } + + // Persists to file if requested + if err := agent.AddService(svc, nil, true); err != nil { t.Fatalf("err: %v", err) } - file := filepath.Join(agent.config.DataDir, servicesDir, svc.ID) if _, err := os.Stat(file); err != nil { t.Fatalf("err: %s", err) } @@ -437,6 +448,53 @@ func TestAgent_PersistService(t *testing.T) { } } +func TestAgent_PurgeServiceOnDuplicate(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + svc1 := &structs.NodeService{ + ID: "redis", + Service: "redis", + Tags: []string{"foo"}, + Port: 8000, + } + + // First persist the service + if err := agent.AddService(svc1, nil, true); err != nil { + t.Fatalf("err: %v", err) + } + agent.Shutdown() + + // Try bringing the agent back up with the service already + // existing in the config + svc2 := &ServiceDefinition{ + ID: "redis", + Name: "redis", + Tags: []string{"bar"}, + Port: 9000, + } + + config.Services = []*ServiceDefinition{svc2} + agent2, err := Create(config, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + defer agent2.Shutdown() + + file := filepath.Join(agent.config.DataDir, servicesDir, svc1.ID) + if _, err := os.Stat(file); err == nil { + t.Fatalf("should have removed persisted service") + } + result, ok := agent2.state.services[svc2.ID] + if !ok { + t.Fatalf("missing service registration") + } + if !reflect.DeepEqual(result.Tags, svc2.Tags) || result.Port != svc2.Port { + t.Fatalf("bad: %#v", result) + } +} + func TestAgent_PersistCheck(t *testing.T) { config := nextConfig() dir, agent := makeAgent(t, config) @@ -451,11 +509,21 @@ func TestAgent_PersistCheck(t *testing.T) { ServiceName: "redis", } - if err := agent.AddCheck(check, nil); err != nil { + file := filepath.Join(agent.config.DataDir, checksDir, check.CheckID) + + // Not persisted if not requested + if err := agent.AddCheck(check, nil, false); err != nil { + t.Fatalf("err: %v", err) + } + if _, err := os.Stat(file); err == nil { + t.Fatalf("should not persist") + } + + // Should persist if requested + if err := agent.AddCheck(check, nil, true); err != nil { t.Fatalf("err: %v", err) } - file := filepath.Join(agent.config.DataDir, checksDir, check.CheckID) if _, err := os.Stat(file); err != nil { t.Fatalf("err: %s", err) } @@ -496,3 +564,55 @@ func TestAgent_PersistCheck(t *testing.T) { t.Fatalf("err: %s", err) } } + +func TestAgent_PurgeCheckOnDuplicate(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + check1 := &structs.HealthCheck{ + Node: config.NodeName, + CheckID: "service:redis1", + Name: "redischeck", + Status: structs.HealthPassing, + ServiceID: "redis", + ServiceName: "redis", + } + + // First persist the check + if err := agent.AddCheck(check1, nil, true); err != nil { + t.Fatalf("err: %v", err) + } + agent.Shutdown() + + // Start again with the check registered in config + check2 := &CheckDefinition{ + ID: "service:redis1", + Name: "redischeck", + Notes: "my cool notes", + CheckType: CheckType{ + Script: "/bin/check-redis.py", + Interval: 30 * time.Second, + }, + } + + config.Checks = []*CheckDefinition{check2} + agent2, err := Create(config, nil) + if err != nil { + t.Fatalf("err: %s", err) + } + defer agent2.Shutdown() + + file := filepath.Join(agent.config.DataDir, checksDir, check1.CheckID) + if _, err := os.Stat(file); err == nil { + t.Fatalf("should have removed persisted check") + } + result, ok := agent2.state.checks[check2.ID] + if !ok { + t.Fatalf("missing check registration") + } + expected := check2.HealthCheck(config.NodeName) + if !reflect.DeepEqual(expected, result) { + t.Fatalf("bad: %#v", result) + } +} diff --git a/command/agent/command.go b/command/agent/command.go index a62dbdd67..3e1f17bf1 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -574,26 +574,6 @@ func (c *Command) Run(args []string) int { return 1 } - // Register the services - for _, service := range config.Services { - ns := service.NodeService() - chkType := service.CheckType() - if err := c.agent.AddService(ns, chkType); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to register service '%s': %v", service.Name, err)) - return 1 - } - } - - // Register the checks - for _, check := range config.Checks { - health := check.HealthCheck(config.NodeName) - chkType := &check.CheckType - if err := c.agent.AddCheck(health, chkType); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to register check '%s': %v %v", check.Name, err, check)) - return 1 - } - } - // Get the new client http listener addr httpAddr, err := config.ClientListenerAddr(config.Addresses.HTTP, config.Ports.HTTP) if err != nil { @@ -758,7 +738,7 @@ func (c *Command) handleReload(config *Config) *Config { for _, service := range newConf.Services { ns := service.NodeService() chkType := service.CheckType() - if err := c.agent.AddService(ns, chkType); err != nil { + if err := c.agent.AddService(ns, chkType, false); err != nil { c.Ui.Error(fmt.Sprintf("Failed to register service '%s': %v", service.Name, err)) } } @@ -767,7 +747,7 @@ func (c *Command) handleReload(config *Config) *Config { for _, check := range newConf.Checks { health := check.HealthCheck(config.NodeName) chkType := &check.CheckType - if err := c.agent.AddCheck(health, chkType); err != nil { + if err := c.agent.AddCheck(health, chkType, false); err != nil { c.Ui.Error(fmt.Sprintf("Failed to register check '%s': %v %v", check.Name, err, check)) } } From 50fb9a9472c7c16fdf3fb03b6a9b5853f00a4aea Mon Sep 17 00:00:00 2001 From: Jack Pearkes Date: Tue, 25 Nov 2014 11:02:49 -0500 Subject: [PATCH 179/238] website: load fonts over https --- website/source/assets/stylesheets/application.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/assets/stylesheets/application.scss b/website/source/assets/stylesheets/application.scss index 7191da1c0..162c5f376 100644 --- a/website/source/assets/stylesheets/application.scss +++ b/website/source/assets/stylesheets/application.scss @@ -3,7 +3,7 @@ @import "bootstrap"; // Remote fonts -@import url("http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,700"); +@import url("//fonts.googleapis.com/css?family=Source+Sans+Pro:400,700"); // Core variables and mixins @import "_variables"; From 73504a01e98ed11499bc9809fb327615a2500e89 Mon Sep 17 00:00:00 2001 From: Ali Abbas Date: Tue, 25 Nov 2014 19:54:30 +0100 Subject: [PATCH 180/238] cleanup unreachable code --- command/agent/dns.go | 1 - command/agent/kvs_endpoint.go | 4 ++-- command/exec.go | 1 - consul/fsm.go | 4 ---- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/command/agent/dns.go b/command/agent/dns.go index 4244f63f8..43ea245df 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -139,7 +139,6 @@ func NewDNSServer(agent *Agent, config *DNSConfig, logOutput io.Writer, domain s case <-time.After(time.Second): return srv, fmt.Errorf("timeout setting up DNS server") } - return srv, nil } // recursorAddr is used to add a port to the recursor if omitted. diff --git a/command/agent/kvs_endpoint.go b/command/agent/kvs_endpoint.go index 48d9ce19d..ffed10f9c 100644 --- a/command/agent/kvs_endpoint.go +++ b/command/agent/kvs_endpoint.go @@ -3,11 +3,12 @@ package agent import ( "bytes" "fmt" - "github.com/hashicorp/consul/consul/structs" "io" "net/http" "strconv" "strings" + + "github.com/hashicorp/consul/consul/structs" ) const ( @@ -50,7 +51,6 @@ func (s *HTTPServer) KVSEndpoint(resp http.ResponseWriter, req *http.Request) (i resp.WriteHeader(405) return nil, nil } - return nil, nil } // KVSGet handles a GET request diff --git a/command/exec.go b/command/exec.go index 996bbd5b2..8e266f4a3 100644 --- a/command/exec.go +++ b/command/exec.go @@ -384,7 +384,6 @@ func (c *ExecCommand) streamResults(doneCh chan struct{}, ackCh chan rExecAck, h } } } - return ERR_EXIT: select { diff --git a/consul/fsm.go b/consul/fsm.go index 2ce6719a4..a6b7c3bb5 100644 --- a/consul/fsm.go +++ b/consul/fsm.go @@ -167,7 +167,6 @@ func (c *consulFSM) applyKVSOperation(buf []byte, index uint64) interface{} { c.logger.Printf("[WARN] consul.fsm: Invalid KVS operation '%s'", req.Op) return fmt.Errorf("Invalid KVS operation '%s'", req.Op) } - return nil } func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{} { @@ -188,7 +187,6 @@ func (c *consulFSM) applySessionOperation(buf []byte, index uint64) interface{} c.logger.Printf("[WARN] consul.fsm: Invalid Session operation '%s'", req.Op) return fmt.Errorf("Invalid Session operation '%s'", req.Op) } - return nil } func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} { @@ -211,7 +209,6 @@ func (c *consulFSM) applyACLOperation(buf []byte, index uint64) interface{} { c.logger.Printf("[WARN] consul.fsm: Invalid ACL operation '%s'", req.Op) return fmt.Errorf("Invalid ACL operation '%s'", req.Op) } - return nil } func (c *consulFSM) Snapshot() (raft.FSMSnapshot, error) { @@ -443,7 +440,6 @@ func (s *consulSnapshot) persistKV(sink raft.SnapshotSink, return err } } - return nil } func (s *consulSnapshot) Release() { From ecac719bb8f02b6d39204125b5c4224b4172dd2b Mon Sep 17 00:00:00 2001 From: Ali Abbas Date: Tue, 25 Nov 2014 20:06:33 +0100 Subject: [PATCH 181/238] fix Sprintf formatting --- command/agent/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/agent/command.go b/command/agent/command.go index 3e1f17bf1..03e92e11e 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -310,7 +310,7 @@ func (c *Command) setupAgent(config *Config, logOutput io.Writer, logWriter *log servers, err := NewHTTPServers(agent, config, logOutput) if err != nil { agent.Shutdown() - c.Ui.Error(fmt.Sprintf("Error starting http servers:", err)) + c.Ui.Error(fmt.Sprintf("Error starting http servers: %s", err)) return err } c.httpServers = servers From 58ba650cb0545a8e58fdac70d2919bbb79b76100 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 25 Nov 2014 19:03:14 -0800 Subject: [PATCH 182/238] website: Improve docs for reloading config. Fixes #407 --- website/source/docs/agent/options.html.markdown | 17 +++++++++++++++-- .../source/docs/commands/reload.html.markdown | 6 ++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index ece3157a3..980d3cd90 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -22,8 +22,10 @@ The exact merging behavior will be specified. Consul also supports reloading of configuration when it receives the SIGHUP signal. Not all changes are respected, but those that are -are documented below. The [reload command](/docs/commands/reload.html) -can also be used to trigger a configuration reload. +are documented below in the +[Reloadable Configuration](#reloadable-configuration) section. The +[reload command](/docs/commands/reload.html) can also be used to trigger a +configuration reload. ## Command-line Options @@ -439,3 +441,14 @@ port. * DNS Interface (Default 8600). Used to resolve DNS queries. TCP and UDP. +## Reloadable Configuration + + +Reloading configuration does not reload all configuration items. The +items which are reloaded include: + +* Log level +* Checks +* Services +* Watches +* HTTP Client Address diff --git a/website/source/docs/commands/reload.html.markdown b/website/source/docs/commands/reload.html.markdown index ca1e5aa4d..b00a0120a 100644 --- a/website/source/docs/commands/reload.html.markdown +++ b/website/source/docs/commands/reload.html.markdown @@ -19,6 +19,12 @@ This command operates the same as the signal, meaning that it will trigger a reload, but does not wait for the reload to complete. Any errors with the reload will be present in the agent logs and not in the output of this command. +**NOTE** + +Not all configuration options are reloadable. See the +[Reloadable Configuration](/docs/agent/options.html#reloadable-configuration) +section on the agent options page for details on which options are supported. + ## Usage Usage: `consul reload` From 818fc22c9f5a0a49643ecd380165bf5f075dcb65 Mon Sep 17 00:00:00 2001 From: Ali Abbas Date: Wed, 26 Nov 2014 10:25:37 +0100 Subject: [PATCH 183/238] * Fix race condition on read/write of shutdown bool variable of server and connection pool. * In connection pool, there is no guarantee that .reap() cannot execute the same time as .Shutdown() is called. It also did not benefit to eval shutdown when a select is run on the shutdown channel. * In server, same principle applies to handleConsulConn. Since we also have a shutdown channel, it makes more to use this than to loop on a bool variable. --- consul/pool.go | 4 ++-- consul/rpc.go | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/consul/pool.go b/consul/pool.go index 4ab75fbc6..495ab2bd5 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -358,12 +358,12 @@ func (p *ConnPool) RPC(addr net.Addr, version int, method string, args interface // Reap is used to close conns open over maxTime func (p *ConnPool) reap() { - for !p.shutdown { + for { // Sleep for a while select { - case <-time.After(time.Second): case <-p.shutdownCh: return + case <-time.After(time.Second): } // Reap all old conns diff --git a/consul/rpc.go b/consul/rpc.go index 8956b0d0f..c98ebb60c 100644 --- a/consul/rpc.go +++ b/consul/rpc.go @@ -3,16 +3,17 @@ package consul import ( "crypto/tls" "fmt" - "github.com/armon/go-metrics" - "github.com/hashicorp/consul/consul/structs" - "github.com/hashicorp/go-msgpack/codec" - "github.com/hashicorp/yamux" - "github.com/inconshreveable/muxado" "io" "math/rand" "net" "strings" "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/go-msgpack/codec" + "github.com/hashicorp/yamux" + "github.com/inconshreveable/muxado" ) type RPCType byte @@ -149,7 +150,13 @@ func (s *Server) handleMultiplexV2(conn net.Conn) { func (s *Server) handleConsulConn(conn net.Conn) { defer conn.Close() rpcCodec := codec.GoRpc.ServerCodec(conn, msgpackHandle) - for !s.shutdown { + for { + select { + case <-s.shutdownCh: + return + default: + } + if err := s.rpcServer.ServeRequest(rpcCodec); err != nil { if err != io.EOF && !strings.Contains(err.Error(), "closed") { s.logger.Printf("[ERR] consul.rpc: RPC error: %v (%v)", err, conn) From 7d75e921041529f69bb214f975949649bb5fe289 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 11:37:28 +0000 Subject: [PATCH 184/238] docs: intro/agent: minor fixes --- website/source/intro/getting-started/agent.html.markdown | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/source/intro/getting-started/agent.html.markdown b/website/source/intro/getting-started/agent.html.markdown index cb3508e48..27dced736 100644 --- a/website/source/intro/getting-started/agent.html.markdown +++ b/website/source/intro/getting-started/agent.html.markdown @@ -8,8 +8,8 @@ description: |- # Run the Consul Agent -After Consul is installed, the agent must be run. The agent can either run -in a server or client mode. Each datacenter must have at least one server, +After Consul is installed, the agent must be run. The agent can run either +in server or client mode. Each datacenter must have at least one server, although 3 or 5 is recommended. A single server deployment is _**highly**_ discouraged as data loss is inevitable in a failure scenario. [This guide](/docs/guides/bootstrapping.html) covers bootstrapping a new datacenter. All other agents run in client mode, which @@ -114,10 +114,11 @@ By gracefully leaving, Consul notifies other cluster members that the node _left_. If you had forcibly killed the agent process, other members of the cluster would have detected that the node _failed_. When a member leaves, its services and checks are removed from the catalog. When a member fails, -its health is simply marked as critical, but is not removed from the catalog. +its health is simply marked as critical, but it is not removed from the catalog. Consul will automatically try to reconnect to _failed_ nodes, which allows it to recover from certain network conditions, while _left_ nodes are no longer contacted. Additionally, if an agent is operating as a server, a graceful leave is important to avoid causing a potential availability outage affecting the [consensus protocol](/docs/internals/consensus.html). -See the [guides section](/docs/guides/index.html) to safely add and remove servers. +See the [guides section](/docs/guides/index.html) for how to safely add +and remove servers. From 2eca9b018240cc7063d2e8433525fa5cc33cb22e Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 11:42:02 +0000 Subject: [PATCH 185/238] docs: intro/services: minor fixes --- website/source/intro/getting-started/services.html.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/source/intro/getting-started/services.html.markdown b/website/source/intro/getting-started/services.html.markdown index 9b5e212bc..a2839af5a 100644 --- a/website/source/intro/getting-started/services.html.markdown +++ b/website/source/intro/getting-started/services.html.markdown @@ -67,9 +67,9 @@ service using either the DNS or HTTP API. Let's first query it using the DNS API. For the DNS API, the DNS name for services is `NAME.service.consul`. All DNS names are always in the -`consul` namespace. The `service` subdomain on that tells Consul we're -querying services, and the `NAME` is the name of the service. For the -web service we registered, that would be `web.service.consul`: +`consul` namespace. The `service` subdomain tells Consul we're querying +services, and the `NAME` is the name of the service. For the web service +we registered, that would be `web.service.consul`: ```text $ dig @127.0.0.1 -p 8600 web.service.consul From 0b4e99dfe524e0c36696e59630ca2a3f89b503d2 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 11:49:39 +0000 Subject: [PATCH 186/238] docs: intro/join: minor fixes --- .../intro/getting-started/join.html.markdown | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/website/source/intro/getting-started/join.html.markdown b/website/source/intro/getting-started/join.html.markdown index bf0f4bacf..eed68b475 100644 --- a/website/source/intro/getting-started/join.html.markdown +++ b/website/source/intro/getting-started/join.html.markdown @@ -14,12 +14,12 @@ Consul, but didn't show how this could be extended to a scalable production service discovery infrastructure. On this page, we'll create our first real cluster with multiple members. -When starting a Consul agent, it begins without knowledge of any other node, and is -an isolated cluster of one. To learn about other cluster members, the agent must -_join_ an existing cluster. To join an existing cluster, it only needs to know -about a _single_ existing member. After it joins, the agent will gossip with this -member and quickly discover the other members in the cluster. A Consul -agent can join any other agent, it doesn't have to be an agent in server mode. +When a Consul agent is started, it begins without knowledge of any other node, +and is an isolated cluster of one. To learn about other cluster members, the +agent must _join_ an existing cluster. To join an existing cluster, it only +needs to know about a _single_ existing member. After it joins, the agent will +gossip with this member and quickly discover the other members in the cluster. +A Consul agent can join any other agent, not just agents in server mode. ## Starting the Agents @@ -78,7 +78,7 @@ agent-one 172.20.20.10:8301 alive role=consul,dc=dc1,vsn=2,vsn_min=1,vsn_m agent-two 172.20.20.11:8301 alive role=node,dc=dc1,vsn=2,vsn_min=1,vsn_max=2 ``` --> **Remember:** To join a cluster, a Consul agent needs to only +-> **Remember:** To join a cluster, a Consul agent only needs to learn about one existing member. After joining the cluster, the agents gossip with each other to propagate full membership information. From a6420171e5273c94891faa3b964256243b551721 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 11:55:42 +0000 Subject: [PATCH 187/238] docs: intro/checks: minor fixes --- website/source/intro/getting-started/checks.html.markdown | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/source/intro/getting-started/checks.html.markdown b/website/source/intro/getting-started/checks.html.markdown index a072c2a64..e4f8210b1 100644 --- a/website/source/intro/getting-started/checks.html.markdown +++ b/website/source/intro/getting-started/checks.html.markdown @@ -19,9 +19,8 @@ two node cluster running. ## Defining Checks Similarly to a service, a check can be registered either by providing a -[check definition](/docs/agent/checks.html) -, or by making the appropriate calls to the -[HTTP API](/docs/agent/http.html). +[check definition](/docs/agent/checks.html), or by making the +appropriate calls to the [HTTP API](/docs/agent/http.html). We will use the check definition, because just like services, definitions are the most common way to setup checks. From e18b3859f1f011669edf43709d200e06884b479f Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 12:00:49 +0000 Subject: [PATCH 188/238] docs: intro/kv: minor fixes --- website/source/intro/getting-started/kv.html.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/source/intro/getting-started/kv.html.markdown b/website/source/intro/getting-started/kv.html.markdown index 7c2249ce8..e2ecd7a6c 100644 --- a/website/source/intro/getting-started/kv.html.markdown +++ b/website/source/intro/getting-started/kv.html.markdown @@ -62,7 +62,7 @@ $ curl http://localhost:8500/v1/kv/?recurse Here we have created 3 keys, each with the value of "test". Note that the `Value` field returned is base64 encoded to allow non-UTF8 characters. For the "web/key2" key, we set a `flag` value of 42. All keys -support setting a 64bit integer flag value. This is opaque to Consul but can +support setting a 64-bit integer flag value. This is opaque to Consul but can be used by clients for any purpose. After setting the values, we then issued a GET request to retrieve multiple @@ -115,5 +115,5 @@ returning the current, unchanged value. This can be used to efficiently wait for key modifications. Additionally, this same technique can be used to wait for a list of keys, waiting only until any of the keys has a newer modification time. -This is only a few example of what the API supports. For full documentation, please -reference the [HTTP API](/docs/agent/http.html). +These are only a few examples of what the API supports. For full +documentation, please see the [HTTP API](/docs/agent/http.html). From 7ff0acfb65dd6a3a5c47a04cc56b59004a25bb2c Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 12:09:36 +0000 Subject: [PATCH 189/238] docs: compatibility: minor fixes --- website/source/docs/compatibility.html.markdown | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/source/docs/compatibility.html.markdown b/website/source/docs/compatibility.html.markdown index bc6aca139..1d5516568 100644 --- a/website/source/docs/compatibility.html.markdown +++ b/website/source/docs/compatibility.html.markdown @@ -10,8 +10,8 @@ description: |- We expect Consul to run in large clusters as long-running agents. Because upgrading agents in this sort of environment relies heavily on protocol -compatibility, this page makes it clear on our promise to keeping different -Consul versions compatible with each other. +compatibility, this page makes clear our promise to keep different Consul +versions compatible with each other. We promise that every subsequent release of Consul will remain backwards compatible with _at least_ one prior version. Concretely: version 0.5 can From dbb365bacae47a5dabd301b4c1b262ff495c03e3 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 12:31:38 +0000 Subject: [PATCH 190/238] docs: internals/architecture: minor fixes --- .../docs/internals/architecture.html.markdown | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/website/source/docs/internals/architecture.html.markdown b/website/source/docs/internals/architecture.html.markdown index ff0cb4669..068bfc0f5 100644 --- a/website/source/docs/internals/architecture.html.markdown +++ b/website/source/docs/internals/architecture.html.markdown @@ -23,20 +23,20 @@ Before describing the architecture, we provide a glossary of terms to help clarify what is being discussed: * Agent - An agent is the long running daemon on every member of the Consul cluster. -It is started by running `consul agent`. The agent is able to run in either *client*, +It is started by running `consul agent`. The agent is able to run in either *client* or *server* mode. Since all nodes must be running an agent, it is simpler to refer to -the node as either being a client or server, but there are other instances of the agent. All +the node as being either a client or server, but there are other instances of the agent. All agents can run the DNS or HTTP interfaces, and are responsible for running checks and keeping services in sync. * Client - A client is an agent that forwards all RPCs to a server. The client is relatively -stateless. The only background activity a client performs is taking part of LAN gossip pool. -This has a minimal resource overhead and consumes only a small amount of network bandwidth. +stateless. The only background activity a client performs is taking part in the LAN gossip +pool. This has a minimal resource overhead and consumes only a small amount of network +bandwidth. -* Server - An agent that is server mode. When in server mode, there is an expanded set -of responsibilities including participating in the Raft quorum, maintaining cluster state, -responding to RPC queries, WAN gossip to other datacenters, and forwarding queries to leaders -or remote datacenters. +* Server - A server is an agent with an expanded set of responsibilities including +participating in the Raft quorum, maintaining cluster state, responding to RPC queries, +WAN gossip to other datacenters, and forwarding queries to leaders or remote datacenters. * Datacenter - A datacenter seems obvious, but there are subtle details such as multiple availability zones in EC2. We define a datacenter to be a networking environment that is @@ -47,13 +47,13 @@ the public internet. the elected leader as well as agreement on the ordering of transactions. Since these transactions are applied to a FSM, we implicitly include the consistency of a replicated state machine. Consensus is described in more detail on [Wikipedia](http://en.wikipedia.org/wiki/Consensus_(computer_science)), -as well as our [implementation here](/docs/internals/consensus.html). +and our implementation is described [here](/docs/internals/consensus.html). * Gossip - Consul is built on top of [Serf](http://www.serfdom.io/), which provides a full [gossip protocol](http://en.wikipedia.org/wiki/Gossip_protocol) that is used for multiple purposes. Serf provides membership, failure detection, and event broadcast mechanisms. Our use of these is described more in the [gossip documentation](/docs/internals/gossip.html). It is enough to know -gossip involves random node-to-node communication, primarily over UDP. +that gossip involves random node-to-node communication, primarily over UDP. * LAN Gossip - Refers to the LAN gossip pool, which contains nodes that are all located on the same local area network or datacenter. @@ -62,8 +62,8 @@ located on the same local area network or datacenter. servers are primarily located in different datacenters and typically communicate over the internet or wide area network. -* RPC - RPC is short for a Remote Procedure Call. This is a request / response mechanism -allowing a client to make a request from a server. +* RPC - Remote Procedure Call. This is a request / response mechanism allowing a +client to make a request of a server. ## 10,000 foot view @@ -73,7 +73,7 @@ From a 10,000 foot altitude the architecture of Consul looks like this: ![Consul Architecture](consul-arch.png) -Lets break down this image and describe each piece. First of all we can see +Let's break down this image and describe each piece. First of all we can see that there are two datacenters, one and two respectively. Consul has first class support for multiple datacenters and expects this to be the common case. @@ -85,9 +85,9 @@ and they can easily scale into the thousands or tens of thousands. All the nodes that are in a datacenter participate in a [gossip protocol](/docs/internals/gossip.html). This means there is a gossip pool that contains all the nodes for a given datacenter. This serves -a few purposes: first, there is no need to configure clients with the addresses of servers, +a few purposes: first, there is no need to configure clients with the addresses of servers; discovery is done automatically. Second, the work of detecting node failures -is not placed on the servers but is distributed. This makes the failure detection much more +is not placed on the servers but is distributed. This makes failure detection much more scalable than naive heartbeating schemes. Thirdly, it is used as a messaging layer to notify when important events such as leader election take place. @@ -97,8 +97,8 @@ processing all queries and transactions. Transactions must also be replicated to as part of the [consensus protocol](/docs/internals/consensus.html). Because of this requirement, when a non-leader server receives an RPC request it forwards it to the cluster leader. -The server nodes also operate as part of a WAN gossip. This pool is different from the LAN pool, -as it is optimized for the higher latency of the internet, and is expected to only contain +The server nodes also operate as part of a WAN gossip pool. This pool is different from the LAN pool, +as it is optimized for the higher latency of the internet, and is expected to contain only other Consul server nodes. The purpose of this pool is to allow datacenters to discover each other in a low touch manner. Bringing a new datacenter online is as easy as joining the existing WAN gossip. Because the servers are all operating in this pool, it also enables cross-datacenter requests. @@ -110,8 +110,8 @@ connection caching and multiplexing, cross-datacenter requests are relatively fa ## Getting in depth -At this point we've covered the high level architecture of Consul, but there are much -more details to each of the sub-systems. The [consensus protocol](/docs/internals/consensus.html) is +At this point we've covered the high level architecture of Consul, but there are many +more details for each of the sub-systems. The [consensus protocol](/docs/internals/consensus.html) is documented in detail, as is the [gossip protocol](/docs/internals/gossip.html). The [documentation](/docs/internals/security.html) for the security model and protocols used are also available. From 2ad884abeec91a2b28faa0962f0bf38803323461 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 12:49:53 +0000 Subject: [PATCH 191/238] docs: internals/sessions: minor fixes --- website/source/docs/internals/sessions.html.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/source/docs/internals/sessions.html.markdown b/website/source/docs/internals/sessions.html.markdown index 62f451e64..7daea5ae8 100644 --- a/website/source/docs/internals/sessions.html.markdown +++ b/website/source/docs/internals/sessions.html.markdown @@ -64,9 +64,9 @@ systems to be built that require an operator to intervene in the case of a failure, but preclude the possibility of a split-brain. The final nuance is that sessions may provide a `lock-delay`. This -is a time duration, between 0 and 60 second. When a session invalidation +is a time duration, between 0 and 60 seconds. When a session invalidation takes place, Consul prevents any of the previously held locks from -being re-acquired for the `lock-delay` interval; this is a safe guard +being re-acquired for the `lock-delay` interval; this is a safeguard inspired by Google's Chubby. The purpose of this delay is to allow the potentially still live leader to detect the invalidation and stop processing requests that may lead to inconsistent state. While not a @@ -79,7 +79,7 @@ mechanism by providing a zero delay value. Integration between the Key/Value store and sessions are the primary place where sessions are used. A session must be created prior to use, -and is then referred to by it's ID. +and is then referred to by its ID. The Key/Value API is extended to support an `acquire` and `release` operation. The `acquire` operation acts like a Check-And-Set operation, except it @@ -93,7 +93,7 @@ since the request will fail if given an invalid session. A critical note is that the lock can be released without being the creator of the session. This is by design, as it allows operators to intervene and force terminate a session if necessary. As mentioned above, a session invalidation will also -cause all held locks to be released. When a lock is released, the `LockIndex`, +cause all held locks to be released. When a lock is released, the `LockIndex` does not change, however the `Session` is cleared and the `ModifyIndex` increments. These semantics (heavily borrowed from Chubby), allow the tuple of (Key, LockIndex, Session) @@ -103,7 +103,7 @@ is incremented on each `acquire`, even if the same session re-acquires a lock, the `sequencer` will be able to detect a stale request. Similarly, if a session is invalided, the Session corresponding to the given `LockIndex` will be blank. -To make clear, this locking system is purely *advisory*. There is no enforcement +To be clear, this locking system is purely *advisory*. There is no enforcement that clients must acquire a lock to perform any operation. Any client can read, write, and delete a key without owning the corresponding lock. It is not the goal of Consul to protect against misbehaving clients. From 548f1a07ad151103c2174a564c64fe77cce3d690 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 12:57:15 +0000 Subject: [PATCH 192/238] docs: internals/acl: minor fixes --- website/source/docs/internals/acl.html.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 08522a707..e2547513e 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -11,7 +11,7 @@ description: |- Consul provides an optional Access Control List (ACL) system which can be used to control access to data and APIs. The ACL system is a [Capability-based system](http://en.wikipedia.org/wiki/Capability-based_security) that relies -on tokens which can have fine grained rules applied to them. It is very similar to +on tokens to which fine grained rules can be applied. It is very similar to [AWS IAM](http://aws.amazon.com/iam/) in many ways. ## ACL Design @@ -30,10 +30,10 @@ perform all actions. The token ID is passed along with each RPC request to the servers. Agents [can be configured](/docs/agent/options.html) with `acl_token` to provide a default token, but the token can also be specified by a client on a [per-request basis](/docs/agent/http.html). -ACLs are new as of Consul 0.4, meaning versions prior do not provide a token. +ACLs are new as of Consul 0.4, meaning prior versions do not provide a token. This is handled by the special "anonymous" token. Anytime there is no token provided, -the rules defined by that token are automatically applied. This lets policy be enforced -on legacy clients. +the rules defined by that token are automatically applied. This allows +policy to be enforced on legacy clients. Enforcement is always done by the server nodes. All servers must be [configured to provide](/docs/agent/options.html) an `acl_datacenter`, which enables @@ -47,7 +47,7 @@ all the tokens. When a request is made to any non-authoritative server with a token, it must be resolved into the appropriate policy. This is done by reading the token from the authoritative server and caching a configurable `acl_ttl`. The implication -of caching is that the cache TTL is an upper-bound on the staleness of policy +of caching is that the cache TTL is an upper bound on the staleness of policy that is enforced. It is possible to set a zero TTL, but this has adverse performance impacts, as every request requires refreshing the policy. From 261c69c4ff32812a29e13a875906a296a8f6c267 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 13:05:33 +0000 Subject: [PATCH 193/238] docs: internals/security: minor fixes --- website/source/docs/internals/security.html.markdown | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/source/docs/internals/security.html.markdown b/website/source/docs/internals/security.html.markdown index 5c8e2f195..d9a22aae8 100644 --- a/website/source/docs/internals/security.html.markdown +++ b/website/source/docs/internals/security.html.markdown @@ -10,8 +10,8 @@ description: |- Consul relies on both a lightweight gossip mechanism and an RPC system to provide various features. Both of the systems have different security -mechanisms that stem from their designs. However, the goals -of Consuls security are to provide [confidentiality, integrity and authentication](http://en.wikipedia.org/wiki/Information_security). +mechanisms that stem from their designs. However, the overall goal +of Consul's security model is to provide [confidentiality, integrity and authentication](http://en.wikipedia.org/wiki/Information_security). The [gossip protocol](/docs/internals/gossip.html) is powered by [Serf](http://www.serfdom.io/), which uses a symmetric key, or shared secret, cryptosystem. There are more @@ -19,10 +19,11 @@ details on the security of [Serf here](http://www.serfdom.io/docs/internals/secu The RPC system supports using end-to-end TLS, with optional client authentication. [TLS](http://en.wikipedia.org/wiki/Transport_Layer_Security) is a widely deployed asymmetric -cryptosystem, and is the foundation of security on the Internet. +cryptosystem, and is the foundation of security on the Web, as well as +some other critical parts of the Internet. This means Consul communication is protected against eavesdropping, tampering, -or spoofing. This makes it possible to run Consul over untrusted networks such +and spoofing. This makes it possible to run Consul over untrusted networks such as EC2 and other shared hosting providers. ~> **Advanced Topic!** This page covers the technical details of From e1a0845f5a94b8d4f60fa5341f3043afa386ed9e Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 13:15:41 +0000 Subject: [PATCH 194/238] docs: agent/basics: minor fixes --- .../source/docs/agent/basics.html.markdown | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/website/source/docs/agent/basics.html.markdown b/website/source/docs/agent/basics.html.markdown index 5740f3b84..f97ca3fbd 100644 --- a/website/source/docs/agent/basics.html.markdown +++ b/website/source/docs/agent/basics.html.markdown @@ -72,7 +72,7 @@ There are several important components that `consul agent` outputs: agent. This is also the address other applications can use over [RPC to control Consul](/docs/agent/rpc.html). * **Cluster Addr**: This is the address and ports used for communication between - Consul agents in a cluster. Every Consul agent in a cluster does not have to + Consul agents in a cluster. Not all Consul agents in a cluster have to use the same port, but this address **MUST** be reachable by all other nodes. ## Stopping an Agent @@ -105,7 +105,7 @@ this lifecycle is useful to building a mental model of an agent's interactions with a cluster, and how the cluster treats a node. When an agent is first started, it does not know about any other node in the cluster. -To discover it's peers, it must _join_ the cluster. This is done with the `join` +To discover its peers, it must _join_ the cluster. This is done with the `join` command or by providing the proper configuration to auto-join on start. Once a node joins, this information is gossiped to the entire cluster, meaning all nodes will eventually be aware of each other. If the agent is a server, existing servers will @@ -115,18 +115,18 @@ In the case of a network failure, some nodes may be unreachable by other nodes. In this case, unreachable nodes are marked as _failed_. It is impossible to distinguish between a network failure and an agent crash, so both cases are handled the same. Once a node is marked as failed, this information is updated in the service catalog. -There is some nuance here relating, since this update is only possible if the -servers can still [form a quorum](/docs/internals/consensus.html). Once the network -failure recovers, or a crashed agent restarts, the cluster will repair itself, -and unmark a node as failed. The health check in the catalog will also be updated -to reflect this. +There is some nuance here since this update is only possible if the servers can +still [form a quorum](/docs/internals/consensus.html). Once the network recovers, +or a crashed agent restarts, the cluster will repair itself, and unmark +a node as failed. The health check in the catalog will also be updated to reflect +this. -When a node _leaves_, it specifies it's intent to do so, and so the cluster +When a node _leaves_, it specifies its intent to do so, and so the cluster marks that node as having _left_. Unlike the _failed_ case, all of the services provided by a node are immediately deregistered. If the agent was a server, replication to it will stop. To prevent an accumulation of dead nodes, Consul will automatically reap _failed_ nodes out of the -catalog as well. This is currently done on a non-configurable interval -which defaults to 72 hours. Reaping is similar to leaving, causing all -associated services to be deregistered. +catalog as well. This is currently done on a non-configurable interval of +72 hours. Reaping is similar to leaving, causing all associated services +to be deregistered. From f0a2fa325acbdef4ab6d853c663fb85152427d93 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 13:32:19 +0000 Subject: [PATCH 195/238] docs: agent/dns: minor fixes --- website/source/docs/agent/dns.html.markdown | 35 +++++++++++---------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/website/source/docs/agent/dns.html.markdown b/website/source/docs/agent/dns.html.markdown index ab6995a35..d61603253 100644 --- a/website/source/docs/agent/dns.html.markdown +++ b/website/source/docs/agent/dns.html.markdown @@ -8,22 +8,23 @@ description: |- # DNS Interface -One of the primary query interfaces for Consul is using DNS. +One of the primary query interfaces for Consul is DNS. The DNS interface allows applications to make use of service discovery without any high-touch integration with Consul. For -example, instead of making any HTTP API requests to Consul, +example, instead of making HTTP API requests to Consul, a host can use the DNS server directly and just do a name lookup for "redis.service.east-aws.consul". This query automatically translates to a lookup of nodes that -provide the redis service, located in the "east-aws" datacenter, -with no failing health checks. It's that simple! +provide the redis service, are located in the "east-aws" datacenter, +and have no failing health checks. It's that simple! There are a number of [configuration options](/docs/agent/options.html) that are important for the DNS interface. They are `client_addr`, `ports.dns`, `recursors`, `domain`, and `dns_config`. By default Consul will listen on 127.0.0.1:8600 for DNS queries -in the "consul." domain, without support for DNS recursion. All queries are case-insensitive, a -name lookup for `PostgreSQL.node.dc1.consul` will find all nodes named `postgresql`, no matter of case. +in the "consul." domain, without support for DNS recursion. All queries are case-insensitive: a +name lookup for `PostgreSQL.node.dc1.consul` will find all nodes named `postgresql`, +regardless of case. There are a few ways to use the DNS interface. One option is to use a custom DNS resolver library and point it at Consul. Another option is to set Consul @@ -76,24 +77,24 @@ consul. 0 IN SOA ns.consul. postmaster.consul. 1392836399 3600 600 86400 0 ## Service Lookups A service lookup is the alternate type of query. It is used to query for service -providers and supports two mode of lookup, a strict RCF style lookup and the -standard lookup. +providers and supports two lookup methods: standard lookup, and strict +[RFC 2782](https://tools.ietf.org/html/rfc2782) lookup. -### Standard Style Lookup +### Standard Lookup -The format of a standard service lookup is like the following: +The format of a standard service lookup is: [tag.].service[.datacenter][.domain] As with node lookups, the `datacenter` is optional, as is the `tag`. If no tag is provided, then no filtering is done on tag. So, if we want to find any redis service -providers in our local datacenter, we could lookup "redis.service.consul.", however +providers in our local datacenter, we could lookup "redis.service.consul.", while if we care about the PostgreSQL master in a particular datacenter, we could lookup "master.postgresql.service.dc2.consul." The DNS query system makes use of health check information to prevent routing to unhealthy nodes. When a service query is made, any services failing their health -check, or failing a node system check will be omitted from the results. To allow +check, or failing a node system check, will be omitted from the results. To allow for simple load balancing, the set of nodes returned is also randomized each time. These simple mechanisms make it easy to use DNS along with application level retries as a simple foundation for an auto-healing service oriented architecture. @@ -124,13 +125,13 @@ consul.service.consul. 0 IN SRV 1 1 8300 foobar.node.dc1.consul. foobar.node.dc1.consul. 0 IN A 10.1.10.12 ``` -### RFC-2782 Style Lookup +### RFC 2782 Lookup -The format for RFC style lookups uses the following format: +The format for RFC 2782 SRV lookups is: _._.service[.datacenter][.domain] -Per [RFC-2782](https://www.ietf.org/rfc/rfc2782.txt), SRV queries should use +Per [RFC 2782](https://tools.ietf.org/html/rfc2782), SRV queries should use underscores (_) as a prefix to the `service` and `protocol` values in a query to prevent DNS collisions. The `protocol` value can be any of the tags for a service or if the service has no tags, the value "tcp" should be used. If "tcp" @@ -139,7 +140,7 @@ is specified as the protocol, the query will not perform any tag filtering. Other than the query format and default "tcp" protocol/tag value, the behavior of the RFC style lookup is the same as the standard style of lookup. -Using the RCF style lookup, If you registered the service "rabbitmq" on port +Using RFC 2782 lookup, If you registered the service "rabbitmq" on port 5672 and tagged it with "amqp" you would query the SRV record as "_rabbitmq._amqp.service.consul" as illustrated in the example below: @@ -168,7 +169,7 @@ rabbitmq.node1.dc1.consul. 0 IN A 10.1.11.20 When the DNS query is performed using UDP, Consul will truncate the results without setting the truncate bit. This is to prevent a redundant lookup over -TCP which generate additional load. If the lookup is done over TCP, the results +TCP that generates additional load. If the lookup is done over TCP, the results are not truncated. ## Caching From bfd6c8aac0f5882529cd3d3c563fd6bf88d9df37 Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 13:42:53 +0000 Subject: [PATCH 196/238] docs: agent/checks: minor fixes --- website/source/docs/agent/checks.html.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/source/docs/agent/checks.html.markdown b/website/source/docs/agent/checks.html.markdown index a5a4bdb56..8a31a0462 100644 --- a/website/source/docs/agent/checks.html.markdown +++ b/website/source/docs/agent/checks.html.markdown @@ -16,12 +16,12 @@ or added at runtime over the HTTP interface. There are two different kinds of checks: * Script + Interval - These checks depend on invoking an external application - which does the health check and exits with an appropriate exit code, potentially + that does the health check and exits with an appropriate exit code, potentially generating some output. A script is paired with an invocation interval (e.g. every 30 seconds). This is similar to the Nagios plugin system. - * TTL - These checks retain their last known state for a given TTL. The state - of the check must be updated periodically over the HTTP interface. If an + * Time to Live (TTL) - These checks retain their last known state for a given TTL. + The state of the check must be updated periodically over the HTTP interface. If an external system fails to update the status within a given TTL, the check is set to the failed state. This mechanism is used to allow an application to directly report its health. For example, a web app can periodically curl the @@ -75,7 +75,7 @@ check can be registered dynamically using the [HTTP API](/docs/agent/http.html). ## Check Scripts A check script is generally free to do anything to determine the status -of the check. The only limitations placed are the exit codes must convey +of the check. The only limitations placed are that the exit codes must convey a specific meaning. Specifically: * Exit code 0 - Check is passing From bd5467c59319448690d227af44cee99d87871bbc Mon Sep 17 00:00:00 2001 From: Dan Frost Date: Wed, 26 Nov 2014 13:48:33 +0000 Subject: [PATCH 197/238] docs: agent/watches: minor fixes --- website/source/docs/agent/watches.html.markdown | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/source/docs/agent/watches.html.markdown b/website/source/docs/agent/watches.html.markdown index 5ab470721..261eef08d 100644 --- a/website/source/docs/agent/watches.html.markdown +++ b/website/source/docs/agent/watches.html.markdown @@ -15,7 +15,7 @@ executable. As an example, you could watch the status of health checks and notify an external system when a check is critical. Watches are implemented using blocking queries in the [HTTP API](/docs/agent/http.html). -Agent's automatically make the proper API calls to watch for changes, +Agents automatically make the proper API calls to watch for changes, and inform a handler when the data view has updated. Watches can be configured as part of the [agent's configuration](/docs/agent/options.html), @@ -36,7 +36,7 @@ The watch specification specifies the view of data to be monitored. Once that view is updated the specified handler is invoked. The handler can be any executable. -A handler should read it's input from stdin, and expect to read +A handler should read its input from stdin, and expect to read JSON formatted data. The format of the data depends on the type of the watch. Each watch type documents the format type, and because they map directly to an HTTP API, handlers should expect the input to @@ -280,7 +280,7 @@ An example of the output of this command: ### Type: checks The "checks" watch type is used to monitor the checks of a given -service or in a specific state. It optionally takes the "service" +service or those in a specific state. It optionally takes the "service" parameter to filter to a specific service, or "state" to filter to a specific state. By default, it will watch all checks. From 4f14ba63267cde4e56cfb56a9c425438d5db0ec4 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Tue, 25 Nov 2014 23:58:02 -0800 Subject: [PATCH 198/238] agent: allow config reload to modify checks/services persistence This change consolidates loading services and checks from both config and persisted state into methods on the agent. As part of this, we introduce optional persistence when calling RemoveCheck/RemoveService. Fixes a bug where config reloads would kill persisted services/checks. Also fixes an edge case: 1. A service or check is registered via the HTTP API 2. A new service or check definition with the same ID is added to config 3. Config is reloaded The desired behavior (which this implements) is: 1. All services and checks deregistered in memory 2. All services and checks in config are registered first 3. All persisted checks are restored using the same logic as the agent start sequence, which prioritizes config over persisted, and removes any persistence files if new config counterparts are present. --- command/agent/agent.go | 98 +++++++++++++++++++++++---------- command/agent/agent_endpoint.go | 4 +- command/agent/agent_test.go | 76 +++++++++++++++++++++---- command/agent/command.go | 34 +++--------- 4 files changed, 143 insertions(+), 69 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index 0b5405da5..bb2c9d63c 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -141,31 +141,11 @@ func Create(config *Config, logOutput io.Writer) (*Agent, error) { return nil, err } - // Register the services from config - for _, service := range config.Services { - ns := service.NodeService() - chkType := service.CheckType() - if err := agent.AddService(ns, chkType, false); err != nil { - return nil, fmt.Errorf( - "Failed to register service '%s': %v", service.Name, err) - } - } - - // Register the checks from config - for _, check := range config.Checks { - health := check.HealthCheck(config.NodeName) - chkType := &check.CheckType - if err := agent.AddCheck(health, chkType, false); err != nil { - return nil, fmt.Errorf( - "Failed to register check '%s': %v %v", check.Name, err, check) - } - } - - // Load any persisted services or checks - if err := agent.restoreServices(); err != nil { + // Load checks/services + if err := agent.reloadServices(config); err != nil { return nil, err } - if err := agent.restoreChecks(); err != nil { + if err := agent.reloadChecks(config); err != nil { return nil, err } @@ -705,7 +685,7 @@ func (a *Agent) AddService(service *structs.NodeService, chkType *CheckType, per // RemoveService is used to remove a service entry. // The agent will make a best effort to ensure it is deregistered -func (a *Agent) RemoveService(serviceID string) error { +func (a *Agent) RemoveService(serviceID string, persist bool) error { // Protect "consul" service from deletion by a user if a.server != nil && serviceID == consul.ConsulServiceID { return fmt.Errorf( @@ -717,13 +697,15 @@ func (a *Agent) RemoveService(serviceID string) error { a.state.RemoveService(serviceID) // Remove the service from the data dir - if err := a.purgeService(serviceID); err != nil { - return err + if persist { + if err := a.purgeService(serviceID); err != nil { + return err + } } // Deregister any associated health checks checkID := fmt.Sprintf("service:%s", serviceID) - return a.RemoveCheck(checkID) + return a.RemoveCheck(checkID, persist) } // AddCheck is used to add a health check to the agent. @@ -792,7 +774,7 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType, persist // RemoveCheck is used to remove a health check. // The agent will make a best effort to ensure it is deregistered -func (a *Agent) RemoveCheck(checkID string) error { +func (a *Agent) RemoveCheck(checkID string, persist bool) error { // Add to the local state for anti-entropy a.state.RemoveCheck(checkID) @@ -808,7 +790,10 @@ func (a *Agent) RemoveCheck(checkID string) error { check.Stop() delete(a.checkTTLs, checkID) } - return a.purgeCheck(checkID) + if persist { + return a.purgeCheck(checkID) + } + return nil } // UpdateCheck is used to update the status of a check. @@ -904,3 +889,58 @@ func (a *Agent) deletePid() error { } return nil } + +// reloadServices reloads all known services from config and state. It is used +// at initial agent startup as well as during config reloads. +func (a *Agent) reloadServices(conf *Config) error { + for _, service := range a.state.Services() { + if service.ID == consul.ConsulServiceID { + continue + } + if err := a.RemoveService(service.ID, false); err != nil { + return fmt.Errorf("Failed deregistering service '%s': %v", service.ID, err) + } + } + + // Register the services from config + for _, service := range conf.Services { + ns := service.NodeService() + chkType := service.CheckType() + if err := a.AddService(ns, chkType, false); err != nil { + return fmt.Errorf("Failed to register service '%s': %v", service.ID, err) + } + } + + // Load any persisted services + if err := a.restoreServices(); err != nil { + return fmt.Errorf("Failed restoring services: %s", err) + } + + return nil +} + +// reloadChecks reloads all known checks from config and state. It can be used +// during initial agent start or for config reloads. +func (a *Agent) reloadChecks(conf *Config) error { + for _, check := range a.state.Checks() { + if err := a.RemoveCheck(check.CheckID, false); err != nil { + return fmt.Errorf("Failed deregistering check '%s': %s", check.CheckID, err) + } + } + + // Register the checks from config + for _, check := range conf.Checks { + health := check.HealthCheck(conf.NodeName) + chkType := &check.CheckType + if err := a.AddCheck(health, chkType, false); err != nil { + return fmt.Errorf("Failed to register check '%s': %v %v", check.Name, err, check) + } + } + + // Load any persisted checks + if err := a.restoreChecks(); err != nil { + return fmt.Errorf("Failed restoring checks: %s", err) + } + + return nil +} diff --git a/command/agent/agent_endpoint.go b/command/agent/agent_endpoint.go index de3c24157..4ee70343f 100644 --- a/command/agent/agent_endpoint.go +++ b/command/agent/agent_endpoint.go @@ -102,7 +102,7 @@ func (s *HTTPServer) AgentRegisterCheck(resp http.ResponseWriter, req *http.Requ func (s *HTTPServer) AgentDeregisterCheck(resp http.ResponseWriter, req *http.Request) (interface{}, error) { checkID := strings.TrimPrefix(req.URL.Path, "/v1/agent/check/deregister/") - return nil, s.agent.RemoveCheck(checkID) + return nil, s.agent.RemoveCheck(checkID, true) } func (s *HTTPServer) AgentCheckPass(resp http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -174,5 +174,5 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { serviceID := strings.TrimPrefix(req.URL.Path, "/v1/agent/service/deregister/") - return nil, s.agent.RemoveService(serviceID) + return nil, s.agent.RemoveService(serviceID, true) } diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 5200df3a8..b7571a085 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -181,12 +181,12 @@ func TestAgent_RemoveService(t *testing.T) { defer agent.Shutdown() // Remove a service that doesn't exist - if err := agent.RemoveService("redis"); err != nil { + if err := agent.RemoveService("redis", false); err != nil { t.Fatalf("err: %v", err) } // Remove the consul service - if err := agent.RemoveService("consul"); err == nil { + if err := agent.RemoveService("consul", false); err == nil { t.Fatalf("should have errored") } @@ -201,7 +201,7 @@ func TestAgent_RemoveService(t *testing.T) { } // Remove the service - if err := agent.RemoveService("redis"); err != nil { + if err := agent.RemoveService("redis", false); err != nil { t.Fatalf("err: %v", err) } @@ -291,7 +291,7 @@ func TestAgent_RemoveCheck(t *testing.T) { defer agent.Shutdown() // Remove check that doesn't exist - if err := agent.RemoveCheck("mem"); err != nil { + if err := agent.RemoveCheck("mem", false); err != nil { t.Fatalf("err: %v", err) } @@ -311,7 +311,7 @@ func TestAgent_RemoveCheck(t *testing.T) { } // Remove check - if err := agent.RemoveCheck("mem"); err != nil { + if err := agent.RemoveCheck("mem", false); err != nil { t.Fatalf("err: %v", err) } @@ -438,13 +438,39 @@ func TestAgent_PersistService(t *testing.T) { if _, ok := agent2.state.services[svc.ID]; !ok { t.Fatalf("bad: %#v", agent2.state.services) } +} - // Should remove the service file - if err := agent2.RemoveService(svc.ID); err != nil { +func TestAgent_PurgeService(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + svc := &structs.NodeService{ + ID: "redis", + Service: "redis", + Tags: []string{"foo"}, + Port: 8000, + } + + file := filepath.Join(agent.config.DataDir, servicesDir, svc.ID) + if err := agent.AddService(svc, nil, true); err != nil { + t.Fatalf("err: %v", err) + } + + // Not removed + if err := agent.RemoveService(svc.ID, false); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(file); err != nil { + t.Fatalf("err: %s", err) + } + + // Removed + if err := agent.RemoveService(svc.ID, true); err != nil { t.Fatalf("err: %s", err) } if _, err := os.Stat(file); !os.IsNotExist(err) { - t.Fatalf("err: %s", err) + t.Fatalf("bad: %#v", err) } } @@ -555,13 +581,41 @@ func TestAgent_PersistCheck(t *testing.T) { if result.Status != structs.HealthCritical { t.Fatalf("bad: %#v", result) } +} - // Should remove the service file - if err := agent2.RemoveCheck(check.CheckID); err != nil { +func TestAgent_PurgeCheck(t *testing.T) { + config := nextConfig() + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + + check := &structs.HealthCheck{ + Node: config.NodeName, + CheckID: "service:redis1", + Name: "redischeck", + Status: structs.HealthPassing, + ServiceID: "redis", + ServiceName: "redis", + } + + file := filepath.Join(agent.config.DataDir, checksDir, check.CheckID) + if err := agent.AddCheck(check, nil, true); err != nil { + t.Fatalf("err: %v", err) + } + + // Not removed + if err := agent.RemoveCheck(check.CheckID, false); err != nil { + t.Fatalf("err: %s", err) + } + if _, err := os.Stat(file); err != nil { + t.Fatalf("err: %s", err) + } + + // Removed + if err := agent.RemoveCheck(check.CheckID, true); err != nil { t.Fatalf("err: %s", err) } if _, err := os.Stat(file); !os.IsNotExist(err) { - t.Fatalf("err: %s", err) + t.Fatalf("bad: %#v", err) } } diff --git a/command/agent/command.go b/command/agent/command.go index 03e92e11e..c537dc3fa 100644 --- a/command/agent/command.go +++ b/command/agent/command.go @@ -722,34 +722,14 @@ func (c *Command) handleReload(config *Config) *Config { c.agent.PauseSync() defer c.agent.ResumeSync() - // Deregister the old services - for _, service := range config.Services { - ns := service.NodeService() - c.agent.RemoveService(ns.ID) + // Reload services and check definitions + if err := c.agent.reloadServices(newConf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed reloading services: %s", err)) + return nil } - - // Deregister the old checks - for _, check := range config.Checks { - health := check.HealthCheck(config.NodeName) - c.agent.RemoveCheck(health.CheckID) - } - - // Register the services - for _, service := range newConf.Services { - ns := service.NodeService() - chkType := service.CheckType() - if err := c.agent.AddService(ns, chkType, false); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to register service '%s': %v", service.Name, err)) - } - } - - // Register the checks - for _, check := range newConf.Checks { - health := check.HealthCheck(config.NodeName) - chkType := &check.CheckType - if err := c.agent.AddCheck(health, chkType, false); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to register check '%s': %v %v", check.Name, err, check)) - } + if err := c.agent.reloadChecks(newConf); err != nil { + c.Ui.Error(fmt.Sprintf("Failed reloading checks: %s", err)) + return nil } // Get the new client listener addr From 9e52588cec5aeefe63c0d951a7b7967baa192a40 Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sat, 29 Nov 2014 12:25:01 -0800 Subject: [PATCH 199/238] agent: persist CheckType with health checks --- command/agent/agent.go | 57 ++++++++++++++++++++----------------- command/agent/agent_test.go | 18 +++++++++--- command/agent/check.go | 7 +++++ 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/command/agent/agent.go b/command/agent/agent.go index bb2c9d63c..facb260b7 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -564,24 +564,29 @@ func (a *Agent) restoreServices() error { } // persistCheck saves a check definition to the local agent's state directory -func (a *Agent) persistCheck(check *structs.HealthCheck) error { +func (a *Agent) persistCheck(check *structs.HealthCheck, chkType *CheckType) error { checkPath := filepath.Join(a.config.DataDir, checksDir, check.CheckID) - if _, err := os.Stat(checkPath); os.IsNotExist(err) { - encoded, err := json.Marshal(check) - if err != nil { - return nil - } - if err := os.MkdirAll(filepath.Dir(checkPath), 0700); err != nil { - return err - } - fh, err := os.OpenFile(checkPath, os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - return err - } - defer fh.Close() - if _, err := fh.Write(encoded); err != nil { - return err - } + if _, err := os.Stat(checkPath); !os.IsNotExist(err) { + return err + } + + // Create the persisted check + p := persistedCheck{check, chkType} + + encoded, err := json.Marshal(p) + if err != nil { + return nil + } + if err := os.MkdirAll(filepath.Dir(checkPath), 0700); err != nil { + return err + } + fh, err := os.OpenFile(checkPath, os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer fh.Close() + if _, err := fh.Write(encoded); err != nil { + return err } return nil } @@ -619,23 +624,23 @@ func (a *Agent) restoreChecks() error { return err } - var check *structs.HealthCheck - if err := json.Unmarshal(content, &check); err != nil { + var p persistedCheck + if err := json.Unmarshal(content, &p); err != nil { return err } - if _, ok := a.state.checks[check.CheckID]; ok { + if _, ok := a.state.checks[p.Check.CheckID]; ok { // Purge previously persisted check. This allows config to be // preferred over persisted checks from the API. - a.logger.Printf("[DEBUG] Check %s exists, not restoring", check.CheckID) - return a.purgeCheck(check.CheckID) + a.logger.Printf("[DEBUG] Check %s exists, not restoring", p.Check.CheckID) + return a.purgeCheck(p.Check.CheckID) } else { // Default check to critical to avoid placing potentially unhealthy // services into the active pool - check.Status = structs.HealthCritical + p.Check.Status = structs.HealthCritical - a.logger.Printf("[DEBUG] Restored health check: %s", check.CheckID) - return a.AddCheck(check, nil, false) + a.logger.Printf("[DEBUG] Restored health check: %s", p.Check.CheckID) + return a.AddCheck(p.Check, p.ChkType, false) } }) return err @@ -766,7 +771,7 @@ func (a *Agent) AddCheck(check *structs.HealthCheck, chkType *CheckType, persist // Persist the check if persist { - return a.persistCheck(check) + return a.persistCheck(check, chkType) } return nil diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index b7571a085..8e78f24a4 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -534,11 +534,15 @@ func TestAgent_PersistCheck(t *testing.T) { ServiceID: "redis", ServiceName: "redis", } + chkType := &CheckType{ + Script: "/bin/true", + Interval: 10 * time.Second, + } file := filepath.Join(agent.config.DataDir, checksDir, check.CheckID) // Not persisted if not requested - if err := agent.AddCheck(check, nil, false); err != nil { + if err := agent.AddCheck(check, chkType, false); err != nil { t.Fatalf("err: %v", err) } if _, err := os.Stat(file); err == nil { @@ -546,7 +550,7 @@ func TestAgent_PersistCheck(t *testing.T) { } // Should persist if requested - if err := agent.AddCheck(check, nil, true); err != nil { + if err := agent.AddCheck(check, chkType, true); err != nil { t.Fatalf("err: %v", err) } @@ -554,7 +558,8 @@ func TestAgent_PersistCheck(t *testing.T) { t.Fatalf("err: %s", err) } - expected, err := json.Marshal(check) + p := persistedCheck{check, chkType} + expected, err := json.Marshal(p) if err != nil { t.Fatalf("err: %s", err) } @@ -574,13 +579,18 @@ func TestAgent_PersistCheck(t *testing.T) { } defer agent2.Shutdown() - result, ok := agent2.state.checks[check.CheckID] + result, ok := agent2.state.checks[p.Check.CheckID] if !ok { t.Fatalf("bad: %#v", agent2.state.checks) } if result.Status != structs.HealthCritical { t.Fatalf("bad: %#v", result) } + + // Should have restored the monitor + if _, ok := agent2.checkMonitors[p.Check.CheckID]; !ok { + t.Fatalf("bad: %#v", agent2.checkMonitors) + } } func TestAgent_PurgeCheck(t *testing.T) { diff --git a/command/agent/check.go b/command/agent/check.go index a2cb588dd..180f6db13 100644 --- a/command/agent/check.go +++ b/command/agent/check.go @@ -234,3 +234,10 @@ func (c *CheckTTL) SetStatus(status, output string) { c.Notify.UpdateCheck(c.CheckID, status, output) c.timer.Reset(c.TTL) } + +// persistedCheck is used to serialize a check and write it to disk +// so that it may be restored later on. +type persistedCheck struct { + Check *structs.HealthCheck + ChkType *CheckType +} From 42bad4af80bd37453140c08f7e535404099229cb Mon Sep 17 00:00:00 2001 From: Ryan Uber Date: Sun, 30 Nov 2014 18:27:37 -0800 Subject: [PATCH 200/238] agent: fixup all check definitions from json config --- command/agent/config.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/command/agent/config.go b/command/agent/config.go index 36683ad16..707cd43ad 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -559,8 +559,13 @@ func DecodeServiceDefinition(raw interface{}) (*ServiceDefinition, error) { } } - sub, ok = rawMap["check"] - if !ok { + for k, v := range rawMap { + if strings.ToLower(k) == "check" { + sub = v + break + } + } + if sub == nil { goto AFTER_FIX } if err := FixupCheckType(sub); err != nil { From 376f9694f4b922ffb384a7cb69ee73d487334531 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 30 Nov 2014 20:12:44 -0700 Subject: [PATCH 201/238] website: Update ACL docs --- .../source/docs/internals/acl.html.markdown | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index e2547513e..33994e3ac 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -114,6 +114,16 @@ key "foo/private/" { # Deny access to the private dir policy = "deny" } + +# Default all services to allowing registration +service "" { + policy = "write" +} + +service "secure" { + # Deny registration access to secure service + policy = "read" +} ``` This is equivalent to the following JSON input: @@ -122,14 +132,22 @@ This is equivalent to the following JSON input: { "key": { "": { - "policy": "read", + "policy": "read" }, "foo/": { - "policy": "write", + "policy": "write" }, "foo/private": { - "policy": "deny", + "policy": "deny" } + }, + "service": { + "": { + "policy": "write" + }, + "secure": { + "policy": "read" + } } } ``` @@ -139,3 +157,11 @@ using a longest-prefix match policy. This means we pick the most specific policy possible. The policy is either "read", "write" or "deny". A "write" policy implies "read", and there is no way to specify write-only. If there is no applicable rule, the `acl_default_policy` is applied. + +Services policies provide both a service name and a policy. The rules are +enforced using an exact match policy. The default rule is provided using +the empty string. The policy is either "read", "write", or "deny". A "write" +policy implies "read", and there is no way to specify write-only. If there +is no applicable rule, the `acl_default_policy` is applied. Currently, only +the "write" level is enforced for registration of services. + From b9810f774c2c63b76c893f2d087652e00eaf547e Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 30 Nov 2014 20:18:16 -0700 Subject: [PATCH 202/238] acl: Support for service policies --- acl/policy.go | 37 ++++++++++++++++++++++++++++++++----- acl/policy_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/acl/policy.go b/acl/policy.go index 014ef51ac..569570a9d 100644 --- a/acl/policy.go +++ b/acl/policy.go @@ -2,20 +2,25 @@ package acl import ( "fmt" + "github.com/hashicorp/hcl" ) const ( - KeyPolicyDeny = "deny" - KeyPolicyRead = "read" - KeyPolicyWrite = "write" + KeyPolicyDeny = "deny" + KeyPolicyRead = "read" + KeyPolicyWrite = "write" + ServicePolicyDeny = "deny" + ServicePolicyRead = "read" + ServicePolicyWrite = "write" ) // Policy is used to represent the policy specified by // an ACL configuration. type Policy struct { - ID string `hcl:"-"` - Keys []*KeyPolicy `hcl:"key,expand"` + ID string `hcl:"-"` + Keys []*KeyPolicy `hcl:"key,expand"` + Services []*ServicePolicy `hcl:"service,expand"` } // KeyPolicy represents a policy for a key @@ -28,6 +33,16 @@ func (k *KeyPolicy) GoString() string { return fmt.Sprintf("%#v", *k) } +// ServicePolicy represents a policy for a service +type ServicePolicy struct { + Name string `hcl:",key"` + Policy string +} + +func (k *ServicePolicy) GoString() string { + return fmt.Sprintf("%#v", *k) +} + // Parse is used to parse the specified ACL rules into an // intermediary set of policies, before being compiled into // the ACL @@ -53,5 +68,17 @@ func Parse(rules string) (*Policy, error) { return nil, fmt.Errorf("Invalid key policy: %#v", kp) } } + + // Validate the service policy + for _, sp := range p.Services { + switch sp.Policy { + case ServicePolicyDeny: + case ServicePolicyRead: + case ServicePolicyWrite: + default: + return nil, fmt.Errorf("Invalid service policy: %#v", sp) + } + } + return p, nil } diff --git a/acl/policy_test.go b/acl/policy_test.go index 0fc75e0fa..0c270e7d5 100644 --- a/acl/policy_test.go +++ b/acl/policy_test.go @@ -18,6 +18,12 @@ key "foo/bar/" { } key "foo/bar/baz" { policy = "deny" +} +service "" { + policy = "write" +} +service "foo" { + policy = "read" } ` exp := &Policy{ @@ -39,6 +45,16 @@ key "foo/bar/baz" { Policy: KeyPolicyDeny, }, }, + Services: []*ServicePolicy{ + &ServicePolicy{ + Name: "", + Policy: ServicePolicyWrite, + }, + &ServicePolicy{ + Name: "foo", + Policy: ServicePolicyRead, + }, + }, } out, err := Parse(inp) @@ -66,6 +82,14 @@ func TestParse_JSON(t *testing.T) { "foo/bar/baz": { "policy": "deny" } + }, + "service": { + "": { + "policy": "write" + }, + "foo": { + "policy": "read" + } } }` exp := &Policy{ @@ -87,6 +111,16 @@ func TestParse_JSON(t *testing.T) { Policy: KeyPolicyDeny, }, }, + Services: []*ServicePolicy{ + &ServicePolicy{ + Name: "", + Policy: ServicePolicyWrite, + }, + &ServicePolicy{ + Name: "foo", + Policy: ServicePolicyRead, + }, + }, } out, err := Parse(inp) From 7b8faf4cb3575c26e6fca49e93697297165a518d Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 30 Nov 2014 20:33:46 -0700 Subject: [PATCH 203/238] acl: Expose service policy checks --- acl/acl.go | 69 ++++++++++++++++++++++++++++++++++- acl/acl_test.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 160 insertions(+), 6 deletions(-) diff --git a/acl/acl.go b/acl/acl.go index 442837340..8a73674d0 100644 --- a/acl/acl.go +++ b/acl/acl.go @@ -46,6 +46,12 @@ type ACL interface { // that deny a write. KeyWritePrefix(string) bool + // ServiceWrite checks for permission to read a given service + ServiceWrite(string) bool + + // ServiceRead checks for permission to read a given service + ServiceRead(string) bool + // ACLList checks for permission to list all the ACLs ACLList() bool @@ -73,6 +79,14 @@ func (s *StaticACL) KeyWritePrefix(string) bool { return s.defaultAllow } +func (s *StaticACL) ServiceRead(string) bool { + return s.defaultAllow +} + +func (s *StaticACL) ServiceWrite(string) bool { + return s.defaultAllow +} + func (s *StaticACL) ACLList() bool { return s.allowManage } @@ -119,20 +133,29 @@ type PolicyACL struct { // keyRules contains the key policies keyRules *radix.Tree + + // serviceRules contains the service policies + serviceRules map[string]string } // New is used to construct a policy based ACL from a set of policies // and a parent policy to resolve missing cases. func New(parent ACL, policy *Policy) (*PolicyACL, error) { p := &PolicyACL{ - parent: parent, - keyRules: radix.New(), + parent: parent, + keyRules: radix.New(), + serviceRules: make(map[string]string, len(policy.Services)), } // Load the key policy for _, kp := range policy.Keys { p.keyRules.Insert(kp.Prefix, kp.Policy) } + + // Load the service policy + for _, sp := range policy.Services { + p.serviceRules[sp.Name] = sp.Policy + } return p, nil } @@ -205,6 +228,48 @@ func (p *PolicyACL) KeyWritePrefix(prefix string) bool { return p.parent.KeyWritePrefix(prefix) } +// ServiceRead checks if reading (discovery) of a service is allowed +func (p *PolicyACL) ServiceRead(name string) bool { + // Check for an exact rule or catch-all + rule, ok := p.serviceRules[name] + if !ok { + rule, ok = p.serviceRules[""] + } + if ok { + switch rule { + case ServicePolicyWrite: + return true + case ServicePolicyRead: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.ServiceRead(name) +} + +// ServiceWrite checks if writing (registering) a service is allowed +func (p *PolicyACL) ServiceWrite(name string) bool { + // Check for an exact rule or catch-all + rule, ok := p.serviceRules[name] + if !ok { + rule, ok = p.serviceRules[""] + } + if ok { + switch rule { + case ServicePolicyWrite: + return true + default: + return false + } + } + + // No matching rule, use the parent. + return p.parent.ServiceWrite(name) +} + // ACLList checks if listing of ACLs is allowed func (p *PolicyACL) ACLList() bool { return p.parent.ACLList() diff --git a/acl/acl_test.go b/acl/acl_test.go index 9be0388db..cecc870b9 100644 --- a/acl/acl_test.go +++ b/acl/acl_test.go @@ -41,6 +41,12 @@ func TestStaticACL(t *testing.T) { if !all.KeyWrite("foobar") { t.Fatalf("should allow") } + if !all.ServiceRead("foobar") { + t.Fatalf("should allow") + } + if !all.ServiceWrite("foobar") { + t.Fatalf("should allow") + } if all.ACLList() { t.Fatalf("should not allow") } @@ -54,6 +60,12 @@ func TestStaticACL(t *testing.T) { if none.KeyWrite("foobar") { t.Fatalf("should not allow") } + if none.ServiceRead("foobar") { + t.Fatalf("should not allow") + } + if none.ServiceWrite("foobar") { + t.Fatalf("should not allow") + } if none.ACLList() { t.Fatalf("should not noneow") } @@ -67,6 +79,12 @@ func TestStaticACL(t *testing.T) { if !manage.KeyWrite("foobar") { t.Fatalf("should allow") } + if !manage.ServiceRead("foobar") { + t.Fatalf("should allow") + } + if !manage.ServiceWrite("foobar") { + t.Fatalf("should allow") + } if !manage.ACLList() { t.Fatalf("should allow") } @@ -96,19 +114,33 @@ func TestPolicyACL(t *testing.T) { Policy: KeyPolicyRead, }, }, + Services: []*ServicePolicy{ + &ServicePolicy{ + Name: "", + Policy: ServicePolicyWrite, + }, + &ServicePolicy{ + Name: "foo", + Policy: ServicePolicyRead, + }, + &ServicePolicy{ + Name: "bar", + Policy: ServicePolicyDeny, + }, + }, } acl, err := New(all, policy) if err != nil { t.Fatalf("err: %v", err) } - type tcase struct { + type keycase struct { inp string read bool write bool writePrefix bool } - cases := []tcase{ + cases := []keycase{ {"other", true, true, true}, {"foo/test", true, true, true}, {"foo/priv/test", false, false, false}, @@ -128,6 +160,26 @@ func TestPolicyACL(t *testing.T) { t.Fatalf("Write prefix fail: %#v", c) } } + + // Test the services + type servicecase struct { + inp string + read bool + write bool + } + scases := []servicecase{ + {"other", true, true}, + {"foo", true, false}, + {"bar", false, false}, + } + for _, c := range scases { + if c.read != acl.ServiceRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.ServiceWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } } func TestPolicyACL_Parent(t *testing.T) { @@ -143,6 +195,16 @@ func TestPolicyACL_Parent(t *testing.T) { Policy: KeyPolicyRead, }, }, + Services: []*ServicePolicy{ + &ServicePolicy{ + Name: "other", + Policy: ServicePolicyWrite, + }, + &ServicePolicy{ + Name: "foo", + Policy: ServicePolicyRead, + }, + }, } root, err := New(deny, policyRoot) if err != nil { @@ -164,19 +226,25 @@ func TestPolicyACL_Parent(t *testing.T) { Policy: KeyPolicyRead, }, }, + Services: []*ServicePolicy{ + &ServicePolicy{ + Name: "bar", + Policy: ServicePolicyDeny, + }, + }, } acl, err := New(root, policy) if err != nil { t.Fatalf("err: %v", err) } - type tcase struct { + type keycase struct { inp string read bool write bool writePrefix bool } - cases := []tcase{ + cases := []keycase{ {"other", false, false, false}, {"foo/test", true, true, true}, {"foo/priv/test", true, false, false}, @@ -194,4 +262,25 @@ func TestPolicyACL_Parent(t *testing.T) { t.Fatalf("Write prefix fail: %#v", c) } } + + // Test the services + type servicecase struct { + inp string + read bool + write bool + } + scases := []servicecase{ + {"fail", false, false}, + {"other", true, true}, + {"foo", true, false}, + {"bar", false, false}, + } + for _, c := range scases { + if c.read != acl.ServiceRead(c.inp) { + t.Fatalf("Read fail: %#v", c) + } + if c.write != acl.ServiceWrite(c.inp) { + t.Fatalf("Write fail: %#v", c) + } + } } From d74f79b3fa021bf9e34be4a315bb6be175556db1 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 30 Nov 2014 21:05:15 -0700 Subject: [PATCH 204/238] consul: Enforce service registration ACLs --- consul/catalog_endpoint.go | 19 ++++++- consul/catalog_endpoint_test.go | 56 +++++++++++++++++++ consul/leader.go | 8 +++ .../source/docs/internals/acl.html.markdown | 3 +- 4 files changed, 83 insertions(+), 3 deletions(-) diff --git a/consul/catalog_endpoint.go b/consul/catalog_endpoint.go index 9fb982e14..24995fdeb 100644 --- a/consul/catalog_endpoint.go +++ b/consul/catalog_endpoint.go @@ -2,10 +2,11 @@ package consul import ( "fmt" - "github.com/armon/go-metrics" - "github.com/hashicorp/consul/consul/structs" "sort" "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/consul/structs" ) // Catalog endpoint is used to manipulate the service catalog @@ -35,6 +36,20 @@ func (c *Catalog) Register(args *structs.RegisterRequest, reply *struct{}) error if args.Service.ID != "" && args.Service.Service == "" { return fmt.Errorf("Must provide service name with ID") } + + // Apply the ACL policy if any + // The 'consul' service is excluded since it is managed + // automatically internally. + if args.Service.Service != ConsulServiceName { + acl, err := c.srv.resolveToken(args.Token) + if err != nil { + return err + } else if acl != nil && !acl.ServiceWrite(args.Service.Service) { + c.srv.logger.Printf("[WARN] consul.catalog: Register of service '%s' on '%s' denied due to ACLs", + args.Service.Service, args.Node) + return permissionDeniedErr + } + } } if args.Check != nil { diff --git a/consul/catalog_endpoint_test.go b/consul/catalog_endpoint_test.go index 4ba76ce01..9c4e86ea9 100644 --- a/consul/catalog_endpoint_test.go +++ b/consul/catalog_endpoint_test.go @@ -45,6 +45,56 @@ func TestCatalogRegister(t *testing.T) { }) } +func TestCatalogRegister_ACLDeny(t *testing.T) { + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + c.ACLToken = "root" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + client := rpcClient(t, s1) + defer client.Close() + + testutil.WaitForLeader(t, client.Call, "dc1") + + // Create the ACL + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testRegisterRules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := client.Call("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + id := out + + argR := structs.RegisterRequest{ + Datacenter: "dc1", + Node: "foo", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 8000, + }, + WriteRequest: structs.WriteRequest{Token: id}, + } + var outR struct{} + + err := client.Call("Catalog.Register", &argR, &outR) + if err == nil || !strings.Contains(err.Error(), permissionDenied) { + t.Fatalf("err: %v", err) + } +} + func TestCatalogRegister_ForwardLeader(t *testing.T) { dir1, s1 := testServer(t) defer os.RemoveAll(dir1) @@ -722,3 +772,9 @@ func TestCatalogRegister_FailedCase1(t *testing.T) { t.Fatalf("Bad: %v", out2) } } + +var testRegisterRules = ` +service "foo" { + policy = "read" +} +` diff --git a/consul/leader.go b/consul/leader.go index b41f612cb..7f4f378a6 100644 --- a/consul/leader.go +++ b/consul/leader.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "strconv" + "strings" "time" "github.com/armon/go-metrics" @@ -265,6 +266,11 @@ func (s *Server) reconcileMember(member serf.Member) error { if err != nil { s.logger.Printf("[ERR] consul: failed to reconcile member: %v: %v", member, err) + + // Permission denied should not bubble up + if strings.Contains(err.Error(), permissionDenied) { + return nil + } return err } return nil @@ -344,6 +350,7 @@ AFTER_CHECK: Status: structs.HealthPassing, Output: SerfCheckAliveOutput, }, + WriteRequest: structs.WriteRequest{Token: s.config.ACLToken}, } var out struct{} return s.endpoints.Catalog.Register(&req, &out) @@ -379,6 +386,7 @@ func (s *Server) handleFailedMember(member serf.Member) error { Status: structs.HealthCritical, Output: SerfCheckFailedOutput, }, + WriteRequest: structs.WriteRequest{Token: s.config.ACLToken}, } var out struct{} return s.endpoints.Catalog.Register(&req, &out) diff --git a/website/source/docs/internals/acl.html.markdown b/website/source/docs/internals/acl.html.markdown index 33994e3ac..b847e17da 100644 --- a/website/source/docs/internals/acl.html.markdown +++ b/website/source/docs/internals/acl.html.markdown @@ -163,5 +163,6 @@ enforced using an exact match policy. The default rule is provided using the empty string. The policy is either "read", "write", or "deny". A "write" policy implies "read", and there is no way to specify write-only. If there is no applicable rule, the `acl_default_policy` is applied. Currently, only -the "write" level is enforced for registration of services. +the "write" level is enforced for registration of services. The policy for +the "consul" service is always "write" as it is managed internally. From 402d5808632e6b7af5677bcaa272fc346fe7f42b Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 30 Nov 2014 21:10:42 -0700 Subject: [PATCH 205/238] consul: Check that ACL also allows registration --- consul/catalog_endpoint_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/consul/catalog_endpoint_test.go b/consul/catalog_endpoint_test.go index 9c4e86ea9..e2674f12b 100644 --- a/consul/catalog_endpoint_test.go +++ b/consul/catalog_endpoint_test.go @@ -50,7 +50,6 @@ func TestCatalogRegister_ACLDeny(t *testing.T) { c.ACLDatacenter = "dc1" c.ACLMasterToken = "root" c.ACLDefaultPolicy = "deny" - c.ACLToken = "root" }) defer os.RemoveAll(dir1) defer s1.Shutdown() @@ -93,6 +92,12 @@ func TestCatalogRegister_ACLDeny(t *testing.T) { if err == nil || !strings.Contains(err.Error(), permissionDenied) { t.Fatalf("err: %v", err) } + + argR.Service.Service = "foo" + err = client.Call("Catalog.Register", &argR, &outR) + if err != nil { + t.Fatalf("err: %v", err) + } } func TestCatalogRegister_ForwardLeader(t *testing.T) { @@ -775,6 +780,6 @@ func TestCatalogRegister_FailedCase1(t *testing.T) { var testRegisterRules = ` service "foo" { - policy = "read" + policy = "write" } ` From b10159a87b406e60400b3b04451c7efa4c370989 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Mon, 1 Dec 2014 11:43:01 -0800 Subject: [PATCH 206/238] agent: Handle service ACLs when doing anti-entropy --- command/agent/local.go | 56 +++++++++++++------- command/agent/local_test.go | 103 ++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 19 deletions(-) diff --git a/command/agent/local.go b/command/agent/local.go index 86b7e45ff..aff135348 100644 --- a/command/agent/local.go +++ b/command/agent/local.go @@ -1,18 +1,23 @@ package agent import ( - "github.com/hashicorp/consul/consul" - "github.com/hashicorp/consul/consul/structs" "log" "reflect" + "strings" "sync" "sync/atomic" "time" + + "github.com/hashicorp/consul/consul" + "github.com/hashicorp/consul/consul/structs" ) const ( syncStaggerIntv = 3 * time.Second syncRetryIntv = 15 * time.Second + + // permissionDenied is returned when an ACL based rejection happens + permissionDenied = "Permission denied" ) // syncStatus is used to represent the difference between @@ -292,8 +297,9 @@ SYNC: // the local syncStatus as appropriate func (l *localState) setSyncState() error { req := structs.NodeSpecificRequest{ - Datacenter: l.config.Datacenter, - Node: l.config.NodeName, + Datacenter: l.config.Datacenter, + Node: l.config.NodeName, + QueryOptions: structs.QueryOptions{Token: l.config.ACLToken}, } var out1 structs.IndexedNodeServices var out2 structs.IndexedHealthChecks @@ -403,9 +409,10 @@ func (l *localState) syncChanges() error { // deleteService is used to delete a service from the server func (l *localState) deleteService(id string) error { req := structs.DeregisterRequest{ - Datacenter: l.config.Datacenter, - Node: l.config.NodeName, - ServiceID: id, + Datacenter: l.config.Datacenter, + Node: l.config.NodeName, + ServiceID: id, + WriteRequest: structs.WriteRequest{Token: l.config.ACLToken}, } var out struct{} err := l.iface.RPC("Catalog.Deregister", &req, &out) @@ -419,9 +426,10 @@ func (l *localState) deleteService(id string) error { // deleteCheck is used to delete a service from the server func (l *localState) deleteCheck(id string) error { req := structs.DeregisterRequest{ - Datacenter: l.config.Datacenter, - Node: l.config.NodeName, - CheckID: id, + Datacenter: l.config.Datacenter, + Node: l.config.NodeName, + CheckID: id, + WriteRequest: structs.WriteRequest{Token: l.config.ACLToken}, } var out struct{} err := l.iface.RPC("Catalog.Deregister", &req, &out) @@ -435,16 +443,21 @@ func (l *localState) deleteCheck(id string) error { // syncService is used to sync a service to the server func (l *localState) syncService(id string) error { req := structs.RegisterRequest{ - Datacenter: l.config.Datacenter, - Node: l.config.NodeName, - Address: l.config.AdvertiseAddr, - Service: l.services[id], + Datacenter: l.config.Datacenter, + Node: l.config.NodeName, + Address: l.config.AdvertiseAddr, + Service: l.services[id], + WriteRequest: structs.WriteRequest{Token: l.config.ACLToken}, } var out struct{} err := l.iface.RPC("Catalog.Register", &req, &out) if err == nil { l.serviceStatus[id] = syncStatus{inSync: true} l.logger.Printf("[INFO] agent: Synced service '%s'", id) + } else if strings.Contains(err.Error(), permissionDenied) { + l.serviceStatus[id] = syncStatus{inSync: true} + l.logger.Printf("[WARN] agent: Service '%s' registration blocked by ACLs", id) + return nil } return err } @@ -460,17 +473,22 @@ func (l *localState) syncCheck(id string) error { } } req := structs.RegisterRequest{ - Datacenter: l.config.Datacenter, - Node: l.config.NodeName, - Address: l.config.AdvertiseAddr, - Service: service, - Check: l.checks[id], + Datacenter: l.config.Datacenter, + Node: l.config.NodeName, + Address: l.config.AdvertiseAddr, + Service: service, + Check: l.checks[id], + WriteRequest: structs.WriteRequest{Token: l.config.ACLToken}, } var out struct{} err := l.iface.RPC("Catalog.Register", &req, &out) if err == nil { l.checkStatus[id] = syncStatus{inSync: true} l.logger.Printf("[INFO] agent: Synced check '%s'", id) + } else if strings.Contains(err.Error(), permissionDenied) { + l.checkStatus[id] = syncStatus{inSync: true} + l.logger.Printf("[WARN] agent: Check '%s' registration blocked by ACLs", id) + return nil } return err } diff --git a/command/agent/local_test.go b/command/agent/local_test.go index 7145d28a2..ded25ad3d 100644 --- a/command/agent/local_test.go +++ b/command/agent/local_test.go @@ -132,6 +132,103 @@ func TestAgentAntiEntropy_Services(t *testing.T) { } } +func TestAgentAntiEntropy_Services_ACLDeny(t *testing.T) { + conf := nextConfig() + conf.ACLDatacenter = "dc1" + conf.ACLMasterToken = "root" + conf.ACLDefaultPolicy = "deny" + dir, agent := makeAgent(t, conf) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + // Create the ACL + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: testRegisterRules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var out string + if err := agent.RPC("ACL.Apply", &arg, &out); err != nil { + t.Fatalf("err: %v", err) + } + + // Update the agent ACL token, resume sync + conf.ACLToken = out + + // Create service (Allowed) + srv1 := &structs.NodeService{ + ID: "mysql", + Service: "mysql", + Tags: []string{"master"}, + Port: 5000, + } + agent.state.AddService(srv1) + + // Create service (Disallowed) + srv2 := &structs.NodeService{ + ID: "api", + Service: "api", + Tags: []string{"foo"}, + Port: 5001, + } + agent.state.AddService(srv2) + + // Trigger anti-entropy run and wait + agent.StartSync() + time.Sleep(200 * time.Millisecond) + + // Verify that we are in sync + req := structs.NodeSpecificRequest{ + Datacenter: "dc1", + Node: agent.config.NodeName, + } + var services structs.IndexedNodeServices + if err := agent.RPC("Catalog.NodeServices", &req, &services); err != nil { + t.Fatalf("err: %v", err) + } + + // We should have 2 services (consul included) + if len(services.NodeServices.Services) != 2 { + t.Fatalf("bad: %v", services.NodeServices.Services) + } + + // All the services should match + for id, serv := range services.NodeServices.Services { + switch id { + case "mysql": + t.Fatalf("should not be permitted") + case "api": + if !reflect.DeepEqual(serv, srv2) { + t.Fatalf("bad: %#v %#v", serv, srv2) + } + case "consul": + // ignore + default: + t.Fatalf("unexpected service: %v", id) + } + } + + // Check the local state + if len(agent.state.services) != 3 { + t.Fatalf("bad: %v", agent.state.services) + } + if len(agent.state.serviceStatus) != 3 { + t.Fatalf("bad: %v", agent.state.serviceStatus) + } + for name, status := range agent.state.serviceStatus { + if !status.inSync { + t.Fatalf("should be in sync: %v %v", name, status) + } + } +} + func TestAgentAntiEntropy_Checks(t *testing.T) { conf := nextConfig() dir, agent := makeAgent(t, conf) @@ -327,3 +424,9 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) { } } } + +var testRegisterRules = ` +service "api" { + policy = "write" +} +` From 693fa207c86d65bf9336ae65fb40533fa0abf283 Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Wed, 3 Dec 2014 22:46:26 -0600 Subject: [PATCH 207/238] Small doc update for exec command the documentation for -tag says it must be used with -service, but example used is -server, which doesn't work. This changes -server to -service. --- website/source/docs/commands/exec.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/commands/exec.html.markdown b/website/source/docs/commands/exec.html.markdown index 98c0af18e..571d56dc1 100644 --- a/website/source/docs/commands/exec.html.markdown +++ b/website/source/docs/commands/exec.html.markdown @@ -49,7 +49,7 @@ The list of available flags are: * `-tag` - Regular expression to filter to only nodes with a service that has a matching tag. This must be used with `-service`. As an example, you may - do "-server mysql -tag slave". + do "-service mysql -tag slave". * `-wait` - Specifies the period of time in which no agent's respond before considering the job finished. This is basically the quiescent time required to assume completion. From 850d5bdc32859b40810c94db28e815ca698b4fdf Mon Sep 17 00:00:00 2001 From: Veres Lajos Date: Thu, 4 Dec 2014 23:25:06 +0000 Subject: [PATCH 208/238] typofixes - https://github.com/vlajos/misspell_fixer --- CHANGELOG.md | 6 +++--- acl/cache.go | 2 +- command/agent/agent.go | 2 +- command/agent/dns.go | 2 +- command/agent/local.go | 8 ++++---- command/agent/local_test.go | 4 ++-- command/agent/log_writer.go | 2 +- command/agent/ui_endpoint_test.go | 2 +- consul/pool.go | 2 +- terraform/aws/scripts/server.sh | 2 +- ui/javascripts/app/controllers.js | 2 +- ui/javascripts/libs/classie.js | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c44ffd7..802dc6777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -204,11 +204,11 @@ FEATURES: * /v1/health/service/ endpoint can take an optional `?passing` flag to filter to only nodes with passing results. [GH-57] * The KV endpoint suports listing keys with the `?keys` query parameter, - and limited up to a seperator using `?seperator=`. + and limited up to a separator using `?separator=`. IMPROVEMENTS: - * Health check output goes into seperate `Output` field instead + * Health check output goes into separate `Output` field instead of overriding `Notes`. [GH-59] * Adding a minimum check interval to prevent checks with extremely low intervals fork bombing. [GH-64] @@ -227,7 +227,7 @@ BUG FIXES: * DNS parser can handler period in a tag name. [GH-39] * "application/json" content-type is sent on HTTP requests. [GH-45] * Work around for LMDB delete issue. [GH-85] - * Fixed tag gossip propogation for rapid restart. [GH-86] + * Fixed tag gossip propagation for rapid restart. [GH-86] MISC: diff --git a/acl/cache.go b/acl/cache.go index 6e7295562..069d13ba7 100644 --- a/acl/cache.go +++ b/acl/cache.go @@ -26,7 +26,7 @@ type Cache struct { ruleCache *lru.Cache // Cache rules -> policy } -// NewCache contructs a new policy and ACL cache of a given size +// NewCache constructs a new policy and ACL cache of a given size func NewCache(size int, faultfn FaultFunc) (*Cache, error) { if size <= 0 { return nil, fmt.Errorf("Must provide positive cache size") diff --git a/command/agent/agent.go b/command/agent/agent.go index facb260b7..cbda67e7a 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -363,7 +363,7 @@ func (a *Agent) Leave() error { } // Shutdown is used to hard stop the agent. Should be -// preceeded by a call to Leave to do it gracefully. +// preceded by a call to Leave to do it gracefully. func (a *Agent) Shutdown() error { a.shutdownLock.Lock() defer a.shutdownLock.Unlock() diff --git a/command/agent/dns.go b/command/agent/dns.go index 43ea245df..ec56fbfc5 100644 --- a/command/agent/dns.go +++ b/command/agent/dns.go @@ -194,7 +194,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { var out structs.IndexedNodes // TODO: Replace ListNodes with an internal RPC that can do the filter - // server side to avoid transfering the entire node list. + // server side to avoid transferring the entire node list. if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil { for _, n := range out.Nodes { arpa, _ := dns.ReverseAddr(n.Address) diff --git a/command/agent/local.go b/command/agent/local.go index aff135348..822546539 100644 --- a/command/agent/local.go +++ b/command/agent/local.go @@ -52,7 +52,7 @@ type localState struct { checks map[string]*structs.HealthCheck checkStatus map[string]syncStatus - // Used to track checks that are being defered + // Used to track checks that are being deferred deferCheck map[string]*time.Timer // consulCh is used to inform of a change to the known @@ -101,13 +101,13 @@ func (l *localState) ConsulServerUp() { } } -// Pause is used to pause state syncronization, this can be +// Pause is used to pause state synchronization, this can be // used to make batch changes func (l *localState) Pause() { atomic.StoreInt32(&l.paused, 1) } -// Resume is used to resume state syncronization +// Resume is used to resume state synchronization func (l *localState) Resume() { atomic.StoreInt32(&l.paused, 0) l.changeMade() @@ -390,7 +390,7 @@ func (l *localState) syncChanges() error { return err } } else if !status.inSync { - // Cancel a defered sync + // Cancel a deferred sync if timer := l.deferCheck[id]; timer != nil { timer.Stop() delete(l.deferCheck, id) diff --git a/command/agent/local_test.go b/command/agent/local_test.go index ded25ad3d..7595b8fc1 100644 --- a/command/agent/local_test.go +++ b/command/agent/local_test.go @@ -389,7 +389,7 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) { t.Fatalf("checks: %v", check) } - // Update the check output! Should be defered + // Update the check output! Should be deferred agent.state.UpdateCheck("web", structs.HealthPassing, "output") // Should not update for 100 milliseconds @@ -408,7 +408,7 @@ func TestAgentAntiEntropy_Check_DeferSync(t *testing.T) { } } - // Wait for a defered update + // Wait for a deferred update time.Sleep(100 * time.Millisecond) if err := agent.RPC("Health.NodeChecks", &req, &checks); err != nil { t.Fatalf("err: %v", err) diff --git a/command/agent/log_writer.go b/command/agent/log_writer.go index eaae8b0df..e6e76d0e8 100644 --- a/command/agent/log_writer.go +++ b/command/agent/log_writer.go @@ -29,7 +29,7 @@ func NewLogWriter(buf int) *logWriter { } } -// RegisterHandler adds a log handler to recieve logs, and sends +// RegisterHandler adds a log handler to receive logs, and sends // the last buffered logs to the handler func (l *logWriter) RegisterHandler(lh LogHandler) { l.Lock() diff --git a/command/agent/ui_endpoint_test.go b/command/agent/ui_endpoint_test.go index 709a6d4f9..7872ce7be 100644 --- a/command/agent/ui_endpoint_test.go +++ b/command/agent/ui_endpoint_test.go @@ -41,7 +41,7 @@ func TestUiIndex(t *testing.T) { t.Fatalf("err: %v", err) } - // Verify teh response + // Verify the response if resp.StatusCode != 200 { t.Fatalf("bad: %v", resp) } diff --git a/consul/pool.go b/consul/pool.go index 495ab2bd5..523cfd525 100644 --- a/consul/pool.go +++ b/consul/pool.go @@ -114,7 +114,7 @@ func (c *Conn) returnClient(client *StreamClient) { // Consul servers. This is used to reduce the latency of // RPC requests between servers. It is only used to pool // connections in the rpcConsul mode. Raft connections -// are pooled seperately. +// are pooled separately. type ConnPool struct { sync.Mutex diff --git a/terraform/aws/scripts/server.sh b/terraform/aws/scripts/server.sh index c30588def..779114e28 100755 --- a/terraform/aws/scripts/server.sh +++ b/terraform/aws/scripts/server.sh @@ -9,6 +9,6 @@ cat >/tmp/consul_flags << EOF export CONSUL_FLAGS="-server -bootstrap-expect=${SERVER_COUNT} -data-dir=/mnt/consul" EOF -# Write it to the full sevice file +# Write it to the full service file sudo mv /tmp/consul_flags /etc/service/consul chmod 0644 /etc/service/consul diff --git a/ui/javascripts/app/controllers.js b/ui/javascripts/app/controllers.js index 7aac6f07c..4aad1f42e 100644 --- a/ui/javascripts/app/controllers.js +++ b/ui/javascripts/app/controllers.js @@ -460,7 +460,7 @@ App.AclsShowController = Ember.ObjectController.extend({ }).then(function(response) { controller.transitionToRoute('acls.show', response.ID); controller.set('isLoading', false); - notify('Succesfully cloned token', 4000); + notify('Successfully cloned token', 4000); }).fail(function(response) { // Render the error message on the form if the request failed controller.set('errorMessage', 'Received error while processing: ' + response.statusText); diff --git a/ui/javascripts/libs/classie.js b/ui/javascripts/libs/classie.js index a96755488..a70914c4f 100755 --- a/ui/javascripts/libs/classie.js +++ b/ui/javascripts/libs/classie.js @@ -22,7 +22,7 @@ function classReg( className ) { } // classList support for class management -// altho to be fair, the api sucks because it won't accept multiple classes at once +// although to be fair, the api sucks because it won't accept multiple classes at once var hasClass, addClass, removeClass; if ( 'classList' in document.documentElement ) { From 12966dc9f77e19a7260382c95bc4e089f7976309 Mon Sep 17 00:00:00 2001 From: Chavez Date: Thu, 4 Dec 2014 21:24:05 -0800 Subject: [PATCH 209/238] Vagrant updates * Add go tools to provision script * Sync project folder * Fix ruby styling issues * Add --cover flag to test commands --- Makefile | 4 ++-- Vagrantfile | 28 ++++++++++++++++------------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index ddf0c548a..3eb003341 100644 --- a/Makefile +++ b/Makefile @@ -16,10 +16,10 @@ deps: test: deps ./scripts/verify_no_uuid.sh - go list ./... | xargs -n1 go test + go list ./... | xargs -n1 go test --cover integ: - go list ./... | INTEG_TESTS=yes xargs -n1 go test + go list ./... | INTEG_TESTS=yes xargs -n1 go test -test --cover format: deps @echo "--> Running go fmt" diff --git a/Vagrantfile b/Vagrantfile index 2cb6c7f0d..101ce30d8 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -2,9 +2,9 @@ # vi: set ft=ruby : # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" +VAGRANTFILE_API_VERSION = '2' -$script = <