core/redis: remove redis (#4768)

* core/redis: remove redis

* 20 minute max wait
This commit is contained in:
Caleb Doxsey 2023-11-28 13:14:36 -07:00 committed by GitHub
parent d610b9c25c
commit bcddbff6e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 24 additions and 1187 deletions

View file

@ -13,8 +13,6 @@ const (
ServiceCache = "cache"
// ServiceDataBroker represents running the databroker service component
ServiceDataBroker = "databroker"
// StorageRedisName is the name of the redis storage backend
StorageRedisName = "redis"
// StoragePostgresName is the name of the Postgres storage backend
StoragePostgresName = "postgres"
// StorageInMemoryName is the name of the in-memory storage backend
@ -39,9 +37,7 @@ func IsValidService(s string) bool {
// IsAuthenticate checks to see if we should be running the authenticate service
func IsAuthenticate(s string) bool {
switch s {
case
ServiceAll,
ServiceAuthenticate:
case ServiceAll, ServiceAuthenticate:
return true
}
return false
@ -50,9 +46,7 @@ func IsAuthenticate(s string) bool {
// IsAuthorize checks to see if we should be running the authorize service
func IsAuthorize(s string) bool {
switch s {
case
ServiceAll,
ServiceAuthorize:
case ServiceAll, ServiceAuthorize:
return true
}
return false
@ -61,9 +55,7 @@ func IsAuthorize(s string) bool {
// IsProxy checks to see if we should be running the proxy service
func IsProxy(s string) bool {
switch s {
case
ServiceAll,
ServiceProxy:
case ServiceAll, ServiceProxy:
return true
}
return false

View file

@ -586,8 +586,6 @@ func (o *Options) Validate() error {
switch o.DataBrokerStorageType {
case StorageInMemoryName:
case StorageRedisName:
return errors.New("config: redis databroker storage backend is no longer supported")
case StoragePostgresName:
if o.DataBrokerStorageConnectionString == "" {
return errors.New("config: missing databroker storage backend dsn")

View file

@ -59,8 +59,6 @@ func Test_Validate(t *testing.T) {
badPolicyFile.PolicyFile = "file"
invalidStorageType := testOptions()
invalidStorageType.DataBrokerStorageType = "foo"
redisStorageType := testOptions()
redisStorageType.DataBrokerStorageType = "redis"
missingStorageDSN := testOptions()
missingStorageDSN.DataBrokerStorageType = "postgres"
badSignoutRedirectURL := testOptions()
@ -80,7 +78,6 @@ func Test_Validate(t *testing.T) {
{"missing shared secret but all service", badSecretAllServices, false},
{"policy file specified", badPolicyFile, true},
{"invalid databroker storage type", invalidStorageType, true},
{"redis databroker storage type", redisStorageType, true},
{"missing databroker storage dsn", missingStorageDSN, true},
{"invalid signout redirect url", badSignoutRedirectURL, true},
{"CookieSameSite none with CookieSecure fale", badCookieSettings, true},
@ -358,7 +355,7 @@ downstream_mtls:
- dns: '.*\.example-2'
`
cfg := filepath.Join(t.TempDir(), "config.yaml")
err := os.WriteFile(cfg, []byte(yaml), 0644)
err := os.WriteFile(cfg, []byte(yaml), 0o644)
require.NoError(t, err)
o, err := optionsFromViper(cfg)
@ -721,7 +718,7 @@ func TestCompareByteSliceSlice(t *testing.T) {
func TestDeprecatedClientCAOptions(t *testing.T) {
fakeCACert := []byte("--- FAKE CA CERT ---")
caFile := filepath.Join(t.TempDir(), "CA.pem")
os.WriteFile(caFile, fakeCACert, 0644)
os.WriteFile(caFile, fakeCACert, 0o644)
var logOutput bytes.Buffer
zl := zerolog.New(&logOutput)
@ -1266,7 +1263,7 @@ func TestOptions_RequestParams(t *testing.T) {
for i := range cases {
c := &cases[i]
t.Run(c.label, func(t *testing.T) {
err := os.WriteFile(cfg, []byte(c.config), 0644)
err := os.WriteFile(cfg, []byte(c.config), 0o644)
require.NoError(t, err)
o, err := newOptionsFromConfig(cfg)
require.NoError(t, err)

View file

@ -271,12 +271,12 @@ func TestPolicy_Matches(t *testing.T) {
})
t.Run("tcp", func(t *testing.T) {
p := &Policy{
From: "tcp+https://proxy.example.com/redis.example.com:6379",
From: "tcp+https://proxy.example.com/tcp.example.com:6379",
To: mustParseWeightedURLs(t, "tcp://localhost:6379"),
}
assert.NoError(t, p.Validate())
assert.True(t, p.Matches(urlutil.MustParseAndValidateURL(`https://redis.example.com:6379`)))
assert.True(t, p.Matches(urlutil.MustParseAndValidateURL(`https://tcp.example.com:6379`)))
})
}

View file

@ -19,18 +19,3 @@ spec:
- authenticate.localhost.pomerium.io
# TODO - If you're not using the Pomerium Ingress controller, you may want a wildcard entry as well.
#- "*.localhost.pomerium.io" # Quotes are required to escape the wildcard
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pomerium-redis-cert
namespace: pomerium
spec:
secretName: pomerium-redis-tls
issuerRef:
name: pomerium-issuer
kind: Issuer
dnsNames:
- pomerium-redis-master.pomerium.svc.cluster.local
- pomerium-redis-headless.pomerium.svc.cluster.local
- pomerium-redis-replicas.pomerium.svc.cluster.local

View file

@ -14,25 +14,10 @@ proxy:
databroker:
existingTLSSecret: pomerium-tls
storage:
connectionString: rediss://pomerium-redis-master.pomerium.svc.cluster.local
type: redis
clientTLS:
existingSecretName: pomerium-tls
existingCASecretKey: ca.crt
authorize:
existingTLSSecret: pomerium-tls
redis:
enabled: true
auth:
enabled: false
usePassword: false
generateTLS: false
tls:
certificateSecret: pomerium-redis-tls
ingressController:
enabled: true

3
go.mod
View file

@ -23,7 +23,6 @@ require (
github.com/envoyproxy/protoc-gen-validate v1.0.2
github.com/go-chi/chi/v5 v5.0.10
github.com/go-jose/go-jose/v3 v3.0.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang/mock v1.6.0
github.com/google/btree v1.1.2
github.com/google/go-cmp v0.5.9
@ -120,7 +119,6 @@ require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.15.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/cli v24.0.4+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.0 // indirect
@ -184,6 +182,7 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/opencontainers/runc v1.1.5 // indirect

16
go.sum
View file

@ -203,8 +203,6 @@ github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5js
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
@ -249,6 +247,7 @@ github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
@ -289,11 +288,10 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@ -570,6 +568,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
@ -577,9 +576,12 @@ github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/open-policy-agent/opa v0.57.0 h1:DftxYfOEHOheXvO2Q6HCIM2ZVdKrvnF4cZlU9C64MIQ=
@ -929,6 +931,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
@ -989,10 +992,13 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1017,6 +1023,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1114,6 +1121,7 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=

View file

@ -35,11 +35,6 @@ local Routes(mode, idp, dns_suffix) =
tls_custom_ca: std.base64(importstr '../files/ca.pem'),
tls_server_name: 'fortio-ping.localhost.pomerium.io',
},
{
from: 'tcp+https://redis.localhost.pomerium.io:6379',
to: 'tcp://redis' + dns_suffix + ':6379',
allow_any_authenticated_user: true,
},
// specify https upstream by IP address
{
from: 'https://httpdetails-ip-address.localhost.pomerium.io',

View file

@ -1,356 +0,0 @@
package redisutil
import (
"crypto/tls"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/pomerium/pomerium/internal/sets"
)
var (
standardSchemes = sets.NewHash("redis", "rediss", "unix")
clusterSchemes = sets.NewHash(
"redis+cluster", "redis-cluster",
"rediss+cluster", "rediss-cluster",
"redis+clusters", "redis-clusters",
)
sentinelSchemes = sets.NewHash(
"redis+sentinel", "redis-sentinel",
"rediss+sentinel", "rediss-sentinel",
"redis+sentinels", "redis-sentinels",
)
sentinelClusterSchemes = sets.NewHash(
"redis+sentinel+cluster", "redis-sentinel-cluster",
"rediss+sentinel+cluster", "rediss-sentinel-cluster",
"redis+sentinels+cluster", "redis-sentinels-cluster",
"redis+sentinel+clusters", "redis-sentinel-clusters",
)
tlsSchemes = sets.NewHash(
"rediss",
"rediss+cluster", "rediss-cluster",
"redis+clusters", "redis-clusters",
"rediss+sentinel", "rediss-sentinel",
"redis+sentinels", "redis-sentinels",
"rediss+sentinel+cluster", "rediss-sentinel-cluster",
"redis+sentinels+cluster", "redis-sentinels-cluster",
"redis+sentinel+clusters", "redis-sentinel-clusters",
)
)
// NewClientFromURL creates a new redis client by parsing the raw URL.
func NewClientFromURL(rawURL string, tlsConfig *tls.Config) (redis.UniversalClient, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
switch {
case standardSchemes.Has(u.Scheme):
opts, err := redis.ParseURL(rawURL)
if err != nil {
return nil, err
}
// when using TLS, the TLS config will not be set to nil, in which case we replace it with our own
if opts.TLSConfig != nil {
opts.TLSConfig = tlsConfig
}
return redis.NewClient(opts), nil
case clusterSchemes.Has(u.Scheme):
opts, err := ParseClusterURL(rawURL)
if err != nil {
return nil, err
}
if opts.TLSConfig != nil {
opts.TLSConfig = tlsConfig
}
return redis.NewClusterClient(opts), nil
case sentinelSchemes.Has(u.Scheme):
opts, err := ParseSentinelURL(rawURL)
if err != nil {
return nil, err
}
if opts.TLSConfig != nil {
opts.TLSConfig = tlsConfig
}
return redis.NewFailoverClient(opts), nil
case sentinelClusterSchemes.Has(u.Scheme):
opts, err := ParseSentinelURL(rawURL)
if err != nil {
return nil, err
}
if opts.TLSConfig != nil {
opts.TLSConfig = tlsConfig
}
return redis.NewFailoverClusterClient(opts), nil
default:
return nil, fmt.Errorf("unsupported URL scheme: %s", u.Scheme)
}
}
// ParseClusterURL parses a redis-cluster URL. Format is:
//
// redis+cluster://[username:password@]host:port[,host2:port2,...]/[?param1=value1[&param2=value=2&...]]
//
// Additionally TLS is supported with rediss+cluster, or redis+clusters. Supported query params:
//
// max_redirects: int
// read_only: bool
// route_by_latency: bool
// route_randomly: bool
// max_retries: int
// min_retry_backoff: duration
// max_retry_backoff: duration
// dial_timeout: duration
// read_timeout: duration
// write_timeout: duration
// pool_size: int
// min_idle_conns: int
// max_conn_age: duration
// pool_timeout: duration
// idle_timeout: duration
// idle_check_frequency: duration
func ParseClusterURL(rawurl string) (*redis.ClusterOptions, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
opts := new(redis.ClusterOptions)
hostParts := strings.Split(u.Host, ",")
for _, hostPart := range hostParts {
host, port, err := net.SplitHostPort(hostPart)
if err != nil {
host = hostPart
port = "6379"
}
opts.Addrs = append(opts.Addrs,
net.JoinHostPort(host, port))
}
q := u.Query()
if err := parseIntParam(&opts.MaxRedirects, q, "max_redirects"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.ReadOnly, q, "read_only"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.RouteByLatency, q, "route_by_latency"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.RouteRandomly, q, "route_randomly"); err != nil {
return nil, err
}
if ui := u.User; ui != nil {
opts.Username = ui.Username()
opts.Password, _ = ui.Password()
}
if err := parseIntParam(&opts.MaxRetries, q, "max_retries"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MinRetryBackoff, q, "min_retry_backoff"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MaxRetryBackoff, q, "max_retry_backoff"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.DialTimeout, q, "dial_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.ReadTimeout, q, "read_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.WriteTimeout, q, "write_timeout"); err != nil {
return nil, err
}
if err := parseIntParam(&opts.PoolSize, q, "pool_size"); err != nil {
return nil, err
}
if err := parseIntParam(&opts.MinIdleConns, q, "min_idle_conns"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MaxConnAge, q, "max_conn_age"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.PoolTimeout, q, "pool_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.IdleTimeout, q, "idle_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.IdleCheckFrequency, q, "idle_check_frequency"); err != nil {
return nil, err
}
if tlsSchemes.Has(u.Scheme) {
opts.TLSConfig = &tls.Config{} //nolint
}
return opts, nil
}
// ParseSentinelURL parses a redis-sentinel URL. Format is based on https://github.com/exponea/redis-sentinel-url:
//
// redis+sentinel://[:password@]host:port[,host2:port2,...][/service_name[/db]][?param1=value1[&param2=value=2&...]]
//
// Additionally TLS is supported with rediss+sentinel, or redis+sentinels. Supported query params:
//
// slave_only: bool
// use_disconnected_slaves: bool
// query_sentinel_randomly: bool
// username: string (username for redis connection)
// password: string (password for redis connection)
// max_retries: int
// min_retry_backoff: duration
// max_retry_backoff: duration
// dial_timeout: duration
// read_timeout: duration
// write_timeout: duration
// pool_size: int
// min_idle_conns: int
// max_conn_age: duration
// pool_timeout: duration
// idle_timeout: duration
// idle_check_frequency: duration
func ParseSentinelURL(rawurl string) (*redis.FailoverOptions, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
opts := new(redis.FailoverOptions)
pathParts := strings.Split(u.Path, "/")
if len(pathParts) > 1 {
opts.MasterName = pathParts[1]
}
if len(pathParts) > 2 {
opts.DB, err = strconv.Atoi(pathParts[2])
if err != nil {
return nil, fmt.Errorf("invalid database: %w", err)
}
}
hostParts := strings.Split(u.Host, ",")
for _, hostPart := range hostParts {
host, port, err := net.SplitHostPort(hostPart)
if err != nil {
host = hostPart
port = "26379" // "By default Sentinel runs using TCP port 26379"
}
opts.SentinelAddrs = append(opts.SentinelAddrs,
net.JoinHostPort(host, port))
}
if u.User != nil {
opts.SentinelPassword, _ = u.User.Password()
}
q := u.Query()
if err := parseBoolParam(&opts.SlaveOnly, q, "slave_only"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.RouteByLatency, q, "route_by_latency"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.RouteRandomly, q, "route_randomly"); err != nil {
return nil, err
}
if err := parseBoolParam(&opts.UseDisconnectedSlaves, q, "use_disconnected_slaves"); err != nil {
return nil, err
}
opts.Username = q.Get("username")
opts.Password = q.Get("password")
if err := parseIntParam(&opts.MaxRetries, q, "max_retries"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MinRetryBackoff, q, "min_retry_backoff"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MaxRetryBackoff, q, "max_retry_backoff"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.DialTimeout, q, "dial_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.ReadTimeout, q, "read_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.WriteTimeout, q, "write_timeout"); err != nil {
return nil, err
}
if err := parseIntParam(&opts.PoolSize, q, "pool_size"); err != nil {
return nil, err
}
if err := parseIntParam(&opts.MinIdleConns, q, "min_idle_conns"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.MaxConnAge, q, "max_conn_age"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.PoolTimeout, q, "pool_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.IdleTimeout, q, "idle_timeout"); err != nil {
return nil, err
}
if err := parseDurationParam(&opts.IdleCheckFrequency, q, "idle_check_frequency"); err != nil {
return nil, err
}
if tlsSchemes.Has(u.Scheme) {
opts.TLSConfig = &tls.Config{} //nolint
}
return opts, nil
}
func parseBoolParam(dst *bool, values url.Values, name string) error {
v := values.Get(name)
if v == "" {
return nil
}
b, err := strconv.ParseBool(v)
if err != nil {
return fmt.Errorf("invalid %s: %w", name, err)
}
*dst = b
return nil
}
func parseIntParam(dst *int, values url.Values, name string) error {
v := values.Get(name)
if v == "" {
return nil
}
i, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid %s: %w", name, err)
}
*dst = i
return nil
}
func parseDurationParam(dst *time.Duration, values url.Values, name string) error {
v := values.Get(name)
if v == "" {
return nil
}
d, err := time.ParseDuration(v)
if err != nil {
return fmt.Errorf("invalid %s: %w", name, err)
}
*dst = d
return nil
}

View file

@ -1,89 +0,0 @@
package redisutil
import (
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseClusterURL(t *testing.T) {
opts, err := ParseClusterURL("redis+cluster://CLUSTER_USERNAME:CLUSTER_PASSWORD@localhost:26379,otherhost:26479/?" + (&url.Values{
"read_only": {"true"},
"username": {"USERNAME"},
"password": {"PASSWORD"},
"max_retries": {"11"},
"min_retry_backoff": {"31s"},
"max_retry_backoff": {"22m"},
"dial_timeout": {"3m"},
"read_timeout": {"4m"},
"write_timeout": {"5m"},
"pool_size": {"7"},
"min_idle_conns": {"2"},
"max_conn_age": {"1h"},
"pool_timeout": {"30m"},
"idle_timeout": {"31m"},
"idle_check_frequency": {"32m"},
}).Encode())
require.NoError(t, err)
assert.Equal(t, []string{"localhost:26379", "otherhost:26479"}, opts.Addrs)
assert.Equal(t, "CLUSTER_USERNAME", opts.Username)
assert.Equal(t, "CLUSTER_PASSWORD", opts.Password)
assert.True(t, opts.ReadOnly)
assert.Equal(t, 11, opts.MaxRetries)
assert.Equal(t, time.Second*31, opts.MinRetryBackoff)
assert.Equal(t, time.Minute*22, opts.MaxRetryBackoff)
assert.Equal(t, time.Minute*3, opts.DialTimeout)
assert.Equal(t, time.Minute*4, opts.ReadTimeout)
assert.Equal(t, time.Minute*5, opts.WriteTimeout)
assert.Equal(t, 7, opts.PoolSize)
assert.Equal(t, 2, opts.MinIdleConns)
assert.Equal(t, time.Hour, opts.MaxConnAge)
assert.Equal(t, time.Minute*30, opts.PoolTimeout)
assert.Equal(t, time.Minute*31, opts.IdleTimeout)
assert.Equal(t, time.Minute*32, opts.IdleCheckFrequency)
}
func TestParseSentinelURL(t *testing.T) {
opts, err := ParseSentinelURL("redis+sentinel://:SENTINEL_PASSWORD@localhost:26379,otherhost:26479/mymaster/3?" + (&url.Values{
"slave_only": {"true"},
"use_disconnected_slaves": {"T"},
"username": {"USERNAME"},
"password": {"PASSWORD"},
"max_retries": {"11"},
"min_retry_backoff": {"31s"},
"max_retry_backoff": {"22m"},
"dial_timeout": {"3m"},
"read_timeout": {"4m"},
"write_timeout": {"5m"},
"pool_size": {"7"},
"min_idle_conns": {"2"},
"max_conn_age": {"1h"},
"pool_timeout": {"30m"},
"idle_timeout": {"31m"},
"idle_check_frequency": {"32m"},
}).Encode())
require.NoError(t, err)
assert.Equal(t, "mymaster", opts.MasterName)
assert.Equal(t, []string{"localhost:26379", "otherhost:26479"}, opts.SentinelAddrs)
assert.Equal(t, "SENTINEL_PASSWORD", opts.SentinelPassword)
assert.True(t, opts.SlaveOnly)
assert.True(t, opts.UseDisconnectedSlaves)
assert.Equal(t, "USERNAME", opts.Username)
assert.Equal(t, "PASSWORD", opts.Password)
assert.Equal(t, 3, opts.DB)
assert.Equal(t, 11, opts.MaxRetries)
assert.Equal(t, time.Second*31, opts.MinRetryBackoff)
assert.Equal(t, time.Minute*22, opts.MaxRetryBackoff)
assert.Equal(t, time.Minute*3, opts.DialTimeout)
assert.Equal(t, time.Minute*4, opts.ReadTimeout)
assert.Equal(t, time.Minute*5, opts.WriteTimeout)
assert.Equal(t, 7, opts.PoolSize)
assert.Equal(t, 2, opts.MinIdleConns)
assert.Equal(t, time.Hour, opts.MaxConnAge)
assert.Equal(t, time.Minute*30, opts.PoolTimeout)
assert.Equal(t, time.Minute*31, opts.IdleTimeout)
assert.Equal(t, time.Minute*32, opts.IdleCheckFrequency)
}

View file

@ -1,5 +0,0 @@
// Package redisutil contains functions for working with redis.
package redisutil
// KeyPrefix is the prefix used for all redis keys.
const KeyPrefix = "{pomerium_v3}."

View file

@ -1,35 +0,0 @@
package metrics
import (
redis "github.com/go-redis/redis/v8"
)
// AddRedisMetrics registers a metrics handler against a redis Client's PoolStats() method
func AddRedisMetrics(stats func() *redis.PoolStats) {
gaugeMetrics := []struct {
name string
desc string
f func() int64
}{
{"redis_conns", "Number of total connections in the pool", func() int64 { return int64(stats().TotalConns) }},
{"redis_idle_conns", "Number of idle connections in the pool", func() int64 { return int64(stats().IdleConns) }},
{"redis_stale_conns", "Number of stale connections in the pool", func() int64 { return int64(stats().StaleConns) }},
}
for _, m := range gaugeMetrics {
registry.addInt64DerivedGaugeMetric(m.name, m.desc, "redis", m.f)
}
cumulativeMetrics := []struct {
name string
desc string
f func() int64
}{
{"redis_miss_count_total", "Total number of times a connection was not found in the pool", func() int64 { return int64(stats().Misses) }},
{"redis_hit_count_total", "Total number of times a connection was found in the pool", func() int64 { return int64(stats().Hits) }},
}
for _, m := range cumulativeMetrics {
registry.addInt64DerivedCumulativeMetric(m.name, m.desc, "redis", m.f)
}
}

View file

@ -1,33 +0,0 @@
package metrics
import (
"testing"
redis "github.com/go-redis/redis/v8"
"go.opencensus.io/metric/metricdata"
)
func Test_AddRedisMetrics(t *testing.T) {
t.Parallel()
tests := []struct {
name string
stat redis.PoolStats
want int64
}{
{"redis_conns", redis.PoolStats{TotalConns: 7}, 7},
{"redis_idle_conns", redis.PoolStats{IdleConns: 3}, 3},
{"redis_miss_count_total", redis.PoolStats{Misses: 2}, 2},
}
labelValues := []metricdata.LabelValue{
metricdata.NewLabelValue("redis"),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
AddRedisMetrics(func() *redis.PoolStats { return &tt.stat })
testMetricRetrieval(registry.registry.Read(), t, labelValues, tt.want, tt.name)
})
}
}

View file

@ -121,33 +121,3 @@ func (r *metricRegistry) setConfigChecksum(service string, configName string, ch
}
m.Set(float64(checksum))
}
func (r *metricRegistry) addInt64DerivedGaugeMetric(name, desc, service string, f func() int64) {
m, err := r.registry.AddInt64DerivedGauge(name, metric.WithDescription(desc),
metric.WithLabelKeys(metrics.ServiceLabel))
if err != nil {
log.Error(context.TODO()).Err(err).Str("service", service).Msg("telemetry/metrics: failed to register metric")
return
}
err = m.UpsertEntry(f, metricdata.NewLabelValue(service))
if err != nil {
log.Error(context.TODO()).Err(err).Str("service", service).Msg("telemetry/metrics: failed to update metric")
return
}
}
func (r *metricRegistry) addInt64DerivedCumulativeMetric(name, desc, service string, f func() int64) {
m, err := r.registry.AddInt64DerivedCumulative(name, metric.WithDescription(desc),
metric.WithLabelKeys(metrics.ServiceLabel))
if err != nil {
log.Error(context.TODO()).Err(err).Str("service", service).Msg("telemetry/metrics: failed to register metric")
return
}
err = m.UpsertEntry(f, metricdata.NewLabelValue(service))
if err != nil {
log.Error(context.TODO()).Err(err).Str("service", service).Msg("telemetry/metrics: failed to update metric")
return
}
}

View file

@ -1,388 +0,0 @@
package testutil
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-redis/redis/v8"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
const maxWait = 20 * time.Minute
// WithTestRedis creates a test a test redis instance using docker.
func WithTestRedis(useTLS bool, handler func(rawURL string) error) error {
ctx, clearTimeout := context.WithTimeout(context.Background(), maxWait)
defer clearTimeout()
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
return err
}
opts := &dockertest.RunOptions{
Repository: "redis",
Tag: "6",
}
scheme := "redis"
if useTLS {
opts.Mounts = []string{
filepath.Join(TestDataRoot(), "tls") + ":/tls",
}
opts.Cmd = []string{
"--port", "0",
"--tls-port", "6379",
"--tls-cert-file", "/tls/redis.crt",
"--tls-key-file", "/tls/redis.key",
"--tls-ca-cert-file", "/tls/ca.crt",
}
scheme = "rediss"
}
resource, err := pool.RunWithOptions(opts)
if err != nil {
return err
}
_ = resource.Expire(uint(maxWait.Seconds()))
redisURL := fmt.Sprintf("%s://%s/0", scheme, resource.GetHostPort("6379/tcp"))
if err := pool.Retry(func() error {
options, err := redis.ParseURL(redisURL)
if err != nil {
return err
}
if useTLS {
options.TLSConfig = RedisTLSConfig()
}
client := redis.NewClient(options)
defer client.Close()
return client.Ping(ctx).Err()
}); err != nil {
_ = pool.Purge(resource)
return err
}
e := handler(redisURL)
if err := pool.Purge(resource); err != nil {
return err
}
return e
}
// WithTestRedisCluster creates a new redis cluster 3 node cluster.
func WithTestRedisCluster(handler func(rawURL string) error) error {
ctx, clearTimeout := context.WithTimeout(context.Background(), maxWait)
defer clearTimeout()
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
return err
}
redises := make([]*dockertest.Resource, 3)
for i := range redises {
conf := "cluster-enabled yes\ncluster-config-file nodes.conf"
r, err := pool.RunWithOptions(&dockertest.RunOptions{
Hostname: fmt.Sprintf("redis%d", i),
Repository: "redis",
Tag: "6",
Entrypoint: []string{
"/bin/bash", "-c",
`echo "` + conf + `" >/tmp/redis.conf && chmod 0777 /tmp/redis.conf && exec docker-entrypoint.sh /tmp/redis.conf`,
},
ExposedPorts: []string{
"6379/tcp",
"26379/tcp",
},
})
if err != nil {
return err
}
defer r.Close()
_ = r.Expire(uint(maxWait.Seconds()))
go func() {
_ = pool.Client.Logs(docker.LogsOptions{
Context: ctx,
Stderr: true,
Stdout: true,
Follow: true,
Timestamps: true,
Container: r.Container.ID,
OutputStream: os.Stderr,
ErrorStream: os.Stderr,
})
}()
redises[i] = r
}
addrs := make([]string, 3)
for i, r := range redises {
addrs[i] = net.JoinHostPort(
r.Container.NetworkSettings.IPAddress,
"6379",
)
}
for _, addr := range addrs {
err := pool.Retry(func() error {
options, err := redis.ParseURL(fmt.Sprintf("redis://%s/0", addr))
if err != nil {
return err
}
client := redis.NewClient(options)
defer client.Close()
return client.Ping(ctx).Err()
})
if err != nil {
return err
}
}
// join the nodes to the cluster
err = bootstrapRedisCluster(ctx, redises)
if err != nil {
return err
}
e := handler(fmt.Sprintf("redis+cluster://%s", strings.Join(addrs, ",")))
for _, r := range redises {
if err := pool.Purge(r); err != nil {
return err
}
}
return e
}
// WithTestRedisSentinel creates a new redis sentinel 3 node cluster.
func WithTestRedisSentinel(handler func(rawURL string) error) error {
ctx, clearTimeout := context.WithTimeout(context.Background(), maxWait)
defer clearTimeout()
// uses a sensible default on windows (tcp/http) and linux/osx (socket)
pool, err := dockertest.NewPool("")
if err != nil {
return err
}
redises := make([]*dockertest.Resource, 3)
for i := range redises {
r, err := pool.RunWithOptions(&dockertest.RunOptions{
Hostname: fmt.Sprintf("redis%d", i),
Repository: "redis",
Tag: "6",
ExposedPorts: []string{
"6379/tcp",
"26379/tcp",
},
})
if err != nil {
return err
}
defer r.Close()
_ = r.Expire(uint(maxWait.Seconds()))
redises[i] = r
}
sentinels := make([]*dockertest.Resource, len(redises))
for i := range sentinels {
conf := fmt.Sprintf("sentinel monitor master %s 6379 %d\n",
redises[0].Container.NetworkSettings.IPAddress, len(redises))
if i > 0 {
conf += fmt.Sprintf("sentinel known-slave master %s 6379\n",
redises[i].Container.NetworkSettings.IPAddress)
}
r, err := pool.RunWithOptions(&dockertest.RunOptions{
Hostname: fmt.Sprintf("sentineld%d", i),
Repository: "redis",
Tag: "6",
Entrypoint: []string{
"/bin/bash", "-c",
`echo "` + conf + `" >/tmp/sentinel.conf && chmod 0777 /tmp/sentinel.conf && exec docker-entrypoint.sh /tmp/sentinel.conf --sentinel`,
},
ExposedPorts: []string{
"6379/tcp",
"26379/tcp",
},
})
if err != nil {
return err
}
defer r.Close()
_ = r.Expire(uint(maxWait.Seconds()))
go func() {
_ = pool.Client.Logs(docker.LogsOptions{
Context: ctx,
Stderr: true,
Stdout: true,
Follow: true,
Timestamps: true,
Container: r.Container.ID,
OutputStream: os.Stderr,
ErrorStream: os.Stderr,
})
}()
sentinels[i] = r
}
addrs := make([]string, len(sentinels))
for i, r := range sentinels {
addrs[i] = net.JoinHostPort(
r.Container.NetworkSettings.IPAddress,
"26379",
)
}
redisURL := fmt.Sprintf("redis+sentinel://%s/master/0", strings.Join(addrs, ","))
for _, r := range redises {
addr := net.JoinHostPort(
r.Container.NetworkSettings.IPAddress,
"6379",
)
if err := pool.Retry(func() error {
options, err := redis.ParseURL(fmt.Sprintf("redis://%s/0", addr))
if err != nil {
return err
}
client := redis.NewClient(options)
defer client.Close()
return client.Ping(ctx).Err()
}); err != nil {
_ = pool.Purge(r)
return err
}
}
for _, r := range sentinels {
if err := pool.Retry(func() error {
options, err := redis.ParseURL(fmt.Sprintf("redis://%s/0", r.GetHostPort("26379/tcp")))
if err != nil {
return err
}
client := redis.NewClient(options)
defer client.Close()
return client.Ping(ctx).Err()
}); err != nil {
_ = pool.Purge(r)
return err
}
}
e := handler(redisURL)
for _, r := range append(redises, sentinels...) {
if err := pool.Purge(r); err != nil {
return err
}
}
return e
}
// RedisTLSConfig returns the TLS Config to use with redis.
func RedisTLSConfig() *tls.Config {
cert, err := cryptutil.CertificateFromFile(
filepath.Join(TestDataRoot(), "tls", "redis.crt"),
filepath.Join(TestDataRoot(), "tls", "redis.key"),
)
if err != nil {
panic(err)
}
caCertPool := x509.NewCertPool()
caCert, err := os.ReadFile(filepath.Join(TestDataRoot(), "tls", "ca.crt"))
if err != nil {
panic(err)
}
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{*cert},
MinVersion: tls.VersionTLS12,
}
return tlsConfig
}
func bootstrapRedisCluster(ctx context.Context, resources []*dockertest.Resource) error {
clients := make([]redis.UniversalClient, len(resources))
for i, r := range resources {
addr := net.JoinHostPort(r.Container.NetworkSettings.IPAddress, "6379")
options, err := redis.ParseURL(fmt.Sprintf("redis://%s/0", addr))
if err != nil {
return err
}
clients[i] = redis.NewClient(options)
defer func() { _ = clients[i].Close() }()
if i > 0 {
err := clients[i].ClusterMeet(ctx, resources[0].Container.NetworkSettings.IPAddress, "6379").Err()
if err != nil {
return err
}
}
}
// set slots
const redisSlotCount = 16384
assignments := make([][]int, len(resources))
for i := 0; i < redisSlotCount; i++ {
assignments[i%len(assignments)] = append(assignments[i%len(assignments)], i)
}
for i, c := range clients {
err := c.ClusterAddSlots(ctx, assignments[i]...).Err()
if err != nil {
return err
}
}
// wait for ready
ticker := time.NewTicker(time.Millisecond * 50)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
ready := 0
for _, c := range clients {
str, err := c.ClusterInfo(ctx).Result()
if err != nil {
return err
}
if strings.Contains(str, "cluster_state:ok") {
ready++
}
}
if ready == len(clients) {
return nil
}
}
}

View file

@ -1,3 +0,0 @@
FROM bitnami/redis:latest@sha256:35347816a3d837db19cdf37f98346ab53aa2fc3389bbd1935ddc39e7d7f1dbff
Add tls /tls

View file

@ -1,13 +0,0 @@
# Redis test server
## Genearte test certs
```sh
./create_test_cert.sh
```
## Build docker image
```sh
docker build -t gnouc/pomerium-redis-tls:latest .
```

View file

@ -1,27 +0,0 @@
#!/bin/bash
#!/bin/bash
mkdir -p tls
openssl genrsa -out tls/ca.key 4096
openssl req \
-x509 -new -nodes -sha256 \
-key tls/ca.key \
-days 3650 \
-subj '/O=Redis Test/CN=Pomerium CA' \
-out tls/ca.crt
openssl genrsa -out tls/redis.key 2048
openssl req \
-new -sha256 \
-key tls/redis.key \
-subj '/O=Redis Test/CN=Server' | \
openssl x509 \
-req -sha256 \
-CA tls/ca.crt \
-CAkey tls/ca.key \
-CAserial tls/ca.txt \
-CAcreateserial \
-days 3650 \
-out tls/redis.crt \
-extensions san \
-extfile tls/req.conf
openssl dhparam -out tls/redis.dh 2048

View file

@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCApACCQCeoNdHbzva/zANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtQ
b21lcml1bSBDQTAeFw0yMDA4MTIwNTM1MzlaFw0zMDA4MTAwNTM1MzlaMBYxFDAS
BgNVBAMMC1BvbWVyaXVtIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAuM38DRYqS7iy7WuoqjO6DBUEXo8WT6Y03Y5BXtQ5StsHKwetSyO8qgQm36ZS
4AXbtncE7KcXQSgF72YHN4fH3mm6Viyfgw1eKJb4yN8BH16K/zFeOqdCBWlyfMs5
CHU/1gQmSrMv1MNbYAfbd9hOYm4S8PUa6SKsgtD5fJKXJtX6ipPYEiz2F5bPS+CH
xGgkmLWmj9QYeiulEb8qEQITU+qwmxUBf2DESyzgE8NI8BNksiy2NiTdOkT3NPqV
8AKJN8ZIqMFS+w+t6Cl10QcIUHdt7EsRgB872ppzqmTE3+rsm9mTDXPw5ngbtDjd
2Hzd0P/nCe+XuLyzAl2IhWDZDcUXZkioq2A+ckb9xu35SzDpJli4xf/jumW7ZCem
AJRd/ZilVuSuQzQThvniQ1wWBj1frEMQf0C+F4iJY7o+KBGeGUV+BpFOdC9u68vZ
LsvIgJTy8VaVYnOzvCHIzkxSkl8dxjz3Mgn4tbPopH5CiD99NaiJjwASLJLVpzM3
EWa9y3paxTJsVmVyh0b1H2ooHc9RpXsGuWcEjBuZ5avk2r7bRltkxLU3D0IR7J1h
kWGE9nOh9YTTmDrS8tLzA4MvhQryTZJnanoAlPgq353YMlNiIe3HelWFISHSe8Gw
kvcWubdYafWqx1h1GdjaIDbZJZ4l7nqGaiaaW1gQFN3x3ZUCAwEAATANBgkqhkiG
9w0BAQsFAAOCAgEAYwG10nom4yqC6cuPrliBWVo9uT9+uUMar2jIt5QqSMvsGpKf
hfh45R8SRYE1vJgB5QpOqXNqzIRz/DA6594Y/Ylig0N3NGB3ky9IxY34QfNPej1B
AQLEbrRD4kDFOrQR2I8LaTROxTevB/v6LHAGvtkgcfyB5bSCkF1oRa4I1qQ3fehs
JbadQlovRnrr1rtnJDfWoqPrEdHEiAqa/abXn+rtxF/72Bp9mfPtlEnPp7duRbiH
wFkxG4m0HPtnyRw6eqLeVIDbVAiG64AMUoR+N7U18GBYbOwhRrObTilYS0TkFxOh
7nWeeet7kOJ5zT0jwRHIRbDA76rNdHqmPqnsMnJm9+R3J2hjFbfiX31vG1p91AdV
ifRP54VeN5l+nGzaw3BJ4h5V0G0xTTVhtrkZIoIVMq+NAHoO02fapn66QfVwC8+7
TyEWzD6w0H2zG4iQAOcZzUoKF/CfDHmmQ+twZT3bF/NESsqmLC2jqE3tDkE6oycv
j3QQ++TT4sV2u1teqHlOwcGZ7qCFsVagsyaaCm1XooLWntoSkuKzflQ5T1YTinGz
6vVTkbpKj6lIv6mHhEloHsDXfLVPMGj/wgzXeDBOv1lucneyNFe6+u8YIoBPFjze
2BiT/mLAi9QO9844x9WWNQbxvI9nKbLhebiHU3SXHInZUT84tFGCDa8c2m8=
-----END CERTIFICATE-----

View file

@ -1,51 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKAIBAAKCAgEAuM38DRYqS7iy7WuoqjO6DBUEXo8WT6Y03Y5BXtQ5StsHKwet
SyO8qgQm36ZS4AXbtncE7KcXQSgF72YHN4fH3mm6Viyfgw1eKJb4yN8BH16K/zFe
OqdCBWlyfMs5CHU/1gQmSrMv1MNbYAfbd9hOYm4S8PUa6SKsgtD5fJKXJtX6ipPY
Eiz2F5bPS+CHxGgkmLWmj9QYeiulEb8qEQITU+qwmxUBf2DESyzgE8NI8BNksiy2
NiTdOkT3NPqV8AKJN8ZIqMFS+w+t6Cl10QcIUHdt7EsRgB872ppzqmTE3+rsm9mT
DXPw5ngbtDjd2Hzd0P/nCe+XuLyzAl2IhWDZDcUXZkioq2A+ckb9xu35SzDpJli4
xf/jumW7ZCemAJRd/ZilVuSuQzQThvniQ1wWBj1frEMQf0C+F4iJY7o+KBGeGUV+
BpFOdC9u68vZLsvIgJTy8VaVYnOzvCHIzkxSkl8dxjz3Mgn4tbPopH5CiD99NaiJ
jwASLJLVpzM3EWa9y3paxTJsVmVyh0b1H2ooHc9RpXsGuWcEjBuZ5avk2r7bRltk
xLU3D0IR7J1hkWGE9nOh9YTTmDrS8tLzA4MvhQryTZJnanoAlPgq353YMlNiIe3H
elWFISHSe8GwkvcWubdYafWqx1h1GdjaIDbZJZ4l7nqGaiaaW1gQFN3x3ZUCAwEA
AQKCAgAcI6k8aOKZ0w7Tne/5spSioFSg/VKdYCZukemcQd5TapRl1e5qIY/pp2Yv
6ch2ug2hc+/5BNxCnJCCyltQ9kjVse2gj3zeXJu4vHw3QdWO7Dtn7iF19t/TqSG4
pM0TX58PvGQEPdKLqA9yyN9/GR2eWTpjHD8zvobcCGvkrwF69VwH28krw1LZdqor
2I1zt5PS+N3ayqXLfHfPAvepzVIaFgM8Ke+ncJmTBMs91x91Bs7vXmWESwqwg63M
kFdiS1CPgI6xu3YiKloFnsKEyHhYoEbJkwigJKFdgOUZzew4WDIc7P06MCw/O4yk
XYBSJXk1CLIqTiQhCgKL2qgd/wgtkVbZO5InQmgkAc5s6bUZJgewFJJyEeHPlQd3
4I3XkNC1nEX2xcFNckve1M5C83nb6K8s7ixUaEiKmzSDqQSiPoj41Ge5/sMPve49
kSigy6C4oGkJ+4Otx0/+6FPjzNbGMmE+isGMu99JXUCiTZhqrrxsuJaAyC675F3H
XVUTHJC9nuSoFJWN2UFxnaAkx2giGnKw7W9sovyjDZffaqeXVDihD/b5g/Q6qZ43
5DU0fhHQ27+nG//iCJSxxkNmgyxqrdXiLCZVrkNgD3LDILE8wuWpvGX+E30KIE5g
SnMgPVvwh3eSbChBYO/JMf8TbyhbUSdITBvujkCcOLILPCmAhQKCAQEA2lkfKNZo
5oe541doKUrOi1TIzASq8OKlQQ54ZpQP2vm04ZmiUyvpc8JYyCC8LTC436mpZkEC
8m/Nsh68Wsw1MsEo/176xWCPMj7YPiZ6wOJe/6O5QfxUYB+8KlzJ0HphtmcKN2ZU
WWPd0wTG5TcY46siOJ3/qPgvAbmAqjCXwWdOVHXB/A60iMByUeE5ipUVCjOBl/a2
bJpENXEMSHpes9Z/+LsYyI5RiiSMOyynCiwMsgrZ9Wr7HwYfh5+ezrGi+/nCffh/
f2jVANXdk/g/OkIL7pnxB/zGfBawkRskdflG6me8XcSrPNcH1qRRqsxzY+G47PU0
CMPOK+VDp4ecMwKCAQEA2KwZp6YP+WiRZZAVjHlCQVqkMZfYJRhj2nuxi4MSImLP
k++9z52DS47mgAYHt0A8PbFaIMlddEZdPQdtli7U2egC/dcNERc1v2NYgDmKjb8A
o+pC0LNk3R7o8lkBNP/ZnIrk3NnQIjpA3a7z/N7+z6u2L16H9cHrOMQ3SM1o2g9y
0aPgx1hctEHzu9mgq7RYdi+gYciddIvVyOPxT1QqhRSy34t9hLdZFQ3KOHcivzzS
73jQZzwhCNlPR86DFxTVjv6zM52JCu7PkXcPwHGteFd01mu8HGB44mrxayQMrHcp
s8VnIQ2AGgxg7O+oeIG3jO75t9bEhDfr4PgMzRzXFwKCAQBZ/Dq0ONDYmP0J7V7X
DaZbk6CBPDc6uR1D13PVSpXSN/DMvOVCA6nddC3kpGEI+rhmLOTMaGSPh3YtPy9+
+APAnAyKWhldOLMrEO1Lh841KdXe4xmZUSVwzANfLghaK+WTJ5n1RO3kPR0RNznF
A1T1lvSugqb3evjcbBfTi90u7qVAd5tvhpvuc+lpRznQnCoknx98gkeiMF2F7MYU
JKJc2Ty6RFktZkHCfddxF7Drp0XAJmq3EtTVb0+VNDpdkqXJ1J/MDJp25rxJ8Nm1
fqyIFOoX9kd4dDtUroEr/BSlrgsE1aWyuzebBj+LvQKPMl0nv8HXniJIrnGMc5rT
MzczAoIBABzSl/8TOiDFZkIKbrNnFgc3lYv7VQdqPS49MhsK7oigeFiHlcpee667
bbIuGyynYNwcEY82+jWTfqe1q0BFLo9mK6+0wco6Oi4hew5jmSjN9bnYWdcFZi98
AyTp7h0sw7ftShCO8P78nNBgi5hh2aeqgeu/OXrZtv6wK2KF4KLRV0bH9AjQmlRo
SZH9mz/8F6BxKXaYh0mPqHq2x8zzt0xIupq+JY5YDYOdd/8W6gpifvTYL3DsMMDK
l59Hu2yAmwAQpaoCFM7dgcMqAXBqLtdJWwODBV0JAEuuSjskaoMuvt9pLRTahOXy
K5qZLuII72/SAmoQKcgk4D3nAVzvrzUCggEBAIKRtbcr3K06tNEPxjSQQN0cknHV
YaNhylzIUaB4+o4qIWVbnupK+GegNVcu7M0Jv4rNrsbNLCeMwQAJgU5LaLJMSSlG
7k9XmndQTI+cEyKDD1fOQ+4GbmPaWuVKCzE3n6ChpF+YXoISjV8xQ9HBQhPgWpFC
LNkd5IZtf+CS4JdUZc7m2Y28UFqyu8SfhjllRDMR/WO6Vk5nvj0TLG9aubpQUpR/
t6DkLjQXhjHFIhwsqe8EqjyERjOBrJEALOc7JUQtcVu3nlNXRkYstvY8qJyzfOqR
wx+SXFuVKI0YmTiH3h4vpM2UdGvu6YpVevYoQgeTvVkNsYrFTLZkwT1UcIc=
-----END RSA PRIVATE KEY-----

View file

@ -1 +0,0 @@
A067BAEC40B36AD7

View file

@ -1,23 +0,0 @@
-----BEGIN CERTIFICATE-----
MIID1zCCAb+gAwIBAgIJAKBnuuxAs2rXMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV
BAMMC1BvbWVyaXVtIENBMB4XDTIwMDgxMjA1MzYyN1oXDTMwMDgxMDA1MzYyN1ow
JjETMBEGA1UECgwKUmVkaXMgVGVzdDEPMA0GA1UEAwwGU2VydmVyMIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyKobpOg3TTZcrbprJttPs9fbF1gVOKOk
P40Rs5k/kjRGnIaqme3rh38KhRN2Fb59td6QpdyNkbq5noD19O+2MOk9k6KD0cN2
ClhGt6//yqp5WXPWLeZ5L5SENPOxxxNkpnF/wAwlbj0mQpQvtLUZ63dqk6wDoJP7
WBTZDbGJy04eTP9wGSyFYCBu4n7LeeeZVzxREoj4mg/1mEHkWy87fvkWfK9ZJXRt
JxkuE+Y62mz6r9BqWhXdKOB065iXhsO4Rc11RbNk5Gu7e/lE6cAcxHLMGbCM0lly
4yz+z+dJUv3uT9uGXM/3KnJmpuGfJElQ0OvnlbUNvcqf2RNtLTb8pQIDAQABoxgw
FjAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggIBAGXEw4cy
S/3kpLa1NMhFI8cwa1WmVTHK9caqH/fZ45ScSk88HJsjQEHai520GH9Hz3Oe1891
2otLGLho10WKrYZeLneJy6vgisNH9oFw/XPzp3B8s4WLccP/mDg3LFSz9D3rp4lp
RrvrDTrth9l1w0JgRUTpfSkMxchl873A3JU2QXusm2IPk2ICBtAiH8DNR+JEAf5n
msKzA+RO14Dc0MYf1/GN7RVtrPVLeahumtdvnJ5hbOpnzTMt9wfIBJU+X6J+WDPE
zkSnyBOtrWVwc/ZrkX8jk5rhVji/AEZrQw6CDliIq3Dh/80FFY+qhQW7O7DqI+J6
r64Z1JKVJA/DiwUhv0dgwtCpXkLcHJN5gq8jWnhj5pSQ2TL+whnEmTQmPTNEmxWz
lep9vUOdGXwW2khmi+TcGNJeVMp6DD0CZEFlGZ9qSKQ7fyRLGgry/iPnBYPKSg6T
OjLMS9G3cdApaod7M4479j4EK7V3bK4dnRp+qmg2TXhF+F+zRLf21THMip6AEz2u
pTFhqCYq42K8Mxa+TjKwSplTE/jS0O5zpJuPDnTXNM57x2Unlhi5fNaveg7fb8Np
Fyn+vMbxNW9z6naR35XeEyvcV5zAluh5oseXXIzr1xurRmWNgg2Fx5E5oPeEZx3L
egElJVL3xwac403pmA3057t4C4/ZFg4gXuUB
-----END CERTIFICATE-----

View file

@ -1,8 +0,0 @@
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEA4BcCbibc5Tc0JoUfkJkoY5L99tpEpB6CL4TCxC1jyiqrNoclEb2D
Tc8f/mnOKS8MuAZOOiyHQh9n1FyN0rudLIOUzGjVzmTNUXo8Md7N3pM3caZeFYbp
AoY7E9ISlYFm20zaeTziWUC8Yxs+WFEJbDbHScCW3KAqEevM/KfG1TpXKSqJkKGV
zkVyKJItObFCTlGclvRxC7Ajq/+o7fF1U6rfkiuMstXTXxOpvE6KiHkppXlf0gmW
81XJ3k7VOWPBIBdbeJGXrLajU7WuKiNnTvUlf7XEJq5/fy1CkJdxFL/QFCNNp9kR
8NYanjTl6vZgaN7+UbWI69LF/CL7kaBOAwIBAg==
-----END DH PARAMETERS-----

View file

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAyKobpOg3TTZcrbprJttPs9fbF1gVOKOkP40Rs5k/kjRGnIaq
me3rh38KhRN2Fb59td6QpdyNkbq5noD19O+2MOk9k6KD0cN2ClhGt6//yqp5WXPW
LeZ5L5SENPOxxxNkpnF/wAwlbj0mQpQvtLUZ63dqk6wDoJP7WBTZDbGJy04eTP9w
GSyFYCBu4n7LeeeZVzxREoj4mg/1mEHkWy87fvkWfK9ZJXRtJxkuE+Y62mz6r9Bq
WhXdKOB065iXhsO4Rc11RbNk5Gu7e/lE6cAcxHLMGbCM0lly4yz+z+dJUv3uT9uG
XM/3KnJmpuGfJElQ0OvnlbUNvcqf2RNtLTb8pQIDAQABAoIBAFE8rNxiNqFHtNWQ
dvjQKMBCTyxwOIcpmMExt0ziad4i08NisYaHz6aXRAcEDfZXnEUYya6cT6QD2EnX
I7v5n+TFSGyQipVNcGhXvKl40zGVOnOAdeE3QTCGC8/0KLDTpRfNM07om+65StgB
bh7WgpvVSIxoQz+rKUJLjmQA0CxByEWi0wFX1c1ngkqYm5ixmJQ6wgXADMCd06sz
j0bJ+z/gVSqgNjPAukjcNRu3xPIYmwOYFIihGTu8xHbayy64VjFNKlszXPDaap2s
wEeP6ScyC+/LIUqkFvTWoS01XommbZsT+gm5skHJSaDkzuSOH1LWcF/2x01Fwvru
dl+P7QECgYEA7TXp70Wy6kjNDqZU4v6fGGJqDeMehpiQnQpVWiyumNNJKkIty+3x
QCDTCIoF79EI1rySrcmB03qJlsq9zPZdbC+vN34rD9+m26E5b9B3K2v5+nvkhGp6
BeqnGiu3dkuchgfurEmjQ184cVqvEhraTlMrwzjcUH0s4PocjYA7+lUCgYEA2I8f
dFu56cYvwXlPA+j1W6fyWgdLsxvo+jfxJbJKvL4tncZwGUcxOB/ggdu6O20k32dX
5xbpUIh/PGtZmC7f+mdd2VoZcpSKkAoSo2Kwy3Q6ul1QXUJEmyjxvxcPLO0Cx6Jq
PGX2l6nE3e07AVlG1WcxsiMd6I4ugzwyGrTU6RECgYEAteiMd5OJuyUNK9jebB07
QGXoUrIDbNB+xg9wmPB/DG+rQh7yI9tbEQSbEYdXOiuhjZubGG7ZgqYL8XmUyCN+
TULcKcA1obyvpuois964JLJvR2nPOsS0wujKMMWpsawWYqqem7z02Ouiyzrx2v6A
v2QEwXdPbOIxkm37i3/1fukCgYAMKq5XuTeOvMW+FvSrgZEXXy4shLBqFa1XMYFo
3sV4KS8i6B0wLmHDh3bzlMa3xAAIVSQJJa5iCeksGdPkyu1mghwxs/AuEc5fHVHC
wC6yn4sVIVz8gFaeaQR7+e2uVnqLgMJ7NjdOeglHdqaUAtIJ90xBd9ucTzCpyt39
xh8YIQKBgD5AcbCzFLDE6NUXzUqjcQoh11oCIjYc223LVpxjjhGfm4tzV+cmB/i0
RWcbxEtmb/d7NRTBwzN60ba6PiMCNkywBcuEXMDAiFDz+QBfJFjCMNOmDMv4GgGS
E7EbeLRuYRJC9L8RfKtuB90EFTY3KgkMotm/25ACZUrqg3S7YXnh
-----END RSA PRIVATE KEY-----

View file

@ -1,4 +0,0 @@
[req]
distinguished_name=req
[san]
subjectAltName=DNS:localhost

View file

@ -7,6 +7,7 @@ import (
"path/filepath"
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
@ -15,6 +16,8 @@ import (
"google.golang.org/protobuf/testing/protocmp"
)
const maxWait = time.Minute * 20
// AssertProtoEqual asserts that two protobuf messages equal. Slices of messages are also supported.
func AssertProtoEqual(t *testing.T, expected, actual interface{}, msgAndArgs ...interface{}) bool {
t.Helper()
@ -70,8 +73,3 @@ func ModRoot() string {
}
return ""
}
// TestDataRoot returns the testdata directory.
func TestDataRoot() string {
return filepath.Join(ModRoot(), "internal", "testutil", "testdata")
}