Skip to content

Commit ac09eea

Browse files
committed
Introduce the concept of a package namespace
This is the beginning of the work for #119. - Add the concept of `PackageNamespace` to `config.Complement`. Set to `""` for now. - Update all filters to look for a matching package namespace when creating blueprints and containers. - Cleanup `docker.Builder` to read from the config more and from copies of config fields less. This change should be invisible to any existing Complement users. Background: Previously, Complement assumed that each container could be uniquely identified by the combination of `deployment_namespace + blueprint_name + hs_name`. For example, if you were running `BlueprintAlice` then a unique string might look like `5_alice_hs1` where `5` is an atomic, monotonically increasing integer incremented when `Deploy()` is called (see #113 for more info). In a parallel by default world this is no longer true because the `deployment_namespace` is not shared between different test processes. This means we cannot co-ordinate non-clashing namespaces like before. Instead, we will bring in another namespace for the test process (which in #119 will be on a per-package basis, hence the name `PackageNamespace`). As of this PR, literally everything Complement makes (images, containers, networks, etc) are prefixed with this package namespace, which allows multiple complement instances to share the same underlying docker daemon, with caveats: - Creating CA certificates will race and needs a lockfile to prevent 2 processes trying to create the certificate at the same time. - Complement federation servers cannot run together due to trying to bind to `:8448` at the same time. That being said, this PR should enable the parallelisation of a number of CS API only tests, which will come in another PR.
1 parent d4a0d89 commit ac09eea

File tree

6 files changed

+79
-54
lines changed

6 files changed

+79
-54
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ update-ca-certificates
131131
## Sytest parity
132132

