diff --git a/.changelog/12098.txt b/.changelog/12098.txt new file mode 100644 index 0000000000..9ddb688a75 --- /dev/null +++ b/.changelog/12098.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Support connect-native services in the Topology view. +``` diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 7ceb98caff..cd87867e81 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -3461,6 +3461,7 @@ func (s *Store) ServiceTopology( err error fullyTransparent bool hasTransparent bool + connectNative bool ) switch kind { case structs.ServiceKindIngressGateway: @@ -3505,6 +3506,9 @@ func (s *Store) ServiceTopology( // transparent proxy mode. If ANY instance isn't in the right mode then the warming applies. fullyTransparent = false } + if proxy.ServiceConnect.Native { + connectNative = true + } } default: @@ -3526,8 +3530,8 @@ func (s *Store) ServiceTopology( upstreamDecisions := make(map[string]structs.IntentionDecisionSummary) - // Only transparent proxies have upstreams from intentions - if hasTransparent { + // Only transparent proxies / connect native services have upstreams from intentions + if hasTransparent || connectNative { idx, intentionUpstreams, err := s.intentionTopologyTxn(tx, ws, sn, false, defaultAllow) if err != nil { return 0, nil, err @@ -3607,8 +3611,8 @@ func (s *Store) ServiceTopology( sn = structs.NewServiceName(upstream.Service.Proxy.DestinationServiceName, &upstream.Service.EnterpriseMeta) } - // Avoid returning upstreams from intentions when none of the proxy instances of the target are in transparent mode. - if !hasTransparent && upstreamSources[sn.String()] != structs.TopologySourceRegistration { + // Avoid returning upstreams from intentions when none of the proxy instances of the target are in transparent mode or connect native. + if !hasTransparent && !connectNative && upstreamSources[sn.String()] != structs.TopologySourceRegistration { continue } upstreams = append(upstreams, upstream) @@ -3711,6 +3715,7 @@ func (s *Store) ServiceTopology( } idx, unfilteredDownstreams, err := s.combinedServiceNodesTxn(tx, ws, downstreamNames) + if err != nil { return 0, nil, fmt.Errorf("failed to get downstreams for %q: %v", sn.String(), err) } @@ -3734,8 +3739,8 @@ func (s *Store) ServiceTopology( if downstream.Service.Kind == structs.ServiceKindConnectProxy { sn = structs.NewServiceName(downstream.Service.Proxy.DestinationServiceName, &downstream.Service.EnterpriseMeta) } - if _, ok := tproxyMap[sn]; !ok && downstreamSources[sn.String()] != structs.TopologySourceRegistration { - // If downstream is not a transparent proxy, remove references + if _, ok := tproxyMap[sn]; !ok && !downstream.Service.Connect.Native && downstreamSources[sn.String()] != structs.TopologySourceRegistration { + // If downstream is not a transparent proxy or connect native, remove references delete(downstreamSources, sn.String()) delete(downstreamDecisions, sn.String()) continue diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index 56de071c08..cba1d1222a 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -35,6 +35,7 @@ type ServiceSummary struct { GatewayConfig GatewayConfig TransparentProxy bool transparentProxySet bool + ConnectNative bool structs.EnterpriseMeta } @@ -422,6 +423,7 @@ func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc s sum.Kind = svc.Kind sum.Datacenter = csn.Node.Datacenter sum.InstanceCount += 1 + sum.ConnectNative = svc.Connect.Native if svc.Kind == structs.ServiceKindConnectProxy { sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta) hasProxy[sn] = true diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index 757975bc5c..f2a17f73e0 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -1281,6 +1281,142 @@ func TestUIServiceTopology(t *testing.T) { }, }, }, + "Node cnative": { + Datacenter: "dc1", + Node: "cnative", + Address: "127.0.0.6", + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:alive", + Name: "cnative-liveness", + Status: api.HealthPassing, + }, + }, + }, + "Service cbackend on cnative": { + Datacenter: "dc1", + Node: "cnative", + SkipNodeUpdate: true, + Service: &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "cbackend", + Service: "cbackend", + Port: 8080, + Address: "198.18.1.70", + }, + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:cbackend", + Name: "cbackend-liveness", + Status: api.HealthPassing, + ServiceID: "cbackend", + ServiceName: "cbackend", + }, + }, + }, + "Service cbackend-proxy on cnative": { + Datacenter: "dc1", + Node: "cnative", + SkipNodeUpdate: true, + Service: &structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + ID: "cbackend-proxy", + Service: "cbackend-proxy", + Port: 8443, + Address: "198.18.1.70", + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "cbackend", + }, + }, + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:cbackend-proxy", + Name: "cbackend proxy listening", + Status: api.HealthCritical, + ServiceID: "cbackend-proxy", + ServiceName: "cbackend-proxy", + }, + }, + }, + "Service cfrontend on cnative": { + Datacenter: "dc1", + Node: "cnative", + SkipNodeUpdate: true, + Service: &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "cfrontend", + Service: "cfrontend", + Port: 9080, + Address: "198.18.1.70", + }, + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:cfrontend", + Name: "cfrontend-liveness", + Status: api.HealthPassing, + ServiceID: "cfrontend", + ServiceName: "cfrontend", + }, + }, + }, + "Service cfrontend-proxy on cnative": { + Datacenter: "dc1", + Node: "cnative", + SkipNodeUpdate: true, + Service: &structs.NodeService{ + Kind: structs.ServiceKindConnectProxy, + ID: "cfrontend-proxy", + Service: "cfrontend-proxy", + Port: 9443, + Address: "198.18.1.70", + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "cfrontend", + Upstreams: structs.Upstreams{ + { + DestinationName: "cproxy", + LocalBindPort: 123, + }, + }, + }, + }, + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:cfrontend-proxy", + Name: "cfrontend proxy listening", + Status: api.HealthCritical, + ServiceID: "cfrontend-proxy", + ServiceName: "cfrontend-proxy", + }, + }, + }, + "Service cproxy on cnative": { + Datacenter: "dc1", + Node: "cnative", + SkipNodeUpdate: true, + Service: &structs.NodeService{ + Kind: structs.ServiceKindTypical, + ID: "cproxy", + Service: "cproxy", + Port: 1111, + Address: "198.18.1.70", + Connect: structs.ServiceConnect{Native: true}, + }, + Checks: structs.HealthChecks{ + &structs.HealthCheck{ + Node: "cnative", + CheckID: "cnative:cproxy", + Name: "cproxy-liveness", + Status: api.HealthPassing, + ServiceID: "cproxy", + ServiceName: "cproxy", + }, + }, + }, } for _, args := range registrations { var out struct{} @@ -1292,6 +1428,8 @@ func TestUIServiceTopology(t *testing.T) { // wildcard deny intention // api -> web exact intention // web -> redis exact intention + // cfrontend -> cproxy exact intention + // cproxy -> cbackend exact intention { entries := []structs.ConfigEntryRequest{ { @@ -1391,6 +1529,32 @@ func TestUIServiceTopology(t *testing.T) { }, }, }, + { + Datacenter: "dc1", + Entry: &structs.ServiceIntentionsConfigEntry{ + Kind: structs.ServiceIntentions, + Name: "cproxy", + Sources: []*structs.SourceIntention{ + { + Action: structs.IntentionActionAllow, + Name: "cfrontend", + }, + }, + }, + }, + { + Datacenter: "dc1", + Entry: &structs.ServiceIntentionsConfigEntry{ + Kind: structs.ServiceIntentions, + Name: "cbackend", + Sources: []*structs.SourceIntention{ + { + Action: structs.IntentionActionAllow, + Name: "cproxy", + }, + }, + }, + }, } for _, req := range entries { out := false @@ -1620,6 +1784,60 @@ func TestUIServiceTopology(t *testing.T) { FilteredByACLs: false, }, }, + { + name: "cproxy", + httpReq: func() *http.Request { + req, _ := http.NewRequest("GET", "/v1/internal/ui/service-topology/cproxy?kind=", nil) + return req + }(), + want: &ServiceTopology{ + Protocol: "http", + TransparentProxy: false, + Upstreams: []*ServiceTopologySummary{ + { + ServiceSummary: ServiceSummary{ + Name: "cbackend", + Datacenter: "dc1", + Nodes: []string{"cnative"}, + InstanceCount: 1, + ChecksPassing: 2, + ChecksWarning: 0, + ChecksCritical: 1, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + Intention: structs.IntentionDecisionSummary{ + DefaultAllow: true, + Allowed: true, + HasPermissions: false, + HasExact: true, + }, + Source: structs.TopologySourceSpecificIntention, + }, + }, + Downstreams: []*ServiceTopologySummary{ + { + ServiceSummary: ServiceSummary{ + Name: "cfrontend", + Datacenter: "dc1", + Nodes: []string{"cnative"}, + InstanceCount: 1, + ChecksPassing: 2, + ChecksWarning: 0, + ChecksCritical: 1, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, + Intention: structs.IntentionDecisionSummary{ + DefaultAllow: true, + Allowed: true, + HasPermissions: false, + HasExact: true, + }, + Source: structs.TopologySourceRegistration, + }, + }, + FilteredByACLs: false, + }, + }, } for _, tc := range tcs { diff --git a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs index c27c6d0289..6f140d348d 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs @@ -92,7 +92,7 @@ @disabled={{disabled}} @oncreate={{action @oncreate item @service}} /> - {{else if (and (not-eq item.Datacenter '') item.Intention.Allowed (not item.TransparentProxy) (eq item.Source 'specific-intention'))}} + {{else if (and (not-eq item.Datacenter '') item.Intention.Allowed (not item.TransparentProxy) (not item.ConnectNative) (eq item.Source 'specific-intention'))}} - item.Source === 'specific-intention' && !item.TransparentProxy && item.Intention.Allowed + item.Source === 'specific-intention' && !item.TransparentProxy && !item.ConnectNative && item.Intention.Allowed ).length !== 0; return undefinedDownstream;