From 4bb5da3e08a7233a4083bf5f23232a0cf49ad067 Mon Sep 17 00:00:00 2001 From: Krishnaswamy Subramanian Date: Sat, 22 Sep 2018 22:57:45 +0530 Subject: [PATCH] Use consul go library to register service --- .../internal/mock/register/discovery.go | 41 ++- cmd/stolonctl/cmd/register.go | 36 +-- cmd/stolonctl/cmd/register/config.go | 55 ++++ cmd/stolonctl/cmd/register/config_test.go | 66 ++++ cmd/stolonctl/cmd/register/discovery.go | 69 ++-- cmd/stolonctl/cmd/register/discovery_test.go | 68 +--- cmd/stolonctl/cmd/register/serviceinfo.go | 31 +- .../cmd/register/serviceinfo_test.go | 39 ++- cmd/stolonctl/cmd/register_test.go | 13 +- vendor/github.com/hashicorp/consul/NOTICE.md | 3 + vendor/github.com/hashicorp/consul/api/acl.go | 2 +- .../github.com/hashicorp/consul/api/agent.go | 213 ++++++++++-- vendor/github.com/hashicorp/consul/api/api.go | 66 +++- .../hashicorp/consul/api/catalog.go | 22 +- .../hashicorp/consul/api/connect.go | 12 + .../hashicorp/consul/api/connect_ca.go | 172 ++++++++++ .../hashicorp/consul/api/connect_intention.go | 302 ++++++++++++++++++ .../github.com/hashicorp/consul/api/health.go | 19 +- .../github.com/hashicorp/consul/api/lock.go | 5 +- .../hashicorp/consul/api/operator_area.go | 3 +- .../hashicorp/consul/api/prepared_query.go | 16 +- .../hashicorp/consul/api/semaphore.go | 5 +- .../ui-v2/app/styles/components/notice.scss | 4 + .../source/api/operator/license.html.md | 143 +++++++++ .../docs/commands/license.html.markdown.erb | 109 +++++++ 25 files changed, 1314 insertions(+), 200 deletions(-) rename cmd/stolonctl/cmd/{register => }/internal/mock/register/discovery.go (60%) create mode 100644 cmd/stolonctl/cmd/register/config.go create mode 100644 cmd/stolonctl/cmd/register/config_test.go create mode 100644 vendor/github.com/hashicorp/consul/NOTICE.md create mode 100644 vendor/github.com/hashicorp/consul/api/connect.go create mode 100644 vendor/github.com/hashicorp/consul/api/connect_ca.go create mode 100644 vendor/github.com/hashicorp/consul/api/connect_intention.go create mode 100644 vendor/github.com/hashicorp/consul/ui-v2/app/styles/components/notice.scss create mode 100644 vendor/github.com/hashicorp/consul/website/source/api/operator/license.html.md create mode 100644 vendor/github.com/hashicorp/consul/website/source/docs/commands/license.html.markdown.erb diff --git a/cmd/stolonctl/cmd/register/internal/mock/register/discovery.go b/cmd/stolonctl/cmd/internal/mock/register/discovery.go similarity index 60% rename from cmd/stolonctl/cmd/register/internal/mock/register/discovery.go rename to cmd/stolonctl/cmd/internal/mock/register/discovery.go index af2733d9d..ec1d2a39b 100644 --- a/cmd/stolonctl/cmd/register/internal/mock/register/discovery.go +++ b/cmd/stolonctl/cmd/internal/mock/register/discovery.go @@ -6,8 +6,8 @@ package mock_register import ( gomock "github.com/golang/mock/gomock" + api "github.com/hashicorp/consul/api" register "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" - http "net/http" reflect "reflect" ) @@ -46,38 +46,37 @@ func (mr *MockServiceDiscoveryMockRecorder) Register(info interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockServiceDiscovery)(nil).Register), info) } -// MockHTTPClient is a mock of HTTPClient interface -type MockHTTPClient struct { +// MockConsulAgent is a mock of ConsulAgent interface +type MockConsulAgent struct { ctrl *gomock.Controller - recorder *MockHTTPClientMockRecorder + recorder *MockConsulAgentMockRecorder } -// MockHTTPClientMockRecorder is the mock recorder for MockHTTPClient -type MockHTTPClientMockRecorder struct { - mock *MockHTTPClient +// MockConsulAgentMockRecorder is the mock recorder for MockConsulAgent +type MockConsulAgentMockRecorder struct { + mock *MockConsulAgent } -// NewMockHTTPClient creates a new mock instance -func NewMockHTTPClient(ctrl *gomock.Controller) *MockHTTPClient { - mock := &MockHTTPClient{ctrl: ctrl} - mock.recorder = &MockHTTPClientMockRecorder{mock} +// NewMockConsulAgent creates a new mock instance +func NewMockConsulAgent(ctrl *gomock.Controller) *MockConsulAgent { + mock := &MockConsulAgent{ctrl: ctrl} + mock.recorder = &MockConsulAgentMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockHTTPClient) EXPECT() *MockHTTPClientMockRecorder { +func (m *MockConsulAgent) EXPECT() *MockConsulAgentMockRecorder { return m.recorder } -// Do mocks base method -func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { - ret := m.ctrl.Call(m, "Do", req) - ret0, _ := ret[0].(*http.Response) - ret1, _ := ret[1].(error) - return ret0, ret1 +// ServiceRegister mocks base method +func (m *MockConsulAgent) ServiceRegister(service *api.AgentServiceRegistration) error { + ret := m.ctrl.Call(m, "ServiceRegister", service) + ret0, _ := ret[0].(error) + return ret0 } -// Do indicates an expected call of Do -func (mr *MockHTTPClientMockRecorder) Do(req interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockHTTPClient)(nil).Do), req) +// ServiceRegister indicates an expected call of ServiceRegister +func (mr *MockConsulAgentMockRecorder) ServiceRegister(service interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServiceRegister", reflect.TypeOf((*MockConsulAgent)(nil).ServiceRegister), service) } diff --git a/cmd/stolonctl/cmd/register.go b/cmd/stolonctl/cmd/register.go index 39d429ba0..c4582171a 100644 --- a/cmd/stolonctl/cmd/register.go +++ b/cmd/stolonctl/cmd/register.go @@ -32,31 +32,21 @@ var Register = &cobra.Command{ Version: cmd.Version, } -type registerConfig struct { - registerBackend string - registerEndpoints string -} - -var rCfg registerConfig +var rCfg register.Config var log = slog.S() func init() { - Register.PersistentFlags().StringVar(&rCfg.registerBackend, "register-backend", "consul", "register backend type (consul)") - Register.PersistentFlags().StringVar(&rCfg.registerEndpoints, "register-endpoints", "http://127.0.0.1:8500", "a common-delimited list of store endpoints (use https scheme for tls communication) defaults: http://127.0.0.1:8500 for consul") + Register.PersistentFlags().StringVar(&rCfg.Backend, "register-backend", "consul", "register backend type (consul)") + Register.PersistentFlags().StringVar(&rCfg.Endpoints, "register-endpoints", "http://127.0.0.1:8500", "a common-delimited list of register endpoints (use https scheme for tls communication) defaults: http://127.0.0.1:8500 for consul") Register.PersistentFlags().BoolVar(&cfg.Debug, "debug", false, "enable debug logging") CmdStolonCtl.AddCommand(Register) } -func checkConfig(cfg *config, rCfg *registerConfig) error { +func checkConfig(cfg *config, rCfg *register.Config) error { if err := cmd.CheckCommonConfig(&cfg.CommonConfig); err != nil { return err } - switch rCfg.registerBackend { - case "consul": - default: - return fmt.Errorf("unknown register backend: %q", rCfg.registerBackend) - } - return nil + return rCfg.Validate() } func runRegister(c *cobra.Command, _ []string) { @@ -87,7 +77,7 @@ func runRegister(c *cobra.Command, _ []string) { } } -func registerCluster(cfg *config, rCfg *registerConfig) error { +func registerCluster(cfg *config, rCfg *register.Config) error { store, err := cmd.NewStore(&cfg.CommonConfig) if err != nil { return err @@ -98,17 +88,17 @@ func registerCluster(cfg *config, rCfg *registerConfig) error { return fmt.Errorf("cannot get cluster data: %v", err) } - service, err := register.NewServiceDiscovery(rCfg.registerBackend, rCfg.registerEndpoints) + service, err := register.NewServiceDiscovery(rCfg) if err != nil { return err } if master, err := cluster.Master(); err == nil { - log.Debugf("found master %s with uid %s", master.Name, master.Id) + log.Debugf("found master %s with uid %s", master.Name, master.ID) if err = service.Register(master); err != nil { - log.Errorf("unable to register master %s with uid %s, reason: %s", master.Name, master.Id, err.Error()) + log.Errorf("unable to register master %s with uid %s, reason: %s", master.Name, master.ID, err.Error()) } else { - log.Infof("successfully registered master %s with uid %s", master.Name, master.Id) + log.Infof("successfully registered master %s with uid %s", master.Name, master.ID) } } else { log.Warnf("no master found %s", err.Error()) @@ -116,11 +106,11 @@ func registerCluster(cfg *config, rCfg *registerConfig) error { if slaves, err := cluster.Slaves(); err == nil { for _, slave := range slaves { - log.Debugf("found slave %s with uid %s", slave.Name, slave.Id) + log.Debugf("found slave %s with uid %s", slave.Name, slave.ID) if err = service.Register(&slave); err != nil { - log.Errorf("unable to register slave %s with uid %s, reason: %s", slave.Name, slave.Id, err.Error()) + log.Errorf("unable to register slave %s with uid %s, reason: %s", slave.Name, slave.ID, err.Error()) } else { - log.Infof("successfully registered slave %s with uid %s", slave.Name, slave.Id) + log.Infof("successfully registered slave %s with uid %s", slave.Name, slave.ID) } } } else { diff --git a/cmd/stolonctl/cmd/register/config.go b/cmd/stolonctl/cmd/register/config.go new file mode 100644 index 000000000..a216a4d21 --- /dev/null +++ b/cmd/stolonctl/cmd/register/config.go @@ -0,0 +1,55 @@ +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and + +package register + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/consul/api" +) + +// Config represents necessary configurations which can passed +// for registering master and slave info for service discovery +type Config struct { + Backend string + Endpoints string +} + +// Validate returns nil if the config is valid, else returns error with +// appropriate reason +func (config *Config) Validate() error { + switch config.Backend { + case "consul": + addresses := strings.Split(config.Endpoints, ",") + if len(addresses) != 1 { + return fmt.Errorf("consul does not support multiple endpoints: %s", config.Endpoints) + } + _, err := url.Parse(config.Endpoints) + return err + default: + return fmt.Errorf("unknown register backend: %q", config.Backend) + } +} + +// ConsulConfig returns consul.api.ConsulConfig if register endpoint is valid consul url +// else will return error with appropriate reason +func (config *Config) ConsulConfig() (*api.Config, error) { + url, err := url.Parse(config.Endpoints) + if err != nil { + return nil, err + } + return &api.Config{Address: url.Host, Scheme: url.Scheme}, nil +} diff --git a/cmd/stolonctl/cmd/register/config_test.go b/cmd/stolonctl/cmd/register/config_test.go new file mode 100644 index 000000000..cbb352eaa --- /dev/null +++ b/cmd/stolonctl/cmd/register/config_test.go @@ -0,0 +1,66 @@ +// Copyright 2015 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and + +package register + +import "testing" + +func TestRegisterConfig(t *testing.T) { + t.Run("validate", func(t *testing.T) { + t.Run("should check for consul register backend", func(t *testing.T) { + config := Config{Backend: "something other than consul"} + err := config.Validate() + + if err == nil || err.Error() != "unknown register backend: \"something other than consul\"" { + t.Errorf("expected unknown register backend but got %s", err.Error()) + } + }) + + t.Run("should not return any error if all valid configurations are specified", func(t *testing.T) { + config := Config{Backend: "consul"} + err := config.Validate() + + if err != nil { + t.Errorf("expected no error but got '%v'", err.Error()) + } + }) + + t.Run("should not support multiple addresses", func(t *testing.T) { + config := Config{Backend: "consul", Endpoints: "http://127.0.0.1:8500,http://127.0.0.2:8500"} + err := config.Validate() + + if err == nil || err.Error() != "consul does not support multiple endpoints: http://127.0.0.1:8500,http://127.0.0.2:8500" { + t.Errorf("expected unknown register backend but got %s", err.Error()) + } + }) + }) + + t.Run("config", func(t *testing.T) { + t.Run("should return config", func(t *testing.T) { + c := Config{Backend: "consul", Endpoints: "http://127.0.0.1:8500"} + config, err := c.ConsulConfig() + + if err != nil { + t.Errorf("expected error to be nil but got %s", err.Error()) + } + + if config.Address != "127.0.0.1:8500" { + t.Errorf("expected address to be %s but got %s", c.Endpoints, config.Address) + } + + if config.Scheme != "http" { + t.Errorf("expected address to be http but got %s", config.Scheme) + } + }) + }) +} diff --git a/cmd/stolonctl/cmd/register/discovery.go b/cmd/stolonctl/cmd/register/discovery.go index 265cec81d..48c805721 100644 --- a/cmd/stolonctl/cmd/register/discovery.go +++ b/cmd/stolonctl/cmd/register/discovery.go @@ -15,13 +15,9 @@ package register import ( - "bytes" - "encoding/json" "errors" - "fmt" - "net/http" - "net/url" - "path" + + "github.com/hashicorp/consul/api" ) // ServiceDiscovery helps to register service @@ -30,63 +26,38 @@ type ServiceDiscovery interface { } // NewServiceDiscovery creates a Discovery from registerBackend and registerEndpoints -func NewServiceDiscovery(registerBackend string, registerEndpoints string) (ServiceDiscovery, error) { - switch registerBackend { +func NewServiceDiscovery(config *Config) (ServiceDiscovery, error) { + switch config.Backend { case "consul": - return NewConsulServiceDiscovery(&http.Client{}, registerEndpoints), nil + if apiConfig, err := config.ConsulConfig(); err != nil { + return nil, err + } else if client, err := api.NewClient(apiConfig); err != nil { + return nil, err + } else { + agent := client.Agent() + return NewConsulServiceDiscovery(agent), nil + } default: return nil, errors.New("register backend not supported") } } -// A HTTPClient has necessary method to make http calls -type HTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} - // ConsulServiceDiscovery helps to register service to consul type ConsulServiceDiscovery struct { - client HTTPClient - registerEndpoints string + agent ConsulAgent } -// NewConsulServiceDiscovery creates a new ConsulDiscovery -func NewConsulServiceDiscovery(client HTTPClient, registerEndpoints string) ServiceDiscovery { - return &ConsulServiceDiscovery{client: client, registerEndpoints: registerEndpoints} +// ConsulAgent interface holds all the necessary methods to interact with consul agent +type ConsulAgent interface { + ServiceRegister(service *api.AgentServiceRegistration) error } -func (cd *ConsulServiceDiscovery) baseURL() (*url.URL, error) { - return url.Parse(cd.registerEndpoints) -} - -func (cd *ConsulServiceDiscovery) registerURL() (*string, error) { - baseURL, err := cd.baseURL() - if err != nil { - return nil, err - } - - baseURL.Path = path.Join(baseURL.Path, "/v1/agent/service/register") - result := baseURL.String() - return &result, nil +// NewConsulServiceDiscovery creates a new ConsulDiscovery +func NewConsulServiceDiscovery(agent ConsulAgent) ServiceDiscovery { + return &ConsulServiceDiscovery{agent: agent} } // Register registers the given service info to consul func (cd *ConsulServiceDiscovery) Register(info *ServiceInfo) error { - registerURL, err := cd.registerURL() - if err != nil { - return err - } - body, err := json.Marshal(info) - request, err := http.NewRequest(http.MethodPut, *registerURL, bytes.NewBuffer(body)) - request.Header.Set("Content-Type", "application/json") - if err != nil { - return err - } - - if response, err := cd.client.Do(request); err != nil { - return err - } else if response.StatusCode != 200 { - return fmt.Errorf("expected 200 to be returned but instead got %v from %s", response.StatusCode, *registerURL) - } - return nil + return cd.agent.ServiceRegister(info.ConsulAgentServiceRegistration()) } diff --git a/cmd/stolonctl/cmd/register/discovery_test.go b/cmd/stolonctl/cmd/register/discovery_test.go index 2cd379a98..15a1802ab 100644 --- a/cmd/stolonctl/cmd/register/discovery_test.go +++ b/cmd/stolonctl/cmd/register/discovery_test.go @@ -15,21 +15,19 @@ package register_test import ( - "bytes" - "encoding/json" "errors" - "net/http" "testing" + mock_register "github.com/sorintlab/stolon/cmd/stolonctl/cmd/internal/mock/register" "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" - "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register/internal/mock/register" "github.com/golang/mock/gomock" ) func TestNewServiceDiscovery(t *testing.T) { t.Run("should return consul service discovery", func(t *testing.T) { - sd, err := register.NewServiceDiscovery("consul", "http://127.0.0.1") + config := register.Config{Backend: "consul", Endpoints: "http://127.0.0.1"} + sd, err := register.NewServiceDiscovery(&config) if err != nil { t.Errorf("expected error to be nil but was %s", err.Error()) @@ -44,11 +42,11 @@ func TestConsulServiceDiscovery(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mock_register.NewMockHTTPClient(ctrl) - serviceDiscovery := register.NewConsulServiceDiscovery(client, "http://127.0.0.1") + client := mock_register.NewMockConsulAgent(ctrl) + serviceDiscovery := register.NewConsulServiceDiscovery(client) expectedServiceInfo := register.ServiceInfo{ Name: "service", - Id: "1", + ID: "1", Port: 5432, Address: "127.0.0.1", Tags: []string{"tag"}, @@ -58,39 +56,8 @@ func TestConsulServiceDiscovery(t *testing.T) { TCP: "tcp", }, } - expectedRequestURL := "http://127.0.0.1/v1/agent/service/register" - expectedContentType := "application/json" - client.EXPECT().Do(gomock.Any()).DoAndReturn(func(req *http.Request) { - contentType := req.Header.Get("Content-Type") - requestURL := req.URL.String() - actualServiceInfo := register.ServiceInfo{} - buf := new(bytes.Buffer) - buf.ReadFrom(req.Body) - json.Unmarshal(buf.Bytes(), &actualServiceInfo) - - if contentType != expectedContentType { - t.Errorf("expected content type to %s but was %s", expectedContentType, contentType) - } else if requestURL != expectedRequestURL { - t.Errorf("expected request url to equal '%s' but was %s", expectedRequestURL, requestURL) - } else if actualServiceInfo.Name != expectedServiceInfo.Name { - t.Errorf("expected service name to be %s but was %s", expectedServiceInfo.Name, actualServiceInfo.Name) - } else if actualServiceInfo.Address != expectedServiceInfo.Address { - t.Errorf("expected service address to be %s but was %s", expectedServiceInfo.Address, actualServiceInfo.Address) - } else if actualServiceInfo.Port != expectedServiceInfo.Port { - t.Errorf("expected service port to be %d but was %d", expectedServiceInfo.Port, actualServiceInfo.Port) - } else if actualServiceInfo.Id != expectedServiceInfo.Id { - t.Errorf("expected service id to be %s but was %s", expectedServiceInfo.Id, actualServiceInfo.Id) - } else if actualServiceInfo.Tags[0] != "tag" { - t.Errorf("expected service tags to be %v but was %v", expectedServiceInfo.Tags[0], actualServiceInfo.Tags[0]) - } else if actualServiceInfo.Check.Name != expectedServiceInfo.Check.Name { - t.Errorf("expected service check name to be %s but was %s", expectedServiceInfo.Check.Name, actualServiceInfo.Check.Name) - } else if actualServiceInfo.Check.Interval != expectedServiceInfo.Check.Interval { - t.Errorf("expected service check interval to be %s but was %s", expectedServiceInfo.Check.Interval, actualServiceInfo.Check.Interval) - } else if actualServiceInfo.Check.TCP != expectedServiceInfo.Check.TCP { - t.Errorf("expected service check type to be %s but was %s", expectedServiceInfo.Check.TCP, actualServiceInfo.Check.TCP) - } - }).Return(&http.Response{StatusCode: 200}, nil) + client.EXPECT().ServiceRegister(expectedServiceInfo.ConsulAgentServiceRegistration()) err := serviceDiscovery.Register(&expectedServiceInfo) @@ -103,9 +70,9 @@ func TestConsulServiceDiscovery(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mock_register.NewMockHTTPClient(ctrl) - serviceDiscovery := register.NewConsulServiceDiscovery(client, "http://127.0.0.1") - client.EXPECT().Do(gomock.Any()).Return(&http.Response{}, errors.New("something went wrong")) + client := mock_register.NewMockConsulAgent(ctrl) + serviceDiscovery := register.NewConsulServiceDiscovery(client) + client.EXPECT().ServiceRegister(gomock.Any()).Return(errors.New("something went wrong")) err := serviceDiscovery.Register(®ister.ServiceInfo{}) @@ -113,19 +80,4 @@ func TestConsulServiceDiscovery(t *testing.T) { t.Errorf("expected error to be something went wrong") } }) - - t.Run("should return error if response code is not 200", func(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - client := mock_register.NewMockHTTPClient(ctrl) - serviceDiscovery := register.NewConsulServiceDiscovery(client, "http://127.0.0.1") - client.EXPECT().Do(gomock.Any()).Return(&http.Response{StatusCode: 404}, nil) - - err := serviceDiscovery.Register(®ister.ServiceInfo{}) - - if err == nil || err.Error() != "expected 200 to be returned but instead got 404 from http://127.0.0.1/v1/agent/service/register" { - t.Errorf("expected 200 to be returned but instead got 404 from http://127.0.0.1/v1/agent/service/register") - } - }) } diff --git a/cmd/stolonctl/cmd/register/serviceinfo.go b/cmd/stolonctl/cmd/register/serviceinfo.go index 5b91500fd..60a93a6a3 100644 --- a/cmd/stolonctl/cmd/register/serviceinfo.go +++ b/cmd/stolonctl/cmd/register/serviceinfo.go @@ -15,41 +15,62 @@ package register import ( - "errors" "fmt" "net" "strconv" + "github.com/hashicorp/consul/api" "github.com/sorintlab/stolon/internal/cluster" ) +// HealthCheck holds necessary information for performing +// health check on the service type HealthCheck struct { Name string TCP string Interval string } -// ServiceInfo represents db as a service +// ServiceInfo holds the necessary information about a service +// for service discovery type ServiceInfo struct { Name string Tags []string - Id string + ID string Address string Port int Check HealthCheck } +// ConsulAgentServiceRegistration returns AgentServiceRegistration +func (info *ServiceInfo) ConsulAgentServiceRegistration() *api.AgentServiceRegistration { + check := api.AgentServiceCheck{ + Name: info.Check.Name, + TCP: info.Check.TCP, + Interval: info.Check.Interval, + } + service := api.AgentServiceRegistration{ + ID: info.ID, + Name: info.Name, + Address: info.Address, + Tags: info.Tags, + Port: info.Port, + Check: &check, + } + return &service +} + // NewServiceInfo return new ServiceInfo from name, db and tags func NewServiceInfo(name string, db *cluster.DB, tags []string) (*ServiceInfo, error) { port, err := strconv.Atoi(db.Status.Port) if err != nil { - return nil, errors.New(fmt.Sprintf("invalid database port %s", db.Status.Port)) + return nil, fmt.Errorf(fmt.Sprintf("invalid database port %s", db.Status.Port)) } return &ServiceInfo{ Name: name, Tags: tags, Address: db.Status.ListenAddress, - Id: db.UID, + ID: db.UID, Port: port, Check: HealthCheck{ Name: "health-check", diff --git a/cmd/stolonctl/cmd/register/serviceinfo_test.go b/cmd/stolonctl/cmd/register/serviceinfo_test.go index 13ebf7436..96900a2cf 100644 --- a/cmd/stolonctl/cmd/register/serviceinfo_test.go +++ b/cmd/stolonctl/cmd/register/serviceinfo_test.go @@ -40,8 +40,8 @@ func TestNewServiceInfo(t *testing.T) { t.Errorf("expected address to be %v but got %v", "127.0.0.1", actual.Address) } else if actual.Port != 5432 { t.Errorf("expected port to be %v but got %v", "5432", actual.Port) - } else if actual.Id != id { - t.Errorf("expected id to be %v but got %v", id, actual.Id) + } else if actual.ID != id { + t.Errorf("expected id to be %v but got %v", id, actual.ID) } }) @@ -58,6 +58,41 @@ func TestNewServiceInfo(t *testing.T) { } else if err == nil || err.Error() != "invalid database port cat" { t.Errorf("expected invalid database port error") } + }) +} +func TestConsulAgentServiceRegistration(t *testing.T) { + t.Run("should return consul agent service registration", func(t *testing.T) { + tags := []string{"tags"} + id := "unique" + service, _ := NewServiceInfo("test", &cluster.DB{ + UID: id, + Status: cluster.DBStatus{ + ListenAddress: "127.0.0.1", + Port: "5432"}, + }, tags) + + actual := service.ConsulAgentServiceRegistration() + + if actual == nil { + t.Errorf("expected consul agent service registration not to be nil") + } + if actual.ID != service.ID { + t.Errorf("expected id to be %s but was %s", service.ID, actual.ID) + } else if actual.Name != service.Name { + t.Errorf("expected name to be %s but was %s", service.Name, actual.Name) + } else if actual.Address != service.Address { + t.Errorf("expected Address to be %s but was %s", service.Address, actual.Name) + } else if !reflect.DeepEqual(service.Tags, actual.Tags) { + t.Errorf("expected tags to be %v but was %v", service.Tags, actual.Tags) + } else if actual.Port != service.Port { + t.Errorf("expected port to be %d but was %d", service.Port, actual.Port) + } else if actual.Check.Name != service.Check.Name { + t.Errorf("expected check name to be %s but was %s", service.Check.Name, actual.Check.Name) + } else if actual.Check.TCP != service.Check.TCP { + t.Errorf("expected check tcp to be %s but was %s", service.Check.TCP, actual.Check.TCP) + } else if actual.Check.Interval != service.Check.Interval { + t.Errorf("expected check interval to be %s but was %s", service.Check.Interval, actual.Check.Interval) + } }) } diff --git a/cmd/stolonctl/cmd/register_test.go b/cmd/stolonctl/cmd/register_test.go index a3e5d9c9b..a109fb386 100644 --- a/cmd/stolonctl/cmd/register_test.go +++ b/cmd/stolonctl/cmd/register_test.go @@ -17,13 +17,14 @@ import ( "testing" "github.com/sorintlab/stolon/cmd" + "github.com/sorintlab/stolon/cmd/stolonctl/cmd/register" ) func TestCheckConfig(t *testing.T) { t.Run("should check for cluster name", func(t *testing.T) { c := config{} - rc := registerConfig{} + rc := register.Config{} err := checkConfig(&c, &rc) if err == nil || err.Error() != "cluster name required" { @@ -33,7 +34,7 @@ func TestCheckConfig(t *testing.T) { t.Run("should check for store backend", func(t *testing.T) { c := config{CommonConfig: cmd.CommonConfig{ClusterName: "test"}} - rc := registerConfig{} + rc := register.Config{} err := checkConfig(&c, &rc) if err == nil || err.Error() != "store backend type required" { @@ -43,20 +44,20 @@ func TestCheckConfig(t *testing.T) { t.Run("should check for consul register backend", func(t *testing.T) { c := config{CommonConfig: cmd.CommonConfig{ClusterName: "test", StoreBackend: "consul"}} - rc := registerConfig{registerBackend: "something other than consul"} + rc := register.Config{Backend: "something other than consul"} err := checkConfig(&c, &rc) if err == nil || err.Error() != "unknown register backend: \"something other than consul\"" { - t.Errorf("expected unknown register backend") + t.Errorf("expected unknown register backend but got %s", err.Error()) } }) t.Run("should not return any error if all valid configurations are specified", func(t *testing.T) { c := config{CommonConfig: cmd.CommonConfig{ClusterName: "test", StoreBackend: "consul"}} - rc := registerConfig{registerBackend: "redis"} + rc := register.Config{Backend: "consul"} err := checkConfig(&c, &rc) - if err == nil { + if err != nil { t.Errorf("expected no error but got '%v'", err.Error()) } }) diff --git a/vendor/github.com/hashicorp/consul/NOTICE.md b/vendor/github.com/hashicorp/consul/NOTICE.md new file mode 100644 index 000000000..fe34b5e57 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/NOTICE.md @@ -0,0 +1,3 @@ +Copyright © 2014-2018 HashiCorp, Inc. + +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this project, you can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/vendor/github.com/hashicorp/consul/api/acl.go b/vendor/github.com/hashicorp/consul/api/acl.go index 6ea0a752e..8ec9aa585 100644 --- a/vendor/github.com/hashicorp/consul/api/acl.go +++ b/vendor/github.com/hashicorp/consul/api/acl.go @@ -5,7 +5,7 @@ import ( ) const ( - // ACLCLientType is the client type token + // ACLClientType is the client type token ACLClientType = "client" // ACLManagementType is the management type token diff --git a/vendor/github.com/hashicorp/consul/api/agent.go b/vendor/github.com/hashicorp/consul/api/agent.go index 2992791b3..41505e769 100644 --- a/vendor/github.com/hashicorp/consul/api/agent.go +++ b/vendor/github.com/hashicorp/consul/api/agent.go @@ -5,6 +5,39 @@ import ( "fmt" ) +// ServiceKind is the kind of service being registered. +type ServiceKind string + +const ( + // ServiceKindTypical is a typical, classic Consul service. This is + // represented by the absence of a value. This was chosen for ease of + // backwards compatibility: existing services in the catalog would + // default to the typical service. + ServiceKindTypical ServiceKind = "" + + // ServiceKindConnectProxy is a proxy for the Connect feature. This + // service proxies another service within Consul and speaks the connect + // protocol. + ServiceKindConnectProxy ServiceKind = "connect-proxy" +) + +// ProxyExecMode is the execution mode for a managed Connect proxy. +type ProxyExecMode string + +const ( + // ProxyExecModeDaemon indicates that the proxy command should be long-running + // and should be started and supervised by the agent until it's target service + // is deregistered. + ProxyExecModeDaemon ProxyExecMode = "daemon" + + // ProxyExecModeScript indicates that the proxy command should be invoke to + // completion on each change to the configuration of lifecycle event. The + // script typically fetches the config and certificates from the agent API and + // then configures an externally managed daemon, perhaps starting and stopping + // it if necessary. + ProxyExecModeScript ProxyExecMode = "script" +) + // AgentCheck represents a check known to the agent type AgentCheck struct { Node string @@ -18,16 +51,41 @@ type AgentCheck struct { Definition HealthCheckDefinition } +// AgentWeights represent optional weights for a service +type AgentWeights struct { + Passing int + Warning int +} + // AgentService represents a service known to the agent type AgentService struct { + Kind ServiceKind ID string Service string Tags []string + Meta map[string]string Port int Address string + Weights AgentWeights EnableTagOverride bool CreateIndex uint64 ModifyIndex uint64 + ProxyDestination string + Connect *AgentServiceConnect +} + +// AgentServiceConnect represents the Connect configuration of a service. +type AgentServiceConnect struct { + Native bool + Proxy *AgentServiceConnectProxy +} + +// AgentServiceConnectProxy represents the Connect Proxy configuration of a +// service. +type AgentServiceConnectProxy struct { + ExecMode ProxyExecMode + Command []string + Config map[string]interface{} } // AgentMember represents a cluster member known to the agent @@ -60,14 +118,19 @@ type MembersOpts struct { // AgentServiceRegistration is used to register a new service type AgentServiceRegistration struct { - ID string `json:",omitempty"` - Name string `json:",omitempty"` - Tags []string `json:",omitempty"` - Port int `json:",omitempty"` - Address string `json:",omitempty"` - EnableTagOverride bool `json:",omitempty"` + Kind ServiceKind `json:",omitempty"` + ID string `json:",omitempty"` + Name string `json:",omitempty"` + Tags []string `json:",omitempty"` + Port int `json:",omitempty"` + Address string `json:",omitempty"` + EnableTagOverride bool `json:",omitempty"` + Meta map[string]string `json:",omitempty"` + Weights *AgentWeights `json:",omitempty"` Check *AgentServiceCheck Checks AgentServiceChecks + ProxyDestination string `json:",omitempty"` + Connect *AgentServiceConnect `json:",omitempty"` } // AgentCheckRegistration is used to register a new check @@ -81,8 +144,9 @@ type AgentCheckRegistration struct { // AgentServiceCheck is used to define a node or service level check type AgentServiceCheck struct { + CheckID string `json:",omitempty"` + Name string `json:",omitempty"` Args []string `json:"ScriptArgs,omitempty"` - Script string `json:",omitempty"` // Deprecated, use Args. DockerContainerID string `json:",omitempty"` Shell string `json:",omitempty"` // Only supported for Docker. Interval string `json:",omitempty"` @@ -95,6 +159,8 @@ type AgentServiceCheck struct { Status string `json:",omitempty"` Notes string `json:",omitempty"` TLSSkipVerify bool `json:",omitempty"` + GRPC string `json:",omitempty"` + GRPCUseTLS bool `json:",omitempty"` // In Consul 0.7 and later, checks that are associated with a service // may also contain this optional DeregisterCriticalServiceAfter field, @@ -147,6 +213,31 @@ type SampledValue struct { Labels map[string]string } +// AgentAuthorizeParams are the request parameters for authorizing a request. +type AgentAuthorizeParams struct { + Target string + ClientCertURI string + ClientCertSerial string +} + +// AgentAuthorize is the response structure for Connect authorization. +type AgentAuthorize struct { + Authorized bool + Reason string +} + +// ConnectProxyConfig is the response structure for agent-local proxy +// configuration. +type ConnectProxyConfig struct { + ProxyServiceID string + TargetServiceID string + TargetServiceName string + ContentHash string + ExecMode ProxyExecMode + Command []string + Config map[string]interface{} +} + // Agent can be used to query the Agent endpoints type Agent struct { c *Client @@ -248,6 +339,7 @@ func (a *Agent) Services() (map[string]*AgentService, error) { if err := decodeBody(resp, &out); err != nil { return nil, err } + return out, nil } @@ -480,6 +572,91 @@ func (a *Agent) ForceLeave(node string) error { return nil } +// ConnectAuthorize is used to authorize an incoming connection +// to a natively integrated Connect service. +func (a *Agent) ConnectAuthorize(auth *AgentAuthorizeParams) (*AgentAuthorize, error) { + r := a.c.newRequest("POST", "/v1/agent/connect/authorize") + r.obj = auth + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out AgentAuthorize + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return &out, nil +} + +// ConnectCARoots returns the list of roots. +func (a *Agent) ConnectCARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/agent/connect/ca/roots") + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CARootList + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// ConnectCALeaf gets the leaf certificate for the given service ID. +func (a *Agent) ConnectCALeaf(serviceID string, q *QueryOptions) (*LeafCert, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/agent/connect/ca/leaf/"+serviceID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out LeafCert + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// ConnectProxyConfig gets the configuration for a local managed proxy instance. +// +// Note that this uses an unconventional blocking mechanism since it's +// agent-local state. That means there is no persistent raft index so we block +// based on object hash instead. +func (a *Agent) ConnectProxyConfig(proxyServiceID string, q *QueryOptions) (*ConnectProxyConfig, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/agent/connect/proxy/"+proxyServiceID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out ConnectProxyConfig + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + // EnableServiceMaintenance toggles service maintenance mode on // for the given service ID. func (a *Agent) EnableServiceMaintenance(serviceID, reason string) error { @@ -582,36 +759,36 @@ func (a *Agent) Monitor(loglevel string, stopCh <-chan struct{}, q *QueryOptions // UpdateACLToken updates the agent's "acl_token". See updateToken for more // details. -func (c *Agent) UpdateACLToken(token string, q *WriteOptions) (*WriteMeta, error) { - return c.updateToken("acl_token", token, q) +func (a *Agent) UpdateACLToken(token string, q *WriteOptions) (*WriteMeta, error) { + return a.updateToken("acl_token", token, q) } // UpdateACLAgentToken updates the agent's "acl_agent_token". See updateToken // for more details. -func (c *Agent) UpdateACLAgentToken(token string, q *WriteOptions) (*WriteMeta, error) { - return c.updateToken("acl_agent_token", token, q) +func (a *Agent) UpdateACLAgentToken(token string, q *WriteOptions) (*WriteMeta, error) { + return a.updateToken("acl_agent_token", token, q) } // UpdateACLAgentMasterToken updates the agent's "acl_agent_master_token". See // updateToken for more details. -func (c *Agent) UpdateACLAgentMasterToken(token string, q *WriteOptions) (*WriteMeta, error) { - return c.updateToken("acl_agent_master_token", token, q) +func (a *Agent) UpdateACLAgentMasterToken(token string, q *WriteOptions) (*WriteMeta, error) { + return a.updateToken("acl_agent_master_token", token, q) } // UpdateACLReplicationToken updates the agent's "acl_replication_token". See // updateToken for more details. -func (c *Agent) UpdateACLReplicationToken(token string, q *WriteOptions) (*WriteMeta, error) { - return c.updateToken("acl_replication_token", token, q) +func (a *Agent) UpdateACLReplicationToken(token string, q *WriteOptions) (*WriteMeta, error) { + return a.updateToken("acl_replication_token", token, q) } // updateToken can be used to update an agent's ACL token after the agent has // started. The tokens are not persisted, so will need to be updated again if // the agent is restarted. -func (c *Agent) updateToken(target, token string, q *WriteOptions) (*WriteMeta, error) { - r := c.c.newRequest("PUT", fmt.Sprintf("/v1/agent/token/%s", target)) +func (a *Agent) updateToken(target, token string, q *WriteOptions) (*WriteMeta, error) { + r := a.c.newRequest("PUT", fmt.Sprintf("/v1/agent/token/%s", target)) r.setWriteOptions(q) r.obj = &AgentToken{Token: token} - rtt, resp, err := requireOK(c.c.doRequest(r)) + rtt, resp, err := requireOK(a.c.doRequest(r)) if err != nil { return nil, err } diff --git a/vendor/github.com/hashicorp/consul/api/api.go b/vendor/github.com/hashicorp/consul/api/api.go index 7db12b49d..649238302 100644 --- a/vendor/github.com/hashicorp/consul/api/api.go +++ b/vendor/github.com/hashicorp/consul/api/api.go @@ -82,6 +82,12 @@ type QueryOptions struct { // until the timeout or the next index is reached WaitIndex uint64 + // WaitHash is used by some endpoints instead of WaitIndex to perform blocking + // on state based on a hash of the response rather than a monotonic index. + // This is required when the state being blocked on is not stored in Raft, for + // example agent-local proxy configuration. + WaitHash string + // WaitTime is used to bound the duration of a wait. // Defaults to that of the Config, but can be overridden. WaitTime time.Duration @@ -101,11 +107,15 @@ type QueryOptions struct { // be provided for filtering. NodeMeta map[string]string - // RelayFactor is used in keyring operations to cause reponses to be + // RelayFactor is used in keyring operations to cause responses to be // relayed back to the sender through N other random nodes. Must be // a value from 0 to 5 (inclusive). RelayFactor uint8 + // Connect filters prepared query execution to only include Connect-capable + // services. This currently affects prepared query execution. + Connect bool + // ctx is an optional context pass through to the underlying HTTP // request layer. Use Context() and WithContext() to manage this. ctx context.Context @@ -137,7 +147,7 @@ type WriteOptions struct { // which overrides the agent's default token. Token string - // RelayFactor is used in keyring operations to cause reponses to be + // RelayFactor is used in keyring operations to cause responses to be // relayed back to the sender through N other random nodes. Must be // a value from 0 to 5 (inclusive). RelayFactor uint8 @@ -169,6 +179,11 @@ type QueryMeta struct { // a blocking query LastIndex uint64 + // LastContentHash. This can be used as a WaitHash to perform a blocking query + // for endpoints that support hash-based blocking. Endpoints that do not + // support it will return an empty hash. + LastContentHash string + // Time of last contact from the leader for the // server servicing the request LastContact time.Duration @@ -390,6 +405,29 @@ func SetupTLSConfig(tlsConfig *TLSConfig) (*tls.Config, error) { return tlsClientConfig, nil } +func (c *Config) GenerateEnv() []string { + env := make([]string, 0, 10) + + env = append(env, + fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address), + fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token), + fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"), + fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile), + fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath), + fmt.Sprintf("%s=%s", HTTPClientCert, c.TLSConfig.CertFile), + fmt.Sprintf("%s=%s", HTTPClientKey, c.TLSConfig.KeyFile), + fmt.Sprintf("%s=%s", HTTPTLSServerName, c.TLSConfig.Address), + fmt.Sprintf("%s=%t", HTTPSSLVerifyEnvName, !c.TLSConfig.InsecureSkipVerify)) + + if c.HttpAuth != nil { + env = append(env, fmt.Sprintf("%s=%s:%s", HTTPAuthEnvName, c.HttpAuth.Username, c.HttpAuth.Password)) + } else { + env = append(env, fmt.Sprintf("%s=", HTTPAuthEnvName)) + } + + return env +} + // Client provides a client to the Consul API type Client struct { config Config @@ -533,6 +571,9 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.WaitTime != 0 { r.params.Set("wait", durToMsec(q.WaitTime)) } + if q.WaitHash != "" { + r.params.Set("hash", q.WaitHash) + } if q.Token != "" { r.header.Set("X-Consul-Token", q.Token) } @@ -547,6 +588,9 @@ func (r *request) setQueryOptions(q *QueryOptions) { if q.RelayFactor != 0 { r.params.Set("relay-factor", strconv.Itoa(int(q.RelayFactor))) } + if q.Connect { + r.params.Set("connect", "true") + } r.ctx = q.ctx } @@ -633,9 +677,9 @@ func (r *request) toHTTP() (*http.Request, error) { } if r.ctx != nil { return req.WithContext(r.ctx), nil - } else { - return req, nil } + + return req, nil } // newRequest is used to create a new request @@ -724,12 +768,16 @@ func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (* func parseQueryMeta(resp *http.Response, q *QueryMeta) error { header := resp.Header - // Parse the X-Consul-Index - index, err := strconv.ParseUint(header.Get("X-Consul-Index"), 10, 64) - if err != nil { - return fmt.Errorf("Failed to parse X-Consul-Index: %v", err) + // Parse the X-Consul-Index (if it's set - hash based blocking queries don't + // set this) + if indexStr := header.Get("X-Consul-Index"); indexStr != "" { + index, err := strconv.ParseUint(indexStr, 10, 64) + if err != nil { + return fmt.Errorf("Failed to parse X-Consul-Index: %v", err) + } + q.LastIndex = index } - q.LastIndex = index + q.LastContentHash = header.Get("X-Consul-ContentHash") // Parse the X-Consul-LastContact last, err := strconv.ParseUint(header.Get("X-Consul-LastContact"), 10, 64) diff --git a/vendor/github.com/hashicorp/consul/api/catalog.go b/vendor/github.com/hashicorp/consul/api/catalog.go index 08da6e16e..6cb745c36 100644 --- a/vendor/github.com/hashicorp/consul/api/catalog.go +++ b/vendor/github.com/hashicorp/consul/api/catalog.go @@ -1,5 +1,10 @@ package api +type Weights struct { + Passing int + Warning int +} + type Node struct { ID string Node string @@ -22,7 +27,9 @@ type CatalogService struct { ServiceName string ServiceAddress string ServiceTags []string + ServiceMeta map[string]string ServicePort int + ServiceWeights Weights ServiceEnableTagOverride bool CreateIndex uint64 ModifyIndex uint64 @@ -155,7 +162,20 @@ func (c *Catalog) Services(q *QueryOptions) (map[string][]string, *QueryMeta, er // Service is used to query catalog entries for a given service func (c *Catalog) Service(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) { - r := c.c.newRequest("GET", "/v1/catalog/service/"+service) + return c.service(service, tag, q, false) +} + +// Connect is used to query catalog entries for a given Connect-enabled service +func (c *Catalog) Connect(service, tag string, q *QueryOptions) ([]*CatalogService, *QueryMeta, error) { + return c.service(service, tag, q, true) +} + +func (c *Catalog) service(service, tag string, q *QueryOptions, connect bool) ([]*CatalogService, *QueryMeta, error) { + path := "/v1/catalog/service/" + service + if connect { + path = "/v1/catalog/connect/" + service + } + r := c.c.newRequest("GET", path) r.setQueryOptions(q) if tag != "" { r.params.Set("tag", tag) diff --git a/vendor/github.com/hashicorp/consul/api/connect.go b/vendor/github.com/hashicorp/consul/api/connect.go new file mode 100644 index 000000000..a40d1e232 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/api/connect.go @@ -0,0 +1,12 @@ +package api + +// Connect can be used to work with endpoints related to Connect, the +// feature for securely connecting services within Consul. +type Connect struct { + c *Client +} + +// Connect returns a handle to the connect-related endpoints +func (c *Client) Connect() *Connect { + return &Connect{c} +} diff --git a/vendor/github.com/hashicorp/consul/api/connect_ca.go b/vendor/github.com/hashicorp/consul/api/connect_ca.go new file mode 100644 index 000000000..a863d21d4 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/api/connect_ca.go @@ -0,0 +1,172 @@ +package api + +import ( + "fmt" + "time" + + "github.com/mitchellh/mapstructure" +) + +// CAConfig is the structure for the Connect CA configuration. +type CAConfig struct { + // Provider is the CA provider implementation to use. + Provider string + + // Configuration is arbitrary configuration for the provider. This + // should only contain primitive values and containers (such as lists + // and maps). + Config map[string]interface{} + + CreateIndex uint64 + ModifyIndex uint64 +} + +// CommonCAProviderConfig is the common options available to all CA providers. +type CommonCAProviderConfig struct { + LeafCertTTL time.Duration +} + +// ConsulCAProviderConfig is the config for the built-in Consul CA provider. +type ConsulCAProviderConfig struct { + CommonCAProviderConfig `mapstructure:",squash"` + + PrivateKey string + RootCert string + RotationPeriod time.Duration +} + +// ParseConsulCAConfig takes a raw config map and returns a parsed +// ConsulCAProviderConfig. +func ParseConsulCAConfig(raw map[string]interface{}) (*ConsulCAProviderConfig, error) { + var config ConsulCAProviderConfig + decodeConf := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.StringToTimeDurationHookFunc(), + ErrorUnused: true, + Result: &config, + WeaklyTypedInput: true, + } + + decoder, err := mapstructure.NewDecoder(decodeConf) + if err != nil { + return nil, err + } + + if err := decoder.Decode(raw); err != nil { + return nil, fmt.Errorf("error decoding config: %s", err) + } + + return &config, nil +} + +// CARootList is the structure for the results of listing roots. +type CARootList struct { + ActiveRootID string + TrustDomain string + Roots []*CARoot +} + +// CARoot represents a root CA certificate that is trusted. +type CARoot struct { + // ID is a globally unique ID (UUID) representing this CA root. + ID string + + // Name is a human-friendly name for this CA root. This value is + // opaque to Consul and is not used for anything internally. + Name string + + // RootCertPEM is the PEM-encoded public certificate. + RootCertPEM string `json:"RootCert"` + + // Active is true if this is the current active CA. This must only + // be true for exactly one CA. For any method that modifies roots in the + // state store, tests should be written to verify that multiple roots + // cannot be active. + Active bool + + CreateIndex uint64 + ModifyIndex uint64 +} + +// LeafCert is a certificate that has been issued by a Connect CA. +type LeafCert struct { + // SerialNumber is the unique serial number for this certificate. + // This is encoded in standard hex separated by :. + SerialNumber string + + // CertPEM and PrivateKeyPEM are the PEM-encoded certificate and private + // key for that cert, respectively. This should not be stored in the + // state store, but is present in the sign API response. + CertPEM string `json:",omitempty"` + PrivateKeyPEM string `json:",omitempty"` + + // Service is the name of the service for which the cert was issued. + // ServiceURI is the cert URI value. + Service string + ServiceURI string + + // ValidAfter and ValidBefore are the validity periods for the + // certificate. + ValidAfter time.Time + ValidBefore time.Time + + CreateIndex uint64 + ModifyIndex uint64 +} + +// CARoots queries the list of available roots. +func (h *Connect) CARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/ca/roots") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CARootList + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// CAGetConfig returns the current CA configuration. +func (h *Connect) CAGetConfig(q *QueryOptions) (*CAConfig, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/ca/configuration") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CAConfig + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// CASetConfig sets the current CA configuration. +func (h *Connect) CASetConfig(conf *CAConfig, q *WriteOptions) (*WriteMeta, error) { + r := h.c.newRequest("PUT", "/v1/connect/ca/configuration") + r.setWriteOptions(q) + r.obj = conf + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{} + wm.RequestTime = rtt + return wm, nil +} diff --git a/vendor/github.com/hashicorp/consul/api/connect_intention.go b/vendor/github.com/hashicorp/consul/api/connect_intention.go new file mode 100644 index 000000000..a996c03e5 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/api/connect_intention.go @@ -0,0 +1,302 @@ +package api + +import ( + "bytes" + "fmt" + "io" + "time" +) + +// Intention defines an intention for the Connect Service Graph. This defines +// the allowed or denied behavior of a connection between two services using +// Connect. +type Intention struct { + // ID is the UUID-based ID for the intention, always generated by Consul. + ID string + + // Description is a human-friendly description of this intention. + // It is opaque to Consul and is only stored and transferred in API + // requests. + Description string + + // SourceNS, SourceName are the namespace and name, respectively, of + // the source service. Either of these may be the wildcard "*", but only + // the full value can be a wildcard. Partial wildcards are not allowed. + // The source may also be a non-Consul service, as specified by SourceType. + // + // DestinationNS, DestinationName is the same, but for the destination + // service. The same rules apply. The destination is always a Consul + // service. + SourceNS, SourceName string + DestinationNS, DestinationName string + + // SourceType is the type of the value for the source. + SourceType IntentionSourceType + + // Action is whether this is a whitelist or blacklist intention. + Action IntentionAction + + // DefaultAddr, DefaultPort of the local listening proxy (if any) to + // make this connection. + DefaultAddr string + DefaultPort int + + // Meta is arbitrary metadata associated with the intention. This is + // opaque to Consul but is served in API responses. + Meta map[string]string + + // Precedence is the order that the intention will be applied, with + // larger numbers being applied first. This is a read-only field, on + // any intention update it is updated. + Precedence int + + // CreatedAt and UpdatedAt keep track of when this record was created + // or modified. + CreatedAt, UpdatedAt time.Time + + CreateIndex uint64 + ModifyIndex uint64 +} + +// String returns human-friendly output describing ths intention. +func (i *Intention) String() string { + return fmt.Sprintf("%s => %s (%s)", + i.SourceString(), + i.DestinationString(), + i.Action) +} + +// SourceString returns the namespace/name format for the source, or +// just "name" if the namespace is the default namespace. +func (i *Intention) SourceString() string { + return i.partString(i.SourceNS, i.SourceName) +} + +// DestinationString returns the namespace/name format for the source, or +// just "name" if the namespace is the default namespace. +func (i *Intention) DestinationString() string { + return i.partString(i.DestinationNS, i.DestinationName) +} + +func (i *Intention) partString(ns, n string) string { + // For now we omit the default namespace from the output. In the future + // we might want to look at this and show this in a multi-namespace world. + if ns != "" && ns != IntentionDefaultNamespace { + n = ns + "/" + n + } + + return n +} + +// IntentionDefaultNamespace is the default namespace value. +const IntentionDefaultNamespace = "default" + +// IntentionAction is the action that the intention represents. This +// can be "allow" or "deny" to whitelist or blacklist intentions. +type IntentionAction string + +const ( + IntentionActionAllow IntentionAction = "allow" + IntentionActionDeny IntentionAction = "deny" +) + +// IntentionSourceType is the type of the source within an intention. +type IntentionSourceType string + +const ( + // IntentionSourceConsul is a service within the Consul catalog. + IntentionSourceConsul IntentionSourceType = "consul" +) + +// IntentionMatch are the arguments for the intention match API. +type IntentionMatch struct { + By IntentionMatchType + Names []string +} + +// IntentionMatchType is the target for a match request. For example, +// matching by source will look for all intentions that match the given +// source value. +type IntentionMatchType string + +const ( + IntentionMatchSource IntentionMatchType = "source" + IntentionMatchDestination IntentionMatchType = "destination" +) + +// IntentionCheck are the arguments for the intention check API. For +// more documentation see the IntentionCheck function. +type IntentionCheck struct { + // Source and Destination are the source and destination values to + // check. The destination is always a Consul service, but the source + // may be other values as defined by the SourceType. + Source, Destination string + + // SourceType is the type of the value for the source. + SourceType IntentionSourceType +} + +// Intentions returns the list of intentions. +func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/intentions") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out []*Intention + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + +// IntentionGet retrieves a single intention. +func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/intentions/"+id) + r.setQueryOptions(q) + rtt, resp, err := h.c.doRequest(r) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + if resp.StatusCode == 404 { + return nil, qm, nil + } else if resp.StatusCode != 200 { + var buf bytes.Buffer + io.Copy(&buf, resp.Body) + return nil, nil, fmt.Errorf( + "Unexpected response %d: %s", resp.StatusCode, buf.String()) + } + + var out Intention + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// IntentionDelete deletes a single intention. +func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) { + r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id) + r.setWriteOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + qm := &WriteMeta{} + qm.RequestTime = rtt + + return qm, nil +} + +// IntentionMatch returns the list of intentions that match a given source +// or destination. The returned intentions are ordered by precedence where +// result[0] is the highest precedence (if that matches, then that rule overrides +// all other rules). +// +// Matching can be done for multiple names at the same time. The resulting +// map is keyed by the given names. Casing is preserved. +func (h *Connect) IntentionMatch(args *IntentionMatch, q *QueryOptions) (map[string][]*Intention, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/intentions/match") + r.setQueryOptions(q) + r.params.Set("by", string(args.By)) + for _, name := range args.Names { + r.params.Add("name", name) + } + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out map[string][]*Intention + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + +// IntentionCheck returns whether a given source/destination would be allowed +// or not given the current set of intentions and the configuration of Consul. +func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/intentions/check") + r.setQueryOptions(q) + r.params.Set("source", args.Source) + r.params.Set("destination", args.Destination) + if args.SourceType != "" { + r.params.Set("source-type", string(args.SourceType)) + } + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return false, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out struct{ Allowed bool } + if err := decodeBody(resp, &out); err != nil { + return false, nil, err + } + return out.Allowed, qm, nil +} + +// IntentionCreate will create a new intention. The ID in the given +// structure must be empty and a generate ID will be returned on +// success. +func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { + r := c.c.newRequest("POST", "/v1/connect/intentions") + r.setWriteOptions(q) + r.obj = ixn + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return "", nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{} + wm.RequestTime = rtt + + var out struct{ ID string } + if err := decodeBody(resp, &out); err != nil { + return "", nil, err + } + return out.ID, wm, nil +} + +// IntentionUpdate will update an existing intention. The ID in the given +// structure must be non-empty. +func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { + r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID) + r.setWriteOptions(q) + r.obj = ixn + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{} + wm.RequestTime = rtt + return wm, nil +} diff --git a/vendor/github.com/hashicorp/consul/api/health.go b/vendor/github.com/hashicorp/consul/api/health.go index 53f3de4f7..1835da559 100644 --- a/vendor/github.com/hashicorp/consul/api/health.go +++ b/vendor/github.com/hashicorp/consul/api/health.go @@ -159,7 +159,24 @@ func (h *Health) Checks(service string, q *QueryOptions) (HealthChecks, *QueryMe // for a given service. It can optionally do server-side filtering on a tag // or nodes with passing health checks only. func (h *Health) Service(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { - r := h.c.newRequest("GET", "/v1/health/service/"+service) + return h.service(service, tag, passingOnly, q, false) +} + +// Connect is equivalent to Service except that it will only return services +// which are Connect-enabled and will returns the connection address for Connect +// client's to use which may be a proxy in front of the named service. If +// passingOnly is true only instances where both the service and any proxy are +// healthy will be returned. +func (h *Health) Connect(service, tag string, passingOnly bool, q *QueryOptions) ([]*ServiceEntry, *QueryMeta, error) { + return h.service(service, tag, passingOnly, q, true) +} + +func (h *Health) service(service, tag string, passingOnly bool, q *QueryOptions, connect bool) ([]*ServiceEntry, *QueryMeta, error) { + path := "/v1/health/service/" + service + if connect { + path = "/v1/health/connect/" + service + } + r := h.c.newRequest("GET", path) r.setQueryOptions(q) if tag != "" { r.params.Set("tag", tag) diff --git a/vendor/github.com/hashicorp/consul/api/lock.go b/vendor/github.com/hashicorp/consul/api/lock.go index 41f72e7d2..82339cb74 100644 --- a/vendor/github.com/hashicorp/consul/api/lock.go +++ b/vendor/github.com/hashicorp/consul/api/lock.go @@ -181,11 +181,12 @@ WAIT: // Handle the one-shot mode. if l.opts.LockTryOnce && attempts > 0 { elapsed := time.Since(start) - if elapsed > qOpts.WaitTime { + if elapsed > l.opts.LockWaitTime { return nil, nil } - qOpts.WaitTime -= elapsed + // Query wait time should not exceed the lock wait time + qOpts.WaitTime = l.opts.LockWaitTime - elapsed } attempts++ diff --git a/vendor/github.com/hashicorp/consul/api/operator_area.go b/vendor/github.com/hashicorp/consul/api/operator_area.go index a630b694c..5cf7e4973 100644 --- a/vendor/github.com/hashicorp/consul/api/operator_area.go +++ b/vendor/github.com/hashicorp/consul/api/operator_area.go @@ -1,9 +1,10 @@ +package api + // The /v1/operator/area endpoints are available only in Consul Enterprise and // interact with its network area subsystem. Network areas are used to link // together Consul servers in different Consul datacenters. With network areas, // Consul datacenters can be linked together in ways other than a fully-connected // mesh, as is required for Consul's WAN. -package api import ( "net" diff --git a/vendor/github.com/hashicorp/consul/api/prepared_query.go b/vendor/github.com/hashicorp/consul/api/prepared_query.go index ff210de3f..8bb1004ee 100644 --- a/vendor/github.com/hashicorp/consul/api/prepared_query.go +++ b/vendor/github.com/hashicorp/consul/api/prepared_query.go @@ -34,6 +34,12 @@ type ServiceQuery struct { // local datacenter. Failover QueryDatacenterOptions + // IgnoreCheckIDs is an optional list of health check IDs to ignore when + // considering which nodes are healthy. It is useful as an emergency measure + // to temporarily override some health check that is producing false negatives + // for example. + IgnoreCheckIDs []string + // If OnlyPassing is true then we will only include nodes with passing // health checks (critical AND warning checks will cause a node to be // discarded) @@ -48,6 +54,14 @@ type ServiceQuery struct { // pair is in this map it must be present on the node in order for the // service entry to be returned. NodeMeta map[string]string + + // Connect if true will filter the prepared query results to only + // include Connect-capable services. These include both native services + // and proxies for matching services. Note that if a proxy matches, + // the constraints in the query above (Near, OnlyPassing, etc.) apply + // to the _proxy_ and not the service being proxied. In practice, proxies + // should be directly next to their services so this isn't an issue. + Connect bool } // QueryTemplate carries the arguments for creating a templated query. @@ -61,7 +75,7 @@ type QueryTemplate struct { Regexp string } -// PrepatedQueryDefinition defines a complete prepared query. +// PreparedQueryDefinition defines a complete prepared query. type PreparedQueryDefinition struct { // ID is this UUID-based ID for the query, always generated by Consul. ID string diff --git a/vendor/github.com/hashicorp/consul/api/semaphore.go b/vendor/github.com/hashicorp/consul/api/semaphore.go index d0c574177..bc4f885fe 100644 --- a/vendor/github.com/hashicorp/consul/api/semaphore.go +++ b/vendor/github.com/hashicorp/consul/api/semaphore.go @@ -199,11 +199,12 @@ WAIT: // Handle the one-shot mode. if s.opts.SemaphoreTryOnce && attempts > 0 { elapsed := time.Since(start) - if elapsed > qOpts.WaitTime { + if elapsed > s.opts.SemaphoreWaitTime { return nil, nil } - qOpts.WaitTime -= elapsed + // Query wait time should not exceed the semaphore wait time + qOpts.WaitTime = s.opts.SemaphoreWaitTime - elapsed } attempts++ diff --git a/vendor/github.com/hashicorp/consul/ui-v2/app/styles/components/notice.scss b/vendor/github.com/hashicorp/consul/ui-v2/app/styles/components/notice.scss new file mode 100644 index 000000000..e7b986dbc --- /dev/null +++ b/vendor/github.com/hashicorp/consul/ui-v2/app/styles/components/notice.scss @@ -0,0 +1,4 @@ +@import './notice/index'; +.notice.warning { + @extend %notice-warning; +} diff --git a/vendor/github.com/hashicorp/consul/website/source/api/operator/license.html.md b/vendor/github.com/hashicorp/consul/website/source/api/operator/license.html.md new file mode 100644 index 000000000..2908c4ba3 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/website/source/api/operator/license.html.md @@ -0,0 +1,143 @@ +--- +layout: api +page_title: License - Operator - HTTP API +sidebar_current: api-operator-license +description: |- + The /operator/license endpoints allow for setting and retrieving the Consul + Enterprise License. +--- + +# License - Operator HTTP API + +~> **Enterprise Only!** This API endpoint and functionality only exists in +Consul Enterprise. This is not present in the open source version of Consul. + +The licensing functionality described here is available only in +[Consul Enterprise](https://www.hashicorp.com/products/consul/) version 1.1.0 and later. + +## Getting the Consul License + +This endpoint gets information about the current license. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `GET` | `/operator/license` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), +[consistency modes](/api/index.html#consistency-modes), and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ---------------- | +| `NO` | `all` | `none` | + +### Parameters + +- `dc` `(string: "")` - Specifies the datacenter whose license should be retrieved. + This will default to the datacenter of the agent serving the HTTP request. + This is specified as a URL query parameter. + +### Sample Request + +```text +$ curl \ + http://127.0.0.1:8500/v1/operator/license +``` + +### Sample Response + +```json +{ + "Valid": true, + "License": { + "license_id": "2afbf681-0d1a-0649-cb6c-333ec9f0989c", + "customer_id": "0259271d-8ffc-e85e-0830-c0822c1f5f2b", + "installation_id": "*", + "issue_time": "2018-05-21T20:03:35.911567355Z", + "start_time": "2018-05-21T04:00:00Z", + "expiration_time": "2019-05-22T03:59:59.999Z", + "product": "consul", + "flags": { + "package": "premium" + }, + "features": [ + "Automated Backups", + "Automated Upgrades", + "Enhanced Read Scalability", + "Network Segments", + "Redundancy Zone", + "Advanced Network Federation" + ], + "temporary": false + }, + "Warnings": [] +} +``` + +## Updating the Consul License + +This endpoint updates the Consul license and returns some of the +license contents as well as any warning messages regarding its validity. + +| Method | Path | Produces | +| ------ | ---------------------------- | -------------------------- | +| `PUT` | `/operator/license` | `application/json` | + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), +[consistency modes](/api/index.html#consistency-modes), and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ---------------- | +| `NO` | `none` | `operator:write` | + +### Parameters + +- `dc` `(string: "")` - Specifies the datacenter whose license should be updated. + This will default to the datacenter of the agent serving the HTTP request. + This is specified as a URL query parameter. + +### Sample Payload + +The payload is the raw license blob. + +### Sample Request + +```text +$ curl \ + --request PUT \ + --data @consul.license \ + http://127.0.0.1:8500/v1/operator/license +``` + +### Sample Response + +```json +{ + "Valid": true, + "License": { + "license_id": "2afbf681-0d1a-0649-cb6c-333ec9f0989c", + "customer_id": "0259271d-8ffc-e85e-0830-c0822c1f5f2b", + "installation_id": "*", + "issue_time": "2018-05-21T20:03:35.911567355Z", + "start_time": "2018-05-21T04:00:00Z", + "expiration_time": "2019-05-22T03:59:59.999Z", + "product": "consul", + "flags": { + "package": "premium" + }, + "features": [ + "Automated Backups", + "Automated Upgrades", + "Enhanced Read Scalability", + "Network Segments", + "Redundancy Zone", + "Advanced Network Federation" + ], + "temporary": false + }, + "Warnings": [] +} +``` diff --git a/vendor/github.com/hashicorp/consul/website/source/docs/commands/license.html.markdown.erb b/vendor/github.com/hashicorp/consul/website/source/docs/commands/license.html.markdown.erb new file mode 100644 index 000000000..b65d171d1 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/website/source/docs/commands/license.html.markdown.erb @@ -0,0 +1,109 @@ +--- +layout: "docs" +page_title: "Commands: License" +sidebar_current: "docs-commands-license" +description: > + The license command provides datacenter-level management of the Consul Enterprise license. +--- + +# Consul License + +Command: `consul license` + +<%= enterprise_alert :consul %> + +The `license` command provides datacenter-level management of the Consul Enterprise license. This was added in Consul 1.1.0. + +If ACLs are enabled then a token with operator privileges may be required in +order to use this command. Requests are forwarded internally to the leader +if required, so this can be run from any Consul node in a cluster. See the +[ACL Guide](/docs/guides/acl.html#operator) for more information. + + +```text +Usage: consul license [options] [args] + + This command has subcommands for managing the Consul Enterprise license + Here are some simple examples, and more detailed examples are + available in the subcommands or the documentation. + + Install a new license from a file: + + $ consul license put @consul.license + + Install a new license from stdin: + + $ consul license put - + + Install a new license from a string: + + $ consul license put "" + + Retrieve the current license: + + $ consul license get + + For more examples, ask for subcommand help or view the documentation. + +Subcommands: + get Get the current license + put Puts a new license in the datacenter +``` + +## put + +This command sets the Consul Enterprise license. + +Usage: `consul license put [options] LICENSE` + +#### API Options + +<%= partial "docs/commands/http_api_options_client" %> +<%= partial "docs/commands/http_api_options_server" %> + +The output looks like this: + +``` +License is valid +License ID: 2afbf681-0d1a-0649-cb6c-333ec9f0989c +Customer ID: 0259271d-8ffc-e85e-0830-c0822c1f5f2b +Expires At: 2019-05-22 03:59:59.999 +0000 UTC +Datacenter: * +Package: premium +Licensed Features: + Automated Backups + Automated Upgrades + Enhanced Read Scalability + Network Segments + Redundancy Zone + Advanced Network Federation +``` + +## get + +This command gets the Consul Enterprise license. + +Usage: `consul license get [options]` + +#### API Options + +<%= partial "docs/commands/http_api_options_client" %> +<%= partial "docs/commands/http_api_options_server" %> + +The output looks like this: + +``` +License is valid +License ID: 2afbf681-0d1a-0649-cb6c-333ec9f0989c +Customer ID: 0259271d-8ffc-e85e-0830-c0822c1f5f2b +Expires At: 2019-05-22 03:59:59.999 +0000 UTC +Datacenter: * +Package: premium +Licensed Features: + Automated Backups + Automated Upgrades + Enhanced Read Scalability + Network Segments + Redundancy Zone + Advanced Network Federation +``` \ No newline at end of file