mirror of https://github.com/hashicorp/consul
Update gateway-services-nodes API endpoint to allow multiple addresses
Previously, we were only returning a single ListenerPort for a single service. However, we actually allow a single service to be serviced over multiple ports, as well as allow users to define what hostnames they expect their services to be contacted over. When no hosts are defined, we return the default ingress domain for any configured DNS domain. To show this in the UI, we modify the gateway-services-nodes API to return a GatewayConfig.Addresses field, which is a list of addresses over which the specific service can be contacted.pull/7932/head
parent
cc1407e867
commit
b3db907bdf
|
@ -318,7 +318,11 @@ START:
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceNodeCanonicalDNSName(sn *structs.ServiceNode, domain string) string {
|
func serviceNodeCanonicalDNSName(sn *structs.ServiceNode, domain string) string {
|
||||||
return serviceCanonicalDNSName(sn.ServiceName, sn.Datacenter, domain, &sn.EnterpriseMeta)
|
return serviceCanonicalDNSName(sn.ServiceName, "service", sn.Datacenter, domain, &sn.EnterpriseMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serviceIngressDNSName(service, datacenter, domain string, entMeta *structs.EnterpriseMeta) string {
|
||||||
|
return serviceCanonicalDNSName(service, "ingress", datacenter, domain, entMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlePtr is used to handle "reverse" DNS queries
|
// handlePtr is used to handle "reverse" DNS queries
|
||||||
|
|
|
@ -26,6 +26,6 @@ func (d *DNSServer) parseDatacenterAndEnterpriseMeta(labels []string, _ *dnsConf
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func serviceCanonicalDNSName(name, datacenter, domain string, _ *structs.EnterpriseMeta) string {
|
func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *structs.EnterpriseMeta) string {
|
||||||
return fmt.Sprintf("%s.service.%s.%s", name, datacenter, domain)
|
return fmt.Sprintf("%s.%s.%s.%s", name, kind, datacenter, domain)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1041,7 +1041,7 @@ func TestDNS_ServiceReverseLookup(t *testing.T) {
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
}
|
}
|
||||||
if ptrRec.Ptr != serviceCanonicalDNSName("db", "dc1", "consul", nil)+"." {
|
if ptrRec.Ptr != serviceCanonicalDNSName("db", "service", "dc1", "consul", nil)+"." {
|
||||||
t.Fatalf("Bad: %#v", ptrRec)
|
t.Fatalf("Bad: %#v", ptrRec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1089,7 +1089,7 @@ func TestDNS_ServiceReverseLookup_IPV6(t *testing.T) {
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
}
|
}
|
||||||
if ptrRec.Ptr != serviceCanonicalDNSName("db", "dc1", "consul", nil)+"." {
|
if ptrRec.Ptr != serviceCanonicalDNSName("db", "service", "dc1", "consul", nil)+"." {
|
||||||
t.Fatalf("Bad: %#v", ptrRec)
|
t.Fatalf("Bad: %#v", ptrRec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1139,7 +1139,7 @@ func TestDNS_ServiceReverseLookup_CustomDomain(t *testing.T) {
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("Bad: %#v", in.Answer[0])
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
}
|
}
|
||||||
if ptrRec.Ptr != serviceCanonicalDNSName("db", "dc1", "custom", nil)+"." {
|
if ptrRec.Ptr != serviceCanonicalDNSName("db", "service", "dc1", "custom", nil)+"." {
|
||||||
t.Fatalf("Bad: %#v", ptrRec)
|
t.Fatalf("Bad: %#v", ptrRec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -376,6 +376,23 @@ type GatewayService struct {
|
||||||
|
|
||||||
type GatewayServices []*GatewayService
|
type GatewayServices []*GatewayService
|
||||||
|
|
||||||
|
func (g *GatewayService) Addresses(defaultHosts []string) []string {
|
||||||
|
if g.Port == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hosts := g.Hosts
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
hosts = defaultHosts
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses []string
|
||||||
|
for _, h := range hosts {
|
||||||
|
addresses = append(addresses, fmt.Sprintf("%s:%d", h, g.Port))
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GatewayService) IsSame(o *GatewayService) bool {
|
func (g *GatewayService) IsSame(o *GatewayService) bool {
|
||||||
return g.Gateway.Matches(&o.Gateway) &&
|
return g.Gateway.Matches(&o.Gateway) &&
|
||||||
g.Service.Matches(&o.Service) &&
|
g.Service.Matches(&o.Service) &&
|
||||||
|
|
|
@ -562,3 +562,61 @@ func TestTerminatingConfigEntry_Validate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGatewayService_Addresses(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input GatewayService
|
||||||
|
argument []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "port is zero",
|
||||||
|
input: GatewayService{},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no hosts with empty array",
|
||||||
|
input: GatewayService{
|
||||||
|
Port: 8080,
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no hosts with default",
|
||||||
|
input: GatewayService{
|
||||||
|
Port: 8080,
|
||||||
|
},
|
||||||
|
argument: []string{
|
||||||
|
"service.ingress.dc.domain",
|
||||||
|
"service.ingress.dc.alt.domain",
|
||||||
|
},
|
||||||
|
expected: []string{
|
||||||
|
"service.ingress.dc.domain:8080",
|
||||||
|
"service.ingress.dc.alt.domain:8080",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user-defined hosts",
|
||||||
|
input: GatewayService{
|
||||||
|
Port: 8080,
|
||||||
|
Hosts: []string{"*.test.example.com", "other.example.com"},
|
||||||
|
},
|
||||||
|
argument: []string{
|
||||||
|
"service.ingress.dc.domain",
|
||||||
|
"service.ingress.alt.domain",
|
||||||
|
},
|
||||||
|
expected: []string{"*.test.example.com:8080", "other.example.com:8080"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range cases {
|
||||||
|
// We explicitly copy the variable for the range statement so that can run
|
||||||
|
// tests in parallel.
|
||||||
|
tc := test
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
addresses := tc.input.Addresses(tc.argument)
|
||||||
|
require.ElementsMatch(t, tc.expected, addresses)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/agent/config"
|
||||||
"github.com/hashicorp/consul/agent/structs"
|
"github.com/hashicorp/consul/agent/structs"
|
||||||
"github.com/hashicorp/consul/api"
|
"github.com/hashicorp/consul/api"
|
||||||
)
|
)
|
||||||
|
@ -16,7 +17,9 @@ import (
|
||||||
const metaExternalSource = "external-source"
|
const metaExternalSource = "external-source"
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
ListenerPort int
|
Addresses []string `json:",omitempty"`
|
||||||
|
// internal to track uniqueness
|
||||||
|
addressesSet map[string]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceSummary is used to summarize a service
|
// ServiceSummary is used to summarize a service
|
||||||
|
@ -161,7 +164,7 @@ RPC:
|
||||||
|
|
||||||
// Generate the summary
|
// Generate the summary
|
||||||
// TODO (gateways) (freddy) Have Internal.ServiceDump return ServiceDump instead. Need to add bexpr filtering for type.
|
// TODO (gateways) (freddy) Have Internal.ServiceDump return ServiceDump instead. Need to add bexpr filtering for type.
|
||||||
return summarizeServices(out.Nodes.ToServiceDump()), nil
|
return summarizeServices(out.Nodes.ToServiceDump(), s.agent.config), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
|
// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
|
||||||
|
@ -195,10 +198,11 @@ RPC:
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return summarizeServices(out.Dump), nil
|
|
||||||
|
return summarizeServices(out.Dump, s.agent.config), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func summarizeServices(dump structs.ServiceDump) []*ServiceSummary {
|
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig) []*ServiceSummary {
|
||||||
// Collect the summary information
|
// Collect the summary information
|
||||||
var services []structs.ServiceID
|
var services []structs.ServiceID
|
||||||
summary := make(map[structs.ServiceID]*ServiceSummary)
|
summary := make(map[structs.ServiceID]*ServiceSummary)
|
||||||
|
@ -220,8 +224,9 @@ func summarizeServices(dump structs.ServiceDump) []*ServiceSummary {
|
||||||
|
|
||||||
for _, csn := range dump {
|
for _, csn := range dump {
|
||||||
if csn.GatewayService != nil {
|
if csn.GatewayService != nil {
|
||||||
sum := getService(csn.GatewayService.Service.ToServiceID())
|
gwsvc := csn.GatewayService
|
||||||
sum.GatewayConfig.ListenerPort = csn.GatewayService.Port
|
sum := getService(gwsvc.Service.ToServiceID())
|
||||||
|
modifySummaryForGatewayService(cfg, sum, gwsvc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will happen in cases where we only have the GatewayServices mapping
|
// Will happen in cases where we only have the GatewayServices mapping
|
||||||
|
@ -299,3 +304,38 @@ func summarizeServices(dump structs.ServiceDump) []*ServiceSummary {
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func modifySummaryForGatewayService(
|
||||||
|
cfg *config.RuntimeConfig,
|
||||||
|
sum *ServiceSummary,
|
||||||
|
gwsvc *structs.GatewayService,
|
||||||
|
) {
|
||||||
|
var dnsAddresses []string
|
||||||
|
for _, domain := range []string{cfg.DNSDomain, cfg.DNSAltDomain} {
|
||||||
|
// If the domain is empty, do not use it to construct a valid DNS
|
||||||
|
// address
|
||||||
|
if domain == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dnsAddresses = append(dnsAddresses, serviceIngressDNSName(
|
||||||
|
gwsvc.Service.Name,
|
||||||
|
cfg.Datacenter,
|
||||||
|
domain,
|
||||||
|
&gwsvc.Service.EnterpriseMeta,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range gwsvc.Addresses(dnsAddresses) {
|
||||||
|
// check for duplicates, a service will have a ServiceInfo struct for
|
||||||
|
// every instance that is registered.
|
||||||
|
if _, ok := sum.GatewayConfig.addressesSet[addr]; !ok {
|
||||||
|
if sum.GatewayConfig.addressesSet == nil {
|
||||||
|
sum.GatewayConfig.addressesSet = make(map[string]struct{})
|
||||||
|
}
|
||||||
|
sum.GatewayConfig.addressesSet[addr] = struct{}{}
|
||||||
|
sum.GatewayConfig.Addresses = append(
|
||||||
|
sum.GatewayConfig.Addresses, addr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -535,7 +535,7 @@ func TestUIGatewayServiceNodes_Terminating(t *testing.T) {
|
||||||
func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
a := NewTestAgent(t, "")
|
a := NewTestAgent(t, `alt_domain = "alt.consul."`)
|
||||||
defer a.Shutdown()
|
defer a.Shutdown()
|
||||||
|
|
||||||
// Register ingress gateway and a service that will be associated with it
|
// Register ingress gateway and a service that will be associated with it
|
||||||
|
@ -593,6 +593,18 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
}
|
}
|
||||||
require.NoError(t, a.RPC("Catalog.Register", &arg, ®Output))
|
require.NoError(t, a.RPC("Catalog.Register", &arg, ®Output))
|
||||||
|
|
||||||
|
// Set web protocol to http
|
||||||
|
svcDefaultsReq := structs.ConfigEntryRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Entry: &structs.ServiceConfigEntry{
|
||||||
|
Name: "web",
|
||||||
|
Protocol: "http",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var configOutput bool
|
||||||
|
require.NoError(t, a.RPC("ConfigEntry.Apply", &svcDefaultsReq, &configOutput))
|
||||||
|
require.True(t, configOutput)
|
||||||
|
|
||||||
// Register ingress-gateway config entry, linking it to db and redis (does not exist)
|
// Register ingress-gateway config entry, linking it to db and redis (does not exist)
|
||||||
args := &structs.IngressGatewayConfigEntry{
|
args := &structs.IngressGatewayConfigEntry{
|
||||||
Name: "ingress-gateway",
|
Name: "ingress-gateway",
|
||||||
|
@ -609,13 +621,23 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Protocol: "tcp",
|
Protocol: "http",
|
||||||
Services: []structs.IngressService{
|
Services: []structs.IngressService{
|
||||||
{
|
{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Port: 8081,
|
||||||
|
Protocol: "http",
|
||||||
|
Services: []structs.IngressService{
|
||||||
|
{
|
||||||
|
Name: "web",
|
||||||
|
Hosts: []string{"*.test.example.com"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -624,7 +646,6 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Entry: args,
|
Entry: args,
|
||||||
}
|
}
|
||||||
var configOutput bool
|
|
||||||
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &configOutput))
|
require.NoError(t, a.RPC("ConfigEntry.Apply", &req, &configOutput))
|
||||||
require.True(t, configOutput)
|
require.True(t, configOutput)
|
||||||
}
|
}
|
||||||
|
@ -636,11 +657,23 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assertIndex(t, resp)
|
assertIndex(t, resp)
|
||||||
|
|
||||||
|
// Construct expected addresses so that differences between OSS/Ent are handled by code
|
||||||
|
webDNS := serviceIngressDNSName("web", "dc1", "consul.", structs.DefaultEnterpriseMeta())
|
||||||
|
webDNSAlt := serviceIngressDNSName("web", "dc1", "alt.consul.", structs.DefaultEnterpriseMeta())
|
||||||
|
dbDNS := serviceIngressDNSName("db", "dc1", "consul.", structs.DefaultEnterpriseMeta())
|
||||||
|
dbDNSAlt := serviceIngressDNSName("db", "dc1", "alt.consul.", structs.DefaultEnterpriseMeta())
|
||||||
|
|
||||||
dump := obj.([]*ServiceSummary)
|
dump := obj.([]*ServiceSummary)
|
||||||
expect := []*ServiceSummary{
|
expect := []*ServiceSummary{
|
||||||
{
|
{
|
||||||
Name: "web",
|
Name: "web",
|
||||||
GatewayConfig: GatewayConfig{ListenerPort: 8080},
|
GatewayConfig: GatewayConfig{
|
||||||
|
Addresses: []string{
|
||||||
|
fmt.Sprintf("%s:8080", webDNS),
|
||||||
|
fmt.Sprintf("%s:8080", webDNSAlt),
|
||||||
|
"*.test.example.com:8081",
|
||||||
|
},
|
||||||
|
},
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -651,9 +684,19 @@ func TestUIGatewayServiceNodes_Ingress(t *testing.T) {
|
||||||
ChecksPassing: 1,
|
ChecksPassing: 1,
|
||||||
ChecksWarning: 1,
|
ChecksWarning: 1,
|
||||||
ChecksCritical: 0,
|
ChecksCritical: 0,
|
||||||
GatewayConfig: GatewayConfig{ListenerPort: 8888},
|
GatewayConfig: GatewayConfig{
|
||||||
|
Addresses: []string{
|
||||||
|
fmt.Sprintf("%s:8888", dbDNS),
|
||||||
|
fmt.Sprintf("%s:8888", dbDNSAlt),
|
||||||
|
},
|
||||||
|
},
|
||||||
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// internal accounting that users don't see can be blown away
|
||||||
|
for _, sum := range dump {
|
||||||
|
sum.GatewayConfig.addressesSet = nil
|
||||||
|
}
|
||||||
assert.ElementsMatch(t, expect, dump)
|
assert.ElementsMatch(t, expect, dump)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue