107 lines
3.4 KiB
Dart
107 lines
3.4 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
|
||
|
|
/// halkyon.li's first start (deploy/host/first-start.md, ADR 0039) against what the
|
||
|
|
/// compose files run: the sidecars' users as their containers run, their folders
|
||
|
|
/// where the containers mount them, and a role and database for each app's server.
|
||
|
|
void main() {
|
||
|
|
final steps = File('deploy/host/first-start.md').readAsStringSync();
|
||
|
|
String compose(String name) => File('deploy/$name').readAsStringSync();
|
||
|
|
|
||
|
|
/// The users step 1 makes, by name: their IDs.
|
||
|
|
final users = {
|
||
|
|
for (final match in RegExp(
|
||
|
|
r'(\w+):(\d{5})',
|
||
|
|
).allMatches(RegExp('for each in (.+); do').firstMatch(steps)![1]!))
|
||
|
|
match[1]!: match[2]!,
|
||
|
|
};
|
||
|
|
|
||
|
|
test("makes each sidecar's user with the ID its container runs as", () {
|
||
|
|
const runs = {
|
||
|
|
'jellyfin': 'compose.jellyfin.yaml',
|
||
|
|
'languagetool': 'compose.languagetool.yaml',
|
||
|
|
'brouter': 'compose.train.yaml',
|
||
|
|
'maptiles': 'compose.maps.yaml',
|
||
|
|
'photon': 'compose.search.yaml',
|
||
|
|
};
|
||
|
|
expect(users.keys, unorderedEquals(runs.keys));
|
||
|
|
for (final MapEntry(key: name, value: file) in runs.entries) {
|
||
|
|
final id = users[name]!;
|
||
|
|
expect(compose(file), contains('user: "$id:$id"'), reason: name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("never gives a sidecar the runner's ID, nor one ID twice", () {
|
||
|
|
expect(users.values, isNot(contains('61000')));
|
||
|
|
expect(users.values.toSet(), hasLength(users.length));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("makes each sidecar's folder its own, where its container mounts "
|
||
|
|
'it', () {
|
||
|
|
const folders = {
|
||
|
|
'languagetool': (r'$d/languagetool/fasttext', './languagetool/fasttext'),
|
||
|
|
'brouter': (r'$d/brouter/segments', './brouter/segments'),
|
||
|
|
'maptiles': (r'$d/maps/tiles', './maps/tiles'),
|
||
|
|
'photon': (r'$d/photon/data', './photon/data'),
|
||
|
|
};
|
||
|
|
final mounted = [
|
||
|
|
for (final file in [
|
||
|
|
'compose.languagetool.yaml',
|
||
|
|
'compose.train.yaml',
|
||
|
|
'compose.maps.yaml',
|
||
|
|
'compose.search.yaml',
|
||
|
|
])
|
||
|
|
compose(file),
|
||
|
|
].join('\n');
|
||
|
|
for (final MapEntry(key: owner, value: (made, mount)) in folders.entries) {
|
||
|
|
expect(
|
||
|
|
RegExp('sudo install -d -o $owner -g $owner .*${RegExp.escape(made)}')
|
||
|
|
.hasMatch(steps),
|
||
|
|
isTrue,
|
||
|
|
reason: made,
|
||
|
|
);
|
||
|
|
expect(mounted, contains(mount), reason: mount);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test("gives each app's server a role of its own, owning its database "
|
||
|
|
'alone, and lets no role connect to a database not its own', () {
|
||
|
|
for (final app in ['train', 'todo', 'meditate']) {
|
||
|
|
expect(
|
||
|
|
steps,
|
||
|
|
contains("CREATE ROLE halkyon_$app LOGIN PASSWORD :'$app';"),
|
||
|
|
reason: app,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
steps,
|
||
|
|
contains('CREATE DATABASE halkyon_$app OWNER halkyon_$app;'),
|
||
|
|
reason: app,
|
||
|
|
);
|
||
|
|
expect(
|
||
|
|
compose('compose.$app.yaml'),
|
||
|
|
contains(
|
||
|
|
r'${'
|
||
|
|
'${app.toUpperCase()}_POSTGRES_PASSWORD',
|
||
|
|
),
|
||
|
|
reason: app,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
for (final app in ['learn', 'train', 'todo', 'meditate']) {
|
||
|
|
expect(
|
||
|
|
steps,
|
||
|
|
contains('REVOKE CONNECT ON DATABASE halkyon_$app FROM PUBLIC;'),
|
||
|
|
reason: app,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
expect(steps, isNot(contains('SUPERUSER')));
|
||
|
|
expect(steps, isNot(contains('CREATEDB')));
|
||
|
|
});
|
||
|
|
|
||
|
|
test("pins the developer's key on deploy to redeploy.sh alone", () {
|
||
|
|
expect(
|
||
|
|
steps,
|
||
|
|
contains('restrict,command="/home/deploy/halkyon/deploy/redeploy.sh" %s'),
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|