Fix infra samples from local verification

This commit is contained in:
Chris Hendrickson 2026-05-05 00:41:28 -04:00
parent 397aed251f
commit f191a276a8
13 changed files with 95 additions and 10 deletions

View file

@ -8,7 +8,7 @@ Image=docker.io/library/postgres:18
ContainerName=dew_keycloak-postgresql
Network=dew_keycloak.network
NetworkAlias=postgres
Volume=dew_keycloak-postgresql.volume:/var/lib/postgresql/data
Volume=dew_keycloak-postgresql.volume:/var/lib/postgresql
Environment=POSTGRES_DB=keycloak
Environment=POSTGRES_USER=keycloak
Environment=POSTGRES_PASSWORD=keycloak_dev_password

View file

@ -1,8 +1,6 @@
FROM docker.io/library/alpine:3.23
FROM docker.io/library/nginx:alpine
RUN mkdir -p /srv/www \
&& printf '{"service":"local-api-build","status":"ok"}\n' > /srv/www/index.html
RUN printf '{"service":"local-api-build","status":"ok"}\n' \
> /usr/share/nginx/html/index.html
EXPOSE 8080
CMD ["busybox", "httpd", "-f", "-p", "8080", "-h", "/srv/www"]
EXPOSE 80

View file

@ -4,6 +4,7 @@ File=Containerfile
SetWorkingDirectory=unit
[Service]
RemainAfterExit=yes
TimeoutStartSec=900
[Install]

View file

@ -6,7 +6,7 @@ After=dew_local-api-build.build
[Container]
Image=dew_local-api-build.build
ContainerName=dew_local-api-build
PublishPort=127.0.0.1:8090:8080
PublishPort=127.0.0.1:8090:80
[Service]
Restart=always

View file

@ -11,6 +11,9 @@ quadlets:
unit: dew_local-api-build.service
container_name: dew_local-api-build
files:
- Containerfile
schemas:
configure: schemas/configure.schema.json
init: schemas/init.schema.json

View file

@ -10,7 +10,7 @@ Environment=POSTGRES_DB=dew
Environment=POSTGRES_USER=dew
Environment=POSTGRES_PASSWORD=dew_dev_password
PublishPort=127.0.0.1:5432:5432
Volume=dew_postgresql-18_data:/var/lib/postgresql/data:Z
Volume=dew_postgresql-18_data:/var/lib/postgresql:Z
HealthCmd=pg_isready -U dew -d dew
HealthInterval=10s
HealthTimeout=5s

View file

@ -0,0 +1,8 @@
---
id: DEW-0036
title: Verify infra samples locally
type: task
created: 2026-05-05T04:28:49.175443Z
---
Bring up the new sample infrastructure services with dew infra, verify they start and respond as expected, then stop them without deleting retained data volumes.

View file

@ -51,6 +51,9 @@ The `quadlets` list can contain any supported Podman Quadlet source type:
from the Quadlet filename. Declare `unit` when the Quadlet file uses a
`ServiceName=` override.
Use `files` for non-Quadlet assets that must be installed beside the Quadlet
files, such as a `Containerfile` used by a `.build` unit.
The package-level schema for this file is
`packages/infra/schemas/service-manifest.schema.json`.

View file

