diff --git a/agent/structs/structs.go b/agent/structs/structs.go index a4e942230..4f25e50f0 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -464,7 +464,7 @@ type ServiceKind string const ( // ServiceKindTypical is a typical, classic Consul service. This is - // represented by the absense of a value. This was chosen for ease of + // 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 = "" diff --git a/api/agent.go b/api/agent.go index 23690d48a..359206c54 100644 --- a/api/agent.go +++ b/api/agent.go @@ -5,6 +5,22 @@ 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" +) + // AgentCheck represents a check known to the agent type AgentCheck struct { Node string @@ -20,6 +36,7 @@ type AgentCheck struct { // AgentService represents a service known to the agent type AgentService struct { + Kind ServiceKind ID string Service string Tags []string @@ -29,6 +46,7 @@ type AgentService struct { EnableTagOverride bool CreateIndex uint64 ModifyIndex uint64 + ProxyDestination string } // AgentMember represents a cluster member known to the agent @@ -61,6 +79,7 @@ type MembersOpts struct { // AgentServiceRegistration is used to register a new service type AgentServiceRegistration struct { + Kind ServiceKind `json:",omitempty"` ID string `json:",omitempty"` Name string `json:",omitempty"` Tags []string `json:",omitempty"` @@ -70,6 +89,7 @@ type AgentServiceRegistration struct { Meta map[string]string `json:",omitempty"` Check *AgentServiceCheck Checks AgentServiceChecks + ProxyDestination string `json:",omitempty"` } // AgentCheckRegistration is used to register a new check diff --git a/api/agent_test.go b/api/agent_test.go index b195fed29..d45a9a131 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -185,6 +185,51 @@ func TestAPI_AgentServices(t *testing.T) { } } +func TestAPI_AgentServices_ConnectProxy(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + + // Register service + reg := &AgentServiceRegistration{ + Name: "foo", + Port: 8000, + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + // Register proxy + reg = &AgentServiceRegistration{ + Kind: ServiceKindConnectProxy, + Name: "foo-proxy", + Port: 8001, + ProxyDestination: "foo", + } + if err := agent.ServiceRegister(reg); err != nil { + t.Fatalf("err: %v", err) + } + + services, err := agent.Services() + if err != nil { + t.Fatalf("err: %v", err) + } + if _, ok := services["foo"]; !ok { + t.Fatalf("missing service: %v", services) + } + if _, ok := services["foo-proxy"]; !ok { + t.Fatalf("missing proxy service: %v", services) + } + + if err := agent.ServiceDeregister("foo"); err != nil { + t.Fatalf("err: %v", err) + } + if err := agent.ServiceDeregister("foo-proxy"); err != nil { + t.Fatalf("err: %v", err) + } +} + func TestAPI_AgentServices_CheckPassing(t *testing.T) { t.Parallel() c, s := makeClient(t) diff --git a/api/catalog.go b/api/catalog.go index 80ce1bc81..1a6bbc3b3 100644 --- a/api/catalog.go +++ b/api/catalog.go @@ -156,7 +156,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/api/catalog_test.go b/api/catalog_test.go index 11f50a919..9db640b9d 100644 --- a/api/catalog_test.go +++ b/api/catalog_test.go @@ -186,6 +186,7 @@ func TestAPI_CatalogService(t *testing.T) { defer s.Stop() catalog := c.Catalog() + retry.Run(t, func(r *retry.R) { services, meta, err := catalog.Service("consul", "", nil) if err != nil { @@ -235,6 +236,80 @@ func TestAPI_CatalogService_NodeMetaFilter(t *testing.T) { }) } +func TestAPI_CatalogConnect(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + catalog := c.Catalog() + + // Register service and proxy instances to test against. + service := &AgentService{ + ID: "redis1", + Service: "redis", + Port: 8000, + } + proxy := &AgentService{ + Kind: ServiceKindConnectProxy, + ProxyDestination: "redis", + ID: "redis-proxy1", + Service: "redis-proxy", + Port: 8001, + } + check := &AgentCheck{ + Node: "foobar", + CheckID: "service:redis1", + Name: "Redis health check", + Notes: "Script based health check", + Status: HealthPassing, + ServiceID: "redis1", + } + + reg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + Service: service, + Check: check, + } + proxyReg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + Service: proxy, + } + + retry.Run(t, func(r *retry.R) { + if _, err := catalog.Register(reg, nil); err != nil { + r.Fatal(err) + } + if _, err := catalog.Register(proxyReg, nil); err != nil { + r.Fatal(err) + } + + services, meta, err := catalog.Connect("redis", "", nil) + if err != nil { + r.Fatal(err) + } + + if meta.LastIndex == 0 { + r.Fatalf("Bad: %v", meta) + } + + if len(services) == 0 { + r.Fatalf("Bad: %v", services) + } + + if services[0].Datacenter != "dc1" { + r.Fatalf("Bad datacenter: %v", services[0]) + } + + if services[0].ServicePort != proxy.Port { + r.Fatalf("Returned port should be for proxy: %v", services[0]) + } + }) +} + func TestAPI_CatalogNode(t *testing.T) { t.Parallel() c, s := makeClient(t) @@ -297,10 +372,28 @@ func TestAPI_CatalogRegistration(t *testing.T) { Service: service, Check: check, } + // Register a connect proxy for that service too + proxy := &AgentService{ + ID: "redis-proxy1", + Service: "redis-proxy", + Port: 8001, + Kind: ServiceKindConnectProxy, + ProxyDestination: service.ID, + } + proxyReg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + NodeMeta: map[string]string{"somekey": "somevalue"}, + Service: proxy, + } retry.Run(t, func(r *retry.R) { if _, err := catalog.Register(reg, nil); err != nil { r.Fatal(err) } + if _, err := catalog.Register(proxyReg, nil); err != nil { + r.Fatal(err) + } node, _, err := catalog.Node("foobar", nil) if err != nil { @@ -311,6 +404,10 @@ func TestAPI_CatalogRegistration(t *testing.T) { r.Fatal("missing service: redis1") } + if _, ok := node.Services["redis-proxy1"]; !ok { + r.Fatal("missing service: redis-proxy1") + } + health, _, err := c.Health().Node("foobar", nil) if err != nil { r.Fatal(err) @@ -333,10 +430,22 @@ func TestAPI_CatalogRegistration(t *testing.T) { ServiceID: "redis1", } + // ... and proxy + deregProxy := &CatalogDeregistration{ + Datacenter: "dc1", + Node: "foobar", + Address: "192.168.10.10", + ServiceID: "redis-proxy1", + } + if _, err := catalog.Deregister(dereg, nil); err != nil { t.Fatalf("err: %v", err) } + if _, err := catalog.Deregister(deregProxy, nil); err != nil { + t.Fatalf("err: %v", err) + } + retry.Run(t, func(r *retry.R) { node, _, err := catalog.Node("foobar", nil) if err != nil { @@ -346,6 +455,10 @@ func TestAPI_CatalogRegistration(t *testing.T) { if _, ok := node.Services["redis1"]; ok { r.Fatal("ServiceID:redis1 is not deregistered") } + + if _, ok := node.Services["redis-proxy1"]; ok { + r.Fatal("ServiceID:redis-proxy1 is not deregistered") + } }) // Test deregistration of the previously registered check diff --git a/api/health.go b/api/health.go index 53f3de4f7..5fcb39b5c 100644 --- a/api/health.go +++ b/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. TODO: 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/api/health_test.go b/api/health_test.go index c4ef11651..5c3c2b6a2 100644 --- a/api/health_test.go +++ b/api/health_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/consul/testutil" "github.com/hashicorp/consul/testutil/retry" "github.com/pascaldekloe/goe/verify" + "github.com/stretchr/testify/require" ) func TestAPI_HealthNode(t *testing.T) { @@ -282,6 +283,56 @@ func TestAPI_HealthService(t *testing.T) { }) } +func TestAPI_HealthConnect(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + health := c.Health() + + // Make a service with a proxy + reg := &AgentServiceRegistration{ + Name: "foo", + Port: 8000, + } + err := agent.ServiceRegister(reg) + require.Nil(t, err) + defer agent.ServiceDeregister("foo") + + // Register the proxy + proxyReg := &AgentServiceRegistration{ + Name: "foo-proxy", + Port: 8001, + Kind: ServiceKindConnectProxy, + ProxyDestination: "foo", + } + err = agent.ServiceRegister(proxyReg) + require.Nil(t, err) + defer agent.ServiceDeregister("foo-proxy") + + retry.Run(t, func(r *retry.R) { + services, meta, err := health.Connect("foo", "", true, nil) + if err != nil { + r.Fatal(err) + } + if meta.LastIndex == 0 { + r.Fatalf("bad: %v", meta) + } + // Should be exactly 1 service - the original shouldn't show up as a connect + // endpoint, only it's proxy. + if len(services) != 1 { + r.Fatalf("Bad: %v", services) + } + if services[0].Node.Datacenter != "dc1" { + r.Fatalf("Bad datacenter: %v", services[0].Node) + } + if services[0].Service.Port != proxyReg.Port { + r.Fatalf("Bad port: %v", services[0]) + } + }) +} + func TestAPI_HealthService_NodeMetaFilter(t *testing.T) { t.Parallel() meta := map[string]string{"somekey": "somevalue"}