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