Initial podman package release

This commit is contained in:
Chris Hendrickson 2026-05-01 18:14:53 -04:00
commit 500914cf10
156 changed files with 11875 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.dart_tool/
.packages
build/
coverage/
*.iml

1
.pubignore Normal file
View file

@ -0,0 +1 @@
*.iml

21
CHANGELOG.md Normal file
View file

@ -0,0 +1,21 @@
# Changelog
## Unreleased
- No changes yet.
## 0.1.0
- Initial release scaffold for a libpod API-first Dart package.
- Added Unix-socket HTTP transport.
- Added typed client APIs for system, containers, and images.
- Added focused option/model types and unit tests.
- Added runnable examples.
- Added typed Secrets, Manifests, Artifacts, and Kube/Generate APIs.
- Added system maintenance APIs for disk usage, storage checks, and prune.
- Added container checkpoint/restore APIs with archive export/import helpers.
- Added expanded unit test coverage and local/integration test backend wiring.
- Added practical examples for secrets, manifests, artifacts, kube, maintenance, and checkpoint/restore workflows.
- Added `doc/examples.md` with copy/paste local commands for all examples.
- Added pod/network parity APIs for exists, pod admin controls, top/stats, and prune/update flows.
- Added container admin and archive APIs for kill/pause/unpause/top/init/rename/update/mount/unmount and archive HEAD/GET/PUT.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Chris Hendrickson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

95
README.md Normal file
View file

@ -0,0 +1,95 @@
# podman
A Dart client for the Podman **libpod API** over the Podman socket.
This package uses HTTP over Unix sockets and targets libpod endpoints directly,
without shelling out to the `podman` CLI.
## Features
- Typed `PodmanClient` API for system, container, and image operations
- Network lifecycle and connect/disconnect APIs
- Network exists/update/prune APIs
- Volume lifecycle APIs
- Pod lifecycle APIs
- Pod exists/admin/top/stats/prune APIs
- Container runtime helpers (`wait`, `healthStatus`, `exec`, `stats`)
- Container admin APIs (kill/pause/unpause/top/init/rename/update/mount/archive)
- Container checkpoint/restore APIs (including archive export/import helpers)
- System maintenance APIs (`system/df`, `system/check`, `system/prune`)
- Typed events API with reconnecting watch stream
- Unix-socket transport (`UnixSocketPodmanTransport`)
- Libpod API-first implementation (no Docker-compat fallback path)
- Focused option and model types
- Testable transport abstraction for deterministic unit tests
## Installation
```bash
dart pub add podman
```
## Quick Start
```dart
import 'package:podman/podman.dart';
Future<void> main() async {
final client = PodmanClient();
final version = await client.version();
print('Podman server: ${version.serverVersion}');
await client.close();
}
```
## Socket Resolution
By default, the transport resolves socket path in this order:
1. `PODMAN_SOCKET`
2. `$XDG_RUNTIME_DIR/podman/podman.sock`
3. `/run/user/$UID/podman/podman.sock`
4. `/run/podman/podman.sock`
You can override via:
```dart
final client = PodmanClient(socketPath: '/custom/path/podman.sock');
```
## Examples
- `example/version_info_example.dart`
- `example/list_containers_example.dart`
- `example/pull_and_run_example.dart`
- `example/inspect_container_example.dart`
- `example/secrets_workflow_example.dart`
- `example/manifest_workflow_example.dart`
- `example/artifact_workflow_example.dart`
- `example/generate_assets_example.dart`
- `example/play_kube_file_example.dart`
- `example/system_maintenance_example.dart`
- `example/checkpoint_restore_example.dart`
Copy/paste command guide:
- `doc/examples.md`
## Testing Backends
Default tests use a fake transport for deterministic unit coverage:
```bash
melos run test
```
Local integration tests can run against your local Podman socket:
```bash
melos run test:integration
```
Optional env vars:
- `PODMAN_TEST_SOCKET`: override socket path for local tests
- `PODMAN_LOCAL_TESTS=1`: alternate way to enable local tests

3
dart_test.yaml Normal file
View file

@ -0,0 +1,3 @@
tags:
local:
skip: false

60
doc/examples.md Normal file
View file

