2018-10-03 12:36:38 +00:00
|
|
|
package proxycfg
|
|
|
|
|
|
|
|
import (
|
2019-06-18 00:52:01 +00:00
|
|
|
"context"
|
2019-07-02 14:29:09 +00:00
|
|
|
"fmt"
|
2020-04-14 23:46:40 +00:00
|
|
|
"io/ioutil"
|
2019-07-12 19:16:21 +00:00
|
|
|
"path"
|
2020-04-14 23:46:40 +00:00
|
|
|
"path/filepath"
|
2022-03-15 14:07:40 +00:00
|
|
|
"runtime"
|
2018-10-03 12:36:38 +00:00
|
|
|
"sync"
|
|
|
|
"sync/atomic"
|
|
|
|
"time"
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
"github.com/hashicorp/go-hclog"
|
2021-04-29 18:54:05 +00:00
|
|
|
"github.com/mitchellh/go-testing-interface"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2018-10-03 12:36:38 +00:00
|
|
|
"github.com/hashicorp/consul/agent/cache"
|
|
|
|
cachetype "github.com/hashicorp/consul/agent/cache-types"
|
|
|
|
"github.com/hashicorp/consul/agent/connect"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
2020-06-19 19:31:39 +00:00
|
|
|
"github.com/hashicorp/consul/api"
|
2018-10-03 12:36:38 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// TestCacheTypes encapsulates all the different cache types proxycfg.State will
|
2018-10-09 16:57:26 +00:00
|
|
|
// watch/request for controlling one during testing.
|
2018-10-03 12:36:38 +00:00
|
|
|
type TestCacheTypes struct {
|
2019-09-26 02:55:52 +00:00
|
|
|
roots *ControllableCacheType
|
|
|
|
leaf *ControllableCacheType
|
|
|
|
intentions *ControllableCacheType
|
|
|
|
health *ControllableCacheType
|
|
|
|
query *ControllableCacheType
|
|
|
|
compiledChain *ControllableCacheType
|
|
|
|
serviceHTTPChecks *ControllableCacheType
|
2018-10-03 12:36:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewTestCacheTypes creates a set of ControllableCacheTypes for all types that
|
|
|
|
// proxycfg will watch suitable for testing a proxycfg.State or Manager.
|
|
|
|
func NewTestCacheTypes(t testing.T) *TestCacheTypes {
|
|
|
|
t.Helper()
|
|
|
|
ct := &TestCacheTypes{
|
2019-09-26 02:55:52 +00:00
|
|
|
roots: NewControllableCacheType(t),
|
|
|
|
leaf: NewControllableCacheType(t),
|
|
|
|
intentions: NewControllableCacheType(t),
|
|
|
|
health: NewControllableCacheType(t),
|
|
|
|
query: NewControllableCacheType(t),
|
|
|
|
compiledChain: NewControllableCacheType(t),
|
|
|
|
serviceHTTPChecks: NewControllableCacheType(t),
|
2018-10-03 12:36:38 +00:00
|
|
|
}
|
|
|
|
ct.query.blocking = false
|
|
|
|
return ct
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestCacheWithTypes registers ControllableCacheTypes for all types that
|
|
|
|
// proxycfg will watch suitable for testing a proxycfg.State or Manager.
|
|
|
|
func TestCacheWithTypes(t testing.T, types *TestCacheTypes) *cache.Cache {
|
2020-07-28 16:24:30 +00:00
|
|
|
c := cache.New(cache.Options{})
|
2020-04-14 22:29:30 +00:00
|
|
|
c.RegisterType(cachetype.ConnectCARootName, types.roots)
|
|
|
|
c.RegisterType(cachetype.ConnectCALeafName, types.leaf)
|
|
|
|
c.RegisterType(cachetype.IntentionMatchName, types.intentions)
|
|
|
|
c.RegisterType(cachetype.HealthServicesName, types.health)
|
|
|
|
c.RegisterType(cachetype.PreparedQueryName, types.query)
|
|
|
|
c.RegisterType(cachetype.CompiledDiscoveryChainName, types.compiledChain)
|
|
|
|
c.RegisterType(cachetype.ServiceHTTPChecksName, types.serviceHTTPChecks)
|
2019-07-02 03:10:51 +00:00
|
|
|
|
2018-10-03 12:36:38 +00:00
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2018-10-09 16:57:26 +00:00
|
|
|
// TestCerts generates a CA and Leaf suitable for returning as mock CA
|
2018-10-03 12:36:38 +00:00
|
|
|
// root/leaf cache requests.
|
|
|
|
func TestCerts(t testing.T) (*structs.IndexedCARoots, *structs.IssuedCert) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
ca := connect.TestCA(t, nil)
|
|
|
|
roots := &structs.IndexedCARoots{
|
|
|
|
ActiveRootID: ca.ID,
|
2019-07-02 14:29:09 +00:00
|
|
|
TrustDomain: fmt.Sprintf("%s.consul", connect.TestClusterID),
|
2018-10-03 12:36:38 +00:00
|
|
|
Roots: []*structs.CARoot{ca},
|
|
|
|
}
|
|
|
|
return roots, TestLeafForCA(t, ca)
|
|
|
|
}
|
|
|
|
|
2018-10-09 16:57:26 +00:00
|
|
|
// TestLeafForCA generates new Leaf suitable for returning as mock CA
|
|
|
|
// leaf cache response, signed by an existing CA.
|
2018-10-03 12:36:38 +00:00
|
|
|
func TestLeafForCA(t testing.T, ca *structs.CARoot) *structs.IssuedCert {
|
|
|
|
leafPEM, pkPEM := connect.TestLeaf(t, "web", ca)
|
|
|
|
|
|
|
|
leafCert, err := connect.ParseCert(leafPEM)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return &structs.IssuedCert{
|
2019-09-23 17:52:35 +00:00
|
|
|
SerialNumber: connect.EncodeSerialNumber(leafCert.SerialNumber),
|
2018-10-03 12:36:38 +00:00
|
|
|
CertPEM: leafPEM,
|
|
|
|
PrivateKeyPEM: pkPEM,
|
|
|
|
Service: "web",
|
|
|
|
ServiceURI: leafCert.URIs[0].String(),
|
|
|
|
ValidAfter: leafCert.NotBefore,
|
|
|
|
ValidBefore: leafCert.NotAfter,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestIntentions returns a sample intentions match result useful to
|
|
|
|
// mocking service discovery cache results.
|
2020-08-27 17:20:58 +00:00
|
|
|
func TestIntentions() *structs.IndexedIntentionMatches {
|
2018-10-03 12:36:38 +00:00
|
|
|
return &structs.IndexedIntentionMatches{
|
|
|
|
Matches: []structs.Intentions{
|
|
|
|
[]*structs.Intention{
|
2020-06-16 17:19:31 +00:00
|
|
|
{
|
2018-10-03 12:36:38 +00:00
|
|
|
ID: "foo",
|
|
|
|
SourceNS: "default",
|
|
|
|
SourceName: "billing",
|
|
|
|
DestinationNS: "default",
|
|
|
|
DestinationName: "web",
|
|
|
|
Action: structs.IntentionActionAllow,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestUpstreamNodes returns a sample service discovery result useful to
|
|
|
|
// mocking service discovery cache results.
|
2021-08-19 00:03:22 +00:00
|
|
|
func TestUpstreamNodes(t testing.T, service string) structs.CheckServiceNodes {
|
2018-10-03 12:36:38 +00:00
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test1",
|
|
|
|
Node: "test1",
|
|
|
|
Address: "10.10.1.1",
|
|
|
|
Datacenter: "dc1",
|
2021-08-19 20:09:42 +00:00
|
|
|
Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(),
|
2018-10-03 12:36:38 +00:00
|
|
|
},
|
2021-08-19 00:03:22 +00:00
|
|
|
Service: structs.TestNodeServiceWithName(t, service),
|
2018-10-03 12:36:38 +00:00
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test2",
|
|
|
|
Node: "test2",
|
|
|
|
Address: "10.10.1.2",
|
|
|
|
Datacenter: "dc1",
|
2021-08-19 20:09:42 +00:00
|
|
|
Partition: structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty(),
|
2018-10-03 12:36:38 +00:00
|
|
|
},
|
2021-08-19 00:03:22 +00:00
|
|
|
Service: structs.TestNodeServiceWithName(t, service),
|
2018-10-03 12:36:38 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-19 22:26:58 +00:00
|
|
|
// TestPreparedQueryNodes returns instances of a service spread across two datacenters.
|
|
|
|
// The service instance names use a "-target" suffix to ensure we don't use the
|
|
|
|
// prepared query's name for SAN validation.
|
|
|
|
// The name of prepared queries won't always match the name of the service they target.
|
|
|
|
func TestPreparedQueryNodes(t testing.T, query string) structs.CheckServiceNodes {
|
2021-08-19 00:06:41 +00:00
|
|
|
nodes := structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test1",
|
|
|
|
Node: "test1",
|
|
|
|
Address: "10.10.1.1",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
2021-08-19 22:26:58 +00:00
|
|
|
Service: query + "-sidecar-proxy",
|
2021-08-19 00:06:41 +00:00
|
|
|
Port: 8080,
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
2021-08-19 22:26:58 +00:00
|
|
|
DestinationServiceName: query + "-target",
|
2021-08-19 00:06:41 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test2",
|
|
|
|
Node: "test2",
|
|
|
|
Address: "10.20.1.2",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindTypical,
|
2021-08-19 22:26:58 +00:00
|
|
|
Service: query + "-target",
|
2021-08-19 00:06:41 +00:00
|
|
|
Port: 8080,
|
|
|
|
Connect: structs.ServiceConnect{Native: true},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return nodes
|
|
|
|
}
|
|
|
|
|
2019-08-05 18:30:35 +00:00
|
|
|
func TestUpstreamNodesInStatus(t testing.T, status string) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test1",
|
|
|
|
Node: "test1",
|
|
|
|
Address: "10.10.1.1",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
&structs.HealthCheck{
|
|
|
|
Node: "test1",
|
|
|
|
ServiceName: "web",
|
|
|
|
Name: "force",
|
|
|
|
Status: status,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test2",
|
|
|
|
Node: "test2",
|
|
|
|
Address: "10.10.1.2",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
&structs.HealthCheck{
|
|
|
|
Node: "test2",
|
|
|
|
ServiceName: "web",
|
|
|
|
Name: "force",
|
|
|
|
Status: status,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-12 19:16:21 +00:00
|
|
|
func TestUpstreamNodesDC2(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test1",
|
|
|
|
Node: "test1",
|
|
|
|
Address: "10.20.1.1",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test2",
|
|
|
|
Node: "test2",
|
|
|
|
Address: "10.20.1.2",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-05 18:30:35 +00:00
|
|
|
func TestUpstreamNodesInStatusDC2(t testing.T, status string) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test1",
|
|
|
|
Node: "test1",
|
|
|
|
Address: "10.20.1.1",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
&structs.HealthCheck{
|
|
|
|
Node: "test1",
|
|
|
|
ServiceName: "web",
|
|
|
|
Name: "force",
|
|
|
|
Status: status,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "test2",
|
|
|
|
Node: "test2",
|
|
|
|
Address: "10.20.1.2",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
&structs.HealthCheck{
|
|
|
|
Node: "test2",
|
|
|
|
ServiceName: "web",
|
|
|
|
Name: "force",
|
|
|
|
Status: status,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-12 19:16:21 +00:00
|
|
|
func TestUpstreamNodesAlternate(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "alt-test1",
|
|
|
|
Node: "alt-test1",
|
|
|
|
Address: "10.20.1.1",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "alt-test2",
|
|
|
|
Node: "alt-test2",
|
|
|
|
Address: "10.20.1.2",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeService(t),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-05 18:30:35 +00:00
|
|
|
func TestGatewayNodesDC1(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.10.1.1",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.10.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.10.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.118.1.1", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-2",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.10.1.2",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.10.1.2", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.2", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.118.1.2", Port: 443}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-18 00:52:01 +00:00
|
|
|
func TestGatewayNodesDC2(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.0.1.1",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.0.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.18.1.1", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-2",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.0.1.2",
|
|
|
|
Datacenter: "dc2",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.0.1.2", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.2", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.18.1.2", Port: 443}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-05 18:30:35 +00:00
|
|
|
func TestGatewayNodesDC3(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.1",
|
|
|
|
Datacenter: "dc3",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.38.1.1", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-2",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.2",
|
|
|
|
Datacenter: "dc3",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.2", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.30.1.2", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.38.1.2", Port: 443}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-03 21:28:45 +00:00
|
|
|
func TestGatewayNodesDC4Hostname(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.1",
|
|
|
|
Datacenter: "dc4",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "123.us-west-2.elb.notaws.com", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-2",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.2",
|
|
|
|
Datacenter: "dc4",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.2", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.30.1.2", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "456.us-west-2.elb.notaws.com", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-3",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.3",
|
|
|
|
Datacenter: "dc4",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.3", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.30.1.3", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.38.1.1", Port: 443}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGatewayNodesDC5Hostname(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.1",
|
|
|
|
Datacenter: "dc5",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "123.us-west-2.elb.notaws.com", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-2",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.2",
|
|
|
|
Datacenter: "dc5",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.2", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.30.1.2", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "456.us-west-2.elb.notaws.com", Port: 443}),
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-3",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.3",
|
|
|
|
Datacenter: "dc5",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.3", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.30.1.3", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "198.38.1.1", Port: 443}),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-19 19:31:39 +00:00
|
|
|
func TestGatewayNodesDC6Hostname(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "mesh-gateway-1",
|
|
|
|
Node: "mesh-gateway",
|
|
|
|
Address: "10.30.1.1",
|
|
|
|
Datacenter: "dc6",
|
|
|
|
},
|
|
|
|
Service: structs.TestNodeServiceMeshGatewayWithAddrs(t,
|
|
|
|
"10.30.1.1", 8443,
|
|
|
|
structs.ServiceAddress{Address: "10.0.1.1", Port: 8443},
|
|
|
|
structs.ServiceAddress{Address: "123.us-east-1.elb.notaws.com", Port: 443}),
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
{
|
|
|
|
Status: api.HealthCritical,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-02 13:43:35 +00:00
|
|
|
func TestGatewayServiceGroupBarDC1(t testing.T) structs.CheckServiceNodes {
|
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "bar-node-1",
|
|
|
|
Node: "bar-node-1",
|
|
|
|
Address: "10.1.1.4",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "bar-sidecar-proxy",
|
|
|
|
Address: "172.16.1.6",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "1",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "bar",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "bar-node-2",
|
|
|
|
Node: "bar-node-2",
|
|
|
|
Address: "10.1.1.5",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "bar-sidecar-proxy",
|
|
|
|
Address: "172.16.1.7",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "1",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "bar",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "bar-node-3",
|
|
|
|
Node: "bar-node-3",
|
|
|
|
Address: "10.1.1.6",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "bar-sidecar-proxy",
|
|
|
|
Address: "172.16.1.8",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "2",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "bar",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGatewayServiceGroupFooDC1(t testing.T) structs.CheckServiceNodes {
|
2019-06-18 00:52:01 +00:00
|
|
|
return structs.CheckServiceNodes{
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "foo-node-1",
|
|
|
|
Node: "foo-node-1",
|
|
|
|
Address: "10.1.1.1",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
2019-07-02 13:43:35 +00:00
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "foo-sidecar-proxy",
|
|
|
|
Address: "172.16.1.3",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "1",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "foo",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
2019-06-18 00:52:01 +00:00
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "foo-node-2",
|
|
|
|
Node: "foo-node-2",
|
|
|
|
Address: "10.1.1.2",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
2019-07-02 13:43:35 +00:00
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "foo-sidecar-proxy",
|
|
|
|
Address: "172.16.1.4",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "1",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "foo",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "foo-node-3",
|
|
|
|
Node: "foo-node-3",
|
|
|
|
Address: "10.1.1.3",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "foo-sidecar-proxy",
|
|
|
|
Address: "172.16.1.5",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "2",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "foo",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
structs.CheckServiceNode{
|
|
|
|
Node: &structs.Node{
|
|
|
|
ID: "foo-node-4",
|
|
|
|
Node: "foo-node-4",
|
|
|
|
Address: "10.1.1.7",
|
|
|
|
Datacenter: "dc1",
|
|
|
|
},
|
|
|
|
Service: &structs.NodeService{
|
|
|
|
Kind: structs.ServiceKindConnectProxy,
|
|
|
|
Service: "foo-sidecar-proxy",
|
|
|
|
Address: "172.16.1.9",
|
|
|
|
Port: 2222,
|
|
|
|
Meta: map[string]string{
|
|
|
|
"version": "2",
|
|
|
|
},
|
|
|
|
Proxy: structs.ConnectProxyConfig{
|
|
|
|
DestinationServiceName: "foo",
|
|
|
|
Upstreams: structs.TestUpstreams(t),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Checks: structs.HealthChecks{
|
|
|
|
&structs.HealthCheck{
|
|
|
|
Node: "foo-node-4",
|
|
|
|
ServiceName: "foo-sidecar-proxy",
|
|
|
|
Name: "proxy-alive",
|
|
|
|
Status: "warning",
|
|
|
|
},
|
|
|
|
},
|
2019-06-18 00:52:01 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
type noopCacheNotifier struct{}
|
2019-08-22 20:11:56 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
var _ CacheNotifier = (*noopCacheNotifier)(nil)
|
2019-08-22 20:11:56 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
func (*noopCacheNotifier) Notify(_ context.Context, _ string, _ cache.Request, _ string, _ chan<- cache.UpdateEvent) error {
|
|
|
|
return nil
|
2021-04-29 18:54:05 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
type noopHealth struct{}
|
2019-08-22 20:11:56 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
var _ Health = (*noopHealth)(nil)
|
2020-04-14 21:30:00 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
func (*noopHealth) Notify(_ context.Context, _ structs.ServiceSpecificRequest, _ string, _ chan<- cache.UpdateEvent) error {
|
|
|
|
return nil
|
2020-04-14 21:30:00 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
// testConfigSnapshotFixture helps you execute normal proxycfg event machinery
|
|
|
|
// to assemble a ConfigSnapshot via standard means to ensure test data used in
|
|
|
|
// any tests is actually a valid configuration.
|
|
|
|
//
|
|
|
|
// The provided ns argument will be manipulated by the nsFn callback if present
|
|
|
|
// before it is used.
|
|
|
|
//
|
|
|
|
// The events provided in the updates slice will be fed into the event
|
|
|
|
// machinery.
|
|
|
|
func testConfigSnapshotFixture(
|
|
|
|
t testing.T,
|
|
|
|
ns *structs.NodeService,
|
|
|
|
nsFn func(ns *structs.NodeService),
|
|
|
|
serverSNIFn ServerSNIFunc,
|
|
|
|
updates []cache.UpdateEvent,
|
|
|
|
) *ConfigSnapshot {
|
|
|
|
const token = ""
|
2020-08-28 20:27:40 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
if nsFn != nil {
|
|
|
|
nsFn(ns)
|
|
|
|
}
|
2019-07-12 19:16:21 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
config := stateConfig{
|
|
|
|
logger: hclog.NewNullLogger(),
|
|
|
|
source: &structs.QuerySource{
|
|
|
|
Datacenter: "dc1",
|
2020-04-13 21:47:53 +00:00
|
|
|
},
|
2022-03-07 17:47:14 +00:00
|
|
|
cache: &noopCacheNotifier{},
|
|
|
|
health: &noopHealth{},
|
|
|
|
dnsConfig: DNSConfig{ // TODO: make configurable
|
|
|
|
Domain: "consul",
|
|
|
|
AltDomain: "",
|
2020-04-13 21:47:53 +00:00
|
|
|
},
|
2022-03-07 17:47:14 +00:00
|
|
|
serverSNIFn: serverSNIFn,
|
|
|
|
intentionDefaultAllow: false, // TODO: make configurable
|
2020-04-13 21:47:53 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
s, err := newServiceInstanceFromNodeService(ns, token)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
return nil
|
2019-07-12 19:16:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
handler, err := newKindHandler(config, s, nil) // NOTE: nil channel
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
return nil
|
2019-07-12 19:16:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
defer cancel()
|
2019-07-12 19:16:21 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
snap, err := handler.initialize(ctx)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
return nil
|
2019-07-12 19:16:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
for _, u := range updates {
|
|
|
|
if err := handler.handleUpdate(ctx, u, &snap); err != nil {
|
|
|
|
t.Fatalf("Failed to handle update from watch %q: %v", u.CorrelationID, err)
|
|
|
|
return nil
|
2019-08-05 18:30:35 +00:00
|
|
|
}
|
2019-07-12 19:16:21 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
return &snap
|
2019-07-12 19:16:21 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
func testSpliceEvents(base, extra []cache.UpdateEvent) []cache.UpdateEvent {
|
|
|
|
if len(extra) == 0 {
|
|
|
|
return base
|
2021-11-09 16:45:36 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
var (
|
|
|
|
hasExtra = make(map[string]cache.UpdateEvent)
|
|
|
|
completeExtra = make(map[string]struct{})
|
2021-11-09 16:45:36 +00:00
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
allEvents []cache.UpdateEvent
|
2021-11-09 16:45:36 +00:00
|
|
|
)
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
for _, e := range extra {
|
|
|
|
hasExtra[e.CorrelationID] = e
|
2021-11-09 16:45:36 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
// Override base events with extras if they share the same correlationID,
|
|
|
|
// then put the rest of the extras at the end.
|
|
|
|
for _, e := range base {
|
|
|
|
if extraEvt, ok := hasExtra[e.CorrelationID]; ok {
|
|
|
|
if extraEvt.Result != nil { // nil results are tombstones
|
|
|
|
allEvents = append(allEvents, extraEvt)
|
2020-03-09 20:59:02 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
completeExtra[e.CorrelationID] = struct{}{}
|
|
|
|
} else {
|
|
|
|
allEvents = append(allEvents, e)
|
2020-03-09 20:59:02 +00:00
|
|
|
}
|
2018-10-03 12:36:38 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
for _, e := range extra {
|
|
|
|
if _, ok := completeExtra[e.CorrelationID]; !ok {
|
|
|
|
allEvents = append(allEvents, e)
|
2020-04-16 21:00:48 +00:00
|
|
|
}
|
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
return allEvents
|
2019-09-26 02:55:52 +00:00
|
|
|
}
|
|
|
|
|
2022-03-07 17:47:14 +00:00
|
|
|
func testSpliceNodeServiceFunc(prev, next func(ns *structs.NodeService)) func(ns *structs.NodeService) {
|
|
|
|
return func(ns *structs.NodeService) {
|
|
|
|
if prev != nil {
|
|
|
|
prev(ns)
|
2020-04-14 14:59:23 +00:00
|
|
|
}
|
2022-03-07 17:47:14 +00:00
|
|
|
next(ns)
|
2020-04-21 21:06:23 +00:00
|
|
|
}
|
2020-04-14 21:30:00 +00:00
|
|
|
}
|
|
|
|
|
2018-10-03 12:36:38 +00:00
|
|
|
// ControllableCacheType is a cache.Type that simulates a typical blocking RPC
|
2018-10-09 16:57:26 +00:00
|
|
|
// but lets us control the responses and when they are delivered easily.
|
2018-10-03 12:36:38 +00:00
|
|
|
type ControllableCacheType struct {
|
|
|
|
index uint64
|
2019-07-12 19:16:21 +00:00
|
|
|
value sync.Map
|
2018-10-03 12:36:38 +00:00
|
|
|
// Need a condvar to trigger all blocking requests (there might be multiple
|
|
|
|
// for same type due to background refresh and timing issues) when values
|
|
|
|
// change. Chans make it nondeterministic which one triggers or need extra
|
2018-10-09 16:57:26 +00:00
|
|
|
// locking to coordinate replacing after close etc.
|
2018-10-03 12:36:38 +00:00
|
|
|
triggerMu sync.Mutex
|
|
|
|
trigger *sync.Cond
|
|
|
|
blocking bool
|
|
|
|
lastReq atomic.Value
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewControllableCacheType returns a cache.Type that can be controlled for
|
|
|
|
// testing.
|
|
|
|
func NewControllableCacheType(t testing.T) *ControllableCacheType {
|
|
|
|
c := &ControllableCacheType{
|
|
|
|
index: 5,
|
|
|
|
blocking: true,
|
|
|
|
}
|
|
|
|
c.trigger = sync.NewCond(&c.triggerMu)
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set sets the response value to be returned from subsequent cache gets for the
|
|
|
|
// type.
|
2019-07-12 19:16:21 +00:00
|
|
|
func (ct *ControllableCacheType) Set(key string, value interface{}) {
|
2018-10-03 12:36:38 +00:00
|
|
|
atomic.AddUint64(&ct.index, 1)
|
2019-07-12 19:16:21 +00:00
|
|
|
ct.value.Store(key, value)
|
2018-10-03 12:36:38 +00:00
|
|
|
ct.triggerMu.Lock()
|
|
|
|
ct.trigger.Broadcast()
|
|
|
|
ct.triggerMu.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch implements cache.Type. It simulates blocking or non-blocking queries.
|
|
|
|
func (ct *ControllableCacheType) Fetch(opts cache.FetchOptions, req cache.Request) (cache.FetchResult, error) {
|
|
|
|
index := atomic.LoadUint64(&ct.index)
|
|
|
|
|
|
|
|
ct.lastReq.Store(req)
|
|
|
|
|
|
|
|
shouldBlock := ct.blocking && opts.MinIndex > 0 && opts.MinIndex == index
|
|
|
|
if shouldBlock {
|
|
|
|
// Wait for return to be triggered. We ignore timeouts based on opts.Timeout
|
|
|
|
// since in practice they will always be way longer than our tests run for
|
|
|
|
// and the caller can simulate timeout by triggering return without changing
|
|
|
|
// index or value.
|
|
|
|
ct.triggerMu.Lock()
|
|
|
|
ct.trigger.Wait()
|
|
|
|
ct.triggerMu.Unlock()
|
|
|
|
}
|
|
|
|
|
2019-07-12 19:16:21 +00:00
|
|
|
info := req.CacheInfo()
|
|
|
|
key := path.Join(info.Key, info.Datacenter) // omit token for testing purposes
|
|
|
|
|
2018-10-03 12:36:38 +00:00
|
|
|
// reload index as it probably got bumped
|
|
|
|
index = atomic.LoadUint64(&ct.index)
|
2019-07-12 19:16:21 +00:00
|
|
|
val, _ := ct.value.Load(key)
|
2018-10-03 12:36:38 +00:00
|
|
|
|
|
|
|
if err, ok := val.(error); ok {
|
|
|
|
return cache.FetchResult{
|
|
|
|
Value: nil,
|
|
|
|
Index: index,
|
|
|
|
}, err
|
|
|
|
}
|
|
|
|
return cache.FetchResult{
|
|
|
|
Value: val,
|
|
|
|
Index: index,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-04-14 22:29:30 +00:00
|
|
|
func (ct *ControllableCacheType) RegisterOptions() cache.RegisterOptions {
|
|
|
|
return cache.RegisterOptions{
|
|
|
|
Refresh: ct.blocking,
|
|
|
|
SupportsBlocking: ct.blocking,
|
2020-07-20 20:09:20 +00:00
|
|
|
QueryTimeout: 10 * time.Minute,
|
2020-04-14 22:29:30 +00:00
|
|
|
}
|
2018-10-03 12:36:38 +00:00
|
|
|
}
|
2020-04-14 23:46:40 +00:00
|
|
|
|
|
|
|
// golden is used to read golden files stores in consul/agent/xds/testdata
|
|
|
|
func golden(t testing.T, name string) string {
|
|
|
|
t.Helper()
|
|
|
|
|
2022-03-15 14:07:40 +00:00
|
|
|
golden := filepath.Join(projectRoot(), "../", "/xds/testdata", name+".golden")
|
2020-04-14 23:46:40 +00:00
|
|
|
expected, err := ioutil.ReadFile(golden)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return string(expected)
|
|
|
|
}
|
2022-03-15 14:07:40 +00:00
|
|
|
|
|
|
|
func projectRoot() string {
|
|
|
|
_, base, _, _ := runtime.Caller(0)
|
|
|
|
return filepath.Dir(base)
|
|
|
|
}
|