Initial podman package release
This commit is contained in:
commit
500914cf10
156 changed files with 11875 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.dart_tool/
|
||||||
|
.packages
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
*.iml
|
||||||
1
.pubignore
Normal file
1
.pubignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.iml
|
||||||
21
CHANGELOG.md
Normal file
21
CHANGELOG.md
Normal 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
21
LICENSE
Normal 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
95
README.md
Normal 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
3
dart_test.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
tags:
|
||||||
|
local:
|
||||||
|
skip: false
|
||||||
60
doc/examples.md
Normal file
60
doc/examples.md
Normal 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
|
||||||
|
```
|
||||||
129
example/artifact_workflow_example.dart
Normal file
129
example/artifact_workflow_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
274
example/checkpoint_restore_example.dart
Normal file
274
example/checkpoint_restore_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
115
example/generate_assets_example.dart
Normal file
115
example/generate_assets_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
21
example/inspect_container_example.dart
Normal file
21
example/inspect_container_example.dart
Normal 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();
|
||||||
|
}
|
||||||
21
example/list_containers_example.dart
Normal file
21
example/list_containers_example.dart
Normal 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();
|
||||||
|
}
|
||||||
127
example/manifest_workflow_example.dart
Normal file
127
example/manifest_workflow_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
101
example/play_kube_file_example.dart
Normal file
101
example/play_kube_file_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
19
example/pull_and_run_example.dart
Normal file
19
example/pull_and_run_example.dart
Normal 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();
|
||||||
|
}
|
||||||
159
example/secrets_workflow_example.dart
Normal file
159
example/secrets_workflow_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
136
example/system_maintenance_example.dart
Normal file
136
example/system_maintenance_example.dart
Normal 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
|
||||||
|
''');
|
||||||
|
}
|
||||||
15
example/version_info_example.dart
Normal file
15
example/version_info_example.dart
Normal 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
103
lib/podman.dart
Normal 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';
|
||||||
191
lib/src/client/artifacts.dart
Normal file
191
lib/src/client/artifacts.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
lib/src/client/containers/container_admin.dart
Normal file
142
lib/src/client/containers/container_admin.dart
Normal 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) ?? ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/src/client/containers/container_archive.dart
Normal file
77
lib/src/client/containers/container_archive.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/src/client/containers/container_checkpoint.dart
Normal file
87
lib/src/client/containers/container_checkpoint.dart
Normal 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'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
242
lib/src/client/containers/container_runtime.dart
Normal file
242
lib/src/client/containers/container_runtime.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
241
lib/src/client/containers/containers.dart
Normal file
241
lib/src/client/containers/containers.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
lib/src/client/events.dart
Normal file
95
lib/src/client/events.dart
Normal 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
359
lib/src/client/images.dart
Normal 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
80
lib/src/client/kube.dart
Normal 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'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
lib/src/client/manifests.dart
Normal file
109
lib/src/client/manifests.dart
Normal 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'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
lib/src/client/networks.dart
Normal file
136
lib/src/client/networks.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
346
lib/src/client/podman_client.dart
Normal file
346
lib/src/client/podman_client.dart
Normal 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
269
lib/src/client/pods.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/src/client/secrets.dart
Normal file
71
lib/src/client/secrets.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/src/client/system.dart
Normal file
57
lib/src/client/system.dart
Normal 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
121
lib/src/client/volumes.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
lib/src/core/http_method.dart
Normal file
2
lib/src/core/http_method.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/// Supported HTTP methods for Podman transport requests.
|
||||||
|
enum HttpMethod { get, post, put, patch, delete, head }
|
||||||
34
lib/src/core/podman_api_exception.dart
Normal file
34
lib/src/core/podman_api_exception.dart
Normal 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;
|
||||||
|
}
|
||||||
11
lib/src/core/podman_exception.dart
Normal file
11
lib/src/core/podman_exception.dart
Normal 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';
|
||||||
|
}
|
||||||
7
lib/src/core/podman_parse_exception.dart
Normal file
7
lib/src/core/podman_parse_exception.dart
Normal 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);
|
||||||
|
}
|
||||||
11
lib/src/core/podman_transport.dart
Normal file
11
lib/src/core/podman_transport.dart
Normal 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();
|
||||||
|
}
|
||||||
32
lib/src/core/podman_transport_request.dart
Normal file
32
lib/src/core/podman_transport_request.dart
Normal 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;
|
||||||
|
}
|
||||||
23
lib/src/core/podman_transport_response.dart
Normal file
23
lib/src/core/podman_transport_response.dart
Normal 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);
|
||||||
|
}
|
||||||
163
lib/src/core/unix_socket_podman_transport.dart
Normal file
163
lib/src/core/unix_socket_podman_transport.dart
Normal 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';
|
||||||
|
}
|
||||||
82
lib/src/internal/json_utils.dart
Normal file
82
lib/src/internal/json_utils.dart
Normal 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) ?? ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
24
lib/src/models/artifacts/artifact_add_result.dart
Normal file
24
lib/src/models/artifacts/artifact_add_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/src/models/artifacts/artifact_details.dart
Normal file
34
lib/src/models/artifacts/artifact_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/src/models/artifacts/artifact_pull_result.dart
Normal file
24
lib/src/models/artifacts/artifact_pull_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/src/models/artifacts/artifact_push_result.dart
Normal file
24
lib/src/models/artifacts/artifact_push_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lib/src/models/artifacts/artifact_remove_result.dart
Normal file
26
lib/src/models/artifacts/artifact_remove_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/src/models/artifacts/artifact_summary.dart
Normal file
34
lib/src/models/artifacts/artifact_summary.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/src/models/containers/container_archive_get_result.dart
Normal file
18
lib/src/models/containers/container_archive_get_result.dart
Normal 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;
|
||||||
|
}
|
||||||
34
lib/src/models/containers/container_checkpoint_report.dart
Normal file
34
lib/src/models/containers/container_checkpoint_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/src/models/containers/container_create_result.dart
Normal file
32
lib/src/models/containers/container_create_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
lib/src/models/containers/container_details.dart
Normal file
71
lib/src/models/containers/container_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/models/containers/container_exec_create_result.dart
Normal file
21
lib/src/models/containers/container_exec_create_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/src/models/containers/container_exec_inspect_result.dart
Normal file
49
lib/src/models/containers/container_exec_inspect_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/models/containers/container_exec_start_result.dart
Normal file
21
lib/src/models/containers/container_exec_start_result.dart
Normal 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';
|
||||||
|
}
|
||||||
40
lib/src/models/containers/container_health_status.dart
Normal file
40
lib/src/models/containers/container_health_status.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/src/models/containers/container_restore_report.dart
Normal file
34
lib/src/models/containers/container_restore_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/src/models/containers/container_stats.dart
Normal file
59
lib/src/models/containers/container_stats.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
lib/src/models/containers/container_summary.dart
Normal file
90
lib/src/models/containers/container_summary.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/src/models/containers/container_top_report.dart
Normal file
37
lib/src/models/containers/container_top_report.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
lib/src/models/containers/container_wait_result.dart
Normal file
22
lib/src/models/containers/container_wait_result.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
lib/src/models/generate_systemd_result.dart
Normal file
18
lib/src/models/generate_systemd_result.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
lib/src/models/image_details.dart
Normal file
53
lib/src/models/image_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/src/models/image_history_entry.dart
Normal file
57
lib/src/models/image_history_entry.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/models/image_import_report.dart
Normal file
21
lib/src/models/image_import_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/src/models/image_load_report.dart
Normal file
23
lib/src/models/image_load_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/src/models/image_push_event.dart
Normal file
37
lib/src/models/image_push_event.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
lib/src/models/image_remove_result.dart
Normal file
45
lib/src/models/image_remove_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
lib/src/models/image_summary.dart
Normal file
60
lib/src/models/image_summary.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
lib/src/models/image_tree_report.dart
Normal file
21
lib/src/models/image_tree_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/src/models/manifests/manifest_create_result.dart
Normal file
25
lib/src/models/manifests/manifest_create_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
lib/src/models/manifests/manifest_delete_result.dart
Normal file
45
lib/src/models/manifests/manifest_delete_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
lib/src/models/manifests/manifest_details.dart
Normal file
38
lib/src/models/manifests/manifest_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/src/models/manifests/manifest_push_result.dart
Normal file
25
lib/src/models/manifests/manifest_push_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
80
lib/src/models/networks/network_details.dart
Normal file
80
lib/src/models/networks/network_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/src/models/networks/network_prune_report.dart
Normal file
36
lib/src/models/networks/network_prune_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/src/models/networks/network_subnet.dart
Normal file
29
lib/src/models/networks/network_subnet.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
lib/src/models/networks/network_summary.dart
Normal file
64
lib/src/models/networks/network_summary.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
lib/src/models/play_kube_report.dart
Normal file
45
lib/src/models/play_kube_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
113
lib/src/models/podman_event.dart
Normal file
113
lib/src/models/podman_event.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
lib/src/models/podman_event_filter.dart
Normal file
14
lib/src/models/podman_event_filter.dart
Normal 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';
|
||||||
|
}
|
||||||
25
lib/src/models/podman_info.dart
Normal file
25
lib/src/models/podman_info.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/src/models/podman_version.dart
Normal file
39
lib/src/models/podman_version.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/src/models/pods/pod_details.dart
Normal file
65
lib/src/models/pods/pod_details.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/src/models/pods/pod_prune_report.dart
Normal file
36
lib/src/models/pods/pod_prune_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
77
lib/src/models/pods/pod_stats_report.dart
Normal file
77
lib/src/models/pods/pod_stats_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/src/models/pods/pod_summary.dart
Normal file
57
lib/src/models/pods/pod_summary.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
lib/src/models/pods/pod_top_report.dart
Normal file
37
lib/src/models/pods/pod_top_report.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/src/models/secrets/secret_create_result.dart
Normal file
25
lib/src/models/secrets/secret_create_result.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/src/models/secrets/secret_details.dart
Normal file
61
lib/src/models/secrets/secret_details.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
lib/src/models/secrets/secret_summary.dart
Normal file
56
lib/src/models/secrets/secret_summary.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
lib/src/models/system/system_check_report.dart
Normal file
81
lib/src/models/system/system_check_report.dart
Normal 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;
|
||||||
|
}
|
||||||
68
lib/src/models/system/system_df_container.dart
Normal file
68
lib/src/models/system/system_df_container.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/src/models/system/system_df_image.dart
Normal file
59
lib/src/models/system/system_df_image.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/src/models/system/system_df_report.dart
Normal file
48
lib/src/models/system/system_df_report.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/src/models/system/system_df_volume.dart
Normal file
39
lib/src/models/system/system_df_volume.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
lib/src/models/system/system_prune_report.dart
Normal file
76
lib/src/models/system/system_prune_report.dart
Normal 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;
|
||||||
|
}
|
||||||
62
lib/src/models/volume_details.dart
Normal file
62
lib/src/models/volume_details.dart
Normal 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
Loading…
Add table
Reference in a new issue