133133
```
134-
$ go run sytest_coverage.go -v
134+
$ go build ./cmd/sytest-coverage
135+
$ ./sytest-coverage -v
135136
10apidoc/01register 3/9 tests
136137
× GET /register yields a set of flows
137138
✓ POST /register can create a user
File renamed without changes.

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type Complement struct {
1414
BestEffort bool
1515
VersionCheckIterations int
1616
KeepBlueprints []string
17+
// The namespace for all complement created blueprints and deployments
18+
PackageNamespace string
1719
}
1820

1921
func NewConfigFromEnvVars() *Complement {

internal/docker/builder.go

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,10 @@ func init() {
6363
const complementLabel = "complement_context"
6464

6565
type Builder struct {
66-
BaseImage string
67-
ImageArgs []string
68-
KeepBlueprints []string
66+
Config *config.Complement
6967
CSAPIPort int
7068
FederationPort int
7169
Docker *client.Client
72-
debugLogging bool
73-
bestEffort bool
74-
config *config.Complement
7570
}
7671

7772
func NewBuilder(cfg *config.Complement) (*Builder, error) {
@@ -81,19 +76,14 @@ func NewBuilder(cfg *config.Complement) (*Builder, error) {
8176
}
8277
return &Builder{
8378
Docker: cli,
84-
BaseImage: cfg.BaseImageURI,
85-
ImageArgs: cfg.BaseImageArgs,
86-
KeepBlueprints: cfg.KeepBlueprints,
79+
Config: cfg,
8780
CSAPIPort: 8008,
8881
FederationPort: 8448,
89-
debugLogging: cfg.DebugLoggingEnabled,
90-
bestEffort: cfg.BestEffort,
91-
config: cfg,
9282
}, nil
9383
}
9484

9585
func (d *Builder) log(str string, args ...interface{}) {
96-
if !d.debugLogging {
86+
if !d.Config.DebugLoggingEnabled {
9787
return
9888
}
9989
log.Printf(str, args...)
@@ -117,7 +107,10 @@ func (d *Builder) Cleanup() {
117107
// removeImages removes all images with `complementLabel`.
118108
func (d *Builder) removeNetworks() error {
119109
networks, err := d.Docker.NetworkList(context.Background(), types.NetworkListOptions{
120-
Filters: label(complementLabel),
110+
Filters: label(
111+
complementLabel,
112+
"complement_pkg="+d.Config.PackageNamespace,
113+
),
121114
})
122115
if err != nil {
123116
return err
@@ -134,7 +127,10 @@ func (d *Builder) removeNetworks() error {
134127
// removeImages removes all images with `complementLabel`.
135128
func (d *Builder) removeImages() error {
136129
images, err := d.Docker.ImageList(context.Background(), types.ImageListOptions{
137-
Filters: label(complementLabel),
130+
Filters: label(
131+
complementLabel,
132+
"complement_pkg="+d.Config.PackageNamespace,
133+
),
138134
})
139135
if err != nil {
140136
return err
@@ -156,7 +152,7 @@ func (d *Builder) removeImages() error {
156152
}
157153
bprintName := img.Labels["complement_blueprint"]
158154
keep := false
159-
for _, keepBprint := range d.KeepBlueprints {
155+
for _, keepBprint := range d.Config.KeepBlueprints {
160156
if bprintName == keepBprint {
161157
keep = true
162158
break
@@ -180,8 +176,11 @@ func (d *Builder) removeImages() error {
180176
// removeContainers removes all containers with `complementLabel`.
181177
func (d *Builder) removeContainers() error {
182178
containers, err := d.Docker.ContainerList(context.Background(), types.ContainerListOptions{
183-
All: true,
184-
Filters: label(complementLabel),
179+
All: true,
180+
Filters: label(
181+
complementLabel,
182+
"complement_pkg="+d.Config.PackageNamespace,
183+
),
185184
})
186185
if err != nil {
187186
return err
@@ -201,7 +200,10 @@ func (d *Builder) ConstructBlueprintsIfNotExist(bs []b.Blueprint) error {
201200
var blueprintsToBuild []b.Blueprint
202201
for _, bprint := range bs {
203202
images, err := d.Docker.ImageList(context.Background(), types.ImageListOptions{
204-
Filters: label("complement_blueprint=" + bprint.Name),
203+
Filters: label(
204+
"complement_blueprint="+bprint.Name,
205+
"complement_pkg="+d.Config.PackageNamespace,
206+
),
205207
})
206208
if err != nil {
207209
return fmt.Errorf("ConstructBlueprintsIfNotExist: failed to ImageList: %w", err)
@@ -242,7 +244,10 @@ func (d *Builder) ConstructBlueprints(bs []b.Blueprint) error {
242244
foundImages := false
243245
for i := 0; i < 50; i++ { // max 5s
244246
images, err := d.Docker.ImageList(context.Background(), types.ImageListOptions{
245-
Filters: label(complementLabel),
247+
Filters: label(
248+
complementLabel,
249+
"complement_pkg="+d.Config.PackageNamespace,
250+
),
246251
})
247252
if err != nil {
248253
return err
@@ -265,12 +270,12 @@ func (d *Builder) ConstructBlueprints(bs []b.Blueprint) error {
265270

266271
// construct all Homeservers sequentially then commits them
267272
func (d *Builder) construct(bprint b.Blueprint) (errs []error) {
268-
networkID, err := CreateNetworkIfNotExists(d.Docker, bprint.Name)
273+
networkID, err := createNetworkIfNotExists(d.Docker, d.Config.PackageNamespace, bprint.Name)
269274
if err != nil {
270275
return []error{err}
271276
}
272277

273-
runner := instruction.NewRunner(bprint.Name, d.bestEffort, d.debugLogging)
278+
runner := instruction.NewRunner(bprint.Name, d.Config.BestEffort, d.Config.DebugLoggingEnabled)
274279
results := make([]result, len(bprint.Homeservers))
275280
for i, hs := range bprint.Homeservers {
276281
res := d.constructHomeserver(bprint.Name, runner, hs, networkID)
@@ -342,7 +347,7 @@ func (d *Builder) construct(bprint b.Blueprint) (errs []error) {
342347

343348
// construct this homeserver and execute its instructions, keeping the container alive.
344349
func (d *Builder) constructHomeserver(blueprintName string, runner *instruction.Runner, hs b.Homeserver, networkID string) result {
345-
contextStr := fmt.Sprintf("%s.%s", blueprintName, hs.Name)
350+
contextStr := fmt.Sprintf("%s.%s.%s", d.Config.PackageNamespace, blueprintName, hs.Name)
346351
d.log("%s : constructing homeserver...\n", contextStr)
347352
dep, err := d.deployBaseImage(blueprintName, hs, contextStr, networkID)
348353
if err != nil {
@@ -376,15 +381,17 @@ func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, context
376381
asIDToRegistrationMap := asIDToRegistrationFromLabels(labelsForApplicationServices(hs))
377382

378383
return deployImage(
379-
d.Docker, d.BaseImage, d.CSAPIPort, fmt.Sprintf("complement_%s", contextStr), blueprintName, hs.Name, asIDToRegistrationMap, contextStr,
380-
networkID, d.config.VersionCheckIterations,
384+
d.Docker, d.Config.BaseImageURI, d.CSAPIPort, fmt.Sprintf("complement_%s", contextStr),
385+
d.Config.PackageNamespace, blueprintName, hs.Name, asIDToRegistrationMap, contextStr,
386+
networkID, d.Config.VersionCheckIterations,
381387
)
382388
}
383389

384390
// getCaVolume returns the correct volume mount for providing a CA to homeserver containers.
385391
// If running CI, returns an error if it's unable to find a volume that has /ca
386392
// Otherwise, returns an error if we're unable to find the <cwd>/ca directory on the local host
387393
func getCaVolume(ctx context.Context, docker *client.Client) (caMount mount.Mount, err error) {
394+
// TODO: wrap in a lockfile
388395
if os.Getenv("CI") == "true" {
389396
// When in CI, Complement itself is a container with the CA volume mounted at /ca.
390397
// We need to mount this volume to all homeserver containers to synchronize the CA cert.
@@ -484,7 +491,7 @@ func generateASRegistrationYaml(as b.ApplicationService) string {
484491
}
485492

486493
func deployImage(
487-
docker *client.Client, imageID string, csPort int, containerName, blueprintName, hsName string, asIDToRegistrationMap map[string]string, contextStr, networkID string, versionCheckIterations int,
494+
docker *client.Client, imageID string, csPort int, containerName, pkgNamespace, blueprintName, hsName string, asIDToRegistrationMap map[string]string, contextStr, networkID string, versionCheckIterations int,
488495
) (*HomeserverDeployment, error) {
489496
ctx := context.Background()
490497
var extraHosts []string
@@ -526,6 +533,7 @@ func deployImage(
526533
Labels: map[string]string{
527534
complementLabel: contextStr,
528535
"complement_blueprint": blueprintName,
536+
"complement_pkg": pkgNamespace,
529537
"complement_hs_name": hsName,
530538
},
531539
}, &container.HostConfig{
@@ -616,12 +624,15 @@ func deployImage(
616624
return d, nil
617625
}
618626

619-
// CreateNetworkIfNotExists creates a docker network and returns its id.
627+
// createNetworkIfNotExists creates a docker network and returns its id.
620628
// ID is guaranteed not to be empty when err == nil
621-
func CreateNetworkIfNotExists(docker *client.Client, blueprintName string) (networkID string, err error) {
629+
func createNetworkIfNotExists(docker *client.Client, pkgNamespace, blueprintName string) (networkID string, err error) {
622630
// check if a network already exists for this blueprint
623631
nws, err := docker.NetworkList(context.Background(), types.NetworkListOptions{
624-
Filters: label("complement_blueprint=" + blueprintName),
632+
Filters: label(
633+
"complement_pkg="+pkgNamespace,
634+
"complement_blueprint="+blueprintName,
635+
),
625636
})
626637
if err != nil {
627638
return "", fmt.Errorf("%s: failed to list networks. %w", blueprintName, err)
@@ -631,10 +642,11 @@ func CreateNetworkIfNotExists(docker *client.Client, blueprintName string) (netw
631642
return nws[0].ID, nil
632643
}
633644
// make a user-defined network so we get DNS based on the container name
634-
nw, err := docker.NetworkCreate(context.Background(), "complement_"+blueprintName, types.NetworkCreate{
645+
nw, err := docker.NetworkCreate(context.Background(), "complement_"+pkgNamespace+"_"+blueprintName, types.NetworkCreate{
635646
Labels: map[string]string{
636647
complementLabel: blueprintName,
637648
"complement_blueprint": blueprintName,
649+
"complement_pkg": pkgNamespace,
638650
},
639651
})
640652
if err != nil {
@@ -668,9 +680,14 @@ func printLogs(docker *client.Client, containerID, contextStr string) {
668680
log.Printf("============== %s : END LOGS ==============\n\n\n", contextStr)
669681
}
670682

671-
func label(in string) filters.Args {
683+
// label returns a filter for the presence of certain labels ("complement_context") or a match of
684+
// labels ("complement_blueprint=foo").
685+
func label(labelFilters ...string) filters.Args {
672686
f := filters.NewArgs()
673-
f.Add("label", in)
687+
// label=<key> or label=<key>=<value>
688+
for _, in := range labelFilters {
689+
f.Add("label", in)
690+
}
674691
return f
675692
}
676693

internal/docker/deployer.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,24 @@ import (
2929
)
3030

3131
type Deployer struct {
32-
Namespace string
33-
Docker *client.Client
34-
Counter int
35-
networkID string
36-
debugLogging bool
37-
config *config.Complement
32+
DeployNamespace string
33+
Docker *client.Client
34+
Counter int
35+
networkID string
36+
debugLogging bool
37+
config *config.Complement
3838
}
3939

40-
func NewDeployer(namespace string, cfg *config.Complement) (*Deployer, error) {
40+
func NewDeployer(deployNamespace string, cfg *config.Complement) (*Deployer, error) {
4141
cli, err := client.NewEnvClient()
4242
if err != nil {
4343
return nil, err
4444
}
4545
return &Deployer{
46-
Namespace: namespace,
47-
Docker: cli,
48-
debugLogging: cfg.DebugLoggingEnabled,
49-
config: cfg,
46+
DeployNamespace: deployNamespace,
47+
Docker: cli,
48+
debugLogging: cfg.DebugLoggingEnabled,
49+
config: cfg,
5050
}, nil
5151
}
5252

@@ -64,15 +64,18 @@ func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deploymen
6464
HS: make(map[string]HomeserverDeployment),
6565
}
6666
images, err := d.Docker.ImageList(ctx, types.ImageListOptions{
67-
Filters: label("complement_blueprint=" + blueprintName),
67+
Filters: label(
68+
"complement_pkg="+d.config.PackageNamespace,
69+
"complement_blueprint="+blueprintName,
70+
),
6871
})
6972
if err != nil {
7073
return nil, fmt.Errorf("Deploy: failed to ImageList: %w", err)
7174
}
7275
if len(images) == 0 {
7376
return nil, fmt.Errorf("Deploy: No images have been built for blueprint %s", blueprintName)
7477
}
75-
networkID, err := CreateNetworkIfNotExists(d.Docker, blueprintName)
78+
networkID, err := createNetworkIfNotExists(d.Docker, d.config.PackageNamespace, blueprintName)
7679
if err != nil {
7780
return nil, fmt.Errorf("Deploy: %w", err)
7881
}
@@ -85,8 +88,8 @@ func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deploymen
8588

8689
// TODO: Make CSAPI port configurable
8790
deployment, err := deployImage(
88-
d.Docker, img.ID, 8008, fmt.Sprintf("complement_%s_%s_%d", d.Namespace, contextStr, d.Counter),
89-
blueprintName, hsName, asIDToRegistrationMap, contextStr, networkID, d.config.VersionCheckIterations)
91+
d.Docker, img.ID, 8008, fmt.Sprintf("complement_%s_%s_%s_%d", d.config.PackageNamespace, d.DeployNamespace, contextStr, d.Counter),
92+
d.config.PackageNamespace, blueprintName, hsName, asIDToRegistrationMap, contextStr, networkID, d.config.VersionCheckIterations)
9093
if err != nil {
9194
if deployment != nil && deployment.ContainerID != "" {
9295
// print logs to help debug

tests/main_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020

2121
var namespaceCounter uint64
2222

23+
// persist the complement builder which is set when the tests start via TestMain
24+
var complementBuilder *docker.Builder
25+
2326
// TestMain is the main entry point for Complement.
2427
//
2528
// It will clean up any old containers/images/networks from the previous run, then run the tests, then clean up
@@ -32,6 +35,7 @@ func TestMain(m *testing.M) {
3235
fmt.Printf("Error: %s", err)
3336
os.Exit(1)
3437
}
38+
complementBuilder = builder
3539
// remove any old images/containers/networks in case we died horribly before
3640
builder.Cleanup()
3741

@@ -60,16 +64,14 @@ func TestMain(m *testing.M) {
6064
func Deploy(t *testing.T, blueprint b.Blueprint) *docker.Deployment {
6165
t.Helper()
6266
timeStartBlueprint := time.Now()
63-
cfg := config.NewConfigFromEnvVars()
64-
builder, err := docker.NewBuilder(cfg)
65-
if err != nil {
66-
t.Fatalf("Deploy: docker.NewBuilder returned error: %s", err)
67+
if complementBuilder == nil {
68+
t.Fatalf("complementBuilder not set, did you forget to call TestMain?")
6769
}
68-
if err = builder.ConstructBlueprintsIfNotExist([]b.Blueprint{blueprint}); err != nil {
70+
if err := complementBuilder.ConstructBlueprintsIfNotExist([]b.Blueprint{blueprint}); err != nil {
6971
t.Fatalf("Deploy: Failed to construct blueprint: %s", err)
7072
}
7173
namespace := fmt.Sprintf("%d", atomic.AddUint64(&namespaceCounter, 1))
72-
d, err := docker.NewDeployer(namespace, cfg)
74+
d, err := docker.NewDeployer(namespace, complementBuilder.Config)
7375
if err != nil {
7476
t.Fatalf("Deploy: NewDeployer returned error %s", err)
7577
}

0 commit comments

Comments
 (0)