// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package peering import ( "fmt" "testing" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/testing/deployer/topology" "github.com/stretchr/testify/require" ) type ac2DiscoChainSuite struct { DC string Peer string clientSID topology.ID } var ac2DiscoChainSuites []sharedTopoSuite = []sharedTopoSuite{ &ac2DiscoChainSuite{DC: "dc1", Peer: "dc2"}, &ac2DiscoChainSuite{DC: "dc2", Peer: "dc1"}, } func TestAC2DiscoChain(t *testing.T) { runShareableSuites(t, ac2DiscoChainSuites) } func (s *ac2DiscoChainSuite) testName() string { return fmt.Sprintf("ac2 disco chain %s->%s", s.DC, s.Peer) } func (s *ac2DiscoChainSuite) setup(t *testing.T, ct *commonTopo) { clu := ct.ClusterByDatacenter(t, s.DC) peerClu := ct.ClusterByDatacenter(t, s.Peer) partition := "default" peer := LocalPeerName(peerClu, "default") // Make an HTTP server with discovery chain config entries server := NewFortioServiceWithDefaults( clu.Datacenter, topology.ID{ Name: "ac2-disco-chain-svc", Partition: partition, }, nil, ) ct.ExportService(clu, partition, api.ExportedService{ Name: server.ID.Name, Consumers: []api.ServiceConsumer{ { Peer: peer, }, }, }, ) clu.InitialConfigEntries = append(clu.InitialConfigEntries, &api.ServiceConfigEntry{ Kind: api.ServiceDefaults, Name: server.ID.Name, Partition: ConfigEntryPartition(partition), Protocol: "http", }, &api.ServiceSplitterConfigEntry{ Kind: api.ServiceSplitter, Name: server.ID.Name, Partition: ConfigEntryPartition(partition), Splits: []api.ServiceSplit{ { Weight: 100.0, ResponseHeaders: &api.HTTPHeaderModifiers{ Add: map[string]string{ "X-Split": "test", }, }, }, }, }, ) ct.AddServiceNode(clu, serviceExt{Workload: server}) // Define server as upstream for client upstream := &topology.Destination{ ID: topology.ID{ Name: server.ID.Name, Partition: partition, // TODO: iterate over all possible partitions }, // TODO: we need to expose this on 0.0.0.0 so we can check it // through our forward proxy. not realistic IMO LocalAddress: "0.0.0.0", LocalPort: 5000, Peer: peer, } // Make client which will dial server clientSID := topology.ID{ Name: "ac2-client", Partition: partition, } client := NewFortioServiceWithDefaults( clu.Datacenter, clientSID, func(s *topology.Workload) { s.Destinations = []*topology.Destination{ upstream, } }, ) ct.ExportService(clu, partition, api.ExportedService{ Name: client.ID.Name, Consumers: []api.ServiceConsumer{ { Peer: peer, }, }, }, ) ct.AddServiceNode(clu, serviceExt{Workload: client}) clu.InitialConfigEntries = append(clu.InitialConfigEntries, &api.ServiceConfigEntry{ Kind: api.ServiceDefaults, Name: client.ID.Name, Partition: ConfigEntryPartition(partition), Protocol: "http", UpstreamConfig: &api.UpstreamConfiguration{ Defaults: &api.UpstreamConfig{ MeshGateway: api.MeshGatewayConfig{ Mode: api.MeshGatewayModeLocal, }, }, }, }, ) // Add intention allowing client to call server clu.InitialConfigEntries = append(clu.InitialConfigEntries, &api.ServiceIntentionsConfigEntry{ Kind: api.ServiceIntentions, Name: server.ID.Name, Partition: ConfigEntryPartition(partition), Sources: []*api.SourceIntention{ { Name: client.ID.Name, Peer: peer, Action: api.IntentionActionAllow, }, }, }, ) s.clientSID = clientSID } func (s *ac2DiscoChainSuite) test(t *testing.T, ct *commonTopo) { dc := ct.Sprawl.Topology().Clusters[s.DC] svcs := dc.WorkloadsByID(s.clientSID) require.Len(t, svcs, 1, "expected exactly one client in datacenter") client := svcs[0] require.Len(t, client.Destinations, 1, "expected exactly one upstream for client") u := client.Destinations[0] t.Run("peered upstream exists in catalog", func(t *testing.T) { t.Parallel() ct.Assert.CatalogServiceExists(t, s.DC, u.ID.Name, &api.QueryOptions{ Peer: u.Peer, }) }) t.Run("peered upstream endpoint status is healthy", func(t *testing.T) { t.Parallel() ct.Assert.DestinationEndpointStatus(t, client, peerClusterPrefix(u), "HEALTHY", 1) }) t.Run("response contains header injected by splitter", func(t *testing.T) { t.Parallel() // TODO: not sure we should call u.LocalPort? it's not realistic from a security // standpoint. prefer the fortio fetch2 stuff myself ct.Assert.HTTPServiceEchoesResHeader(t, client, u.LocalPort, "", map[string]string{ "X-Split": "test", }, ) }) } // For reference see consul/xds/clusters.go: // // func (s *ResourceGenerator) getTargetClusterName // // and connect/sni.go func peerClusterPrefix(u *topology.Destination) string { if u.Peer == "" { panic("upstream is not from a peer") } u.ID.Normalize() return u.ID.Name + "." + u.ID.Namespace + "." + u.Peer + ".external" }