@ -0,0 +1,60 @@
# podman examples quickstart
This guide provides copy/paste commands for each example in
`packages/podman/example`.
## Prerequisites
Run from package directory:
```bash
cd /home/artificer/Projects/groupware/packages/podman
```
Ensure Podman API socket is available (optional override):
```bash
export PODMAN_SOCKET="${PODMAN_SOCKET:-$XDG_RUNTIME_DIR/podman/podman.sock}"
```
## Command Matrix
| Example | Purpose | Command |
| --------------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version_info_example.dart` | Show Podman version and host info | `dart run example/version_info_example.dart` |
| `list_containers_example.dart` | List local containers | `dart run example/list_containers_example.dart` |
| `pull_and_run_example.dart` | Pull and run `hello-world` | `dart run example/pull_and_run_example.dart` |
| `inspect_container_example.dart` | Inspect one container | `dart run example/inspect_container_example.dart <container-name-or-id>` |
| `secrets_workflow_example.dart` | Create/inspect/list/remove a secret | `PODMAN_EXAMPLE_SECRET='change-me' dart run example/secrets_workflow_example.dart --name=gw-jwt --replace --show-secret --cleanup` |
| `manifest_workflow_example.dart` | Create/inspect/push/delete a manifest list | `dart run example/manifest_workflow_example.dart groupware-stack quay.io/groupware/api:amd64 quay.io/groupware/api:arm64 --push=quay.io/groupware/api:latest --cleanup` |
| `artifact_workflow_example.dart` | Pull/inspect/list/push/remove OCI artifact | `dart run example/artifact_workflow_example.dart quay.io/groupware/policy:latest --push --cleanup=batch` |
| `generate_assets_example.dart` | Generate kube YAML and systemd units | `dart run example/generate_assets_example.dart groupware-orchestrator --kube-out=./generated/stack.yaml --systemd-dir=./generated/systemd` |
| `play_kube_file_example.dart` | Apply or tear down kube YAML through Podman | `dart run example/play_kube_file_example.dart ./generated/stack.yaml --replace` |
| `system_maintenance_example.dart` | Disk usage + optional check/prune | `dart run example/system_maintenance_example.dart --check --quick` |
| `checkpoint_restore_example.dart` | Checkpoint/export/restore flows | `dart run example/checkpoint_restore_example.dart checkpoint groupware-orchestrator --print-stats` |
## Additional Checkpoint/Restore Commands
```bash
# Export checkpoint archive
dart run example/checkpoint_restore_example.dart export groupware-orchestrator ./generated/orchestrator-checkpoint.tar
# Restore from container checkpoint state
dart run example/checkpoint_restore_example.dart restore groupware-orchestrator --print-stats
# Restore from archive
dart run example/checkpoint_restore_example.dart restore-archive ./generated/orchestrator-checkpoint.tar --name=orchestrator-restored
```
## Safer Maintenance Commands
```bash
# Read-only usage snapshot
dart run example/system_maintenance_example.dart
# Consistency checks only
dart run example/system_maintenance_example.dart --check --quick --max-age=24h
# Destructive prune (requires --yes)
dart run example/system_maintenance_example.dart --prune --all --volumes --yes
```

View file

@ -0,0 +1,129 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty || args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final flags = args
.where((arg) => arg.startsWith('--'))
.toList(growable: false);
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.isEmpty) {
stderr.writeln('Provide an OCI artifact reference to pull.');
_printUsage();
exitCode = 64;
return;
}
final artifactRef = positional.first;
final runPush = flags.contains('--push');
final cleanupMode = _readOption(flags, 'cleanup');
final retry = int.tryParse(_readOption(flags, 'retry') ?? '');
final retryDelay = _readOption(flags, 'retry-delay');
final tlsVerify = _parseOptionalBool(_readOption(flags, 'tls-verify'));
final client = PodmanClient();
try {
final pulled = await client.pullArtifact(
artifactRef,
options: ArtifactPullOptions(
retry: retry,
retryDelay: retryDelay,
tlsVerify: tlsVerify,
),
);
print('Pulled artifact: ${pulled.artifactDigest}');
final details = await client.inspectArtifact(artifactRef);
print('Name: ${details.name}');
print('Digest: ${details.digest}');
print('Manifest keys: ${details.manifest.keys.toList()..sort()}');
final allArtifacts = await client.listArtifacts();
print('Local artifact count: ${allArtifacts.length}');
if (runPush) {
final pushed = await client.pushArtifact(
artifactRef,
options: ArtifactPushOptions(
retry: retry,
retryDelay: retryDelay,
tlsVerify: tlsVerify,
),
);
print('Pushed artifact digest: ${pushed.artifactDigest}');
}
if (cleanupMode == 'single') {
final removed = await client.removeArtifact(
artifactRef,
ignoreMissing: true,
);
print('Removed (single): ${removed.artifactDigests}');
}
if (cleanupMode == 'batch') {
final removed = await client.removeArtifacts(
ArtifactRemoveOptions(artifacts: <String>[artifactRef], ignore: true),
);
print('Removed (batch): ${removed.artifactDigests}');
}
} finally {
await client.close();
}
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
bool? _parseOptionalBool(String? input) {
if (input == null || input.isEmpty) {
return null;
}
final normalized = input.toLowerCase();
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
return true;
}
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
return false;
}
return null;
}
void _printUsage() {
print('''
Artifact workflow example
Usage:
dart run example/artifact_workflow_example.dart <artifact-ref> [options]
Examples:
dart run example/artifact_workflow_example.dart quay.io/groupware/policy:latest
dart run example/artifact_workflow_example.dart quay.io/groupware/policy:latest --cleanup=batch
Options:
--retry=<count> Retry count for pull/push
--retry-delay=<duration> Retry delay (for example: 2s)
--tls-verify=<bool> TLS verification for registry operations
--push Push artifact using its current reference
--cleanup=single|batch Remove pulled artifact after example run
--help, -h Show this help
''');
}

View file

@ -0,0 +1,274 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty || args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final command = args.first;
final remaining = args.sublist(1);
switch (command) {
case 'checkpoint':
await _runCheckpoint(remaining);
return;
case 'export':
await _runExport(remaining);
return;
case 'restore':
await _runRestore(remaining);
return;
case 'restore-archive':
await _runRestoreArchive(remaining);
return;
default:
stderr.writeln('Unknown command: $command');
_printUsage();
exitCode = 64;
return;
}
}
Future<void> _runCheckpoint(List<String> args) async {
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.length != 1) {
stderr.writeln('checkpoint requires exactly one <container> argument.');
_printUsage();
exitCode = 64;
return;
}
final container = positional.first;
final options = _checkpointOptionsFromArgs(args);
final client = PodmanClient();
try {
final report = await client.checkpointContainer(
container,
options: options,
);
print('Checkpoint completed for "$container"');
print(' id: ${report.id}');
print(' runtime duration: ${report.runtimeDuration}');
print(' criu stats keys: ${report.criuStatistics.keys.toList()..sort()}');
} finally {
await client.close();
}
}
Future<void> _runExport(List<String> args) async {
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.length != 2) {
stderr.writeln('export requires <container> <archive-path>.');
_printUsage();
exitCode = 64;
return;
}
final container = positional.first;
final archivePath = positional[1];
final options = _checkpointOptionsFromArgs(args);
final client = PodmanClient();
try {
final archive = await client.exportContainerCheckpoint(
container,
options: options,
);
await File(archivePath).writeAsBytes(archive);
print(
'Exported checkpoint for "$container" to $archivePath (${archive.length} bytes).',
);
} finally {
await client.close();
}
}
Future<void> _runRestore(List<String> args) async {
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.length != 1) {
stderr.writeln('restore requires exactly one <container> argument.');
_printUsage();
exitCode = 64;
return;
}
final container = positional.first;
final options = _restoreOptionsFromArgs(args);
final client = PodmanClient();
try {
final report = await client.restoreContainer(container, options: options);
print('Restore completed for "$container"');
print(' id: ${report.id}');
print(' runtime duration: ${report.runtimeDuration}');
print(' criu stats keys: ${report.criuStatistics.keys.toList()..sort()}');
} finally {
await client.close();
}
}
Future<void> _runRestoreArchive(List<String> args) async {
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.length != 1) {
stderr.writeln(
'restore-archive requires exactly one <archive-path> argument.',
);
_printUsage();
exitCode = 64;
return;
}
final archivePath = positional.first;
final archiveFile = File(archivePath);
if (!await archiveFile.exists()) {
stderr.writeln('Archive file not found: $archivePath');
exitCode = 66;
return;
}
final importName = _readOption(args, 'import-name') ?? 'import';
final options = _restoreOptionsFromArgs(args);
final bytes = await archiveFile.readAsBytes();
final client = PodmanClient();
try {
final report = await client.restoreContainerFromArchive(
bytes,
importName: importName,
options: options,
);
print('Archive restore completed from $archivePath');
print(' id: ${report.id}');
print(' runtime duration: ${report.runtimeDuration}');
print(' criu stats keys: ${report.criuStatistics.keys.toList()..sort()}');
} finally {
await client.close();
}
}
ContainerCheckpointOptions _checkpointOptionsFromArgs(List<String> args) {
return ContainerCheckpointOptions(
keep: args.contains('--keep'),
leaveRunning: args.contains('--leave-running'),
tcpEstablished: args.contains('--tcp-established'),
ignoreRootFs: args.contains('--ignore-rootfs'),
ignoreVolumes: args.contains('--ignore-volumes'),
printStats: args.contains('--print-stats'),
preCheckpoint: args.contains('--pre-checkpoint'),
withPrevious: args.contains('--with-previous'),
fileLocks: args.contains('--file-locks'),
createImage: _readOption(args, 'create-image'),
);
}
ContainerRestoreOptions _restoreOptionsFromArgs(List<String> args) {
final publishPorts = _readMultiOptions(args, 'publish-port');
return ContainerRestoreOptions(
name: _readOption(args, 'name'),
keep: args.contains('--keep'),
tcpEstablished: args.contains('--tcp-established'),
tcpClose: args.contains('--tcp-close'),
ignoreRootFs: args.contains('--ignore-rootfs'),
ignoreVolumes: args.contains('--ignore-volumes'),
ignoreStaticIp: args.contains('--ignore-static-ip'),
ignoreStaticMac: args.contains('--ignore-static-mac'),
printStats: args.contains('--print-stats'),
fileLocks: args.contains('--file-locks'),
publishPorts: publishPorts,
pod: _readOption(args, 'pod'),
);
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
List<String> _readMultiOptions(List<String> args, String name) {
final prefix = '--$name=';
final values = <String>[];
for (final arg in args) {
if (arg.startsWith(prefix)) {
final value = arg.substring(prefix.length);
if (value.isNotEmpty) {
values.add(value);
}
}
}
return values;
}
void _printUsage() {
print('''
Checkpoint/restore example
Usage:
dart run example/checkpoint_restore_example.dart <command> [args] [options]
Commands:
checkpoint <container>
Create an in-place checkpoint and return report metadata.
export <container> <archive-path>
Export checkpoint archive bytes to the provided file path.
restore <container>
Restore a container from an existing checkpoint state/image.
restore-archive <archive-path>
Restore from a checkpoint archive tarball.
Shared options:
--keep
--tcp-established
--print-stats
--file-locks
Checkpoint/export options:
--leave-running
--ignore-rootfs
--ignore-volumes
--pre-checkpoint
--with-previous
--create-image=<name>
Restore/restore-archive options:
--name=<name>
--tcp-close
--ignore-rootfs
--ignore-volumes
--ignore-static-ip
--ignore-static-mac
--pod=<pod-name>
--publish-port=<mapping> (repeatable)
restore-archive-only options:
--import-name=<name> Defaults to "import"
Examples:
dart run example/checkpoint_restore_example.dart checkpoint gw-service --print-stats
dart run example/checkpoint_restore_example.dart export gw-service ./gw-service-checkpoint.tar
dart run example/checkpoint_restore_example.dart restore-archive ./gw-service-checkpoint.tar --name=gw-restored
''');
}

View file

@ -0,0 +1,115 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty || args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final flags = args
.where((arg) => arg.startsWith('--'))
.toList(growable: false);
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.isEmpty) {
stderr.writeln('Provide at least one container or pod name.');
_printUsage();
exitCode = 64;
return;
}
final kubeOutPath = _readOption(flags, 'kube-out');
final systemdDirPath = _readOption(flags, 'systemd-dir');
final systemdTarget =
_readOption(flags, 'systemd-target') ?? positional.first;
final includeService = !flags.contains('--no-service');
final podmanOnly = !flags.contains('--no-podman-only');
final noTrunc = flags.contains('--no-trunc');
final client = PodmanClient();
try {
final kubeYaml = await client.generateKube(
GenerateKubeOptions(
names: positional,
service: includeService,
podmanOnly: podmanOnly,
noTrunc: noTrunc,
),
);
if (kubeOutPath != null && kubeOutPath.isNotEmpty) {
await File(kubeOutPath).writeAsString(kubeYaml);
print('Wrote generated Kubernetes YAML to $kubeOutPath');
} else {
print('--- begin generated kube yaml ---');
print(kubeYaml.trim());
print('--- end generated kube yaml ---');
}
final systemd = await client.generateSystemd(
systemdTarget,
options: const GenerateSystemdOptions(
useName: true,
noHeader: true,
restartPolicy: 'always',
),
);
if (systemd.units.isEmpty) {
print('No systemd units were generated for "$systemdTarget".');
return;
}
if (systemdDirPath != null && systemdDirPath.isNotEmpty) {
final outputDir = Directory(systemdDirPath);
await outputDir.create(recursive: true);
for (final entry in systemd.units.entries) {
final file = File('${outputDir.path}/${entry.key}');
await file.writeAsString(entry.value);
print('Wrote ${entry.key}');
}
} else {
print('Generated systemd units: ${systemd.units.keys.toList()..sort()}');
}
} finally {
await client.close();
}
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
void _printUsage() {
print('''
Generate assets example
Usage:
dart run example/generate_assets_example.dart <name> [name ...] [options]
Examples:
dart run example/generate_assets_example.dart groupware-orchestrator
dart run example/generate_assets_example.dart groupware-pod --kube-out=./stack.yaml --systemd-dir=./units
Options:
--kube-out=<path> Write generated kube YAML to file (default: stdout)
--systemd-dir=<path> Write generated systemd units to directory
--systemd-target=<name> Specific name for systemd generation (defaults to first positional)
--no-service Do not include generated Service objects in kube YAML
--no-podman-only Do not include Podman-only annotations in kube YAML
--no-trunc Use non-truncated annotations
--help, -h Show this help
''');
}

View file

@ -0,0 +1,21 @@
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty) {
print(
'Usage: dart run inspect_container_example.dart <container-name-or-id>',
);
return;
}
final client = PodmanClient();
final details = await client.inspectContainer(args.first);
print('ID: ${details.id}');
print('Name: ${details.name}');
print('Image: ${details.image}');
print('State: ${details.state}');
print('Status: ${details.status}');
await client.close();
}

View file

@ -0,0 +1,21 @@
import 'package:podman/podman.dart';
Future<void> main() async {
final client = PodmanClient();
final containers = await client.listContainers(all: true);
if (containers.isEmpty) {
print('No containers found.');
await client.close();
return;
}
for (final container in containers) {
final shortId = container.id.length > 12
? container.id.substring(0, 12)
: container.id;
print('$shortId ${container.name} ${container.status}');
}
await client.close();
}

View file

@ -0,0 +1,127 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty || args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final flags = args
.where((arg) => arg.startsWith('--'))
.toList(growable: false);
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.length < 2) {
stderr.writeln('Provide a manifest name and at least one image reference.');
_printUsage();
exitCode = 64;
return;
}
final manifestName = positional.first;
final images = positional.sublist(1);
final pushDestination = _readOption(flags, 'push');
final cleanup = flags.contains('--cleanup');
final amend = flags.contains('--amend');
final annotations = _readKeyValueOptions(flags, 'annotation');
if (annotations.isEmpty) {
annotations['org.opencontainers.image.title'] = manifestName;
}
final client = PodmanClient();
try {
final created = await client.createManifest(
manifestName,
options: ManifestCreateOptions(
images: images,
all: true,
amend: amend,
annotations: annotations,
),
);
print('Created/updated manifest "$manifestName" (${created.id}).');
final exists = await client.manifestExists(manifestName);
print('Manifest exists: $exists');
final details = await client.inspectManifest(manifestName);
print('Schema version: ${details.schemaVersion}');
print('Media type: ${details.mediaType}');
print('Embedded manifests: ${details.manifests.length}');
if (pushDestination != null && pushDestination.isNotEmpty) {
final pushed = await client.pushManifest(manifestName, pushDestination);
print('Pushed to $pushDestination (${pushed.id}).');
}
if (cleanup) {
final result = await client.deleteManifest(
manifestName,
ignoreMissing: true,
);
print(
'Deleted entries: ${result.deleted.length}; untagged: ${result.untagged.length}',
);
}
} finally {
await client.close();
}
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
Map<String, String> _readKeyValueOptions(List<String> args, String name) {
final prefix = '--$name=';
final output = <String, String>{};
for (final arg in args) {
if (!arg.startsWith(prefix)) {
continue;
}
final raw = arg.substring(prefix.length);
final index = raw.indexOf('=');
if (index <= 0 || index == raw.length - 1) {
continue;
}
final key = raw.substring(0, index);
final value = raw.substring(index + 1);
output[key] = value;
}
return output;
}
void _printUsage() {
print('''
Manifest workflow example
Usage:
dart run example/manifest_workflow_example.dart <manifest-name> <image> [image ...] [options]
Examples:
dart run example/manifest_workflow_example.dart groupware-stack
quay.io/groupware/api:amd64 quay.io/groupware/api:arm64 --push=quay.io/groupware/api:latest
Options:
--annotation=<k=v> Add top-level annotation (repeatable)
--push=<destination> Push manifest to destination reference
--amend Amend if manifest already exists
--cleanup Delete manifest at end of run
--help, -h Show this help
''');
}

View file

@ -0,0 +1,101 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.isEmpty || args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final flags = args
.where((arg) => arg.startsWith('--'))
.toList(growable: false);
final positional = args
.where((arg) => !arg.startsWith('--'))
.toList(growable: false);
if (positional.isEmpty) {
stderr.writeln('Provide a kube YAML file path.');
_printUsage();
exitCode = 64;
return;
}
final yamlPath = positional.first;
final yamlFile = File(yamlPath);
if (!await yamlFile.exists()) {
stderr.writeln('YAML file not found: $yamlPath');
exitCode = 66;
return;
}
final yaml = await yamlFile.readAsString();
final down = flags.contains('--down');
final force = flags.contains('--force');
final replace = flags.contains('--replace');
final serviceContainer = !flags.contains('--no-service-container');
final networkValues = _readMultiOptions(flags, 'network');
final client = PodmanClient();
try {
final report = down
? await client.playKubeDown(yaml, force: force)
: await client.playKube(
yaml,
options: PlayKubeOptions(
replace: replace,
serviceContainer: serviceContainer,
networks: networkValues,
),
);
print('Operation: ${down ? 'down' : 'up'}');
print('Pods: ${report.pods.isEmpty ? '(none)' : report.pods}');
print('Volumes: ${report.volumes.isEmpty ? '(none)' : report.volumes}');
print('Secrets: ${report.secrets.isEmpty ? '(none)' : report.secrets}');
print(
'Service container: ${report.serviceContainerId.isEmpty ? '(none)' : report.serviceContainerId}',
);
} finally {
await client.close();
}
}
List<String> _readMultiOptions(List<String> args, String name) {
final prefix = '--$name=';
final values = <String>[];
for (final arg in args) {
if (arg.startsWith(prefix)) {
final value = arg.substring(prefix.length);
if (value.isNotEmpty) {
values.add(value);
}
}
}
return values;
}
void _printUsage() {
print('''
Play kube example
Usage:
dart run example/play_kube_file_example.dart <kube-yaml-path> [options]
Examples:
dart run example/play_kube_file_example.dart ./stack.yaml --replace --network=groupware
dart run example/play_kube_file_example.dart ./stack.yaml --down --force
Options:
--down Tear down resources instead of creating them
--force Force delete on down operation
--replace Replace existing resources on up operation
--network=<name> Attach created pods to network (repeatable)
--no-service-container Disable service container on up operation
--help, -h Show this help
''');
}

View file

@ -0,0 +1,19 @@
import 'package:podman/podman.dart';
Future<void> main() async {
final client = PodmanClient();
await client.pull('docker.io/library/hello-world:latest', quiet: true);
final containerId = await client.run(
const RunOptions(
image: 'docker.io/library/hello-world:latest',
name: 'podman_package_example_hello_world',
labels: <String, String>{'example': 'podman-package'},
removeWhenStopped: true,
),
);
print('Started container: $containerId');
await client.close();
}

View file

@ -0,0 +1,159 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final name =
_readOption(args, 'name') ?? (args.isNotEmpty ? args.first : null);
final rawValue = _readOption(args, 'value');
final filePath = _readOption(args, 'file');
final driver = _readOption(args, 'driver');
final showSecret = args.contains('--show-secret');
final cleanup = args.contains('--cleanup');
final replace = args.contains('--replace');
final ignore = args.contains('--ignore');
if (name == null || name.isEmpty) {
stderr.writeln('Missing secret name.');
_printUsage();
exitCode = 64;
return;
}
final value = await _resolveSecretValue(
rawValue: rawValue,
filePath: filePath,
);
if (value == null || value.isEmpty) {
stderr.writeln(
'Provide a secret value via --value, --file, or PODMAN_EXAMPLE_SECRET.',
);
_printUsage();
exitCode = 64;
return;
}
final labels = _readKeyValueOptions(args, 'label');
final driverOptions = _readKeyValueOptions(args, 'driver-opt');
final client = PodmanClient();
try {
final created = await client.createSecret(
SecretCreateOptions(
name: name,
data: value,
driver: driver,
labels: labels,
driverOptions: driverOptions,
replace: replace,
ignore: ignore,
),
);
print('Created/updated secret "$name" with id ${created.id}.');
final exists = await client.secretExists(name);
print('Secret exists: $exists');
final details = await client.inspectSecret(name, showSecret: showSecret);
print('Driver: ${details.driver.isEmpty ? 'unknown' : details.driver}');
print('Labels: ${details.labels.isEmpty ? '(none)' : details.labels}');
print('Updated: ${details.updatedAt?.toIso8601String() ?? 'unknown'}');
if (showSecret) {
print('Secret payload length: ${details.secretData.length}');
}
final allSecrets = await client.listSecrets();
print('Total secrets visible to this user: ${allSecrets.length}');
if (cleanup) {
await client.removeSecret(name, ignoreMissing: true);
print('Removed secret "$name" (--cleanup).');
}
} finally {
await client.close();
}
}
Future<String?> _resolveSecretValue({
required String? rawValue,
required String? filePath,
}) async {
if (rawValue != null) {
return rawValue;
}
if (filePath != null && filePath.isNotEmpty) {
final file = File(filePath);
if (!await file.exists()) {
stderr.writeln('Secret file not found: $filePath');
return null;
}
return file.readAsString();
}
return Platform.environment['PODMAN_EXAMPLE_SECRET'];
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
Map<String, String> _readKeyValueOptions(List<String> args, String name) {
final prefix = '--$name=';
final output = <String, String>{};
for (final arg in args) {
if (!arg.startsWith(prefix)) {
continue;
}
final raw = arg.substring(prefix.length);
final index = raw.indexOf('=');
if (index <= 0 || index == raw.length - 1) {
continue;
}
final key = raw.substring(0, index);
final value = raw.substring(index + 1);
output[key] = value;
}
return output;
}
void _printUsage() {
print('''
Secrets workflow example
Usage:
dart run example/secrets_workflow_example.dart --name=<secret-name> [options]
Options:
--name=<name> Secret name (required)
--value=<plaintext> Secret payload value
--file=<path> Read secret payload from file
--driver=<driver> Optional driver (for example: file, pass)
--label=<k=v> Add label (repeatable)
--driver-opt=<k=v> Add driver option (repeatable)
--replace Replace existing secret with same name
--ignore Ignore creation conflict errors
--show-secret Request and display secret payload length
--cleanup Remove secret at the end of the run
--help, -h Show this help
Environment fallback:
PODMAN_EXAMPLE_SECRET Used when --value/--file are not set
''');
}

View file

@ -0,0 +1,136 @@
import 'dart:io';
import 'package:podman/podman.dart';
Future<void> main(List<String> args) async {
if (args.contains('--help') || args.contains('-h')) {
_printUsage();
return;
}
final runCheck = args.contains('--check');
final runPrune = args.contains('--prune');
final confirmPrune = args.contains('--yes');
final checkOptions = SystemCheckOptions(
quick: args.contains('--quick'),
repair: args.contains('--repair'),
repairLossy: args.contains('--repair-lossy'),
unreferencedLayerMaxAge: _readOption(args, 'max-age'),
);
final pruneOptions = SystemPruneOptions(
all: args.contains('--all'),
volumes: args.contains('--volumes'),
external: args.contains('--external'),
build: args.contains('--build'),
filters: _readFilters(args),
);
final client = PodmanClient();
try {
final df = await client.systemDf();
print('Disk usage snapshot');
print(' Images size: ${df.imagesSize} bytes');
print(' Images: ${df.images.length}');
print(' Containers: ${df.containers.length}');
print(' Volumes: ${df.volumes.length}');
if (runCheck) {
final check = await client.systemCheck(options: checkOptions);
print('System check completed');
print(' Errors detected: ${check.errors}');
print(' Layer findings: ${check.layers.length}');
print(' Image findings: ${check.images.length}');
print(' Container findings: ${check.containers.length}');
print(' Removed images: ${check.removedImages.length}');
print(' Removed containers: ${check.removedContainers.length}');
}
if (runPrune && !confirmPrune) {
stderr.writeln(
'Refusing to run prune without --yes. Add --yes to confirm destructive cleanup.',
);
exitCode = 64;
return;
}
if (runPrune && confirmPrune) {
final prune = await client.systemPrune(options: pruneOptions);
print('System prune completed');
print(' Reclaimed space: ${prune.reclaimedSpace} bytes');
print(' Pruned pods: ${prune.podIds.length}');
print(' Pruned containers: ${prune.containerIds.length}');
print(' Pruned images: ${prune.imageIds.length}');
print(' Pruned networks: ${prune.networkNames.length}');
print(' Pruned volumes: ${prune.volumeIds.length}');
}
} finally {
await client.close();
}
}
String? _readOption(List<String> args, String name) {
final prefix = '--$name=';
for (final arg in args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length);
}
}
return null;
}
Map<String, List<String>> _readFilters(List<String> args) {
final prefix = '--filter=';
final output = <String, List<String>>{};
for (final arg in args) {
if (!arg.startsWith(prefix)) {
continue;
}
final raw = arg.substring(prefix.length);
final index = raw.indexOf('=');
if (index <= 0 || index == raw.length - 1) {
continue;
}
final key = raw.substring(0, index);
final value = raw.substring(index + 1);
output.putIfAbsent(key, () => <String>[]).add(value);
}
return output;
}
void _printUsage() {
print('''
System maintenance example
Usage:
dart run example/system_maintenance_example.dart [options]
Behavior:
- Always prints a system df snapshot
- Optionally runs consistency check and/or prune
Examples:
dart run example/system_maintenance_example.dart --check --quick
dart run example/system_maintenance_example.dart --prune --all --volumes --yes
Options:
--check Run system consistency checks
--quick With --check, skip slower checks
--repair With --check, remove inconsistent images
--repair-lossy With --check, remove inconsistent containers/images
--max-age=<duration> With --check, max age for unreferenced layers
--prune Run system prune (destructive)
--yes Required with --prune to confirm cleanup
--all With --prune, include all unused images
--volumes With --prune, include volumes
--external With --prune, include external data
--build With --prune, include build cache
--filter=<k=v> With --prune, add filter (repeatable)
--help, -h Show this help
''');
}

View file

@ -0,0 +1,15 @@
import 'package:podman/podman.dart';
Future<void> main() async {
final client = PodmanClient();
final version = await client.version();
final info = await client.info();
print('Podman client: ${version.clientVersion ?? 'unknown'}');
print('Podman server: ${version.serverVersion ?? 'unknown'}');
print('Host OS: ${info.hostOs ?? 'unknown'}');
print('Host arch: ${info.hostArch ?? 'unknown'}');
await client.close();
}

103
lib/podman.dart Normal file
View file

@ -0,0 +1,103 @@
/// Dart client library for interacting with the Podman libpod API.
library;
export 'src/client/podman_client.dart';
export 'src/core/http_method.dart';
export 'src/core/podman_api_exception.dart';
export 'src/core/podman_exception.dart';
export 'src/core/podman_parse_exception.dart';
export 'src/core/podman_transport.dart';
export 'src/core/podman_transport_request.dart';
export 'src/core/podman_transport_response.dart';
export 'src/core/unix_socket_podman_transport.dart';
export 'src/models/artifacts/artifact_details.dart';
export 'src/models/artifacts/artifact_add_result.dart';
export 'src/models/artifacts/artifact_pull_result.dart';
export 'src/models/artifacts/artifact_push_result.dart';
export 'src/models/artifacts/artifact_remove_result.dart';
export 'src/models/artifacts/artifact_summary.dart';
export 'src/models/containers/container_archive_get_result.dart';
export 'src/models/containers/container_checkpoint_report.dart';
export 'src/models/containers/container_create_result.dart';
export 'src/models/containers/container_details.dart';
export 'src/models/containers/container_exec_create_result.dart';
export 'src/models/containers/container_exec_inspect_result.dart';
export 'src/models/containers/container_exec_start_result.dart';
export 'src/models/containers/container_health_status.dart';
export 'src/models/containers/container_restore_report.dart';
export 'src/models/containers/container_stats.dart';
export 'src/models/containers/container_summary.dart';
export 'src/models/containers/container_top_report.dart';
export 'src/models/containers/container_wait_result.dart';
export 'src/models/generate_systemd_result.dart';
export 'src/models/image_details.dart';
export 'src/models/image_history_entry.dart';
export 'src/models/image_import_report.dart';
export 'src/models/image_load_report.dart';
export 'src/models/image_push_event.dart';
export 'src/models/image_remove_result.dart';
export 'src/models/image_summary.dart';
export 'src/models/image_tree_report.dart';
export 'src/models/manifests/manifest_create_result.dart';
export 'src/models/manifests/manifest_delete_result.dart';
export 'src/models/manifests/manifest_details.dart';
export 'src/models/manifests/manifest_push_result.dart';
export 'src/models/networks/network_details.dart';
export 'src/models/networks/network_prune_report.dart';
export 'src/models/networks/network_subnet.dart';
export 'src/models/networks/network_summary.dart';
export 'src/models/play_kube_report.dart';
export 'src/models/pods/pod_details.dart';
export 'src/models/pods/pod_prune_report.dart';
export 'src/models/pods/pod_stats_report.dart';
export 'src/models/pods/pod_summary.dart';
export 'src/models/pods/pod_top_report.dart';
export 'src/models/podman_event.dart';
export 'src/models/podman_event_filter.dart';
export 'src/models/podman_info.dart';
export 'src/models/podman_version.dart';
export 'src/models/secrets/secret_create_result.dart';
export 'src/models/secrets/secret_details.dart';
export 'src/models/secrets/secret_summary.dart';
export 'src/models/system/system_check_report.dart';
export 'src/models/system/system_df_container.dart';
export 'src/models/system/system_df_image.dart';
export 'src/models/system/system_df_report.dart';
export 'src/models/system/system_df_volume.dart';
export 'src/models/system/system_prune_report.dart';
export 'src/models/volume_details.dart';
export 'src/models/volume_prune_report.dart';
export 'src/models/volume_summary.dart';
export 'src/options/artifacts/artifact_add_options.dart';
export 'src/options/artifacts/artifact_pull_options.dart';
export 'src/options/artifacts/artifact_push_options.dart';
export 'src/options/artifacts/artifact_remove_options.dart';
export 'src/options/containers/container_checkpoint_options.dart';
export 'src/options/containers/container_restore_options.dart';
export 'src/options/containers/container_top_options.dart';
export 'src/options/containers/container_update_options.dart';
export 'src/options/containers/exec_create_options.dart';
export 'src/options/containers/mount_binding.dart';
export 'src/options/containers/port_binding.dart';
export 'src/options/containers/run_options.dart';
export 'src/options/images/image_export_options.dart';
export 'src/options/images/image_import_options.dart';
export 'src/options/images/image_push_options.dart';
export 'src/options/images/image_remove_options.dart';
export 'src/options/orchestration/generate_kube_options.dart';
export 'src/options/orchestration/generate_systemd_options.dart';
export 'src/options/manifest_create_options.dart';
export 'src/options/networks/network_connect_options.dart';
export 'src/options/networks/network_create_options.dart';
export 'src/options/networks/network_prune_options.dart';
export 'src/options/networks/network_update_options.dart';
export 'src/options/orchestration/play_kube_options.dart';
export 'src/options/pods/pod_create_options.dart';
export 'src/options/pods/pod_stats_options.dart';
export 'src/options/pods/pod_top_options.dart';
export 'src/options/secret_create_options.dart';
export 'src/options/system_check_options.dart';
export 'src/options/system_prune_options.dart';
export 'src/options/volume_create_options.dart';
export 'src/options/volume_list_options.dart';
export 'src/options/volume_prune_options.dart';

View file

@ -0,0 +1,191 @@
part of 'podman_client.dart';
extension PodmanClientArtifactsApi on PodmanClient {
/// Lists local OCI artifacts.
Future<List<ArtifactSummary>> listArtifacts({Duration? timeout}) async {
final payload = await _getList('/artifacts/json', timeout: timeout);
return payload.map(ArtifactSummary.fromJson).toList(growable: false);
}
/// Inspects a local OCI artifact by name or digest.
Future<ArtifactDetails> inspectArtifact(
String name, {
Duration? timeout,
}) async {
final payload = await _getObject(
'/artifacts/${_encodePath(name)}/json',
timeout: timeout,
);
return ArtifactDetails.fromJson(payload);
}
/// Pulls an OCI artifact from a registry.
Future<ArtifactPullResult> pullArtifact(
String name, {
ArtifactPullOptions options = const ArtifactPullOptions(),
Duration? timeout,
}) async {
final query = <String, List<String>>{
'name': <String>[name],
...options.toQueryParameters(),
};
final response = await _send(
method: HttpMethod.post,
path: '/artifacts/pull',
queryParameters: query,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ArtifactPullResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/pull'),
);
}
/// Adds a binary blob as a new or existing OCI artifact entry.
Future<ArtifactAddResult> addArtifact(
String name, {
required String fileName,
required List<int> fileBytes,
ArtifactAddOptions options = const ArtifactAddOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/artifacts/add',
queryParameters: options.toQueryParameters(
name: name,
fileName: fileName,
),
body: fileBytes,
expectedStatusCodes: const <int>{201},
timeout: timeout,
);
return ArtifactAddResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/add'),
);
}
/// Adds a server-local file as an OCI artifact entry.
Future<ArtifactAddResult> addLocalArtifact(
String name, {
required String path,
required String fileName,
ArtifactAddOptions options = const ArtifactAddOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/artifacts/local/add',
queryParameters: options.toQueryParameters(
name: name,
fileName: fileName,
path: path,
),
expectedStatusCodes: const <int>{201},
timeout: timeout,
);
return ArtifactAddResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/local/add'),
);
}
/// Pushes an OCI artifact to a registry.
Future<ArtifactPushResult> pushArtifact(
String name, {
ArtifactPushOptions options = const ArtifactPushOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/artifacts/${_encodePath(name)}/push',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ArtifactPushResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/$name/push'),
);
}
/// Removes a single OCI artifact.
Future<ArtifactRemoveResult> removeArtifact(
String name, {
bool ignoreMissing = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.delete,
path: '/artifacts/${_encodePath(name)}',
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
if (response.bodyText.trim().isEmpty) {
return const ArtifactRemoveResult(
artifactDigests: <String>[],
raw: <String, Object?>{},
);
}
return ArtifactRemoveResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/$name'),
);
}
/// Removes one or more artifacts, or all artifacts.
Future<ArtifactRemoveResult> removeArtifacts(
ArtifactRemoveOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.delete,
path: '/artifacts/remove',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200, 202, 204},
timeout: timeout,
);
if (response.bodyText.trim().isEmpty) {
return const ArtifactRemoveResult(
artifactDigests: <String>[],
raw: <String, Object?>{},
);
}
return ArtifactRemoveResult.fromJson(
_decodeObject(response.bodyText, '/artifacts/remove'),
);
}
/// Extracts artifact content as a tar stream.
Future<Uint8List> extractArtifact(
String name, {
String? title,
String? digest,
bool excludeTitle = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/artifacts/${_encodePath(name)}/extract',
queryParameters: <String, List<String>>{
if (digest != null && digest.trim().isNotEmpty)
'digest': <String>[digest.trim()],
if (title != null && title.trim().isNotEmpty)
'title': <String>[title.trim()],
if (excludeTitle) 'excludetitle': const <String>['true'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return Uint8List.fromList(response.bodyBytes);
}
}

View file

@ -0,0 +1,142 @@
part of '../podman_client.dart';
extension PodmanClientContainerAdminApi on PodmanClient {
/// Sends a signal to a running container.
Future<void> killContainer(
String container, {
String signal = 'KILL',
Duration? timeout,
}) async {
final trimmedSignal = signal.trim();
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/kill',
queryParameters: <String, List<String>>{
if (trimmedSignal.isNotEmpty && trimmedSignal != 'KILL')
'signal': <String>[trimmedSignal],
},
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
/// Pauses all processes in a container.
Future<void> pauseContainer(String container, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/pause',
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
/// Unpauses a paused container.
Future<void> unpauseContainer(String container, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/unpause',
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
/// Lists processes running in a container.
///
/// Streaming top output is intentionally unsupported in this high-level API
/// because this transport currently captures full HTTP bodies.
Future<ContainerTopReport> topContainer(
String container, {
ContainerTopOptions options = const ContainerTopOptions(),
Duration? timeout,
}) async {
final payload = await _getObject(
'/containers/${_encodePath(container)}/top',
queryParameters: options.toQueryParameters(),
timeout: timeout,
);
return ContainerTopReport.fromJson(payload);
}
/// Performs container initialization tasks without starting it.
Future<void> initContainer(String container, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/init',
expectedStatusCodes: const <int>{204, 304},
timeout: timeout,
);
}
/// Renames a container.
Future<void> renameContainer(
String container, {
required String name,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/rename',
queryParameters: <String, List<String>>{
'name': <String>[name],
},
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
/// Updates mutable container configuration values.
///
/// Returns the container ID from Podman's update response when present.
Future<String?> updateContainer(
String container,
ContainerUpdateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/update',
queryParameters: options.toQueryParameters(),
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
final body = response.bodyText.trim();
if (body.isEmpty) {
return null;
}
return body;
}
/// Mounts a container filesystem and returns its host mount path.
Future<String> mountContainer(String container, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/mount',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return response.bodyText.trim();
}
/// Unmounts a mounted container filesystem.
Future<void> unmountContainer(String container, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/unmount',
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
/// Lists mounted containers and their mount paths keyed by container ID.
Future<Map<String, String>> showMountedContainers({Duration? timeout}) async {
final payload = await _getObject(
'/containers/showmounted',
timeout: timeout,
);
return payload.map((key, value) => MapEntry(key, asString(value) ?? ''));
}
}

View file

@ -0,0 +1,77 @@
part of '../podman_client.dart';
extension PodmanClientContainerArchiveApi on PodmanClient {
/// Reads archive metadata headers for a path in a container.
Future<String?> headContainerArchive(
String container, {
required String path,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.head,
path: '/containers/${_encodePath(container)}/archive',
queryParameters: <String, List<String>>{
'path': <String>[path],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return _firstHeaderValue(response.headers, 'X-Docker-Container-Path-Stat');
}
/// Copies a container path as a tar archive.
Future<ContainerArchiveGetResult> getContainerArchive(
String container, {
required String path,
Map<String, String> rename = const <String, String>{},
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/containers/${_encodePath(container)}/archive',
queryParameters: <String, List<String>>{
'path': <String>[path],
if (rename.isNotEmpty) 'rename': <String>[jsonEncode(rename)],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ContainerArchiveGetResult(
archiveBytes: response.bodyBytes,
pathStatHeader: _firstHeaderValue(
response.headers,
'X-Docker-Container-Path-Stat',
),
headers: response.headers,
);
}
/// Extracts a tar archive into a container path.
Future<void> putContainerArchive(
String container, {
required String path,
required List<int> archiveBytes,
bool copyUidGid = true,
bool noOverwriteDirNonDir = false,
Map<String, String> rename = const <String, String>{},
Duration? timeout,
}) async {
await _send(
method: HttpMethod.put,
path: '/containers/${_encodePath(container)}/archive',
queryParameters: <String, List<String>>{
'path': <String>[path],
if (!copyUidGid) 'copyUIDGID': const <String>['false'],
if (noOverwriteDirNonDir)
'noOverwriteDirNonDir': const <String>['true'],
if (rename.isNotEmpty) 'rename': <String>[jsonEncode(rename)],
},
headers: const <String, String>{'content-type': 'application/x-tar'},
body: archiveBytes,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
}

View file

@ -0,0 +1,87 @@
part of '../podman_client.dart';
extension PodmanClientContainerCheckpointApi on PodmanClient {
/// Checkpoints a container and returns the report.
Future<ContainerCheckpointReport> checkpointContainer(
String container, {
ContainerCheckpointOptions options = const ContainerCheckpointOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/checkpoint',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ContainerCheckpointReport.fromJson(
_decodeObject(response.bodyText, '/containers/$container/checkpoint'),
);
}
/// Exports a container checkpoint archive as tar bytes.
Future<List<int>> exportContainerCheckpoint(
String container, {
ContainerCheckpointOptions options = const ContainerCheckpointOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/checkpoint',
queryParameters: options.toQueryParameters(exportArchive: true),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return response.bodyBytes;
}
/// Restores a previously checkpointed container.
Future<ContainerRestoreReport> restoreContainer(
String container, {
ContainerRestoreOptions options = const ContainerRestoreOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/restore',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ContainerRestoreReport.fromJson(
_decodeObject(response.bodyText, '/containers/$container/restore'),
);
}
/// Restores a container from checkpoint archive bytes.
Future<ContainerRestoreReport> restoreContainerFromArchive(
List<int> archiveBytes, {
String importName = 'import',
ContainerRestoreOptions options = const ContainerRestoreOptions(),
Duration? timeout,
}) async {
if (archiveBytes.isEmpty) {
throw ArgumentError.value(
archiveBytes,
'archiveBytes',
'archiveBytes cannot be empty.',
);
}
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(importName)}/restore',
queryParameters: options.toQueryParameters(importArchive: true),
body: archiveBytes,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ContainerRestoreReport.fromJson(
_decodeObject(response.bodyText, '/containers/$importName/restore'),
);
}
}

View file

@ -0,0 +1,242 @@
part of '../podman_client.dart';
extension PodmanClientContainerRuntimeApi on PodmanClient {
/// Waits for container state changes and returns wait status code.
Future<ContainerWaitResult> wait(
String container, {
Iterable<String> conditions = const <String>['stopped'],
Duration? timeout,
}) async {
final conditionValues = conditions
.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toList(growable: false);
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/wait',
queryParameters: <String, List<String>>{
if (conditionValues.isNotEmpty) 'condition': conditionValues,
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ContainerWaitResult.fromBody(response.bodyText);
}
/// Returns container health status (or `none` when no healthcheck exists).
Future<ContainerHealthStatus> healthStatus(
String container, {
Duration? timeout,
}) async {
final details = await inspectContainer(container, timeout: timeout);
return ContainerHealthStatus.fromInspect(details.raw);
}
/// Creates an exec process in a running container.
Future<ContainerExecCreateResult> createExec(
String container,
ExecCreateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/exec',
body: options.toApiBody(),
expectedStatusCodes: const <int>{201},
timeout: timeout,
);
return ContainerExecCreateResult.fromJson(
_decodeObject(response.bodyText, '/containers/$container/exec'),
);
}
/// Starts an exec process and captures output.
Future<ContainerExecStartResult> startExec(
String execId, {
bool detach = false,
bool tty = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/exec/${_encodePath(execId)}/start',
body: <String, Object?>{'Detach': detach, 'Tty': tty},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
if (detach) {
return const ContainerExecStartResult();
}
return _decodeExecOutput(response.bodyBytes);
}
/// Inspects an exec process.
Future<ContainerExecInspectResult> inspectExec(
String execId, {
Duration? timeout,
}) async {
final payload = await _getObject(
'/exec/${_encodePath(execId)}/json',
timeout: timeout,
);
return ContainerExecInspectResult.fromJson(payload);
}
/// Resizes a container TTY.
Future<void> resizeContainerTty(
String container, {
required int width,
required int height,
bool ignoreNotRunning = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/resize',
queryParameters: <String, List<String>>{
'w': <String>['$width'],
'h': <String>['$height'],
if (ignoreNotRunning) 'running': const <String>['true'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Resizes an exec session TTY.
Future<void> resizeExecTty(
String execId, {
required int width,
required int height,
bool ignoreNotRunning = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/exec/${_encodePath(execId)}/resize',
queryParameters: <String, List<String>>{
'w': <String>['$width'],
'h': <String>['$height'],
if (ignoreNotRunning) 'running': const <String>['true'],
},
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
}
/// Returns a single stats snapshot.
///
/// `stream=true` is intentionally unsupported in this high-level API because
/// this transport currently captures full HTTP bodies.
Future<ContainerStats> stats(
String container, {
bool stream = false,
Duration? timeout,
}) async {
if (stream) {
throw ArgumentError.value(
stream,
'stream',
'stream=true is not supported by this client yet.',
);
}
final payload = await _getObject(
'/containers/${_encodePath(container)}/stats',
queryParameters: <String, List<String>>{
'stream': <String>['false'],
},
timeout: timeout,
);
return ContainerStats.fromJson(payload);
}
/// Polls container stats continuously.
Stream<ContainerStats> watchStats(
String container, {
Duration pollInterval = const Duration(seconds: 2),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
Duration? timeout,
}) {
final controller = StreamController<ContainerStats>();
var closed = false;
Future<void> loop() async {
while (!closed) {
try {
final snapshot = await stats(container, timeout: timeout);
if (closed) {
break;
}
controller.add(snapshot);
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
/// Polls container `top` output continuously.
Stream<ContainerTopReport> watchContainerTop(
String container, {
ContainerTopOptions options = const ContainerTopOptions(),
Duration pollInterval = const Duration(seconds: 2),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
Duration? timeout,
}) {
final controller = StreamController<ContainerTopReport>();
var closed = false;
Future<void> loop() async {
while (!closed) {
try {
final report = await topContainer(
container,
options: options,
timeout: timeout,
);
if (closed) {
break;
}
controller.add(report);
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
}

View file

@ -0,0 +1,241 @@
part of '../podman_client.dart';
extension PodmanClientContainerApi on PodmanClient {
/// Lists containers.
Future<List<ContainerSummary>> listContainers({
bool all = true,
Duration? timeout,
}) async {
final payload = await _getList(
'/containers/json',
queryParameters: <String, List<String>>{
'all': <String>['$all'],
},
timeout: timeout,
);
return payload.map(ContainerSummary.fromJson).toList(growable: false);
}
/// Creates a container without starting it.
Future<ContainerCreateResult> createContainer(
RunOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/containers/create',
queryParameters: <String, List<String>>{
if (options.name != null && options.name!.isNotEmpty)
'name': <String>[options.name!],
},
body: options.toCreateBody(),
expectedStatusCodes: const <int>{201},
timeout: timeout,
);
return ContainerCreateResult.fromJson(
_decodeObject(response.bodyText, '/containers/create'),
);
}
/// Creates and starts a container, returning the container ID.
Future<String> run(RunOptions options, {Duration? timeout}) async {
final created = await createContainer(options, timeout: timeout);
await start(created.id, timeout: timeout);
return created.id;
}
/// Inspects a single container.
Future<ContainerDetails> inspectContainer(
String container, {
Duration? timeout,
}) async {
final payload = await _getObject(
'/containers/${_encodePath(container)}/json',
timeout: timeout,
);
return ContainerDetails.fromJson(payload);
}
/// Whether a container exists.
Future<bool> containerExists(String container, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/containers/${_encodePath(container)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Starts a container.
Future<void> start(String container, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/start',
expectedStatusCodes: const <int>{204, 304},
timeout: timeout,
);
}
/// Stops a container.
Future<void> stop(
String container, {
int? timeoutSeconds,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/stop',
queryParameters: <String, List<String>>{
if (timeoutSeconds != null) 't': <String>['$timeoutSeconds'],
},
expectedStatusCodes: const <int>{204, 304},
timeout: timeout,
);
}
/// Restarts a container.
Future<void> restart(
String container, {
int? timeoutSeconds,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/containers/${_encodePath(container)}/restart',
queryParameters: <String, List<String>>{
if (timeoutSeconds != null) 't': <String>['$timeoutSeconds'],
},
expectedStatusCodes: const <int>{204, 304},
timeout: timeout,
);
}
/// Removes a container.
Future<void> removeContainer(
String container, {
bool force = false,
bool removeVolumes = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/containers/${_encodePath(container)}',
queryParameters: <String, List<String>>{
'force': <String>['$force'],
'v': <String>['$removeVolumes'],
},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
/// Fetches container logs.
Future<String> logs(
String container, {
int? tail,
DateTime? since,
DateTime? until,
bool follow = false,
bool timestamps = false,
bool stdout = true,
bool stderr = true,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/containers/${_encodePath(container)}/logs',
queryParameters: <String, List<String>>{
'stdout': <String>['$stdout'],
'stderr': <String>['$stderr'],
if (follow) 'follow': const <String>['true'],
'timestamps': <String>['$timestamps'],
if (since != null) 'since': <String>[since.toUtc().toIso8601String()],
if (until != null) 'until': <String>[until.toUtc().toIso8601String()],
if (tail != null) 'tail': <String>['$tail'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return response.bodyText;
}
/// Polls container logs with `since` cursors.
Stream<String> watchLogs(
String container, {
int? tail,
bool timestamps = true,
bool stdout = true,
bool stderr = true,
DateTime? since,
Duration pollInterval = const Duration(seconds: 2),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
Duration? timeout,
}) {
final controller = StreamController<String>();
var closed = false;
var firstRequest = true;
var cursorSince = since?.toUtc() ?? DateTime.now().toUtc();
Future<void> loop() async {
while (!closed) {
try {
final nextCursor = DateTime.now().toUtc();
final chunk = await logs(
container,
tail: firstRequest ? tail : null,
since: cursorSince,
timestamps: timestamps,
stdout: stdout,
stderr: stderr,
timeout: timeout,
);
firstRequest = false;
cursorSince = nextCursor;
if (closed) {
break;
}
if (chunk.trim().isNotEmpty) {
controller.add(chunk);
}
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
/// Removes stopped containers.
Future<void> pruneContainers({Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/containers/prune',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
}

View file

@ -0,0 +1,95 @@
part of 'podman_client.dart';
extension PodmanClientEventsApi on PodmanClient {
/// Fetches events from `/libpod/events` in non-streaming mode.
Future<List<PodmanEvent>> listEvents({
Iterable<PodmanEventFilter> filters = const <PodmanEventFilter>[],
DateTime? since,
DateTime? until,
bool fromStart = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/events',
queryParameters: <String, List<String>>{
'stream': const <String>['false'],
if (fromStart) 'fromStart': const <String>['true'],
if (since != null)
'since': <String>['${since.millisecondsSinceEpoch ~/ 1000}'],
if (until != null)
'until': <String>['${until.millisecondsSinceEpoch ~/ 1000}'],
if (filters.isNotEmpty)
'filters': filters
.map((item) => item.asQueryValue())
.toList(growable: false),
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
final items = _decodeNdjsonObjects(response.bodyText, '/events');
return items.map(PodmanEvent.fromJson).toList(growable: false);
}
/// Watches events continuously with reconnect support.
Stream<PodmanEvent> watchEvents({
Iterable<PodmanEventFilter> filters = const <PodmanEventFilter>[],
DateTime? since,
bool fromStart = false,
Duration pollInterval = const Duration(seconds: 1),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
}) {
final controller = StreamController<PodmanEvent>();
var closed = false;
var cursorSince = since;
var firstRequest = true;
Future<void> loop() async {
while (!closed) {
try {
final events = await listEvents(
filters: filters,
since: cursorSince,
fromStart: firstRequest && fromStart,
);
firstRequest = false;
for (final event in events) {
if (closed) {
break;
}
controller.add(event);
final eventTime = event.timestamp;
if (eventTime != null) {
cursorSince = eventTime.toUtc();
}
}
if (closed) {
break;
}
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
}

359
lib/src/client/images.dart Normal file
View file

@ -0,0 +1,359 @@
part of 'podman_client.dart';
extension PodmanClientImageApi on PodmanClient {
/// Lists available images.
Future<List<ImageSummary>> listImages({
bool all = false,
Map<String, List<String>> filters = const <String, List<String>>{},
Duration? timeout,
}) async {
final normalizedFilters = _normalizeFilters(filters);
final payload = await _getList(
'/images/json',
queryParameters: <String, List<String>>{
'all': <String>['$all'],
if (normalizedFilters.isNotEmpty)
'filters': <String>[jsonEncode(normalizedFilters)],
},
timeout: timeout,
);
return payload.map(ImageSummary.fromJson).toList(growable: false);
}
/// Inspects a local image by name or ID.
Future<ImageDetails> inspectImage(String image, {Duration? timeout}) async {
final payload = await _getObject(
'/images/${_encodePath(image)}/json',
timeout: timeout,
);
return ImageDetails.fromJson(payload);
}
/// Returns image history entries.
Future<List<ImageHistoryEntry>> imageHistory(
String image, {
Duration? timeout,
}) async {
final payload = await _getList(
'/images/${_encodePath(image)}/history',
timeout: timeout,
);
return payload.map(ImageHistoryEntry.fromJson).toList(growable: false);
}
/// Returns a printable image tree report.
Future<ImageTreeReport> imageTree(
String image, {
bool whatRequires = false,
Duration? timeout,
}) async {
final payload = await _getObject(
'/images/${_encodePath(image)}/tree',
queryParameters: <String, List<String>>{
if (whatRequires) 'whatrequires': const <String>['true'],
},
timeout: timeout,
);
return ImageTreeReport.fromJson(payload);
}
/// Pulls an image and returns the raw API response body.
Future<String> pull(
String image, {
bool quiet = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/images/pull',
queryParameters: <String, List<String>>{
'reference': <String>[image],
'quiet': <String>['$quiet'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return response.bodyText;
}
/// Pushes an image to a registry.
///
/// In quiet mode, Podman may return an empty body.
Future<List<ImagePushEvent>> pushImage(
String image, {
ImagePushOptions options = const ImagePushOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/images/${_encodePath(image)}/push',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
final events = _decodeNdjsonObjects(
response.bodyText,
'/images/$image/push',
);
return events.map(ImagePushEvent.fromJson).toList(growable: false);
}
/// Loads images from a tar archive payload.
Future<ImageLoadReport> loadImages(
List<int> archiveBytes, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/images/load',
body: archiveBytes,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ImageLoadReport.fromJson(
_decodeObject(response.bodyText, '/images/load'),
);
}
/// Loads images from a server-local archive path.
Future<ImageLoadReport> loadImagesFromPath(
String path, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/images/load',
queryParameters: <String, List<String>>{
'path': <String>[path],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ImageLoadReport.fromJson(
_decodeObject(response.bodyText, '/images/load'),
);
}
/// Imports an image from request body bytes or from a URL in [options].
Future<ImageImportReport> importImage({
ImageImportOptions options = const ImageImportOptions(),
List<int>? archiveBytes,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/images/import',
queryParameters: options.toQueryParameters(),
body: archiveBytes,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ImageImportReport.fromJson(
_decodeObject(response.bodyText, '/images/import'),
);
}
/// Exports a single image archive.
Future<Uint8List> exportImage(
String image, {
ImageExportOptions options = const ImageExportOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/images/${_encodePath(image)}/get',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return Uint8List.fromList(response.bodyBytes);
}
/// Exports one or more image references into a single archive.
Future<Uint8List> exportImages(
Iterable<String> references, {
ImageExportOptions options = const ImageExportOptions(),
Duration? timeout,
}) async {
final refs = references
.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toList(growable: false);
if (refs.isEmpty) {
throw ArgumentError.value(
references,
'references',
'At least one image reference is required.',
);
}
if (refs.length > 1 && options.format != 'docker-archive') {
throw ArgumentError.value(
options.format,
'options.format',
'Multi-image export requires format `docker-archive`.',
);
}
final response = await _send(
method: HttpMethod.get,
path: '/images/export',
queryParameters: <String, List<String>>{
...options.toQueryParameters(),
'references': refs,
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return Uint8List.fromList(response.bodyBytes);
}
/// Whether an image exists locally.
Future<bool> imageExists(String image, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/images/${_encodePath(image)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Removes an image.
Future<void> removeImage(
String image, {
bool force = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/images/${_encodePath(image)}',
queryParameters: <String, List<String>>{
'force': <String>['$force'],
},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
/// Adds a tag to an image.
Future<void> tagImage(
String source,
String target, {
Duration? timeout,
}) async {
final parsed = _parseReference(target);
await _send(
method: HttpMethod.post,
path: '/images/${_encodePath(source)}/tag',
queryParameters: <String, List<String>>{
'repo': <String>[parsed.repo],
'tag': <String>[parsed.tag],
},
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
}
/// Removes unused images.
Future<void> pruneImages({bool all = false, Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/images/prune',
queryParameters: <String, List<String>>{
'all': <String>['$all'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Removes one or more images.
Future<ImageRemoveResult> removeImages(
ImageRemoveOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.delete,
path: '/images/remove',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
if (response.bodyText.trim().isEmpty) {
return const ImageRemoveResult(
deleted: <String>[],
untagged: <String>[],
exitCode: 0,
errors: <String>[],
raw: <String, Object?>{},
);
}
return ImageRemoveResult.fromJson(
_decodeObject(response.bodyText, '/images/remove'),
);
}
_ImageReference _parseReference(String reference) {
final digestIndex = reference.indexOf('@');
final withoutDigest = digestIndex >= 0
? reference.substring(0, digestIndex)
: reference;
final slashIndex = withoutDigest.lastIndexOf('/');
final colonIndex = withoutDigest.lastIndexOf(':');
if (colonIndex > slashIndex) {
return _ImageReference(
repo: withoutDigest.substring(0, colonIndex),
tag: withoutDigest.substring(colonIndex + 1),
);
}
return _ImageReference(repo: withoutDigest, tag: 'latest');
}
Map<String, List<String>> _normalizeFilters(Map<String, List<String>> input) {
final sortedKeys = input.keys.toList(growable: false)..sort();
final normalized = <String, List<String>>{};
for (final key in sortedKeys) {
final values = input[key];
if (values == null || values.isEmpty) {
continue;
}
final candidates = values
.map((value) => value.trim())
.where((value) => value.isNotEmpty)
.toList(growable: false);
if (candidates.isNotEmpty) {
normalized[key] = candidates;
}
}
return normalized;
}
}
final class _ImageReference {
const _ImageReference({required this.repo, required this.tag});
final String repo;
final String tag;
}

80
lib/src/client/kube.dart Normal file
View file

@ -0,0 +1,80 @@
part of 'podman_client.dart';
extension PodmanClientKubeApi on PodmanClient {
/// Plays a Kubernetes YAML document.
Future<PlayKubeReport> playKube(
String yaml, {
PlayKubeOptions options = const PlayKubeOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/play/kube',
queryParameters: options.toQueryParameters(),
body: yaml,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return PlayKubeReport.fromJson(
_decodeObject(response.bodyText, '/play/kube'),
);
}
/// Tears down resources defined in a Kubernetes YAML document.
Future<PlayKubeReport> playKubeDown(
String yaml, {
bool force = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.delete,
path: '/play/kube',
queryParameters: <String, List<String>>{
if (force) 'force': const <String>['true'],
},
body: yaml,
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return PlayKubeReport.fromJson(
_decodeObject(response.bodyText, '/play/kube'),
);
}
/// Generates Kubernetes YAML for containers/pods.
Future<String> generateKube(
GenerateKubeOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/generate/kube',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return response.bodyText;
}
/// Generates systemd units for a pod/container.
Future<GenerateSystemdResult> generateSystemd(
String name, {
GenerateSystemdOptions options = const GenerateSystemdOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: '/generate/${_encodePath(name)}/systemd',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return GenerateSystemdResult.fromJson(
_decodeObject(response.bodyText, '/generate/$name/systemd'),
);
}
}

View file

@ -0,0 +1,109 @@
part of 'podman_client.dart';
extension PodmanClientManifestApi on PodmanClient {
/// Creates a manifest list.
Future<ManifestCreateResult> createManifest(
String name, {
ManifestCreateOptions options = const ManifestCreateOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/manifests/${_encodePath(name)}',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
return ManifestCreateResult.fromJson(
_decodeObject(response.bodyText, '/manifests/$name'),
);
}
/// Whether a manifest list exists.
Future<bool> manifestExists(String name, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/manifests/${_encodePath(name)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Inspects a manifest list.
Future<ManifestDetails> inspectManifest(
String name, {
bool? tlsVerify,
Duration? timeout,
}) async {
final payload = await _getObject(
'/manifests/${_encodePath(name)}/json',
queryParameters: <String, List<String>>{
if (tlsVerify != null) 'tlsVerify': <String>['$tlsVerify'],
},
timeout: timeout,
);
return ManifestDetails.fromJson(payload);
}
/// Pushes a manifest list to a registry.
Future<ManifestPushResult> pushManifest(
String name,
String destination, {
bool all = true,
bool? tlsVerify,
bool quiet = true,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path:
'/manifests/${_encodePath(name)}/registry/${_encodePath(destination)}',
queryParameters: <String, List<String>>{
'all': <String>['$all'],
'quiet': <String>['$quiet'],
if (tlsVerify != null) 'tlsVerify': <String>['$tlsVerify'],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return ManifestPushResult.fromJson(
_decodeObject(
response.bodyText,
'/manifests/$name/registry/$destination',
),
);
}
/// Deletes a manifest list.
Future<ManifestDeleteResult> deleteManifest(
String name, {
bool ignoreMissing = false,
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.delete,
path: '/manifests/${_encodePath(name)}',
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
if (response.bodyText.trim().isEmpty) {
return const ManifestDeleteResult(
deleted: <String>[],
untagged: <String>[],
exitCode: 0,
errors: <String>[],
raw: <String, Object?>{},
);
}
return ManifestDeleteResult.fromJson(
_decodeObject(response.bodyText, '/manifests/$name'),
);
}
}

View file

@ -0,0 +1,136 @@
part of 'podman_client.dart';
extension PodmanClientNetworkApi on PodmanClient {
/// Lists available networks.
Future<List<NetworkSummary>> listNetworks({Duration? timeout}) async {
final payload = await _getList('/networks/json', timeout: timeout);
return payload.map(NetworkSummary.fromJson).toList(growable: false);
}
/// Creates a network.
Future<NetworkDetails> createNetwork(
NetworkCreateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/networks/create',
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
return NetworkDetails.fromJson(
_decodeObject(response.bodyText, '/networks/create'),
);
}
/// Inspects a network by name or ID.
Future<NetworkDetails> inspectNetwork(
String network, {
Duration? timeout,
}) async {
final payload = await _getObject(
'/networks/${_encodePath(network)}/json',
timeout: timeout,
);
return NetworkDetails.fromJson(payload);
}
/// Whether a network exists.
Future<bool> networkExists(String network, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/networks/${_encodePath(network)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Updates mutable network settings.
Future<void> updateNetwork(
String network,
NetworkUpdateOptions options, {
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/networks/${_encodePath(network)}/update',
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 204},
timeout: timeout,
);
}
/// Removes a network.
Future<void> removeNetwork(
String network, {
bool force = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/networks/${_encodePath(network)}',
queryParameters: <String, List<String>>{
if (force) 'force': const <String>['true'],
},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
/// Connects a container to a network.
Future<void> connectNetwork(
String network,
NetworkConnectOptions options, {
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/networks/${_encodePath(network)}/connect',
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 204},
timeout: timeout,
);
}
/// Disconnects a container from a network.
Future<void> disconnectNetwork(
String network, {
required String container,
bool force = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/networks/${_encodePath(network)}/disconnect',
body: <String, Object?>{'Container': container, 'Force': force},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
/// Prunes unused networks.
Future<List<NetworkPruneReport>> pruneNetworks({
NetworkPruneOptions options = const NetworkPruneOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/networks/prune',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
final payload = _decodeList(response.bodyText, '/networks/prune');
return payload.map(NetworkPruneReport.fromJson).toList(growable: false);
}
}

View file

@ -0,0 +1,346 @@
library;
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import '../core/http_method.dart';
import '../core/podman_api_exception.dart';
import '../core/podman_parse_exception.dart';
import '../core/podman_transport.dart';
import '../core/podman_transport_request.dart';
import '../core/podman_transport_response.dart';
import '../core/unix_socket_podman_transport.dart';
import '../internal/json_utils.dart';
import '../models/artifacts/artifact_details.dart';
import '../models/artifacts/artifact_add_result.dart';
import '../models/artifacts/artifact_pull_result.dart';
import '../models/artifacts/artifact_push_result.dart';
import '../models/artifacts/artifact_remove_result.dart';
import '../models/artifacts/artifact_summary.dart';
import '../models/containers/container_archive_get_result.dart';
import '../models/containers/container_checkpoint_report.dart';
import '../models/containers/container_create_result.dart';
import '../models/containers/container_details.dart';
import '../models/containers/container_exec_create_result.dart';
import '../models/containers/container_exec_inspect_result.dart';
import '../models/containers/container_exec_start_result.dart';
import '../models/containers/container_health_status.dart';
import '../models/containers/container_restore_report.dart';
import '../models/containers/container_stats.dart';
import '../models/containers/container_summary.dart';
import '../models/containers/container_top_report.dart';
import '../models/containers/container_wait_result.dart';
import '../models/generate_systemd_result.dart';
import '../models/image_details.dart';
import '../models/image_history_entry.dart';
import '../models/image_import_report.dart';
import '../models/image_load_report.dart';
import '../models/image_push_event.dart';
import '../models/image_remove_result.dart';
import '../models/image_summary.dart';
import '../models/image_tree_report.dart';
import '../models/manifests/manifest_create_result.dart';
import '../models/manifests/manifest_delete_result.dart';
import '../models/manifests/manifest_details.dart';
import '../models/manifests/manifest_push_result.dart';
import '../models/networks/network_details.dart';
import '../models/networks/network_prune_report.dart';
import '../models/networks/network_summary.dart';
import '../models/play_kube_report.dart';
import '../models/podman_event.dart';
import '../models/podman_event_filter.dart';
import '../models/podman_info.dart';
import '../models/podman_version.dart';
import '../models/pods/pod_details.dart';
import '../models/pods/pod_prune_report.dart';
import '../models/pods/pod_stats_report.dart';
import '../models/pods/pod_summary.dart';
import '../models/pods/pod_top_report.dart';
import '../models/secrets/secret_create_result.dart';
import '../models/secrets/secret_details.dart';
import '../models/secrets/secret_summary.dart';
import '../models/system/system_check_report.dart';
import '../models/system/system_df_report.dart';
import '../models/system/system_prune_report.dart';
import '../models/volume_details.dart';
import '../models/volume_prune_report.dart';
import '../models/volume_summary.dart';
import '../options/artifacts/artifact_add_options.dart';
import '../options/artifacts/artifact_pull_options.dart';
import '../options/artifacts/artifact_push_options.dart';
import '../options/artifacts/artifact_remove_options.dart';
import '../options/containers/container_checkpoint_options.dart';
import '../options/containers/container_restore_options.dart';
import '../options/containers/container_top_options.dart';
import '../options/containers/container_update_options.dart';
import '../options/containers/exec_create_options.dart';
import '../options/containers/run_options.dart';
import '../options/images/image_export_options.dart';
import '../options/images/image_import_options.dart';
import '../options/images/image_push_options.dart';
import '../options/images/image_remove_options.dart';
import '../options/manifest_create_options.dart';
import '../options/networks/network_connect_options.dart';
import '../options/networks/network_create_options.dart';
import '../options/networks/network_prune_options.dart';
import '../options/networks/network_update_options.dart';
import '../options/orchestration/generate_kube_options.dart';
import '../options/orchestration/generate_systemd_options.dart';
import '../options/orchestration/play_kube_options.dart';
import '../options/pods/pod_create_options.dart';
import '../options/pods/pod_stats_options.dart';
import '../options/pods/pod_top_options.dart';
import '../options/secret_create_options.dart';
import '../options/system_check_options.dart';
import '../options/system_prune_options.dart';
import '../options/volume_create_options.dart';
import '../options/volume_list_options.dart';
import '../options/volume_prune_options.dart';
part 'artifacts.dart';
part 'containers/container_admin.dart';
part 'containers/container_archive.dart';
part 'containers/container_checkpoint.dart';
part 'containers/container_runtime.dart';
part 'containers/containers.dart';
part 'events.dart';
part 'images.dart';
part 'kube.dart';
part 'manifests.dart';
part 'networks.dart';
part 'pods.dart';
part 'secrets.dart';
part 'system.dart';
part 'volumes.dart';
/// High-level Podman libpod API client.
final class PodmanClient {
/// Creates a Podman API client.
PodmanClient({
PodmanTransport? transport,
this.apiVersion = 'v5.0.0',
String? socketPath,
}) : _transport =
transport ?? UnixSocketPodmanTransport(socketPath: socketPath);
final PodmanTransport _transport;
/// API version prefix.
final String apiVersion;
/// Closes underlying transport resources.
Future<void> close() => _transport.close();
Future<PodmanTransportResponse> _send({
required HttpMethod method,
required String path,
Map<String, List<String>> queryParameters = const <String, List<String>>{},
Map<String, String> headers = const <String, String>{},
Object? body,
Set<int> expectedStatusCodes = const <int>{200},
Duration? timeout,
}) async {
final request = PodmanTransportRequest(
method: method,
path: _endpoint(path),
queryParameters: queryParameters,
headers: headers,
body: body,
timeout: timeout,
);
final response = await _transport.send(request);
if (!expectedStatusCodes.contains(response.statusCode)) {
throw PodmanApiException(
method: method,
path: _endpoint(path),
statusCode: response.statusCode,
responseBody: response.bodyText,
);
}
return response;
}
Future<Map<String, Object?>> _getObject(
String path, {
Map<String, List<String>> queryParameters = const <String, List<String>>{},
Set<int> expectedStatusCodes = const <int>{200},
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: path,
queryParameters: queryParameters,
expectedStatusCodes: expectedStatusCodes,
timeout: timeout,
);
return _decodeObject(response.bodyText, path);
}
Future<List<Map<String, Object?>>> _getList(
String path, {
Map<String, List<String>> queryParameters = const <String, List<String>>{},
Set<int> expectedStatusCodes = const <int>{200},
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.get,
path: path,
queryParameters: queryParameters,
expectedStatusCodes: expectedStatusCodes,
timeout: timeout,
);
return _decodeList(response.bodyText, path);
}
Map<String, Object?> _decodeObject(String bodyText, String path) {
final decoded = _decode(bodyText, path);
if (decoded is Map<String, Object?>) {
return decoded;
}
if (decoded is Map) {
return decoded.map((key, value) => MapEntry(key.toString(), value));
}
throw PodmanParseException('Expected JSON object from `$path`.');
}
List<Map<String, Object?>> _decodeList(String bodyText, String path) {
final trimmed = bodyText.trim();
if (trimmed.isEmpty) {
return const <Map<String, Object?>>[];
}
final decoded = _decode(trimmed, path);
if (decoded is List) {
return decoded.map(asJsonMap).toList(growable: false);
}
throw PodmanParseException('Expected JSON array from `$path`.');
}
Object _decode(String bodyText, String path) {
try {
return jsonDecode(bodyText);
} on FormatException catch (error) {
throw PodmanParseException('Invalid JSON from `$path`: ${error.message}');
}
}
String _endpoint(String path) {
final version = apiVersion.startsWith('/')
? apiVersion.substring(1)
: apiVersion;
final normalizedPath = path.startsWith('/') ? path : '/$path';
return '/$version/libpod$normalizedPath';
}
String _encodePath(String value) => Uri.encodeComponent(value);
String? _firstHeaderValue(
Map<String, List<String>> headers,
String headerName,
) {
final normalizedTarget = headerName.toLowerCase();
for (final entry in headers.entries) {
if (entry.key.toLowerCase() != normalizedTarget) {
continue;
}
if (entry.value.isEmpty) {
return null;
}
return entry.value.first;
}
return null;
}
List<Map<String, Object?>> _decodeNdjsonObjects(
String bodyText,
String path,
) {
final trimmed = bodyText.trim();
if (trimmed.isEmpty) {
return const <Map<String, Object?>>[];
}
if (trimmed.startsWith('[')) {
return _decodeList(trimmed, path);
}
if (trimmed.startsWith('{') && !trimmed.contains('\n')) {
return <Map<String, Object?>>[_decodeObject(trimmed, path)];
}
final items = <Map<String, Object?>>[];
for (final line in bodyText.split('\n')) {
final candidate = line.trim();
if (candidate.isEmpty) {
continue;
}
final decoded = _decode(candidate, path);
if (decoded is Map<String, Object?>) {
items.add(decoded);
} else if (decoded is Map) {
items.add(decoded.map((key, value) => MapEntry(key.toString(), value)));
} else {
throw PodmanParseException('Expected NDJSON object line from `$path`.');
}
}
return items;
}
ContainerExecStartResult _decodeExecOutput(List<int> bytes) {
if (bytes.isEmpty) {
return const ContainerExecStartResult();
}
final stdout = BytesBuilder(copy: false);
final stderr = BytesBuilder(copy: false);
var index = 0;
var usedMultiplexFrames = false;
while (index + 8 <= bytes.length) {
final streamType = bytes[index];
final header = ByteData.sublistView(
Uint8List.fromList(bytes),
index + 4,
index + 8,
);
final frameLength = header.getUint32(0, Endian.big);
final payloadStart = index + 8;
final payloadEnd = payloadStart + frameLength;
if (frameLength < 0 || payloadEnd > bytes.length) {
break;
}
final frame = bytes.sublist(payloadStart, payloadEnd);
if (streamType == 1) {
stdout.add(frame);
} else if (streamType == 2) {
stderr.add(frame);
} else {
stdout.add(frame);
}
usedMultiplexFrames = true;
index = payloadEnd;
}
if (!usedMultiplexFrames) {
return ContainerExecStartResult(
stdout: utf8.decode(bytes, allowMalformed: true),
stderr: '',
rawBytes: bytes,
);
}
return ContainerExecStartResult(
stdout: utf8.decode(stdout.takeBytes(), allowMalformed: true),
stderr: utf8.decode(stderr.takeBytes(), allowMalformed: true),
rawBytes: bytes,
);
}
}

269
lib/src/client/pods.dart Normal file
View file

@ -0,0 +1,269 @@
part of 'podman_client.dart';
extension PodmanClientPodApi on PodmanClient {
/// Lists pods.
Future<List<PodSummary>> listPods({Duration? timeout}) async {
final payload = await _getList('/pods/json', timeout: timeout);
return payload.map(PodSummary.fromJson).toList(growable: false);
}
/// Creates a pod.
Future<PodDetails> createPod(
PodCreateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/pods/create',
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
final created = _decodeObject(response.bodyText, '/pods/create');
final id =
asString(created['Id']) ?? asString(created['ID']) ?? options.name;
return inspectPod(id, timeout: timeout);
}
/// Inspects a pod by name or ID.
Future<PodDetails> inspectPod(String pod, {Duration? timeout}) async {
final payload = await _getObject(
'/pods/${_encodePath(pod)}/json',
timeout: timeout,
);
return PodDetails.fromJson(payload);
}
/// Whether a pod exists.
Future<bool> podExists(String pod, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/pods/${_encodePath(pod)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Kills running containers in a pod.
Future<void> killPod(
String pod, {
String signal = 'SIGKILL',
Duration? timeout,
}) async {
final trimmedSignal = signal.trim();
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/kill',
queryParameters: <String, List<String>>{
if (trimmedSignal.isNotEmpty && trimmedSignal != 'SIGKILL')
'signal': <String>[trimmedSignal],
},
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Pauses running containers in a pod.
Future<void> pausePod(String pod, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/pause',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Restarts containers in a pod.
Future<void> restartPod(String pod, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/restart',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Starts a pod.
Future<void> startPod(String pod, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/start',
expectedStatusCodes: const <int>{200, 204, 304},
timeout: timeout,
);
}
/// Stops a pod.
Future<void> stopPod(
String pod, {
int? timeoutSeconds,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/stop',
queryParameters: <String, List<String>>{
if (timeoutSeconds != null) 't': <String>['$timeoutSeconds'],
},
expectedStatusCodes: const <int>{200, 204, 304},
timeout: timeout,
);
}
/// Unpauses containers in a pod.
Future<void> unpausePod(String pod, {Duration? timeout}) async {
await _send(
method: HttpMethod.post,
path: '/pods/${_encodePath(pod)}/unpause',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
}
/// Lists processes running in a pod.
///
/// Streaming top output is intentionally unsupported in this high-level API
/// because this transport currently captures full HTTP bodies.
Future<PodTopReport> topPod(
String pod, {
PodTopOptions options = const PodTopOptions(),
Duration? timeout,
}) async {
final payload = await _getObject(
'/pods/${_encodePath(pod)}/top',
queryParameters: options.toQueryParameters(),
timeout: timeout,
);
return PodTopReport.fromJson(payload);
}
/// Returns pod stats for one or more pods.
///
/// Streaming stats are intentionally unsupported in this high-level API
/// because this transport currently captures full HTTP bodies.
Future<List<PodStatsReport>> podStats({
PodStatsOptions options = const PodStatsOptions(),
Duration? timeout,
}) async {
final payload = await _getList(
'/pods/stats',
queryParameters: options.toQueryParameters(),
timeout: timeout,
);
return payload.map(PodStatsReport.fromJson).toList(growable: false);
}
/// Polls pod stats continuously.
Stream<List<PodStatsReport>> watchPodStats({
PodStatsOptions options = const PodStatsOptions(),
Duration pollInterval = const Duration(seconds: 2),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
Duration? timeout,
}) {
final controller = StreamController<List<PodStatsReport>>();
var closed = false;
Future<void> loop() async {
while (!closed) {
try {
final reports = await podStats(options: options, timeout: timeout);
if (closed) {
break;
}
controller.add(reports);
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
/// Polls pod top output continuously.
Stream<PodTopReport> watchPodTop(
String pod, {
PodTopOptions options = const PodTopOptions(),
Duration pollInterval = const Duration(seconds: 2),
bool reconnect = true,
Duration reconnectDelay = const Duration(seconds: 1),
Duration? timeout,
}) {
final controller = StreamController<PodTopReport>();
var closed = false;
Future<void> loop() async {
while (!closed) {
try {
final report = await topPod(pod, options: options, timeout: timeout);
if (closed) {
break;
}
controller.add(report);
await Future<void>.delayed(pollInterval);
} catch (error, stackTrace) {
if (!reconnect || closed) {
controller.addError(error, stackTrace);
await controller.close();
return;
}
await Future<void>.delayed(reconnectDelay);
}
}
}
controller.onListen = loop;
controller.onCancel = () async {
closed = true;
};
return controller.stream;
}
/// Prunes unused pods.
Future<List<PodPruneReport>> prunePods({Duration? timeout}) async {
final response = await _send(
method: HttpMethod.post,
path: '/pods/prune',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
final payload = _decodeList(response.bodyText, '/pods/prune');
return payload.map(PodPruneReport.fromJson).toList(growable: false);
}
/// Removes a pod.
Future<void> removePod(
String pod, {
bool force = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/pods/${_encodePath(pod)}',
queryParameters: <String, List<String>>{
'force': <String>['$force'],
},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
}

View file

@ -0,0 +1,71 @@
part of 'podman_client.dart';
extension PodmanClientSecretsApi on PodmanClient {
/// Creates a secret.
Future<SecretCreateResult> createSecret(
SecretCreateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/secrets/create',
queryParameters: options.toQueryParameters(),
body: options.data,
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
return SecretCreateResult.fromJson(
_decodeObject(response.bodyText, '/secrets/create'),
);
}
/// Lists secrets.
Future<List<SecretSummary>> listSecrets({Duration? timeout}) async {
final payload = await _getList('/secrets/json', timeout: timeout);
return payload.map(SecretSummary.fromJson).toList(growable: false);
}
/// Inspects a secret.
Future<SecretDetails> inspectSecret(
String secret, {
bool showSecret = false,
Duration? timeout,
}) async {
final payload = await _getObject(
'/secrets/${_encodePath(secret)}/json',
queryParameters: <String, List<String>>{
if (showSecret) 'showsecret': const <String>['true'],
},
timeout: timeout,
);
return SecretDetails.fromJson(payload);
}
/// Whether a secret exists.
Future<bool> secretExists(String secret, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/secrets/${_encodePath(secret)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Removes a secret.
Future<void> removeSecret(
String secret, {
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/secrets/${_encodePath(secret)}',
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
}

View file

@ -0,0 +1,57 @@
part of 'podman_client.dart';
extension PodmanClientSystemApi on PodmanClient {
/// Returns Podman version metadata.
Future<PodmanVersion> version({Duration? timeout}) async {
final object = await _getObject('/version', timeout: timeout);
return PodmanVersion.fromJson(object);
}
/// Returns Podman host/runtime information.
Future<PodmanInfo> info({Duration? timeout}) async {
final object = await _getObject('/info', timeout: timeout);
return PodmanInfo.fromJson(object);
}
/// Returns libpod disk usage details.
Future<SystemDfReport> systemDf({Duration? timeout}) async {
final object = await _getObject('/system/df', timeout: timeout);
return SystemDfReport.fromJson(object);
}
/// Performs storage consistency checks.
Future<SystemCheckReport> systemCheck({
SystemCheckOptions options = const SystemCheckOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/system/check',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return SystemCheckReport.fromJson(
_decodeObject(response.bodyText, '/system/check'),
);
}
/// Prunes unused data from the local podman store.
Future<SystemPruneReport> systemPrune({
SystemPruneOptions options = const SystemPruneOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/system/prune',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return SystemPruneReport.fromJson(
_decodeObject(response.bodyText, '/system/prune'),
);
}
}

121
lib/src/client/volumes.dart Normal file
View file

@ -0,0 +1,121 @@
part of 'podman_client.dart';
extension PodmanClientVolumeApi on PodmanClient {
/// Lists known volumes.
Future<List<VolumeSummary>> listVolumes({
VolumeListOptions options = const VolumeListOptions(),
Duration? timeout,
}) async {
final payload = await _getList(
'/volumes/json',
queryParameters: options.toQueryParameters(),
timeout: timeout,
);
return payload.map(VolumeSummary.fromJson).toList(growable: false);
}
/// Creates a volume.
Future<VolumeDetails> createVolume(
VolumeCreateOptions options, {
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/volumes/create',
body: options.toApiBody(),
expectedStatusCodes: const <int>{200, 201},
timeout: timeout,
);
return VolumeDetails.fromJson(
_decodeObject(response.bodyText, '/volumes/create'),
);
}
/// Inspects a volume.
Future<VolumeDetails> inspectVolume(
String volume, {
Duration? timeout,
}) async {
final payload = await _getObject(
'/volumes/${_encodePath(volume)}/json',
timeout: timeout,
);
return VolumeDetails.fromJson(payload);
}
/// Whether a volume exists.
Future<bool> volumeExists(String volume, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/volumes/${_encodePath(volume)}/exists',
expectedStatusCodes: const <int>{204, 404},
timeout: timeout,
);
return response.statusCode == 204;
}
/// Removes a volume.
Future<void> removeVolume(
String volume, {
bool force = false,
bool ignoreMissing = false,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.delete,
path: '/volumes/${_encodePath(volume)}',
queryParameters: <String, List<String>>{
'force': <String>['$force'],
},
expectedStatusCodes: ignoreMissing
? const <int>{200, 202, 204, 404}
: const <int>{200, 202, 204},
timeout: timeout,
);
}
/// Prunes unused volumes.
Future<List<VolumePruneReport>> pruneVolumes({
VolumePruneOptions options = const VolumePruneOptions(),
Duration? timeout,
}) async {
final response = await _send(
method: HttpMethod.post,
path: '/volumes/prune',
queryParameters: options.toQueryParameters(),
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
final payload = _decodeList(response.bodyText, '/volumes/prune');
return payload.map(VolumePruneReport.fromJson).toList(growable: false);
}
/// Exports volume contents as a tar archive.
Future<Uint8List> exportVolume(String volume, {Duration? timeout}) async {
final response = await _send(
method: HttpMethod.get,
path: '/volumes/${_encodePath(volume)}/export',
expectedStatusCodes: const <int>{200},
timeout: timeout,
);
return Uint8List.fromList(response.bodyBytes);
}
/// Imports volume contents from an uncompressed tar archive.
Future<void> importVolume(
String volume, {
required List<int> archiveBytes,
Duration? timeout,
}) async {
await _send(
method: HttpMethod.post,
path: '/volumes/${_encodePath(volume)}/import',
body: archiveBytes,
expectedStatusCodes: const <int>{204},
timeout: timeout,
);
}
}

View file

@ -0,0 +1,2 @@
/// Supported HTTP methods for Podman transport requests.
enum HttpMethod { get, post, put, patch, delete, head }

View file

@ -0,0 +1,34 @@
import 'http_method.dart';
import 'podman_exception.dart';
/// Exception thrown when Podman API returns an unexpected response.
final class PodmanApiException extends PodmanException {
/// Creates an API exception.
PodmanApiException({
required this.method,
required this.path,
required this.statusCode,
required this.responseBody,
String? message,
}) : super(
message ??
'Unexpected Podman API response for '
'${method.name.toUpperCase()} $path: '
'HTTP $statusCode ${responseBody.trim()}',
);
/// Request method.
final HttpMethod method;
/// Request path.
final String path;
/// HTTP status code.
final int statusCode;
/// Raw response body.
final String responseBody;
/// Whether the response represents a not-found condition.
bool get isNotFound => statusCode == 404;
}

View file

@ -0,0 +1,11 @@
/// Base exception for Podman package failures.
class PodmanException implements Exception {
/// Creates a Podman exception.
const PodmanException(this.message);
/// Error message.
final String message;
@override
String toString() => 'PodmanException: $message';
}

View file

@ -0,0 +1,7 @@
import 'podman_exception.dart';
/// Exception thrown when Podman output cannot be parsed.
final class PodmanParseException extends PodmanException {
/// Creates a parse exception.
const PodmanParseException(super.message);
}

View file

@ -0,0 +1,11 @@
import 'podman_transport_request.dart';
import 'podman_transport_response.dart';
/// Abstraction for sending Podman HTTP API requests.
abstract interface class PodmanTransport {
/// Sends a request to the Podman API.
Future<PodmanTransportResponse> send(PodmanTransportRequest request);
/// Closes any open transport resources.
Future<void> close();
}

View file

@ -0,0 +1,32 @@
import 'http_method.dart';
/// Transport-level request to the Podman API.
final class PodmanTransportRequest {
/// Creates a transport request.
const PodmanTransportRequest({
required this.method,
required this.path,
this.queryParameters = const <String, List<String>>{},
this.headers = const <String, String>{},
this.body,
this.timeout,
});
/// HTTP method.
final HttpMethod method;
/// Absolute request path (for example `/v5.0.0/libpod/version`).
final String path;
/// Query parameters.
final Map<String, List<String>> queryParameters;
/// Request headers.
final Map<String, String> headers;
/// Request body.
final Object? body;
/// Optional request timeout override.
final Duration? timeout;
}

View file

@ -0,0 +1,23 @@
import 'dart:convert';
/// Transport-level response from the Podman API.
final class PodmanTransportResponse {
/// Creates a transport response.
const PodmanTransportResponse({
required this.statusCode,
required this.headers,
required this.bodyBytes,
});
/// HTTP status code.
final int statusCode;
/// Response headers.
final Map<String, List<String>> headers;
/// Raw response body bytes.
final List<int> bodyBytes;
/// Decoded response body as UTF-8 text.
String get bodyText => utf8.decode(bodyBytes);
}

View file

@ -0,0 +1,163 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'http_method.dart';
import 'podman_transport.dart';
import 'podman_transport_request.dart';
import 'podman_transport_response.dart';
/// Unix-socket transport for the Podman API.
final class UnixSocketPodmanTransport implements PodmanTransport {
/// Creates a Unix-socket Podman transport.
UnixSocketPodmanTransport({
String? socketPath,
String authority = 'podman.local',
HttpClient? httpClient,
}) : socketPath = socketPath ?? defaultPodmanSocketPath(),
_authority = authority,
_httpClient = httpClient ?? HttpClient() {
_httpClient.connectionFactory = (Uri _, String? proxyHost, int? proxyPort) {
if (proxyHost != null || proxyPort != null) {}
final address = InternetAddress(
this.socketPath,
type: InternetAddressType.unix,
);
return Socket.startConnect(address, 0);
};
}
/// Path to the Podman Unix socket.
final String socketPath;
final String _authority;
final HttpClient _httpClient;
@override
Future<PodmanTransportResponse> send(PodmanTransportRequest request) {
Future<PodmanTransportResponse> operation() async {
final uri = Uri(
scheme: 'http',
host: _authority,
path: _normalizePath(request.path),
queryParameters: _toUriQuery(request.queryParameters),
);
final httpRequest = await _httpClient.openUrl(
_methodName(request.method),
uri,
);
for (final header in request.headers.entries) {
httpRequest.headers.set(header.key, header.value);
}
final body = request.body;
if (body != null) {
if (body is String) {
httpRequest.headers.contentType ??= ContentType.text;
httpRequest.write(body);
} else if (body is List<int>) {
httpRequest.add(body);
} else {
httpRequest.headers.contentType ??= ContentType.json;
httpRequest.write(jsonEncode(body));
}
}
final httpResponse = await httpRequest.close();
final bytes = await _readAllBytes(httpResponse);
final headers = <String, List<String>>{};
httpResponse.headers.forEach((name, values) {
headers[name] = List<String>.from(values);
});
return PodmanTransportResponse(
statusCode: httpResponse.statusCode,
headers: headers,
bodyBytes: bytes,
);
}
final timeout = request.timeout;
if (timeout == null) {
return operation();
}
return operation().timeout(timeout);
}
@override
Future<void> close() async {
_httpClient.close(force: true);
}
Future<List<int>> _readAllBytes(HttpClientResponse response) async {
final builder = BytesBuilder(copy: false);
await for (final chunk in response) {
builder.add(chunk);
}
return builder.takeBytes();
}
Map<String, dynamic>? _toUriQuery(Map<String, List<String>> queryParameters) {
if (queryParameters.isEmpty) {
return null;
}
final query = <String, dynamic>{};
for (final entry in queryParameters.entries) {
final values = entry.value;
if (values.isEmpty) {
continue;
}
if (values.length == 1) {
query[entry.key] = values.first;
} else {
query[entry.key] = values;
}
}
return query.isEmpty ? null : query;
}
String _normalizePath(String path) {
if (path.startsWith('/')) {
return path;
}
return '/$path';
}
String _methodName(HttpMethod method) {
return switch (method) {
HttpMethod.get => 'GET',
HttpMethod.post => 'POST',
HttpMethod.put => 'PUT',
HttpMethod.patch => 'PATCH',
HttpMethod.delete => 'DELETE',
HttpMethod.head => 'HEAD',
};
}
}
/// Resolves the default Podman Unix socket path from environment.
String defaultPodmanSocketPath() {
final explicit = Platform.environment['PODMAN_SOCKET'];
if (explicit != null && explicit.isNotEmpty) {
return explicit;
}
final xdgRuntimeDir = Platform.environment['XDG_RUNTIME_DIR'];
if (xdgRuntimeDir != null && xdgRuntimeDir.isNotEmpty) {
return '$xdgRuntimeDir/podman/podman.sock';
}
final uid = Platform.environment['UID'];
if (uid != null && uid.isNotEmpty) {
return '/run/user/$uid/podman/podman.sock';
}
return '/run/podman/podman.sock';
}

View file

@ -0,0 +1,82 @@
Map<String, Object?> asJsonMap(Object? value) {
if (value is Map<String, Object?>) {
return value;
}
if (value is Map) {
return value.map(
(key, mappedValue) => MapEntry(key.toString(), mappedValue),
);
}
return const <String, Object?>{};
}
List<Object?> asJsonList(Object? value) {
if (value is List<Object?>) {
return value;
}
if (value is List) {
return value.cast<Object?>();
}
return const <Object?>[];
}
String? asString(Object? value) {
if (value == null) {
return null;
}
if (value is String) {
return value;
}
return value.toString();
}
int? asInt(Object? value) {
if (value == null) {
return null;
}
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.tryParse(value.toString());
}
double? asDouble(Object? value) {
if (value == null) {
return null;
}
if (value is double) {
return value;
}
if (value is num) {
return value.toDouble();
}
return double.tryParse(value.toString());
}
bool? asBool(Object? value) {
if (value == null) {
return null;
}
if (value is bool) {
return value;
}
final normalized = value.toString().toLowerCase();
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
return true;
}
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
return false;
}
return null;
}
Map<String, String> asStringMap(Object? value) {
final map = asJsonMap(value);
return map.map(
(key, mappedValue) => MapEntry(key, asString(mappedValue) ?? ''),
);
}

View file

@ -0,0 +1,24 @@
import '../../internal/json_utils.dart';
/// Result from adding content to an artifact.
final class ArtifactAddResult {
/// Creates artifact add result.
const ArtifactAddResult({required this.artifactDigest, required this.raw});
/// Artifact digest after add.
final String artifactDigest;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactAddResult] from JSON.
factory ArtifactAddResult.fromJson(Map<String, Object?> json) {
return ArtifactAddResult(
artifactDigest:
asString(json['ArtifactDigest']) ??
asString(json['artifactDigest']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,34 @@
import '../../internal/json_utils.dart';
/// Artifact details from `GET /libpod/artifacts/{name}/json`.
final class ArtifactDetails {
/// Creates artifact details.
const ArtifactDetails({
required this.name,
required this.digest,
required this.manifest,
required this.raw,
});
/// Artifact name/reference.
final String name;
/// Artifact digest.
final String digest;
/// Embedded manifest object.
final Map<String, Object?> manifest;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactDetails] from JSON.
factory ArtifactDetails.fromJson(Map<String, Object?> json) {
return ArtifactDetails(
name: asString(json['Name']) ?? asString(json['name']) ?? '',
digest: asString(json['Digest']) ?? asString(json['digest']) ?? '',
manifest: asJsonMap(json['Manifest']),
raw: json,
);
}
}

View file

@ -0,0 +1,24 @@
import '../../internal/json_utils.dart';
/// Result from pulling an artifact.
final class ArtifactPullResult {
/// Creates artifact pull result.
const ArtifactPullResult({required this.artifactDigest, required this.raw});
/// Pulled artifact digest.
final String artifactDigest;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactPullResult] from JSON.
factory ArtifactPullResult.fromJson(Map<String, Object?> json) {
return ArtifactPullResult(
artifactDigest:
asString(json['ArtifactDigest']) ??
asString(json['artifactDigest']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,24 @@
import '../../internal/json_utils.dart';
/// Result from pushing an artifact.
final class ArtifactPushResult {
/// Creates artifact push result.
const ArtifactPushResult({required this.artifactDigest, required this.raw});
/// Pushed artifact digest.
final String artifactDigest;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactPushResult] from JSON.
factory ArtifactPushResult.fromJson(Map<String, Object?> json) {
return ArtifactPushResult(
artifactDigest:
asString(json['ArtifactDigest']) ??
asString(json['artifactDigest']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,26 @@
import '../../internal/json_utils.dart';
/// Result from removing artifacts.
final class ArtifactRemoveResult {
/// Creates artifact remove result.
const ArtifactRemoveResult({
required this.artifactDigests,
required this.raw,
});
/// Removed artifact digests.
final List<String> artifactDigests;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactRemoveResult] from JSON.
factory ArtifactRemoveResult.fromJson(Map<String, Object?> json) {
return ArtifactRemoveResult(
artifactDigests: asJsonList(
json['ArtifactDigests'],
).map(asString).whereType<String>().toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,34 @@
import '../../internal/json_utils.dart';
/// Artifact summary from `GET /libpod/artifacts/json`.
final class ArtifactSummary {
/// Creates artifact summary.
const ArtifactSummary({
required this.name,
required this.digest,
required this.manifest,
required this.raw,
});
/// Artifact name/reference.
final String name;
/// Artifact digest.
final String digest;
/// Embedded manifest object.
final Map<String, Object?> manifest;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ArtifactSummary] from JSON.
factory ArtifactSummary.fromJson(Map<String, Object?> json) {
return ArtifactSummary(
name: asString(json['Name']) ?? asString(json['name']) ?? '',
digest: asString(json['Digest']) ?? asString(json['digest']) ?? '',
manifest: asJsonMap(json['Manifest']),
raw: json,
);
}
}

View file

@ -0,0 +1,18 @@
/// Result of copying a container path as a tar archive.
final class ContainerArchiveGetResult {
/// Creates an archive get result.
const ContainerArchiveGetResult({
required this.archiveBytes,
required this.pathStatHeader,
required this.headers,
});
/// Tar archive bytes.
final List<int> archiveBytes;
/// `X-Docker-Container-Path-Stat` header value when present.
final String? pathStatHeader;
/// Raw response headers.
final Map<String, List<String>> headers;
}

View file

@ -0,0 +1,34 @@
import '../../internal/json_utils.dart';
/// Report from `POST /libpod/containers/{name}/checkpoint`.
final class ContainerCheckpointReport {
/// Creates a container checkpoint report.
const ContainerCheckpointReport({
required this.id,
required this.runtimeDuration,
required this.criuStatistics,
required this.raw,
});
/// Container ID.
final String id;
/// Runtime checkpoint duration.
final int runtimeDuration;
/// CRIU statistics, when included.
final Map<String, Object?> criuStatistics;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerCheckpointReport] from JSON.
factory ContainerCheckpointReport.fromJson(Map<String, Object?> json) {
return ContainerCheckpointReport(
id: asString(json['Id']) ?? '',
runtimeDuration: asInt(json['runtime_checkpoint_duration']) ?? 0,
criuStatistics: asJsonMap(json['criu_statistics']),
raw: json,
);
}
}

View file

@ -0,0 +1,32 @@
import '../../internal/json_utils.dart';
/// Result from `POST /containers/create`.
final class ContainerCreateResult {
/// Creates a container create result.
const ContainerCreateResult({
required this.id,
required this.warnings,
required this.raw,
});
/// Created container ID.
final String id;
/// Podman warning messages.
final List<String> warnings;
/// Raw parsed JSON payload.
final Map<String, Object?> raw;
/// Builds [ContainerCreateResult] from API JSON payload.
factory ContainerCreateResult.fromJson(Map<String, Object?> json) {
return ContainerCreateResult(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
warnings: asJsonList(json['Warnings'])
.map((value) => asString(value) ?? '')
.where((value) => value.isNotEmpty)
.toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,71 @@
import '../../internal/json_utils.dart';
/// Detailed container data parsed from `GET /libpod/containers/{id}/json`.
final class ContainerDetails {
/// Creates container details.
const ContainerDetails({
required this.id,
required this.name,
required this.image,
required this.state,
required this.status,
required this.createdAt,
required this.labels,
required this.raw,
});
/// Container ID.
final String id;
/// Container name.
final String name;
/// Image reference/name.
final String image;
/// State string.
final String state;
/// Human-readable status.
final String status;
/// Creation timestamp.
final DateTime? createdAt;
/// Labels.
final Map<String, String> labels;
/// Raw inspect payload.
final Map<String, Object?> raw;
/// Builds [ContainerDetails] from inspect JSON payload.
factory ContainerDetails.fromJson(Map<String, Object?> json) {
final stateMap = asJsonMap(json['State']);
final configMap = asJsonMap(json['Config']);
final rawName = asString(json['Name']) ?? '';
final normalizedName = rawName.startsWith('/')
? rawName.substring(1)
: rawName;
return ContainerDetails(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
name: normalizedName,
image:
asString(json['ImageName']) ??
asString(configMap['Image']) ??
asString(json['Image']) ??
'',
state:
asString(stateMap['Status']) ?? asString(json['State']) ?? 'unknown',
status:
asString(stateMap['Status']) ??
asString(stateMap['State']) ??
asString(json['Status']) ??
'',
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
labels: asStringMap(configMap['Labels']),
raw: json,
);
}
}

View file

@ -0,0 +1,21 @@
import '../../internal/json_utils.dart';
/// Exec creation result from `POST /libpod/containers/{id}/exec`.
final class ContainerExecCreateResult {
/// Creates exec-create result.
const ContainerExecCreateResult({required this.id, required this.raw});
/// Exec ID.
final String id;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerExecCreateResult] from JSON.
factory ContainerExecCreateResult.fromJson(Map<String, Object?> json) {
return ContainerExecCreateResult(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,49 @@
import '../../internal/json_utils.dart';
/// Exec inspect result from `GET /libpod/exec/{id}/json`.
final class ContainerExecInspectResult {
/// Creates exec inspect result.
const ContainerExecInspectResult({
required this.id,
required this.containerId,
required this.running,
required this.exitCode,
required this.openStdout,
required this.openStderr,
required this.raw,
});
/// Exec ID.
final String id;
/// Container ID the exec belongs to.
final String containerId;
/// Whether exec process is still running.
final bool running;
/// Exit code when available.
final int? exitCode;
/// Whether stdout was attached.
final bool openStdout;
/// Whether stderr was attached.
final bool openStderr;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerExecInspectResult] from JSON.
factory ContainerExecInspectResult.fromJson(Map<String, Object?> json) {
return ContainerExecInspectResult(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
containerId: asString(json['ContainerID']) ?? '',
running: asBool(json['Running']) ?? false,
exitCode: asInt(json['ExitCode']),
openStdout: asBool(json['OpenStdout']) ?? false,
openStderr: asBool(json['OpenStderr']) ?? false,
raw: json,
);
}
}

View file

@ -0,0 +1,21 @@
/// Captured output from `POST /libpod/exec/{id}/start`.
final class ContainerExecStartResult {
/// Creates exec-start result.
const ContainerExecStartResult({
this.stdout = '',
this.stderr = '',
this.rawBytes = const <int>[],
});
/// Captured stdout text.
final String stdout;
/// Captured stderr text.
final String stderr;
/// Raw response payload bytes.
final List<int> rawBytes;
/// Combined stdout+stderr output.
String get output => '$stdout$stderr';
}

View file

@ -0,0 +1,40 @@
import '../../internal/json_utils.dart';
/// Container health status from inspect payload.
final class ContainerHealthStatus {
/// Creates health status.
const ContainerHealthStatus({
required this.status,
required this.failingStreak,
required this.raw,
});
/// Health status (`healthy`, `unhealthy`, `starting`, or `none`).
final String status;
/// Consecutive failing checks.
final int failingStreak;
/// Raw health-check object from inspect payload.
final Map<String, Object?> raw;
/// Builds [ContainerHealthStatus] from inspect JSON.
factory ContainerHealthStatus.fromInspect(Map<String, Object?> inspectJson) {
final state = asJsonMap(inspectJson['State']);
final healthcheck = asJsonMap(state['Healthcheck']);
if (healthcheck.isEmpty) {
return const ContainerHealthStatus(
status: 'none',
failingStreak: 0,
raw: <String, Object?>{},
);
}
return ContainerHealthStatus(
status: asString(healthcheck['Status']) ?? 'unknown',
failingStreak: asInt(healthcheck['FailingStreak']) ?? 0,
raw: healthcheck,
);
}
}

View file

@ -0,0 +1,34 @@
import '../../internal/json_utils.dart';
/// Report from `POST /libpod/containers/{name}/restore`.
final class ContainerRestoreReport {
/// Creates a container restore report.
const ContainerRestoreReport({
required this.id,
required this.runtimeDuration,
required this.criuStatistics,
required this.raw,
});
/// Restored container ID.
final String id;
/// Runtime restore duration.
final int runtimeDuration;
/// CRIU statistics, when included.
final Map<String, Object?> criuStatistics;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerRestoreReport] from JSON.
factory ContainerRestoreReport.fromJson(Map<String, Object?> json) {
return ContainerRestoreReport(
id: asString(json['Id']) ?? '',
runtimeDuration: asInt(json['runtime_restore_duration']) ?? 0,
criuStatistics: asJsonMap(json['criu_statistics']),
raw: json,
);
}
}

View file

@ -0,0 +1,59 @@
import '../../internal/json_utils.dart';
/// Snapshot stats from `GET /libpod/containers/{id}/stats?stream=false`.
final class ContainerStats {
/// Creates container stats.
const ContainerStats({
required this.id,
required this.name,
required this.readAt,
required this.cpuTotalUsage,
required this.memoryUsage,
required this.memoryLimit,
required this.pidsCurrent,
required this.raw,
});
/// Container ID.
final String id;
/// Container name.
final String name;
/// Read timestamp.
final DateTime? readAt;
/// Total CPU usage (nanoseconds).
final int cpuTotalUsage;
/// Memory usage in bytes.
final int memoryUsage;
/// Memory limit in bytes.
final int memoryLimit;
/// Current PID count.
final int pidsCurrent;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerStats] from JSON.
factory ContainerStats.fromJson(Map<String, Object?> json) {
final cpuStats = asJsonMap(json['cpu_stats']);
final cpuUsage = asJsonMap(cpuStats['cpu_usage']);
final memoryStats = asJsonMap(json['memory_stats']);
final pidsStats = asJsonMap(json['pids_stats']);
return ContainerStats(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
name: asString(json['name']) ?? asString(json['Name']) ?? '',
readAt: DateTime.tryParse(asString(json['read']) ?? ''),
cpuTotalUsage: asInt(cpuUsage['total_usage']) ?? 0,
memoryUsage: asInt(memoryStats['usage']) ?? 0,
memoryLimit: asInt(memoryStats['limit']) ?? 0,
pidsCurrent: asInt(pidsStats['current']) ?? 0,
raw: json,
);
}
}

View file

@ -0,0 +1,90 @@
import '../../internal/json_utils.dart';
/// Lightweight container data from `GET /libpod/containers/json`.
final class ContainerSummary {
/// Creates a container summary.
const ContainerSummary({
required this.id,
required this.image,
required this.name,
required this.state,
required this.status,
required this.command,
required this.createdAt,
required this.labels,
required this.raw,
});
/// Container ID.
final String id;
/// Container image reference.
final String image;
/// Container name.
final String name;
/// Container state.
final String state;
/// Human-readable status.
final String status;
/// Container command.
final String command;
/// Creation timestamp if available.
final DateTime? createdAt;
/// Container labels.
final Map<String, String> labels;
/// Raw parsed JSON payload.
final Map<String, Object?> raw;
/// Builds [ContainerSummary] from API JSON output.
factory ContainerSummary.fromJson(Map<String, Object?> json) {
return ContainerSummary(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
image: asString(json['Image']) ?? '',
name: _parseName(json['Names'], json['Name']),
state: asString(json['State']) ?? 'unknown',
status: asString(json['Status']) ?? '',
command: asString(json['Command']) ?? '',
createdAt: DateTime.tryParse(asString(json['CreatedAt']) ?? ''),
labels: _parseLabels(json['Labels']),
raw: json,
);
}
static String _parseName(Object? namesValue, Object? nameValue) {
final names = asJsonList(namesValue);
if (names.isNotEmpty) {
return asString(names.first) ?? '';
}
final fromNames = asString(namesValue);
if (fromNames != null && fromNames.isNotEmpty) {
return fromNames;
}
return asString(nameValue) ?? '';
}
static Map<String, String> _parseLabels(Object? labelsValue) {
if (labelsValue is String && labelsValue.isNotEmpty) {
final map = <String, String>{};
final pairs = labelsValue.split(',');
for (final pair in pairs) {
final index = pair.indexOf('=');
if (index <= 0) {
continue;
}
map[pair.substring(0, index)] = pair.substring(index + 1);
}
return map;
}
return asStringMap(labelsValue);
}
}

View file

@ -0,0 +1,37 @@
import '../../internal/json_utils.dart';
/// Process listing from `GET /libpod/containers/{name}/top`.
final class ContainerTopReport {
/// Creates a container top report.
const ContainerTopReport({
required this.titles,
required this.processes,
required this.raw,
});
/// Column titles.
final List<String> titles;
/// Process rows.
final List<List<String>> processes;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ContainerTopReport] from JSON.
factory ContainerTopReport.fromJson(Map<String, Object?> json) {
final titles = asJsonList(
json['Titles'] ?? json['titles'],
).map((value) => asString(value) ?? '').toList(growable: false);
final processes = asJsonList(json['Processes'] ?? json['processes'])
.map(
(row) => asJsonList(
row,
).map((value) => asString(value) ?? '').toList(growable: false),
)
.toList(growable: false);
return ContainerTopReport(titles: titles, processes: processes, raw: json);
}
}

View file

@ -0,0 +1,22 @@
/// Wait response parsed from `POST /libpod/containers/{id}/wait`.
final class ContainerWaitResult {
/// Creates wait result.
const ContainerWaitResult({required this.statusCode, required this.rawBody});
/// Exit/status code value returned by Podman wait API.
final int statusCode;
/// Raw response body.
final String rawBody;
/// Parses a wait result from raw response body.
factory ContainerWaitResult.fromBody(String body) {
final trimmed = body.trim();
final direct = int.tryParse(trimmed);
if (direct != null) {
return ContainerWaitResult(statusCode: direct, rawBody: body);
}
return ContainerWaitResult(statusCode: -1, rawBody: body);
}
}

View file

@ -0,0 +1,18 @@
import '../internal/json_utils.dart';
/// Result from `GET /libpod/generate/{name}/systemd`.
final class GenerateSystemdResult {
/// Creates generate-systemd result.
const GenerateSystemdResult({required this.units, required this.raw});
/// Generated unit file text by filename.
final Map<String, String> units;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [GenerateSystemdResult] from JSON.
factory GenerateSystemdResult.fromJson(Map<String, Object?> json) {
return GenerateSystemdResult(units: asStringMap(json), raw: json);
}
}

View file

@ -0,0 +1,53 @@
import '../internal/json_utils.dart';
/// Detailed image data from `GET /libpod/images/{name}/json`.
final class ImageDetails {
/// Creates image details.
const ImageDetails({
required this.id,
required this.digest,
required this.repoTags,
required this.repoDigests,
required this.createdAt,
required this.size,
required this.raw,
});
/// Image ID.
final String id;
/// Image digest.
final String digest;
/// Known image tags.
final List<String> repoTags;
/// Known image digests.
final List<String> repoDigests;
/// Creation timestamp if present.
final DateTime? createdAt;
/// Image size in bytes.
final int size;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageDetails] from JSON.
factory ImageDetails.fromJson(Map<String, Object?> json) {
return ImageDetails(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
digest: asString(json['Digest']) ?? asString(json['digest']) ?? '',
repoTags: asJsonList(
json['RepoTags'],
).map(asString).whereType<String>().toList(growable: false),
repoDigests: asJsonList(
json['RepoDigests'],
).map(asString).whereType<String>().toList(growable: false),
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
size: asInt(json['Size']) ?? asInt(json['size']) ?? 0,
raw: json,
);
}
}

View file

@ -0,0 +1,57 @@
import '../internal/json_utils.dart';
/// Image history entry from `GET /libpod/images/{name}/history`.
final class ImageHistoryEntry {
/// Creates image history entry.
const ImageHistoryEntry({
required this.id,
required this.createdAt,
required this.createdBy,
required this.tags,
required this.size,
required this.comment,
required this.raw,
});
/// Layer ID.
final String id;
/// Layer creation timestamp.
final DateTime? createdAt;
/// Layer creator command.
final String createdBy;
/// Layer tags.
final List<String> tags;
/// Layer size.
final int size;
/// Layer comment.
final String comment;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageHistoryEntry] from JSON.
factory ImageHistoryEntry.fromJson(Map<String, Object?> json) {
final createdUnix = asInt(json['Created']);
final createdAt = createdUnix == null
? DateTime.tryParse(asString(json['created']) ?? '')
: DateTime.fromMillisecondsSinceEpoch(createdUnix * 1000, isUtc: true);
return ImageHistoryEntry(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
createdAt: createdAt,
createdBy:
asString(json['CreatedBy']) ?? asString(json['createdBy']) ?? '',
tags: asJsonList(
json['Tags'],
).map(asString).whereType<String>().toList(growable: false),
size: asInt(json['Size']) ?? asInt(json['size']) ?? 0,
comment: asString(json['Comment']) ?? asString(json['comment']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,21 @@
import '../internal/json_utils.dart';
/// Result from `POST /libpod/images/import`.
final class ImageImportReport {
/// Creates image import report.
const ImageImportReport({required this.id, required this.raw});
/// Imported image ID.
final String id;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageImportReport] from JSON.
factory ImageImportReport.fromJson(Map<String, Object?> json) {
return ImageImportReport(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,23 @@
import '../internal/json_utils.dart';
/// Result from `POST /libpod/images/load`.
final class ImageLoadReport {
/// Creates image load report.
const ImageLoadReport({required this.names, required this.raw});
/// Loaded image names.
final List<String> names;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageLoadReport] from JSON.
factory ImageLoadReport.fromJson(Map<String, Object?> json) {
return ImageLoadReport(
names: asJsonList(
json['Names'],
).map(asString).whereType<String>().toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,37 @@
import '../internal/json_utils.dart';
/// Push stream event from `POST /libpod/images/{name}/push`.
final class ImagePushEvent {
/// Creates image push event.
const ImagePushEvent({
required this.stream,
required this.manifestDigest,
required this.error,
required this.raw,
});
/// Stream output line.
final String stream;
/// Manifest digest, when provided.
final String manifestDigest;
/// Error text, when provided.
final String error;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImagePushEvent] from JSON.
factory ImagePushEvent.fromJson(Map<String, Object?> json) {
return ImagePushEvent(
stream: asString(json['stream']) ?? asString(json['Stream']) ?? '',
manifestDigest:
asString(json['manifestdigest']) ??
asString(json['ManifestDigest']) ??
'',
error: asString(json['error']) ?? asString(json['Error']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,45 @@
import '../internal/json_utils.dart';
/// Image removal result from libpod image remove endpoints.
final class ImageRemoveResult {
/// Creates image remove result.
const ImageRemoveResult({
required this.deleted,
required this.untagged,
required this.exitCode,
required this.errors,
required this.raw,
});
/// Deleted image IDs.
final List<String> deleted;
/// Untagged image references.
final List<String> untagged;
/// Command-style exit code from Podman.
final int exitCode;
/// Removal errors.
final List<String> errors;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageRemoveResult] from JSON.
factory ImageRemoveResult.fromJson(Map<String, Object?> json) {
return ImageRemoveResult(
deleted: asJsonList(
json['Deleted'],
).map(asString).whereType<String>().toList(growable: false),
untagged: asJsonList(
json['Untagged'],
).map(asString).whereType<String>().toList(growable: false),
exitCode: asInt(json['ExitCode']) ?? asInt(json['exitCode']) ?? 0,
errors: asJsonList(
json['Errors'],
).map(asString).whereType<String>().toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,60 @@
import '../internal/json_utils.dart';
/// Image data from `GET /libpod/images/json`.
final class ImageSummary {
/// Creates image summary.
const ImageSummary({
required this.id,
required this.repository,
required this.tag,
required this.digest,
required this.createdAt,
required this.size,
required this.raw,
});
/// Image ID.
final String id;
/// Repository name.
final String repository;
/// Image tag.
final String tag;
/// Digest string.
final String digest;
/// Created timestamp if provided.
final DateTime? createdAt;
/// Human-readable size string.
final String size;
/// Raw parsed JSON payload.
final Map<String, Object?> raw;
/// Full repository reference.
String get reference {
if (repository.isEmpty) {
return id;
}
if (tag.isEmpty) {
return repository;
}
return '$repository:$tag';
}
/// Builds [ImageSummary] from API JSON output.
factory ImageSummary.fromJson(Map<String, Object?> json) {
return ImageSummary(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
repository: asString(json['Repository']) ?? '',
tag: asString(json['Tag']) ?? '',
digest: asString(json['Digest']) ?? '',
createdAt: DateTime.tryParse(asString(json['CreatedAt']) ?? ''),
size: asString(json['Size']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,21 @@
import '../internal/json_utils.dart';
/// Image tree report from `GET /libpod/images/{name}/tree`.
final class ImageTreeReport {
/// Creates image tree report.
const ImageTreeReport({required this.tree, required this.raw});
/// Printable tree representation.
final String tree;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ImageTreeReport] from JSON.
factory ImageTreeReport.fromJson(Map<String, Object?> json) {
return ImageTreeReport(
tree: asString(json['Tree']) ?? asString(json['tree']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,25 @@
import '../../internal/json_utils.dart';
/// Manifest create result from `POST /libpod/manifests/{name}`.
final class ManifestCreateResult {
/// Creates manifest create result.
const ManifestCreateResult({required this.id, required this.raw});
/// Manifest/image ID.
final String id;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ManifestCreateResult] from JSON.
factory ManifestCreateResult.fromJson(Map<String, Object?> json) {
return ManifestCreateResult(
id:
asString(json['Id']) ??
asString(json['ID']) ??
asString(json['id']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,45 @@
import '../../internal/json_utils.dart';
/// Result from deleting a manifest list.
final class ManifestDeleteResult {
/// Creates manifest delete result.
const ManifestDeleteResult({
required this.deleted,
required this.untagged,
required this.exitCode,
required this.errors,
required this.raw,
});
/// Deleted object IDs.
final List<String> deleted;
/// Untagged references.
final List<String> untagged;
/// Command-style exit code.
final int exitCode;
/// Error messages.
final List<String> errors;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ManifestDeleteResult] from JSON.
factory ManifestDeleteResult.fromJson(Map<String, Object?> json) {
return ManifestDeleteResult(
deleted: asJsonList(
json['Deleted'],
).map(asString).whereType<String>().toList(growable: false),
untagged: asJsonList(
json['Untagged'],
).map(asString).whereType<String>().toList(growable: false),
exitCode: asInt(json['ExitCode']) ?? 0,
errors: asJsonList(
json['Errors'],
).map(asString).whereType<String>().toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,38 @@
import '../../internal/json_utils.dart';
/// Manifest details from `GET /libpod/manifests/{name}/json`.
final class ManifestDetails {
/// Creates manifest details.
const ManifestDetails({
required this.schemaVersion,
required this.mediaType,
required this.manifests,
required this.raw,
});
/// Manifest schema version.
final int schemaVersion;
/// Manifest media type.
final String mediaType;
/// Embedded manifests list.
final List<Map<String, Object?>> manifests;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ManifestDetails] from JSON.
factory ManifestDetails.fromJson(Map<String, Object?> json) {
final manifests = asJsonList(
json['manifests'],
).map(asJsonMap).toList(growable: false);
return ManifestDetails(
schemaVersion: asInt(json['schemaVersion']) ?? 0,
mediaType: asString(json['mediaType']) ?? '',
manifests: manifests,
raw: json,
);
}
}

View file

@ -0,0 +1,25 @@
import '../../internal/json_utils.dart';
/// Result from pushing a manifest list.
final class ManifestPushResult {
/// Creates manifest push result.
const ManifestPushResult({required this.id, required this.raw});
/// Digest/ID returned by push.
final String id;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [ManifestPushResult] from JSON.
factory ManifestPushResult.fromJson(Map<String, Object?> json) {
return ManifestPushResult(
id:
asString(json['Id']) ??
asString(json['ID']) ??
asString(json['id']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,80 @@
import '../../internal/json_utils.dart';
import 'network_subnet.dart';
/// Detailed network data from `GET /libpod/networks/{name}/json`.
final class NetworkDetails {
/// Creates network details.
const NetworkDetails({
required this.id,
required this.name,
required this.driver,
required this.networkInterface,
required this.internal,
required this.ipv6Enabled,
required this.dnsEnabled,
required this.createdAt,
required this.labels,
required this.options,
required this.subnets,
required this.raw,
});
/// Network ID.
final String id;
/// Network name.
final String name;
/// Driver name.
final String driver;
/// Host interface name.
final String networkInterface;
/// Internal-only flag.
final bool internal;
/// IPv6-enabled flag.
final bool ipv6Enabled;
/// DNS-enabled flag.
final bool dnsEnabled;
/// Creation timestamp.
final DateTime? createdAt;
/// Labels.
final Map<String, String> labels;
/// Driver options.
final Map<String, String> options;
/// Subnet definitions.
final List<NetworkSubnet> subnets;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [NetworkDetails] from JSON.
factory NetworkDetails.fromJson(Map<String, Object?> json) {
return NetworkDetails(
id: asString(json['id']) ?? asString(json['Id']) ?? '',
name: asString(json['name']) ?? asString(json['Name']) ?? '',
driver: asString(json['driver']) ?? asString(json['Driver']) ?? '',
networkInterface:
asString(json['network_interface']) ??
asString(json['NetworkInterface']) ??
'',
internal: asBool(json['internal']) ?? false,
ipv6Enabled: asBool(json['ipv6_enabled']) ?? false,
dnsEnabled: asBool(json['dns_enabled']) ?? false,
createdAt: DateTime.tryParse(asString(json['created']) ?? ''),
labels: asStringMap(json['labels']),
options: asStringMap(json['options']),
subnets: asJsonList(
json['subnets'],
).map(asJsonMap).map(NetworkSubnet.fromJson).toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,36 @@
import '../../internal/json_utils.dart';
/// Network prune result item from `POST /libpod/networks/prune`.
final class NetworkPruneReport {
/// Creates a network prune report.
const NetworkPruneReport({
required this.name,
required this.error,
required this.raw,
});
/// Network name.
final String name;
/// Prune error message (if any).
final String? error;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [NetworkPruneReport] from JSON.
factory NetworkPruneReport.fromJson(Map<String, Object?> json) {
return NetworkPruneReport(
name:
asString(json['Name']) ??
asString(json['name']) ??
asString(json['Id']) ??
'',
error:
asString(json['Error']) ??
asString(json['Err']) ??
asString(json['error']),
raw: json,
);
}
}

View file

@ -0,0 +1,29 @@
import '../../internal/json_utils.dart';
/// Network subnet/gateway pair from Podman network APIs.
final class NetworkSubnet {
/// Creates a subnet model.
const NetworkSubnet({
required this.subnet,
required this.gateway,
required this.raw,
});
/// Subnet CIDR.
final String subnet;
/// Gateway IP.
final String gateway;
/// Raw JSON payload.
final Map<String, Object?> raw;
/// Builds [NetworkSubnet] from JSON.
factory NetworkSubnet.fromJson(Map<String, Object?> json) {
return NetworkSubnet(
subnet: asString(json['subnet']) ?? asString(json['Subnet']) ?? '',
gateway: asString(json['gateway']) ?? asString(json['Gateway']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,64 @@
import '../../internal/json_utils.dart';
import 'network_subnet.dart';
/// Network summary from `GET /libpod/networks/json`.
final class NetworkSummary {
/// Creates a network summary.
const NetworkSummary({
required this.id,
required this.name,
required this.driver,
required this.internal,
required this.ipv6Enabled,
required this.dnsEnabled,
required this.labels,
required this.subnets,
required this.raw,
});
/// Network ID.
final String id;
/// Network name.
final String name;
/// Network driver.
final String driver;
/// Internal-only flag.
final bool internal;
/// IPv6-enabled flag.
final bool ipv6Enabled;
/// DNS-enabled flag.
final bool dnsEnabled;
/// Labels.
final Map<String, String> labels;
/// Subnet definitions.
final List<NetworkSubnet> subnets;
/// Raw JSON payload.
final Map<String, Object?> raw;
/// Builds [NetworkSummary] from JSON.
factory NetworkSummary.fromJson(Map<String, Object?> json) {
final subnets = asJsonList(
json['subnets'],
).map(asJsonMap).map(NetworkSubnet.fromJson).toList(growable: false);
return NetworkSummary(
id: asString(json['id']) ?? asString(json['Id']) ?? '',
name: asString(json['name']) ?? asString(json['Name']) ?? '',
driver: asString(json['driver']) ?? asString(json['Driver']) ?? '',
internal: asBool(json['internal']) ?? false,
ipv6Enabled: asBool(json['ipv6_enabled']) ?? false,
dnsEnabled: asBool(json['dns_enabled']) ?? false,
labels: asStringMap(json['labels']),
subnets: subnets,
raw: json,
);
}
}

View file

@ -0,0 +1,45 @@
import '../internal/json_utils.dart';
/// Report from play-kube up/down APIs.
final class PlayKubeReport {
/// Creates play-kube report.
const PlayKubeReport({
required this.pods,
required this.volumes,
required this.secrets,
required this.serviceContainerId,
required this.raw,
});
/// Pod IDs/names in report.
final List<String> pods;
/// Volume names in report.
final List<String> volumes;
/// Secret names in report.
final List<String> secrets;
/// Service container ID, when present.
final String serviceContainerId;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PlayKubeReport] from JSON.
factory PlayKubeReport.fromJson(Map<String, Object?> json) {
return PlayKubeReport(
pods: asJsonList(
json['Pods'],
).map(asString).whereType<String>().toList(growable: false),
volumes: asJsonList(
json['Volumes'],
).map(asString).whereType<String>().toList(growable: false),
secrets: asJsonList(
json['Secrets'],
).map(asString).whereType<String>().toList(growable: false),
serviceContainerId: asString(json['ServiceContainerID']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,113 @@
import '../internal/json_utils.dart';
/// Event actor object included in Podman events.
final class PodmanEventActor {
/// Creates event actor.
const PodmanEventActor({
required this.id,
required this.attributes,
required this.raw,
});
/// Actor ID.
final String id;
/// Actor attributes.
final Map<String, String> attributes;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodmanEventActor] from JSON.
factory PodmanEventActor.fromJson(Map<String, Object?> json) {
return PodmanEventActor(
id: asString(json['ID']) ?? asString(json['Id']) ?? '',
attributes: asStringMap(json['Attributes']),
raw: json,
);
}
}
/// Event from `GET /libpod/events`.
final class PodmanEvent {
/// Creates Podman event.
const PodmanEvent({
required this.type,
required this.action,
required this.status,
required this.id,
required this.name,
required this.scope,
required this.time,
required this.timeNano,
required this.actor,
required this.raw,
});
/// Event type (`container`, `image`, `volume`, ...).
final String type;
/// Event action.
final String action;
/// Event status.
final String status;
/// Resource ID.
final String id;
/// Resource name.
final String name;
/// Event scope.
final String scope;
/// Event time in seconds since epoch.
final int time;
/// Event time in nanoseconds since epoch.
final int timeNano;
/// Event actor info.
final PodmanEventActor actor;
/// Raw payload.
final Map<String, Object?> raw;
/// Event timestamp as [DateTime] when available.
DateTime? get timestamp {
if (timeNano > 0) {
final micros = timeNano ~/ 1000;
return DateTime.fromMicrosecondsSinceEpoch(micros, isUtc: true);
}
if (time > 0) {
return DateTime.fromMillisecondsSinceEpoch(time * 1000, isUtc: true);
}
return null;
}
/// Builds [PodmanEvent] from JSON.
factory PodmanEvent.fromJson(Map<String, Object?> json) {
final actorMap = asJsonMap(json['Actor']);
final actor = PodmanEventActor.fromJson(actorMap);
final eventId = asString(json['id']) ?? asString(json['ID']) ?? actor.id;
final attributes = actor.attributes;
final eventName =
asString(attributes['name']) ?? asString(json['name']) ?? '';
return PodmanEvent(
type: asString(json['Type']) ?? '',
action: asString(json['Action']) ?? '',
status: asString(json['status']) ?? asString(json['Status']) ?? '',
id: eventId,
name: eventName,
scope: asString(json['scope']) ?? '',
time: asInt(json['time']) ?? 0,
timeNano: asInt(json['timeNano']) ?? 0,
actor: actor,
raw: json,
);
}
}

View file

@ -0,0 +1,14 @@
/// Single key-value event filter for Podman events API.
final class PodmanEventFilter {
/// Creates event filter.
const PodmanEventFilter(this.key, this.value);
/// Filter key (for example `type`, `event`, `container`).
final String key;
/// Filter value.
final String value;
/// Serializes filter as `key=value`.
String asQueryValue() => '$key=$value';
}

View file

@ -0,0 +1,25 @@
import '../internal/json_utils.dart';
/// Parsed output from `GET /libpod/info`.
final class PodmanInfo {
/// Creates parsed info model.
const PodmanInfo({required this.raw});
/// Raw parsed JSON payload.
final Map<String, Object?> raw;
/// Host operating system when available.
String? get hostOs => asString(asJsonMap(raw['host'])['os']);
/// Host architecture when available.
String? get hostArch => asString(asJsonMap(raw['host'])['arch']);
/// Host cgroup version when available.
String? get cgroupVersion =>
asString(asJsonMap(raw['host'])['cgroupVersion']);
/// Build from API JSON output.
factory PodmanInfo.fromJson(Map<String, Object?> json) {
return PodmanInfo(raw: json);
}
}

View file

@ -0,0 +1,39 @@
import '../internal/json_utils.dart';
/// Parsed output from `GET /libpod/version`.
final class PodmanVersion {
/// Creates a parsed version model.
const PodmanVersion({
required this.clientVersion,
required this.serverVersion,
required this.raw,
});
/// Client version string.
final String? clientVersion;
/// Server version string.
final String? serverVersion;
/// Raw parsed JSON payload.
final Map<String, Object?> raw;
/// Builds [PodmanVersion] from API JSON output.
factory PodmanVersion.fromJson(Map<String, Object?> json) {
final client = asJsonMap(json['Client']);
final server = asJsonMap(json['Server']);
final fallback = asString(json['Version']);
return PodmanVersion(
clientVersion:
asString(client['Version']) ??
asString(client['version']) ??
fallback,
serverVersion:
asString(server['Version']) ??
asString(server['version']) ??
fallback,
raw: json,
);
}
}

View file

@ -0,0 +1,65 @@
import '../../internal/json_utils.dart';
/// Detailed pod data from `GET /libpod/pods/{name}/json`.
final class PodDetails {
/// Creates pod details.
const PodDetails({
required this.id,
required this.name,
required this.state,
required this.cgroup,
required this.labels,
required this.createdAt,
required this.containerIds,
required this.raw,
});
/// Pod ID.
final String id;
/// Pod name.
final String name;
/// Pod state.
final String state;
/// Pod cgroup parent.
final String cgroup;
/// Pod labels.
final Map<String, String> labels;
/// Creation timestamp.
final DateTime? createdAt;
/// IDs of containers belonging to the pod.
final List<String> containerIds;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodDetails] from JSON.
factory PodDetails.fromJson(Map<String, Object?> json) {
return PodDetails(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
name: asString(json['Name']) ?? '',
state: asString(json['State']) ?? asString(json['Status']) ?? '',
cgroup: asString(json['CgroupParent']) ?? asString(json['Cgroup']) ?? '',
labels: asStringMap(json['Labels']),
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
containerIds: _parseContainerIds(json['Containers']),
raw: json,
);
}
static List<String> _parseContainerIds(Object? value) {
final items = asJsonList(value);
return items
.map((item) {
final map = asJsonMap(item);
return asString(map['Id']) ?? asString(map['ID']) ?? '';
})
.where((id) => id.isNotEmpty)
.toList(growable: false);
}
}

View file

@ -0,0 +1,36 @@
import '../../internal/json_utils.dart';
/// Pod prune result item from `POST /libpod/pods/prune`.
final class PodPruneReport {
/// Creates a pod prune report.
const PodPruneReport({
required this.id,
required this.error,
required this.raw,
});
/// Pod ID.
final String id;
/// Prune error message (if any).
final String? error;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodPruneReport] from JSON.
factory PodPruneReport.fromJson(Map<String, Object?> json) {
return PodPruneReport(
id:
asString(json['Id']) ??
asString(json['ID']) ??
asString(json['id']) ??
'',
error:
asString(json['Err']) ??
asString(json['Error']) ??
asString(json['err']),
raw: json,
);
}
}

View file

@ -0,0 +1,77 @@
import '../../internal/json_utils.dart';
/// Pod stats item from `GET /libpod/pods/stats`.
final class PodStatsReport {
/// Creates a pod stats report.
const PodStatsReport({
required this.podId,
required this.containerId,
required this.name,
required this.cpuPercent,
required this.memoryUsage,
required this.memoryUsageBytes,
required this.memoryPercent,
required this.networkIo,
required this.blockIo,
required this.pids,
required this.raw,
});
/// Pod ID.
final String podId;
/// Container ID.
final String containerId;
/// Pod name.
final String name;
/// CPU usage percent.
final String cpuPercent;
/// Humanized memory usage.
final String memoryUsage;
/// Memory usage/limit in bytes.
final String memoryUsageBytes;
/// Memory percent.
final String memoryPercent;
/// Network I/O value.
final String networkIo;
/// Block I/O value.
final String blockIo;
/// PID count.
final String pids;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodStatsReport] from JSON.
factory PodStatsReport.fromJson(Map<String, Object?> json) {
return PodStatsReport(
podId: asString(json['Pod']) ?? asString(json['pod']) ?? '',
containerId:
asString(json['CID']) ??
asString(json['Cid']) ??
asString(json['cid']) ??
'',
name: asString(json['Name']) ?? asString(json['name']) ?? '',
cpuPercent: asString(json['CPU']) ?? asString(json['cpu']) ?? '',
memoryUsage:
asString(json['MemUsage']) ?? asString(json['memUsage']) ?? '',
memoryUsageBytes:
asString(json['MemUsageBytes']) ??
asString(json['memUsageBytes']) ??
'',
memoryPercent: asString(json['Mem']) ?? asString(json['mem']) ?? '',
networkIo: asString(json['NetIO']) ?? asString(json['netIO']) ?? '',
blockIo: asString(json['BlockIO']) ?? asString(json['blockIO']) ?? '',
pids: asString(json['PIDS']) ?? asString(json['pids']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,57 @@
import '../../internal/json_utils.dart';
/// Pod summary from `GET /libpod/pods/json`.
final class PodSummary {
/// Creates a pod summary.
const PodSummary({
required this.id,
required this.name,
required this.status,
required this.cgroup,
required this.containers,
required this.labels,
required this.createdAt,
required this.raw,
});
/// Pod ID.
final String id;
/// Pod name.
final String name;
/// Pod status.
final String status;
/// Pod cgroup parent.
final String cgroup;
/// Number of containers in the pod.
final int containers;
/// Pod labels.
final Map<String, String> labels;
/// Creation timestamp.
final DateTime? createdAt;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodSummary] from JSON.
factory PodSummary.fromJson(Map<String, Object?> json) {
return PodSummary(
id: asString(json['Id']) ?? asString(json['ID']) ?? '',
name: asString(json['Name']) ?? '',
status: asString(json['Status']) ?? '',
cgroup: asString(json['Cgroup']) ?? '',
containers:
asInt(json['NumberOfContainers']) ??
asInt(json['Containers']) ??
asJsonList(json['Containers']).length,
labels: asStringMap(json['Labels']),
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
raw: json,
);
}
}

View file

@ -0,0 +1,37 @@
import '../../internal/json_utils.dart';
/// Process listing from `GET /libpod/pods/{name}/top`.
final class PodTopReport {
/// Creates a pod top report.
const PodTopReport({
required this.titles,
required this.processes,
required this.raw,
});
/// Column titles.
final List<String> titles;
/// Process rows.
final List<List<String>> processes;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [PodTopReport] from JSON.
factory PodTopReport.fromJson(Map<String, Object?> json) {
final titles = asJsonList(
json['Titles'] ?? json['titles'],
).map((value) => asString(value) ?? '').toList(growable: false);
final processes = asJsonList(json['Processes'] ?? json['processes'])
.map(
(row) => asJsonList(
row,
).map((value) => asString(value) ?? '').toList(growable: false),
)
.toList(growable: false);
return PodTopReport(titles: titles, processes: processes, raw: json);
}
}

View file

@ -0,0 +1,25 @@
import '../../internal/json_utils.dart';
/// Secret creation result from `POST /libpod/secrets/create`.
final class SecretCreateResult {
/// Creates secret create result.
const SecretCreateResult({required this.id, required this.raw});
/// Secret ID.
final String id;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SecretCreateResult] from JSON.
factory SecretCreateResult.fromJson(Map<String, Object?> json) {
return SecretCreateResult(
id:
asString(json['ID']) ??
asString(json['Id']) ??
asString(json['id']) ??
'',
raw: json,
);
}
}

View file

@ -0,0 +1,61 @@
import '../../internal/json_utils.dart';
/// Secret details from `GET /libpod/secrets/{name}/json`.
final class SecretDetails {
/// Creates secret details.
const SecretDetails({
required this.id,
required this.name,
required this.driver,
required this.createdAt,
required this.updatedAt,
required this.labels,
required this.secretData,
required this.raw,
});
/// Secret ID.
final String id;
/// Secret name.
final String name;
/// Secret driver.
final String driver;
/// Created timestamp.
final DateTime? createdAt;
/// Last update timestamp.
final DateTime? updatedAt;
/// Secret labels.
final Map<String, String> labels;
/// Optional clear secret data when `showsecret=true`.
final String secretData;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SecretDetails] from JSON.
factory SecretDetails.fromJson(Map<String, Object?> json) {
final spec = asJsonMap(json['Spec']);
final driverMap = asJsonMap(spec['Driver']);
return SecretDetails(
id:
asString(json['ID']) ??
asString(json['Id']) ??
asString(json['id']) ??
'',
name: asString(spec['Name']) ?? asString(json['Name']) ?? '',
driver: asString(driverMap['Name']) ?? asString(spec['Driver']) ?? '',
createdAt: DateTime.tryParse(asString(json['CreatedAt']) ?? ''),
updatedAt: DateTime.tryParse(asString(json['UpdatedAt']) ?? ''),
labels: asStringMap(spec['Labels']),
secretData: asString(json['SecretData']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,56 @@
import '../../internal/json_utils.dart';
/// Secret summary from `GET /libpod/secrets/json`.
final class SecretSummary {
/// Creates secret summary.
const SecretSummary({
required this.id,
required this.name,
required this.driver,
required this.createdAt,
required this.updatedAt,
required this.labels,
required this.raw,
});
/// Secret ID.
final String id;
/// Secret name.
final String name;
/// Secret driver.
final String driver;
/// Created timestamp.
final DateTime? createdAt;
/// Last update timestamp.
final DateTime? updatedAt;
/// Labels.
final Map<String, String> labels;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SecretSummary] from JSON.
factory SecretSummary.fromJson(Map<String, Object?> json) {
final spec = asJsonMap(json['Spec']);
final driverMap = asJsonMap(spec['Driver']);
return SecretSummary(
id:
asString(json['ID']) ??
asString(json['Id']) ??
asString(json['id']) ??
'',
name: asString(spec['Name']) ?? asString(json['Name']) ?? '',
driver: asString(driverMap['Name']) ?? asString(spec['Driver']) ?? '',
createdAt: DateTime.tryParse(asString(json['CreatedAt']) ?? ''),
updatedAt: DateTime.tryParse(asString(json['UpdatedAt']) ?? ''),
labels: asStringMap(spec['Labels']),
raw: json,
);
}
}

View file

@ -0,0 +1,81 @@
import '../../internal/json_utils.dart';
/// Storage consistency report from `POST /libpod/system/check`.
final class SystemCheckReport {
/// Creates a system-check report.
const SystemCheckReport({
required this.errors,
required this.layers,
required this.roLayers,
required this.removedLayers,
required this.images,
required this.roImages,
required this.removedImages,
required this.containers,
required this.removedContainers,
required this.raw,
});
/// Whether check found errors.
final bool errors;
/// Layer findings keyed by layer ID.
final Map<String, List<String>> layers;
/// Read-only layer findings keyed by layer ID.
final Map<String, List<String>> roLayers;
/// Removed layer IDs.
final List<String> removedLayers;
/// Image findings keyed by image ID.
final Map<String, List<String>> images;
/// Read-only image findings keyed by image ID.
final Map<String, List<String>> roImages;
/// Removed image names keyed by image ID.
final Map<String, List<String>> removedImages;
/// Container findings keyed by container ID.
final Map<String, List<String>> containers;
/// Removed container names keyed by container ID.
final Map<String, String> removedContainers;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemCheckReport] from JSON.
factory SystemCheckReport.fromJson(Map<String, Object?> json) {
return SystemCheckReport(
errors: asBool(json['Errors']) ?? false,
layers: _stringListMap(json['Layers']),
roLayers: _stringListMap(json['ROLayers']),
removedLayers: asJsonList(
json['RemovedLayers'],
).map(asString).whereType<String>().toList(growable: false),
images: _stringListMap(json['Images']),
roImages: _stringListMap(json['ROImages']),
removedImages: _stringListMap(json['RemovedImages']),
containers: _stringListMap(json['Containers']),
removedContainers: asStringMap(json['RemovedContainers']),
raw: json,
);
}
}
Map<String, List<String>> _stringListMap(Object? value) {
final map = asJsonMap(value);
final keys = map.keys.toList(growable: false)..sort();
final output = <String, List<String>>{};
for (final key in keys) {
final list = asJsonList(
map[key],
).map(asString).whereType<String>().toList(growable: false);
output[key] = list;
}
return output;
}

View file

@ -0,0 +1,68 @@
import '../../internal/json_utils.dart';
/// Container row from `GET /libpod/system/df`.
final class SystemDfContainer {
/// Creates a system-df container row.
const SystemDfContainer({
required this.containerId,
required this.image,
required this.command,
required this.localVolumes,
required this.size,
required this.rwSize,
required this.createdAt,
required this.status,
required this.names,
required this.raw,
});
/// Container ID.
final String containerId;
/// Image used by container.
final String image;
/// Container command.
final List<String> command;
/// Count of local volumes.
final int localVolumes;
/// Container size in bytes.
final int size;
/// Container read-write layer size in bytes.
final int rwSize;
/// Creation timestamp.
final DateTime? createdAt;
/// Human-readable status.
final String status;
/// Container name.
final String names;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemDfContainer] from JSON.
factory SystemDfContainer.fromJson(Map<String, Object?> json) {
final commandList = asJsonList(
json['Command'],
).map(asString).whereType<String>().toList(growable: false);
return SystemDfContainer(
containerId: asString(json['ContainerID']) ?? '',
image: asString(json['Image']) ?? '',
command: commandList,
localVolumes: asInt(json['LocalVolumes']) ?? 0,
size: asInt(json['Size']) ?? 0,
rwSize: asInt(json['RWSize']) ?? 0,
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
status: asString(json['Status']) ?? '',
names: asString(json['Names']) ?? '',
raw: json,
);
}
}

View file

@ -0,0 +1,59 @@
import '../../internal/json_utils.dart';
/// Image row from `GET /libpod/system/df`.
final class SystemDfImage {
/// Creates a system-df image row.
const SystemDfImage({
required this.repository,
required this.tag,
required this.imageId,
required this.createdAt,
required this.size,
required this.sharedSize,
required this.uniqueSize,
required this.containers,
required this.raw,
});
/// Repository name.
final String repository;
/// Image tag.
final String tag;
/// Image ID.
final String imageId;
/// Creation timestamp.
final DateTime? createdAt;
/// Total image size in bytes.
final int size;
/// Shared size in bytes.
final int sharedSize;
/// Unique size in bytes.
final int uniqueSize;
/// Number of containers using this image.
final int containers;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemDfImage] from JSON.
factory SystemDfImage.fromJson(Map<String, Object?> json) {
return SystemDfImage(
repository: asString(json['Repository']) ?? '',
tag: asString(json['Tag']) ?? '',
imageId: asString(json['ImageID']) ?? '',
createdAt: DateTime.tryParse(asString(json['Created']) ?? ''),
size: asInt(json['Size']) ?? 0,
sharedSize: asInt(json['SharedSize']) ?? 0,
uniqueSize: asInt(json['UniqueSize']) ?? 0,
containers: asInt(json['Containers']) ?? 0,
raw: json,
);
}
}

View file

@ -0,0 +1,48 @@
import '../../internal/json_utils.dart';
import 'system_df_container.dart';
import 'system_df_image.dart';
import 'system_df_volume.dart';
/// Disk usage report from `GET /libpod/system/df`.
final class SystemDfReport {
/// Creates a system-df report.
const SystemDfReport({
required this.imagesSize,
required this.images,
required this.containers,
required this.volumes,
required this.raw,
});
/// Aggregate image size in bytes.
final int imagesSize;
/// Image usage rows.
final List<SystemDfImage> images;
/// Container usage rows.
final List<SystemDfContainer> containers;
/// Volume usage rows.
final List<SystemDfVolume> volumes;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemDfReport] from JSON.
factory SystemDfReport.fromJson(Map<String, Object?> json) {
return SystemDfReport(
imagesSize: asInt(json['ImagesSize']) ?? 0,
images: asJsonList(
json['Images'],
).map(asJsonMap).map(SystemDfImage.fromJson).toList(growable: false),
containers: asJsonList(
json['Containers'],
).map(asJsonMap).map(SystemDfContainer.fromJson).toList(growable: false),
volumes: asJsonList(
json['Volumes'],
).map(asJsonMap).map(SystemDfVolume.fromJson).toList(growable: false),
raw: json,
);
}
}

View file

@ -0,0 +1,39 @@
import '../../internal/json_utils.dart';
/// Volume row from `GET /libpod/system/df`.
final class SystemDfVolume {
/// Creates a system-df volume row.
const SystemDfVolume({
required this.volumeName,
required this.links,
required this.size,
required this.reclaimableSize,
required this.raw,
});
/// Volume name.
final String volumeName;
/// Number of links/users.
final int links;
/// Total size in bytes.
final int size;
/// Reclaimable size in bytes.
final int reclaimableSize;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemDfVolume] from JSON.
factory SystemDfVolume.fromJson(Map<String, Object?> json) {
return SystemDfVolume(
volumeName: asString(json['VolumeName']) ?? '',
links: asInt(json['Links']) ?? 0,
size: asInt(json['Size']) ?? 0,
reclaimableSize: asInt(json['ReclaimableSize']) ?? 0,
raw: json,
);
}
}

View file

@ -0,0 +1,76 @@
import '../../internal/json_utils.dart';
/// System prune report from `POST /libpod/system/prune`.
final class SystemPruneReport {
/// Creates a system-prune report.
const SystemPruneReport({
required this.podIds,
required this.containerIds,
required this.imageIds,
required this.networkNames,
required this.volumeIds,
required this.reclaimedSpace,
required this.raw,
});
/// Pruned pod IDs.
final List<String> podIds;
/// Pruned container IDs.
final List<String> containerIds;
/// Pruned image IDs.
final List<String> imageIds;
/// Pruned network names.
final List<String> networkNames;
/// Pruned volume IDs.
final List<String> volumeIds;
/// Total reclaimed bytes.
final int reclaimedSpace;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [SystemPruneReport] from JSON.
factory SystemPruneReport.fromJson(Map<String, Object?> json) {
return SystemPruneReport(
podIds: _extractReportIds(json['PodPruneReport']),
containerIds: _extractReportIds(json['ContainerPruneReports']),
imageIds: _extractReportIds(json['ImagePruneReports']),
networkNames: _extractReportIds(
json['NetworkPruneReports'],
preferredKeys: const <String>['Name', 'Id'],
),
volumeIds: _extractReportIds(json['VolumePruneReports']),
reclaimedSpace: asInt(json['ReclaimedSpace']) ?? 0,
raw: json,
);
}
}
List<String> _extractReportIds(
Object? value, {
List<String> preferredKeys = const <String>['Id', 'ID', 'id'],
}) {
final list = asJsonList(value);
final output = <String>[];
for (final item in list) {
final map = asJsonMap(item);
String? id;
for (final key in preferredKeys) {
id = asString(map[key]);
if (id != null && id.isNotEmpty) {
break;
}
}
if (id != null && id.isNotEmpty) {
output.add(id);
}
}
return output;
}

View file

@ -0,0 +1,62 @@
import '../internal/json_utils.dart';
/// Detailed volume data from `GET /libpod/volumes/{name}/json`.
final class VolumeDetails {
/// Creates detailed volume data.
const VolumeDetails({
required this.name,
required this.driver,
required this.mountpoint,
required this.createdAt,
required this.scope,
required this.mountCount,
required this.labels,
required this.options,
required this.raw,
});
/// Volume name.
final String name;
/// Driver name.
final String driver;
/// Mount path.
final String mountpoint;
/// Creation timestamp.
final DateTime? createdAt;
/// Scope value.
final String scope;
/// Active mount count.
final int mountCount;
/// Labels.
final Map<String, String> labels;
/// Driver options.
final Map<String, String> options;
/// Raw payload.
final Map<String, Object?> raw;
/// Builds [VolumeDetails] from JSON.
factory VolumeDetails.fromJson(Map<String, Object?> json) {
return VolumeDetails(
name: asString(json['Name']) ?? asString(json['name']) ?? '',
driver: asString(json['Driver']) ?? asString(json['driver']) ?? '',
mountpoint:
asString(json['Mountpoint']) ?? asString(json['mountpoint']) ?? '',
createdAt: DateTime.tryParse(
asString(json['CreatedAt']) ?? asString(json['created_at']) ?? '',
),
scope: asString(json['Scope']) ?? asString(json['scope']) ?? '',
mountCount: asInt(json['MountCount']) ?? asInt(json['mount_count']) ?? 0,
labels: asStringMap(json['Labels']),
options: asStringMap(json['Options']),
raw: json,
);
}
}

Some files were not shown because too many files have changed in this diff Show more