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 ContainerName=dew_keycloak-postgresql
Network=dew_keycloak.network Network=dew_keycloak.network
NetworkAlias=postgres 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_DB=keycloak
Environment=POSTGRES_USER=keycloak Environment=POSTGRES_USER=keycloak
Environment=POSTGRES_PASSWORD=keycloak_dev_password 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 \ RUN printf '{"service":"local-api-build","status":"ok"}\n' \
&& printf '{"service":"local-api-build","status":"ok"}\n' > /srv/www/index.html > /usr/share/nginx/html/index.html
EXPOSE 8080 EXPOSE 80
CMD ["busybox", "httpd", "-f", "-p", "8080", "-h", "/srv/www"]

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ Environment=POSTGRES_DB=dew
Environment=POSTGRES_USER=dew Environment=POSTGRES_USER=dew
Environment=POSTGRES_PASSWORD=dew_dev_password Environment=POSTGRES_PASSWORD=dew_dev_password
PublishPort=127.0.0.1:5432:5432 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 HealthCmd=pg_isready -U dew -d dew
HealthInterval=10s HealthInterval=10s
HealthTimeout=5s 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 from the Quadlet filename. Declare `unit` when the Quadlet file uses a
`ServiceName=` override. `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 The package-level schema for this file is
`packages/infra/schemas/service-manifest.schema.json`. `packages/infra/schemas/service-manifest.schema.json`.

View file

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

View file

@ -222,6 +222,12 @@ class PodmanQuadletRuntime implements ContainerRuntime {
await fs.link(target).exists() || await fs.file(target).exists(); await fs.link(target).exists() || await fs.file(target).exists();
if (!exists) return false; 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; 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); 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); return InfraRuntimeResult(actions: actions);
} }
@ -409,6 +426,9 @@ class PodmanQuadletRuntime implements ContainerRuntime {
p.basename(quadlet.file), p.basename(quadlet.file),
); );
String _targetFilePath(String file, InfraScope scope) =>
p.join(quadletSearchPath(scope, environment: environment), file);
Future<void> _link( Future<void> _link(
List<String> actions, List<String> actions,
bool dryRun, bool dryRun,
@ -416,6 +436,7 @@ class PodmanQuadletRuntime implements ContainerRuntime {
String target, String target,
) async { ) async {
await _action(actions, dryRun, 'link $source -> $target', () async { await _action(actions, dryRun, 'link $source -> $target', () async {
await fs.directory(p.dirname(target)).create(recursive: true);
await _deleteIfExists(target); await _deleteIfExists(target);
await fs.link(target).create(source, recursive: true); await fs.link(target).create(source, recursive: true);
}); });

View file

@ -153,6 +153,7 @@ class InfraServiceManifest {
required this.serviceDir, required this.serviceDir,
required this.manifestPath, required this.manifestPath,
required this.quadlets, required this.quadlets,
this.files = const [],
this.configureSchema, this.configureSchema,
this.initSchema, this.initSchema,
}); });
@ -175,6 +176,9 @@ class InfraServiceManifest {
/// Quadlet files deployed for this service. /// Quadlet files deployed for this service.
final List<InfraQuadletManifest> quadlets; 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. /// Optional relative path to the configure JSON Schema.
final String? configureSchema; final String? configureSchema;
@ -198,6 +202,9 @@ class InfraServiceManifest {
/// Active init payload path. /// Active init payload path.
String get activeInitPath => p.join(configDir, 'init.json'); 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. /// Units generated by the declared Quadlet files.
List<String> get units => List<String> get units =>
quadlets.map((quadlet) => quadlet.serviceUnit).toList(); quadlets.map((quadlet) => quadlet.serviceUnit).toList();
@ -231,6 +238,7 @@ class InfraServiceManifest {
serviceDir: serviceDir, serviceDir: serviceDir,
manifestPath: manifestPath, manifestPath: manifestPath,
quadlets: quadlets, quadlets: quadlets,
files: _optionalStringList(map, 'files'),
configureSchema: schemas == null configureSchema: schemas == null
? null ? null
: _optionalString(schemas, 'configure'), : _optionalString(schemas, 'configure'),
@ -246,6 +254,7 @@ class InfraServiceManifest {
'manifest': manifestPath, 'manifest': manifestPath,
'units': units, 'units': units,
'quadlets': quadlets.map((quadlet) => quadlet.toJson()).toList(), 'quadlets': quadlets.map((quadlet) => quadlet.toJson()).toList(),
'files': filePaths,
'configure_schema': configureSchemaPath, 'configure_schema': configureSchemaPath,
'init_schema': initSchemaPath, 'init_schema': initSchemaPath,
'active_config': activeConfigurePath, 'active_config': activeConfigurePath,
@ -311,6 +320,20 @@ List<dynamic> _requiredList(Map<String, dynamic> map, String key) {
throw FormatException('manifest.yaml is missing list "$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) { String _requiredString(Map<String, dynamic> map, String key) {
final value = _optionalString(map, key); final value = _optionalString(map, key);
if (value == null || value.isEmpty) { 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": { "schemas": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,

View file

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