@ -167,6 +167,9 @@ class InfraValidator {
issues,
);
}
for (final path in manifest.filePaths) {
await _requireFile(manifest, path, issues);
}
await _validateJsonSchema(
manifest,
label: 'configure schema',

View file

@ -222,6 +222,12 @@ class PodmanQuadletRuntime implements ContainerRuntime {
await fs.link(target).exists() || await fs.file(target).exists();
if (!exists) return false;
}
for (final file in manifest.files) {
final target = _targetFilePath(file, scope);
final exists =
await fs.link(target).exists() || await fs.file(target).exists();
if (!exists) return false;
}
return true;
}
@ -268,6 +274,14 @@ class PodmanQuadletRuntime implements ContainerRuntime {
}
}
}
for (var i = 0; i < manifest.files.length; i++) {
await _link(
actions,
dryRun,
manifest.filePaths[i],
_targetFilePath(manifest.files[i], scope),
);
}
return InfraRuntimeResult(actions: actions);
}
@ -291,6 +305,9 @@ class PodmanQuadletRuntime implements ContainerRuntime {
);
}
}
for (final file in manifest.files) {
await _deletePath(actions, dryRun, _targetFilePath(file, scope));
}
return InfraRuntimeResult(actions: actions);
}
@ -409,6 +426,9 @@ class PodmanQuadletRuntime implements ContainerRuntime {
p.basename(quadlet.file),
);
String _targetFilePath(String file, InfraScope scope) =>
p.join(quadletSearchPath(scope, environment: environment), file);
Future<void> _link(
List<String> actions,
bool dryRun,
@ -416,6 +436,7 @@ class PodmanQuadletRuntime implements ContainerRuntime {
String target,
) async {
await _action(actions, dryRun, 'link $source -> $target', () async {
await fs.directory(p.dirname(target)).create(recursive: true);
await _deleteIfExists(target);
await fs.link(target).create(source, recursive: true);
});

View file

@ -153,6 +153,7 @@ class InfraServiceManifest {
required this.serviceDir,
required this.manifestPath,
required this.quadlets,
this.files = const [],
this.configureSchema,
this.initSchema,
});
@ -175,6 +176,9 @@ class InfraServiceManifest {
/// Quadlet files deployed for this service.
final List<InfraQuadletManifest> quadlets;
/// Additional files installed beside Quadlets for build contexts or assets.
final List<String> files;
/// Optional relative path to the configure JSON Schema.
final String? configureSchema;
@ -198,6 +202,9 @@ class InfraServiceManifest {
/// Active init payload path.
String get activeInitPath => p.join(configDir, 'init.json');
/// Absolute paths to additional installed files.
List<String> get filePaths => files.map(_resolve).toList();
/// Units generated by the declared Quadlet files.
List<String> get units =>
quadlets.map((quadlet) => quadlet.serviceUnit).toList();
@ -231,6 +238,7 @@ class InfraServiceManifest {
serviceDir: serviceDir,
manifestPath: manifestPath,
quadlets: quadlets,
files: _optionalStringList(map, 'files'),
configureSchema: schemas == null
? null
: _optionalString(schemas, 'configure'),
@ -246,6 +254,7 @@ class InfraServiceManifest {
'manifest': manifestPath,
'units': units,
'quadlets': quadlets.map((quadlet) => quadlet.toJson()).toList(),
'files': filePaths,
'configure_schema': configureSchemaPath,
'init_schema': initSchemaPath,
'active_config': activeConfigurePath,
@ -311,6 +320,20 @@ List<dynamic> _requiredList(Map<String, dynamic> map, String key) {
throw FormatException('manifest.yaml is missing list "$key".');
}
List<String> _optionalStringList(Map<String, dynamic> map, String key) {
final value = map[key];
if (value == null) return const [];
if (value is! List) {
throw FormatException('manifest.yaml field "$key" must be a list.');
}
return value.map((item) {
if (item is String && item.isNotEmpty) return item;
throw FormatException(
'manifest.yaml field "$key" must contain non-empty strings.',
);
}).toList();
}
String _requiredString(Map<String, dynamic> map, String key) {
final value = _optionalString(map, key);
if (value == null || value.isEmpty) {

View file

@ -61,6 +61,15 @@
}
}
},
"files": {
"type": "array",
"description": "Additional files installed beside Quadlets, such as Containerfiles for build contexts.",
"items": {
"type": "string",
"minLength": 1,
"pattern": "^[^/].*$"
}
},
"schemas": {
"type": "object",
"additionalProperties": false,

View file

@ -115,7 +115,7 @@ void main() {
'install dry-run reports symlink actions without writing files',
() async {
final fs = MemoryFileSystem.test();
_writeService(fs, includeNetwork: true);
_writeService(fs, includeNetwork: true, includeFile: true);
final manifest = await InfraRepository(
infraDir: '/project/.project/infrastructure',
fs: fs,
@ -133,6 +133,7 @@ void main() {
expect(result.actions.join('\n'), contains('app_postgres.container'));
expect(result.actions.join('\n'), contains('app_postgres.network'));
expect(result.actions.join('\n'), contains('Containerfile'));
expect(
await fs
.link(
@ -174,6 +175,7 @@ void _writeService(
String serviceId = 'postgres',
String unit = 'app_postgres.service',
bool includeNetwork = false,
bool includeFile = false,
}) {
final serviceDir = fs.directory(
'/project/.project/infrastructure/services/postgres',
@ -188,6 +190,11 @@ void _writeService(
.file('${serviceDir.path}/app_postgres.network')
.writeAsStringSync('[Network]\nNetworkName=app_postgres\n');
}
if (includeFile) {
fs
.file('${serviceDir.path}/Containerfile')
.writeAsStringSync('FROM scratch\n');
}
fs
.file('${serviceDir.path}/configure.schema.json')
.writeAsStringSync('{"type":"object"}');
@ -197,6 +204,13 @@ void _writeService(
final networkQuadlet = includeNetwork
? '''
- file: app_postgres.network
'''
: '';
final files = includeFile
? '''
files:
- Containerfile
'''
: '';
fs.file('${serviceDir.path}/manifest.yaml').writeAsStringSync('''
@ -214,6 +228,7 @@ quadlets:
profiles_dir: app_postgres.profiles.d
$networkQuadlet
$files
schemas:
configure: configure.schema.json
init: init.schema.json
@ -234,6 +249,7 @@ Map<String, Object?> _manifestObject() => {
},
{'file': 'app_postgres.network'},
],
'files': ['Containerfile'],
'schemas': {'configure': 'configure.schema.json', 'init': 'init.schema.json'},
};