diff --git a/agent/catalog_endpoint_test.go b/agent/catalog_endpoint_test.go index e1a836a029..0b2cca2a1a 100644 --- a/agent/catalog_endpoint_test.go +++ b/agent/catalog_endpoint_test.go @@ -1329,38 +1329,15 @@ func TestCatalog_GatewayServices_Terminating(t *testing.T) { testrpc.WaitForTestAgent(t, a.RPC, "dc1") - // Register a terminating gateway - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Kind: structs.ServiceKindTerminatingGateway, - Service: "terminating", - Port: 443, - }, - } - - var out struct{} - assert.NoError(t, a.RPC("Catalog.Register", &args, &out)) - - // Register two services the gateway will route to - args = structs.TestRegisterRequest(t) + // Register a service to be covered by the wildcard in the config entry + args := structs.TestRegisterRequest(t) args.Service.Service = "redis" args.Check = &structs.HealthCheck{ Name: "redis", Status: api.HealthPassing, ServiceID: args.Service.Service, } - assert.NoError(t, a.RPC("Catalog.Register", &args, &out)) - - args = structs.TestRegisterRequest(t) - args.Service.Service = "api" - args.Check = &structs.HealthCheck{ - Name: "api", - Status: api.HealthPassing, - ServiceID: args.Service.Service, - } + var out struct{} assert.NoError(t, a.RPC("Catalog.Register", &args, &out)) // Associate the gateway and api/redis services @@ -1441,41 +1418,7 @@ func TestCatalog_GatewayServices_Ingress(t *testing.T) { testrpc.WaitForTestAgent(t, a.RPC, "dc1") - // Register an ingress gateway - args := &structs.RegisterRequest{ - Datacenter: "dc1", - Node: "foo", - Address: "127.0.0.1", - Service: &structs.NodeService{ - Kind: structs.ServiceKindTerminatingGateway, - Service: "ingress", - Port: 444, - }, - } - - var out struct{} - require.NoError(t, a.RPC("Catalog.Register", &args, &out)) - - // Register two services the gateway will route to - args = structs.TestRegisterRequest(t) - args.Service.Service = "redis" - args.Check = &structs.HealthCheck{ - Name: "redis", - Status: api.HealthPassing, - ServiceID: args.Service.Service, - } - require.NoError(t, a.RPC("Catalog.Register", &args, &out)) - - args = structs.TestRegisterRequest(t) - args.Service.Service = "api" - args.Check = &structs.HealthCheck{ - Name: "api", - Status: api.HealthPassing, - ServiceID: args.Service.Service, - } - require.NoError(t, a.RPC("Catalog.Register", &args, &out)) - - // Associate the gateway and db service + // Associate an ingress gateway with api/redis entryArgs := &structs.ConfigEntryRequest{ Op: structs.ConfigEntryUpsert, Datacenter: "dc1", diff --git a/api/catalog.go b/api/catalog.go index dd34c17db4..607d5d065c 100644 --- a/api/catalog.go +++ b/api/catalog.go @@ -81,6 +81,29 @@ type CatalogDeregistration struct { Namespace string `json:",omitempty"` } +type CompoundServiceName struct { + Name string + + // Namespacing is a Consul Enterprise feature. + Namespace string `json:",omitempty"` +} + +// GatewayService associates a gateway with a linked service. +// It also contains service-specific gateway configuration like ingress listener port and protocol. +type GatewayService struct { + Gateway CompoundServiceName + Service CompoundServiceName + GatewayKind ServiceKind + Port int `json:",omitempty"` + Protocol string `json:",omitempty"` + Hosts []string `json:",omitempty"` + CAFile string `json:",omitempty"` + CertFile string `json:",omitempty"` + KeyFile string `json:",omitempty"` + SNI string `json:",omitempty"` + FromWildcard bool `json:",omitempty"` +} + // Catalog can be used to query the Catalog endpoints type Catalog struct { c *Client @@ -283,6 +306,27 @@ func (c *Catalog) NodeServiceList(node string, q *QueryOptions) (*CatalogNodeSer return out, qm, nil } +// GatewayServices is used to query the services associated with an ingress gateway or terminating gateway. +func (c *Catalog) GatewayServices(gateway string, q *QueryOptions) ([]*GatewayService, *QueryMeta, error) { + r := c.c.newRequest("GET", "/v1/catalog/gateway-services/"+gateway) + r.setQueryOptions(q) + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out []*GatewayService + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return out, qm, nil +} + func ParseServiceAddr(addrPort string) (ServiceAddress, error) { port := 0 host, portStr, err := net.SplitHostPort(addrPort) diff --git a/api/catalog_test.go b/api/catalog_test.go index 8b74c6fe89..0b8af734f0 100644 --- a/api/catalog_test.go +++ b/api/catalog_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -1088,3 +1089,149 @@ func TestAPI_CatalogEnableTagOverride(t *testing.T) { } }) } + +func TestAPI_CatalogGatewayServices_Terminating(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + s.WaitForSerfCheck(t) + + catalog := c.Catalog() + + // Register a service to be covered by a wildcard in the config entry + svc := &AgentService{ + ID: "redis", + Service: "redis", + Port: 6379, + } + reg := &CatalogRegistration{ + Datacenter: "dc1", + Node: "bar", + Address: "192.168.10.11", + Service: svc, + } + retry.Run(t, func(r *retry.R) { + if _, err := catalog.Register(reg, nil); err != nil { + r.Fatal(err) + } + }) + + entries := c.ConfigEntries() + + // Associate the gateway and api/redis services + gwEntry := TerminatingGatewayConfigEntry{ + Kind: TerminatingGateway, + Name: "terminating", + Services: []LinkedService{ + { + Name: "api", + CAFile: "api/ca.crt", + CertFile: "api/client.crt", + KeyFile: "api/client.key", + SNI: "my-domain", + }, + { + Name: "*", + CAFile: "ca.crt", + CertFile: "client.crt", + KeyFile: "client.key", + SNI: "my-alt-domain", + }, + }, + } + retry.Run(t, func(r *retry.R) { + if success, _, err := entries.Set(&gwEntry, nil); err != nil || !success { + r.Fatal(err) + } + }) + + expect := []*GatewayService{ + { + Service: CompoundServiceName{"api", defaultNamespace}, + Gateway: CompoundServiceName{"terminating", defaultNamespace}, + GatewayKind: ServiceKindTerminatingGateway, + CAFile: "api/ca.crt", + CertFile: "api/client.crt", + KeyFile: "api/client.key", + SNI: "my-domain", + }, + { + Service: CompoundServiceName{"redis", defaultNamespace}, + Gateway: CompoundServiceName{"terminating", defaultNamespace}, + GatewayKind: ServiceKindTerminatingGateway, + CAFile: "ca.crt", + CertFile: "client.crt", + KeyFile: "client.key", + SNI: "my-alt-domain", + FromWildcard: true, + }, + } + retry.Run(t, func(r *retry.R) { + resp, _, err := catalog.GatewayServices("terminating", nil) + assert.NoError(r, err) + assert.Equal(r, expect, resp) + }) +} + +func TestAPI_CatalogGatewayServices_Ingress(t *testing.T) { + t.Parallel() + c, s := makeClient(t) + defer s.Stop() + + s.WaitForSerfCheck(t) + + entries := c.ConfigEntries() + + // Associate the gateway and api/redis services + gwEntry := IngressGatewayConfigEntry{ + Kind: "ingress-gateway", + Name: "ingress", + Listeners: []IngressListener{ + { + Port: 8888, + Services: []IngressService{ + { + Name: "api", + }, + }, + }, + { + Port: 9999, + Services: []IngressService{ + { + Name: "redis", + }, + }, + }, + }, + } + retry.Run(t, func(r *retry.R) { + if success, _, err := entries.Set(&gwEntry, nil); err != nil || !success { + r.Fatal(err) + } + }) + + catalog := c.Catalog() + + expect := []*GatewayService{ + { + Service: CompoundServiceName{"api", defaultNamespace}, + Gateway: CompoundServiceName{"ingress", defaultNamespace}, + GatewayKind: ServiceKindIngressGateway, + Protocol: "tcp", + Port: 8888, + }, + { + Service: CompoundServiceName{"redis", defaultNamespace}, + Gateway: CompoundServiceName{"ingress", defaultNamespace}, + GatewayKind: ServiceKindIngressGateway, + Protocol: "tcp", + Port: 9999, + }, + } + retry.Run(t, func(r *retry.R) { + resp, _, err := catalog.GatewayServices("ingress", nil) + assert.NoError(r, err) + assert.Equal(r, expect, resp) + }) +}