forked from matrix-org/complement
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
306 lines (283 loc) · 12.5 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
package config
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"regexp"
"strconv"
"strings"
"time"
)
type HostMount struct {
HostPath string
ContainerPath string
ReadOnly bool
}
// The config for running Complement. This is configured using environment variables. The comments
// in this struct are structured so they can be automatically parsed via gendoc. See /cmd/gendoc.
type Complement struct {
// Name: COMPLEMENT_BASE_IMAGE
// Description: **Required.** The name of the Docker image to use as a base homeserver when generating
// blueprints. This image must conform to Complement's rules on containers, such as listening on the
// correct ports.
BaseImageURI string
// Name: COMPLEMENT_BASE_ARCH
// Default: ""
// Description: The architecture of the Docker image to use as a base homeserver when generating
// blueprints. This can be used to emulate a particular architecture with a multi-platform image.
// If "", the architecture of the host running Complement is used.
BaseImageArch string
// Name: COMPLEMENT_DEBUG
// Default: 0
// Description: If 1, prints out more verbose logging such as HTTP request/response bodies.
DebugLoggingEnabled bool
// Name: COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS
// Default: 0
// Description: If 1, always prints the Homeserver container logs even on success. When used with
// COMPLEMENT_ENABLE_DIRTY_RUNS, server logs are only printed once for reused deployments, at the very
// end of the test suite.
AlwaysPrintServerLogs bool
// Name: COMPLEMENT_SHARE_ENV_PREFIX
// Description: If set, all environment variables on the host with this prefix will be shared with
// every homeserver, with the prefix removed. For example, if the prefix was `FOO_` then setting
// `FOO_BAR=baz` on the host would translate to `BAR=baz` on the container. Useful for passing through
// extra Homeserver configuration options without sharing all host environment variables.
EnvVarsPropagatePrefix string
// Name: COMPLEMENT_SPAWN_HS_TIMEOUT_SECS
// Default: 30
// Description: The number of seconds to wait for a Homeserver container to be responsive after
// starting the container. Responsiveness is detected by `HEALTHCHECK` being healthy *and*
// the `/versions` endpoint returning 200 OK.
SpawnHSTimeout time.Duration
// Name: COMPLEMENT_KEEP_BLUEPRINTS
// Description: A list of space separated blueprint names to not clean up after running. For example,
// `one_to_one_room alice` would not delete the homeserver images for the blueprints `alice` and
// `one_to_one_room`. This can speed up homeserver runs if you frequently run the same base image
// over and over again. If the base image changes, this should not be set as it means an older version
// of the base image will be used for the named blueprints.
KeepBlueprints []string
// Name: COMPLEMENT_HOST_MOUNTS
// Description: A list of semicolon separated host mounts to mount on every container. The structure
// of the mount is `host-path:container-path:[ro]` for example `/path/on/host:/path/on/container` - you
// can optionally specify `:ro` to mount the path as readonly. A complete example with multiple mounts
// would look like `/host/a:/container/a:ro;/host/b:/container/b;/host/c:/container/c`
HostMounts []HostMount
// Name: COMPLEMENT_BASE_IMAGE_*
// Description: This allows you to override the base image used for a particular named homeserver.
// For example, `COMPLEMENT_BASE_IMAGE_HS1=complement-dendrite:latest` would use `complement-dendrite:latest`
// for the `hs1` homeserver in blueprints, but not any other homeserver (e.g `hs2`). This matching
// is case-insensitive. This allows Complement to test how different homeserver implementations work with each other.
BaseImageURIs map[string]string
// Name: COMPLEMENT_BASE_ARCH_*
// Description: This allows you to set the architecture of the base image used for a particular named homeserver.
// For example, `COMPLEMENT_BASE_ARCH_HS1=arm64` would use an `arm64` image for the `hs1` homeserver in blueprints,
// but not any other homeserver (e.g `hs2`). This matching is case-insensitive.
BaseImageArchs map[string]string
// The namespace for all complement created blueprints and deployments
PackageNamespace string
// Certificate Authority generated values for this run of complement. Homeservers will use this
// as a base to derive their own signed Federation certificates.
CACertificate *x509.Certificate
CAPrivateKey *rsa.PrivateKey
BestEffort bool
// Name: COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT
// Default: host.docker.internal
// Description: The hostname of Complement from the perspective of a Homeserver running inside a container.
// This can be useful for container runtimes using another hostname to access the host from a container,
// like Podman that uses `host.containers.internal` instead.
HostnameRunningComplement string
// Name: COMPLEMENT_ENABLE_DIRTY_RUNS
// Default: 0
// Description: If 1, eligible tests will be provided with reusable deployments rather than a clean deployment.
// Eligible tests are tests run with `Deploy(t, numHomeservers)`. If enabled, COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS
// and COMPLEMENT_POST_TEST_SCRIPT are run exactly once, at the end of all tests in the package. The post test script
// is run with the test name "COMPLEMENT_ENABLE_DIRTY_RUNS", and failed=false.
//
// Enabling dirty runs can greatly speed up tests, at the cost of clear server logs and the chance of tests
// polluting each other. Tests using `OldDeploy` and blueprints will still have a fresh image for each test.
// Fresh images can still be desirable e.g user directory tests need a clean homeserver else search results can
// be polluted, tests which can blacklist a server over federation also need isolated deployments to stop failures
// impacting other tests. For these reasons, there will always be a way for a test to override this setting and
// get a dedicated deployment.
//
// Eventually, dirty runs will become the default running mode of Complement, with an environment variable to
// disable this behaviour being added later, once this has stablised.
EnableDirtyRuns bool
HSPortBindingIP string
// Name: COMPLEMENT_POST_TEST_SCRIPT
// Default: ""
// Description: An arbitrary script to execute after a test was executed and before the container is removed.
// This can be used to extract, for example, server logs or database files. The script is passed the parameters:
// ContainerID, TestName, TestFailed (true/false). When combined with COMPLEMENT_ENABLE_DIRTY_RUNS, the script is
// called exactly once at the end of the test suite, and is called with the TestName of "COMPLEMENT_ENABLE_DIRTY_RUNS"
// and TestFailed=false.
PostTestScript string
}
var hsUriRegex = regexp.MustCompile(`COMPLEMENT_BASE_IMAGE_(.+)=(.+)$`)
var hsArchRegex = regexp.MustCompile(`COMPLEMENT_BASE_ARCH_(.+)=(.+)$`)
func NewConfigFromEnvVars(pkgNamespace, baseImageURI string) *Complement {
cfg := &Complement{
BaseImageURIs: map[string]string{},
BaseImageArchs: map[string]string{},
}
cfg.BaseImageURI = os.Getenv("COMPLEMENT_BASE_IMAGE")
if cfg.BaseImageURI == "" {
cfg.BaseImageURI = baseImageURI
}
cfg.BaseImageArch = os.Getenv("COMPLEMENT_BASE_ARCH")
cfg.DebugLoggingEnabled = os.Getenv("COMPLEMENT_DEBUG") == "1"
cfg.AlwaysPrintServerLogs = os.Getenv("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS") == "1"
cfg.EnableDirtyRuns = os.Getenv("COMPLEMENT_ENABLE_DIRTY_RUNS") == "1"
cfg.EnvVarsPropagatePrefix = os.Getenv("COMPLEMENT_SHARE_ENV_PREFIX")
cfg.PostTestScript = os.Getenv("COMPLEMENT_POST_TEST_SCRIPT")
cfg.SpawnHSTimeout = time.Duration(parseEnvWithDefault("COMPLEMENT_SPAWN_HS_TIMEOUT_SECS", 30)) * time.Second
if os.Getenv("COMPLEMENT_VERSION_CHECK_ITERATIONS") != "" {
fmt.Fprintln(os.Stderr, "Deprecated: COMPLEMENT_VERSION_CHECK_ITERATIONS will be removed in a later version. Use COMPLEMENT_SPAWN_HS_TIMEOUT_SECS instead which does the same thing and is clearer.")
// each iteration had a 50ms sleep between tries so the timeout is 50 * iteration ms
cfg.SpawnHSTimeout = time.Duration(50*parseEnvWithDefault("COMPLEMENT_VERSION_CHECK_ITERATIONS", 100)) * time.Millisecond
}
cfg.KeepBlueprints = strings.Split(os.Getenv("COMPLEMENT_KEEP_BLUEPRINTS"), " ")
var err error
hostMounts := os.Getenv("COMPLEMENT_HOST_MOUNTS")
if hostMounts != "" {
cfg.HostMounts, err = newHostMounts(strings.Split(hostMounts, ";"))
if err != nil {
panic("COMPLEMENT_HOST_MOUNTS parse error: " + err.Error())
}
}
if cfg.BaseImageURI == "" {
panic("COMPLEMENT_BASE_IMAGE must be set")
}
// Parse HS specific base images
for _, env := range os.Environ() {
// FindStringSubmatch returns the complete match as well as the capture groups.
// In this case we expect there to be 3 matches.
if matches := hsUriRegex.FindStringSubmatch(env); len(matches) == 3 {
hs := matches[1] // first capture group; homeserver name
cfg.BaseImageURIs[hs] = matches[2] // second capture group; homeserver image
}
if matches := hsArchRegex.FindStringSubmatch(env); len(matches) == 3 {
hs := matches[1] // first capture group; homeserver name
cfg.BaseImageArchs[hs] = matches[2] // second capture group; homeserver arch
}
}
cfg.PackageNamespace = pkgNamespace
// create CA certs and keys
if err := cfg.GenerateCA(); err != nil {
panic("Failed to generate CA certificate/key: " + err.Error())
}
if cfg.PackageNamespace == "" {
panic("package namespace must be set")
}
HostnameRunningComplement := os.Getenv("COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT")
if HostnameRunningComplement != "" {
cfg.HostnameRunningComplement = HostnameRunningComplement
} else {
cfg.HostnameRunningComplement = "host.docker.internal"
}
// HSPortBindingIP is fixed here, but used by homerunner to override.
cfg.HSPortBindingIP = "127.0.0.1"
return cfg
}
func (c *Complement) GenerateCA() error {
cert, key, err := generateCAValues()
if err != nil {
return err
}
c.CACertificate = cert
c.CAPrivateKey = key
return nil
}
func (c *Complement) CACertificateBytes() ([]byte, error) {
cert := bytes.NewBuffer(nil)
err := pem.Encode(cert, &pem.Block{Type: "CERTIFICATE", Bytes: c.CACertificate.Raw})
return cert.Bytes(), err
}
func (c *Complement) CAPrivateKeyBytes() ([]byte, error) {
caKey := bytes.NewBuffer(nil)
err := pem.Encode(caKey, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(c.CAPrivateKey),
})
return caKey.Bytes(), err
}
func parseEnvWithDefault(key string, def int) int {
s := os.Getenv(key)
if s != "" {
i, err := strconv.Atoi(s)
if err != nil {
// Don't bother trying to report it
return def
}
return i
}
return def
}
func newHostMounts(mounts []string) ([]HostMount, error) {
var hostMounts []HostMount
for _, m := range mounts {
segments := strings.Split(m, ":")
if len(segments) < 2 {
return nil, fmt.Errorf("mount '%s' malformed", m)
}
var ro string
if len(segments) == 3 {
ro = segments[2]
}
hostMounts = append(hostMounts, HostMount{
HostPath: segments[0],
ContainerPath: segments[1],
ReadOnly: ro == "ro" || ro == "readonly",
})
}
return hostMounts, nil
}
// Generate a certificate and private key
func generateCAValues() (*x509.Certificate, *rsa.PrivateKey, error) {
// valid for 10 years
certificateDuration := time.Hour * 24 * 365 * 10
priv, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, nil, err
}
notBefore := time.Now()
notAfter := notBefore.Add(certificateDuration)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, err
}
caCert := x509.Certificate{
SerialNumber: serialNumber,
NotBefore: notBefore,
NotAfter: notAfter,
IsCA: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
Organization: []string{"matrix.org"},
Country: []string{"GB"},
Province: []string{"London"},
Locality: []string{"London"},
StreetAddress: []string{"123 Street"},
PostalCode: []string{"12345"},
CommonName: "Complement Test CA",
},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &caCert, &caCert, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
selfSignedCert, err := x509.ParseCertificates(derBytes)
if err != nil {
return nil, nil, err
}
return selfSignedCert[0], priv, nil
}