(feature): add powershell client and e2e

This commit is contained in:
Maksym Sadovnychyy 2026-07-27 01:32:21 +02:00
parent 755f7b2341
commit 9f6284ba6f
163 changed files with 7525 additions and 1700 deletions

View File

@ -4,6 +4,7 @@
"skills": [
"common/csharp",
"common/maksit-repo-maintenance",
"local-ollama"
"local-ollama",
"gh-cli"
]
}

View File

@ -9,5 +9,6 @@ alwaysApply: true
1. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md`
2. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md`
3. `E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md` — local Ollama offload (`@local-ollama`)
4. `E:\Users\maksym\source\repos\private\homelab\ai\skills\gh-cli\SKILL.md` — git/gh commits & PRs (`/gh-cli`, `@gh-cli`)
Manifest: `.cursor/maksit-skills.json`.

118
.editorconfig Normal file
View File

@ -0,0 +1,118 @@
# EditorConfig: https://editorconfig.org
# Shared code style for VS Code, Cursor, Visual Studio, Rider
root = true
[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
# =========================
# C#
# =========================
[*.cs]
indent_size = 2
# K&R braces (opening brace on same line)
csharp_new_line_before_open_brace = none
# keep else/catch/finally on new line
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
# indentation
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = one_less_than_current
# spacing
csharp_space_after_cast = false
csharp_space_after_dot = false
csharp_space_after_semicolon_in_for_statement = true
csharp_space_before_semicolon_in_for_statement = false
# Fluent / chained method calls (ReSharper compatibility)
resharper_csharp_continuous_indent_multiplier = 1
resharper_continuous_indent_multiplier = 1
resharper_csharp_align_multiline_calls_chain = false
resharper_align_multiline_calls_chain = false
resharper_csharp_outdent_dots = false
resharper_outdent_dots = false
# Two blank lines between last using and namespace
resharper_csharp_blank_lines_between_using_and_namespace = 2
resharper_blank_lines_between_using_and_namespace = 2
# Usings: System first; blank line between System and other groups (Roslyn)
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
resharper_sort_usings_system_first = true
# Remove unnecessary using directives (dotnet format style --diagnostics IDE0005)
dotnet_diagnostic.IDE0005.severity = warning
# File-scoped namespace declarations (IDE0160)
csharp_style_namespace_declarations = file_scoped:warning
dotnet_diagnostic.IDE0160.severity = warning
# Omit braces on single-line if/else/for/foreach/while/using (IDE0011)
csharp_prefer_braces = when_multiline:none
dotnet_diagnostic.IDE0011.severity = none
# Prefer 'var' over explicit type (IDE0008)
csharp_style_var_for_built_in_types = true:none
csharp_style_var_when_type_is_apparent = true:none
csharp_style_var_elsewhere = true:none
dotnet_diagnostic.IDE0008.severity = none
# Prefer if-return over ternary for conditional returns (IDE0046) — multi-line guards stay readable
dotnet_style_prefer_conditional_expression_over_return = false:none
dotnet_diagnostic.IDE0046.severity = none
# Expression-bodied members: prefer => over { return ...; } (IDE0022IDE0027)
csharp_style_expression_bodied_methods = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_lambdas = true:none
csharp_style_expression_bodied_local_functions = false:none
csharp_style_expression_bodied_constructors = false:none
dotnet_diagnostic.IDE0022.severity = warning
dotnet_diagnostic.IDE0023.severity = warning
dotnet_diagnostic.IDE0024.severity = warning
dotnet_diagnostic.IDE0025.severity = warning
dotnet_diagnostic.IDE0026.severity = warning
dotnet_diagnostic.IDE0027.severity = warning
# => on signature line; expression indented on next line (no blank line after =>)
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning
dotnet_diagnostic.IDE2006.severity = warning
resharper_csharp_wrap_before_arrow_with_expressions = false
resharper_wrap_before_arrow_with_expressions = false
# =========================
# TypeScript / JavaScript
# =========================
[*.{ts,tsx,js,jsx}]
indent_size = 2
# =========================
# JSON / YAML
# =========================
[*.{json,yml,yaml}]
indent_size = 2
# =========================
# Markdown
# =========================
[*.md]
trim_trailing_whitespace = false

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ bld/
# MSTest test Results
[Tt]est[Rr]esult*/
test-results/
[Bb]uild[Ll]og.*
# NUNIT

View File

@ -5,5 +5,6 @@
| csharp | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md) |
| maksit-repo-maintenance | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md) |
| local-ollama | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md) |
| gh-cli | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\gh-cli\SKILL.md) |
Manifest: `.cursor/maksit-skills.json`.

View File

@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.3.0] - 2026-07-27
### Added
- **PowerShell module** `MaksIT.PodmanClientDotNet.PowerShell` wrapping the full `IPodmanClient` surface (`Connect-Podman` and domain cmdlets).
- **PowerShell E2E** harness under `src/e2e-tests/` (requires `PODMAN_TEST_URL`) covering system, images, containers, exec, volumes, networks, pods, build, manifests, and generate. Validated against **Podman 5.4.0**.
### Changed
- **Default `ApiVersion`**: `v5.4.0` (libpod path). Docker-compat `v1.41` is not used — network endpoints reject it.
- **Manifests (libpod v4+)**: create via `POST /libpod/manifests/{name}`; add via `PUT` modify body; push via `POST .../registry/{destination}` (replaces deprecated v3 `/create`, `/add`, `/push`).
- **Pod stats**: `GetPodsStatsAsync` returns `List<PodStatsDto>` (libpod array), not a dictionary wrapper.
### Fixed
- **Ping** (`/_ping`): treat Podman's plain-text `OK` body as success instead of JSON-deserializing it (which threw `JsonException`).
- **Mount container**: treat plain filesystem path body as `ContainerMountDto.Path`.
- **Wait container**: accept bare exit-code integer responses from libpod.
- **System DTOs** (`LibpodVersionDto`, `InfoDto`, `SystemDfDto`): align with live libpod JSON shapes (e.g. version `Platform` object, host `distribution` object, numeric memory fields).
- **Image DTOs**: `RepoTags`/`RepoDigests` as string arrays; image tree `{Tree}`; image changes as path/kind entries; delete/remove returns a single `ImageDeleteDto` object (not an array).
- **Prune APIs**: image/container/volume/pod prune return a list of `PruneReportEntryDto`; system prune returns `SystemPruneReportDto`.
- **Container list/inspect/stats/changes/mounted DTOs**: align with live libpod JSON (including multi-container stats wrapper).
- **Container inspect**: `Config.StopSignal` is a string (e.g. `SIGTERM`), not `Int64`.
- **Pod DTOs**: inspect/list `Containers` as object summaries.
### Removed
- C# xUnit live Integration tests (`Category=Integration`); replaced by PowerShell E2E scenarios.
## [1.2.1] - 2026-07-10
### Fixed
- **Inspect exec** deserialization: `ProcessConfig` is now typed as an object (`InspectExecProcessDto`) matching the Podman libpod API, instead of `string` (which threw `JsonException` when reading exit codes after exec).
## [1.2.0] - 2026-07-02
### Added

View File

@ -8,7 +8,8 @@ Thank you for contributing. This repo follows MaksIT conventions (see `AGENTS.md
- .NET 10 SDK
- Git
- Optional: reachable Podman API for integration tests (`PODMAN_TEST_URL` or `PODMAN_INTEGRATION_URL`)
- PowerShell 7 hosted on **.NET 10** (for the PowerShell module and E2E)
- Optional: reachable Podman REST API for live E2E (`PODMAN_TEST_URL`)
### Build
@ -16,7 +17,7 @@ Thank you for contributing. This repo follows MaksIT conventions (see `AGENTS.md
dotnet build src/PodmanClientDotNet.slnx
```
### Tests
### Unit tests
**RepoUtils test engine** (coverage + badges):
@ -30,7 +31,80 @@ utils/Invoke-TestEngine.bat
dotnet test src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj
```
When coverage changes and the test engine runs **CoverageBadges**, commit updated SVGs under `assets/badges/` (cited in `README.md`).
Coverage badges in `README.md` are rewritten by the test engine (`CoverageBadges` with `badgeFormat: shields` and `readmePath` in `utils/engines/test/scriptSettings.json`) using `img.shields.io` URLs. Commit the updated README when coverage changes.
### Integration / E2E tests (Podman API VM)
Live API coverage is **PowerShell E2E** under `src/e2e-tests/` (not part of `Invoke-TestEngine`). It builds and imports `MaksIT.PodmanClientDotNet.PowerShell`, then runs domain scenarios against a real Podman API.
**Validated target:** [Podman](https://podman.io/) **5.4.0** (libpod path default `ApiVersion` = `v5.4.0`). Older engines that expose at least API `4.0.0` may work; Docker-compat path `v1.41` is not used (network endpoints reject it).
#### 1. Expose Podman on a Linux VM (e.g. Alma)
```bash
podman version # expect 5.4.x for this repo's E2E baseline
# Bind all interfaces (not only 127.0.0.1)
podman system service --time=0 tcp://0.0.0.0:8080
```
On the VM:
```bash
curl -s http://127.0.0.1:8080/v5.4.0/_ping # expect OK
ss -tlnp | grep 8080 # expect 0.0.0.0:8080 (or LAN IP)
sudo firewall-cmd --add-port=8080/tcp --permanent && sudo firewall-cmd --reload
podman pull alpine:latest # scenarios need registry access
```
Optional systemd unit (lab):
```ini
[Unit]
Description=Podman API service
After=network.target
[Service]
ExecStart=/usr/bin/podman system service --time=0 tcp://0.0.0.0:8080
Restart=on-failure
[Install]
WantedBy=multi-user.target
```
Unauthenticated TCP API is lab-only — restrict firewall to your developer machine when possible.
#### 2. Run E2E from Windows
```powershell
Invoke-WebRequest http://<vm-ip>:8080/v5.4.0/_ping # expect 200 / OK
$env:PODMAN_TEST_URL = "http://<vm-ip>:8080" # must include http:// or https://
.\src\e2e-tests\Test-PodmanE2E.bat
# or:
pwsh -File .\src\e2e-tests\Test-PodmanE2E.ps1 -Scenario 'System','Images'
```
`PODMAN_TEST_URL` must be an absolute URI. Bare `host:port` causes `UriFormatException` in the .NET client.
Scenarios cover system, images, containers, exec, volumes, networks, pods, build, manifests, and generate (full PowerShell cmdlet surface). Filter with `-Scenario`.
#### Troubleshooting
1. **Pull fails** — VM cannot reach a registry; pre-pull `alpine:latest` on the VM.
2. **Container create/start fails** — confirm `podman run --rm alpine:latest echo ok` as the same user running `system service`.
3. **Attach/exec session failures** — hijack opens a second TCP connection to the same host:port; allow it in the firewall; do not put the API behind a proxy that strips `Upgrade: tcp`.
4. **pwsh / .NET 10** — binary module requires PowerShell hosted on .NET 10.
5. **`version is not supported` on networks** — use libpod path `v4.0.0+` (default `v5.4.0`), not Docker-compat `v1.41`.
### PowerShell module
```powershell
dotnet build src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj
Import-Module .\src\PodmanClient.PowerShell\bin\Debug\net10.0\MaksIT.PodmanClientDotNet.PowerShell.psd1 -Force
Connect-Podman -BaseAddress $env:PODMAN_TEST_URL
```
See [src/PodmanClient.PowerShell/README.md](src/PodmanClient.PowerShell/README.md).
## Commit message format
@ -49,15 +123,14 @@ Types: `(feature):`, `(bugfix):`, `(refactor):`, `(perf):`, `(test):`, `(docs):`
- **MaksIT.Results** for API outcomes; **System.Text.Json** source generation via `PodmanJsonContext` for JSON serialization (AOT/trim-safe).
- File-scoped namespaces and same-line braces; **Models/** use nullable reference types (`string?`, `List<T>?`, …) for optional JSON fields.
- XML documentation on public types (DTOs, interfaces, entry types). Method-level docs on large interfaces are optional (`CS1591` suppressed).
- Model layout helpers: `utils/tools/Polish-PodmanClientSources.ps1`, `utils/tools/Enable-ModelsNullable.ps1`.
## Pull requests
1. Build and tests pass.
1. Build and unit tests pass; run E2E when changing the client or PowerShell surface if a Podman API is available.
2. Update **README.md** / **CHANGELOG.md** when behavior or public API changes.
3. Refresh **`assets/badges/*.svg`** when coverage badges change.
3. If coverage changed: ensure **README.md** shields.io badge lines were updated by the test engine.
4. Keep diffs scoped.
## Versioning
[Semantic Versioning](https://semver.org): bump `Version` in `src/PodmanClient/PodmanClientDotNet.csproj` with **CHANGELOG.md** for releases. Use `utils/Invoke-ReleasePackage.bat` when releasing.
[Semantic Versioning](https://semver.org): bump `Version` in `src/PodmanClient/PodmanClientDotNet.csproj` with **CHANGELOG.md** for releases. Use `utils\Invoke-ReleasePackage-Single.bat` when releasing.

View File

@ -1,6 +1,8 @@
# PodmanClient.DotNet
![Line Coverage](assets/badges/coverage-lines.svg) ![Branch Coverage](assets/badges/coverage-branches.svg) ![Method Coverage](assets/badges/coverage-methods.svg)
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-6.2%25-red)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-14.5%25-orange)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-4.4%25-red)
## Description
@ -37,7 +39,7 @@ dotnet add package PodmanClient.DotNet
{
"PodmanClient": {
"ServerUrl": "http://localhost:8080",
"ApiVersion": "v1.41",
"ApiVersion": "v5.4.0",
"TimeoutMinutes": 5
}
}
@ -50,7 +52,7 @@ using MaksIT.PodmanClientDotNet.Extensions;
// Host-owned options type (not shipped in this package)
public sealed class PodmanClientOptions : IPodmanClientConfiguration {
public string ServerUrl { get; set; } = string.Empty;
public string ApiVersion { get; set; } = "v1.41";
public string ApiVersion { get; set; } = "v5.4.0";
public int TimeoutMinutes { get; set; } = 60;
}
@ -162,16 +164,28 @@ Register with `AddPodmanClient` or construct `PodmanClient` manually. Methods re
API responses are typed under `Dtos/` (for example `ContainerInspectDto`, `ImageInspectDto`, `InfoDto`). Request/spec payloads remain in `Models/`.
## Tests
## PowerShell module
Unit tests cover multiplex framing, attach sessions, NDJSON progress, and a local hijack mock server. Integration tests require a reachable Podman API:
Binary module wrapping the full `IPodmanClient` surface — see [src/PodmanClient.PowerShell/README.md](src/PodmanClient.PowerShell/README.md).
```shell
$env:PODMAN_TEST_URL = "http://localhost:8080"
dotnet test src/PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj
```powershell
dotnet build src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj
Import-Module .\src\PodmanClient.PowerShell\bin\Debug\net10.0\MaksIT.PodmanClientDotNet.PowerShell.psd1 -Force
Connect-Podman -BaseAddress 'http://192.168.x.x:8080'
```
Without `PODMAN_TEST_URL` (or `PODMAN_INTEGRATION_URL`), integration tests are skipped automatically. Filter them in CI with `--filter "Category!=Integration"`.
## Tests
Unit tests cover multiplex framing, attach sessions, NDJSON progress, and a local hijack mock server (`dotnet test` / `utils/Invoke-TestEngine.bat`).
Live API E2E uses the PowerShell module against a reachable **Podman 5.4.0** API (baseline):
```powershell
$env:PODMAN_TEST_URL = "http://192.168.x.x:8080"
.\src\e2e-tests\Test-PodmanE2E.bat
```
Default client `ApiVersion` is `v5.4.0`. VM setup, firewall, and troubleshooting: [CONTRIBUTING.md](CONTRIBUTING.md).
**Note:** Full-duplex attach/exec sessions use a raw TCP hijack connection and do not flow through `HttpClient` delegating handlers (proxy, client certificates, etc.). Configure network access accordingly.

View File

@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 14.2%">
<title>Branch Coverage: 14.2%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="150" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="107.5" height="20" fill="#555"/>
<rect x="107.5" width="42.5" height="20" fill="#fe7d37"/>
<rect width="150" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">14.2%</text>
<text x="128.75" y="14" fill="#fff">14.2%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="134.5" height="20" role="img" aria-label="Line Coverage: 5%">
<title>Line Coverage: 5%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="134.5" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="94.5" height="20" fill="#555"/>
<rect x="94.5" width="40" height="20" fill="#e05d44"/>
<rect width="134.5" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
<text aria-hidden="true" x="114.5" y="15" fill="#010101" fill-opacity=".3">5%</text>
<text x="114.5" y="14" fill="#fff">5%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Method Coverage: 3.4%">
<title>Method Coverage: 3.4%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="147.5" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="107.5" height="20" fill="#555"/>
<rect x="107.5" width="40" height="20" fill="#e05d44"/>
<rect width="147.5" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">3.4%</text>
<text x="127.5" y="14" fill="#fff">3.4%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,186 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Build;
using MaksIT.PodmanClientDotNet.Streaming;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsLifecycle.Invoke, "PodmanBuildImage")]
[OutputType(typeof(BuildReportDto))]
public sealed class InvokePodmanBuildImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Dockerfile { get; set; } = null!;
[Parameter]
public string? ContextPath { get; set; }
[Parameter]
public Stream? ContextStream { get; set; }
[Parameter]
public SwitchParameter Pull { get; set; }
[Parameter]
public SwitchParameter Rm { get; set; } = true;
[Parameter]
public SwitchParameter ForceRm { get; set; }
[Parameter]
public SwitchParameter NoCache { get; set; }
[Parameter]
public string? Remote { get; set; }
[Parameter]
[Alias("t")]
public string? Tag { get; set; }
[Parameter]
public string? Platform { get; set; }
[Parameter]
public string? BuildArgs { get; set; }
[Parameter]
public string? Labels { get; set; }
protected override void ProcessRecord() {
Stream? owned = null;
try {
var client = RequireClient();
var context = ResolveContext(ref owned);
var result = client.BuildImageAsync(
Dockerfile,
context,
Pull.IsPresent,
Rm.IsPresent,
ForceRm.IsPresent,
NoCache.IsPresent,
Remote,
Tag,
Platform,
BuildArgs,
Labels
).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
finally {
owned?.Dispose();
}
}
private Stream? ResolveContext(ref Stream? owned) {
if (ContextStream is not null)
return ContextStream;
if (string.IsNullOrWhiteSpace(ContextPath))
return null;
owned = File.OpenRead(ContextPath);
return owned;
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanBuildImageProgress")]
[OutputType(typeof(BuildProgressLineDto))]
[OutputType(typeof(IPodmanProgressSession<BuildProgressLineDto>))]
public sealed class InvokePodmanBuildImageProgressCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Dockerfile { get; set; } = null!;
[Parameter]
public string? ContextPath { get; set; }
[Parameter]
public Stream? ContextStream { get; set; }
[Parameter]
public SwitchParameter Pull { get; set; }
[Parameter]
public SwitchParameter Rm { get; set; } = true;
[Parameter]
public SwitchParameter ForceRm { get; set; }
[Parameter]
public SwitchParameter NoCache { get; set; }
[Parameter]
public string? Remote { get; set; }
[Parameter]
[Alias("t")]
public string? Tag { get; set; }
[Parameter]
public string? Platform { get; set; }
[Parameter]
public string? BuildArgs { get; set; }
[Parameter]
public string? Labels { get; set; }
[Parameter]
public SwitchParameter Wait { get; set; } = true;
protected override void ProcessRecord() {
Stream? owned = null;
try {
var client = RequireClient();
var context = ResolveContext(ref owned);
var result = client.BuildImageWithProgressAsync(
Dockerfile,
context,
Pull.IsPresent,
Rm.IsPresent,
ForceRm.IsPresent,
NoCache.IsPresent,
Remote,
Tag,
Platform,
BuildArgs,
Labels
).GetAwaiter().GetResult();
if (!Wait.IsPresent) {
WritePodmanResult(result);
return;
}
if (!result.IsSuccess) {
WritePodmanResult(result);
return;
}
if (result.Value is null)
return;
var items = CollectProgress(result.Value);
WriteObject(items, true);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
finally {
owned?.Dispose();
}
}
private Stream? ResolveContext(ref Stream? owned) {
if (ContextStream is not null)
return ContextStream;
if (string.IsNullOrWhiteSpace(ContextPath))
return null;
owned = File.OpenRead(ContextPath);
return owned;
}
}

View File

@ -0,0 +1,30 @@
using System.Management.Automation;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommunications.Connect, "Podman")]
[OutputType(typeof(void))]
public sealed class ConnectPodmanCmdlet : PSCmdlet {
[Parameter(Mandatory = true, Position = 0)]
public string BaseAddress { get; set; } = null!;
[Parameter]
public int TimeoutMinutes { get; set; } = 60;
[Parameter]
public string? ApiVersion { get; set; }
protected override void ProcessRecord() {
PodmanConnectionState.SetConnection(BaseAddress, TimeoutMinutes, ApiVersion);
WriteVerbose($"Connected to Podman at {BaseAddress}");
}
}
[Cmdlet(VerbsCommunications.Disconnect, "Podman")]
[OutputType(typeof(void))]
public sealed class DisconnectPodmanCmdlet : PSCmdlet {
protected override void ProcessRecord() {
PodmanConnectionState.ClearConnection();
WriteVerbose("Disconnected from Podman");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,192 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Exec;
using MaksIT.PodmanClientDotNet.Streaming;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommon.New, "PodmanExec")]
[OutputType(typeof(CreateExecResponseDto))]
public sealed class NewPodmanExecCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string ContainerName { get; set; } = null!;
[Parameter(Mandatory = true, Position = 1)]
public string[] Cmd { get; set; } = null!;
[Parameter]
public bool AttachStderr { get; set; } = true;
[Parameter]
public SwitchParameter AttachStdin { get; set; }
[Parameter]
public bool AttachStdout { get; set; } = true;
[Parameter]
public string? DetachKeys { get; set; }
[Parameter]
public string[]? Env { get; set; }
[Parameter]
public SwitchParameter Privileged { get; set; }
[Parameter]
public SwitchParameter Tty { get; set; }
[Parameter]
public string? User { get; set; }
[Parameter]
public string? WorkingDir { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.CreateExecAsync(
ContainerName,
Cmd,
AttachStderr,
AttachStdin.IsPresent,
AttachStdout,
DetachKeys,
Env,
Privileged.IsPresent,
Tty.IsPresent,
User,
WorkingDir).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Start, "PodmanExec")]
public sealed class StartPodmanExecCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string ExecId { get; set; } = null!;
[Parameter]
public SwitchParameter Detach { get; set; }
[Parameter]
public SwitchParameter Tty { get; set; }
[Parameter]
public int? Height { get; set; }
[Parameter]
public int? Width { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.StartExecAsync(ExecId, Detach.IsPresent, Tty.IsPresent, Height, Width)
.GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanExec")]
[OutputType(typeof(InspectExecResponseDto))]
public sealed class GetPodmanExecCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string ExecId { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectExecAsync(ExecId).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Resize, "PodmanExec")]
public sealed class ResizePodmanExecCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string ExecId { get; set; } = null!;
[Parameter(Mandatory = true)]
public int Height { get; set; }
[Parameter(Mandatory = true)]
public int Width { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ResizeExecAsync(ExecId, Height, Width).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanExecSession")]
[OutputType(typeof(string))]
[OutputType(typeof(IPodmanAttachSession))]
public sealed class InvokePodmanExecSessionCmdlet : PodmanCmdletBase {
public InvokePodmanExecSessionCmdlet() {
CollectOutput = true;
}
[Parameter(Mandatory = true, Position = 0)]
public string ExecId { get; set; } = null!;
[Parameter]
public SwitchParameter Tty { get; set; }
[Parameter]
public int? Height { get; set; }
[Parameter]
public int? Width { get; set; }
[Parameter]
public SwitchParameter CollectOutput { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.StartExecSessionAsync(ExecId, Tty.IsPresent, Height, Width)
.GetAwaiter().GetResult();
if (!CollectOutput) {
WritePodmanResult(result);
return;
}
if (!result.IsSuccess) {
WritePodmanResult(result);
return;
}
if (result.Value is null)
return;
try {
WriteObject(CollectAttachOutput(result.Value));
}
finally {
result.Value.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,122 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Generate;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsLifecycle.Invoke, "PodmanGenerateSystemd")]
[OutputType(typeof(GenerateSystemdDto))]
public sealed class InvokePodmanGenerateSystemdCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public SwitchParameter UseName { get; set; }
[Parameter]
public SwitchParameter CreateNew { get; set; }
[Parameter]
public int? RestartSec { get; set; }
[Parameter]
public string? RestartPolicy { get; set; }
[Parameter]
public string? ContainerPrefix { get; set; }
[Parameter]
public string? PodPrefix { get; set; }
[Parameter]
public string? Separator { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GenerateSystemdAsync(
Name,
UseName.IsPresent,
CreateNew.IsPresent,
RestartSec,
RestartPolicy,
ContainerPrefix,
PodPrefix,
Separator
).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanGenerateKube")]
[OutputType(typeof(string))]
public sealed class InvokePodmanGenerateKubeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)]
public string[] Name { get; set; } = null!;
[Parameter]
public SwitchParameter Service { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GenerateKubeAsync(Name, Service.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPlayKube", DefaultParameterSetName = "Path")]
[OutputType(typeof(PlayKubeReportDto))]
public sealed class InvokePodmanPlayKubeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, ParameterSetName = "Path", Position = 0)]
public string? Path { get; set; }
[Parameter(Mandatory = true, ParameterSetName = "InputStream")]
public Stream? InputStream { get; set; }
[Parameter]
public string? Network { get; set; }
[Parameter]
public SwitchParameter TlsVerify { get; set; } = true;
[Parameter]
public SwitchParameter Start { get; set; } = true;
[Parameter]
public string? LogDriver { get; set; }
protected override void ProcessRecord() {
Stream? owned = null;
try {
var client = RequireClient();
var yaml = OpenInputStream(Path, InputStream);
if (!ReferenceEquals(yaml, InputStream))
owned = yaml;
var result = client.PlayKubeAsync(
yaml,
Network,
TlsVerify.IsPresent,
Start.IsPresent,
LogDriver
).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
finally {
owned?.Dispose();
}
}
}

View File

@ -0,0 +1,537 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.Image;
using MaksIT.PodmanClientDotNet.Streaming;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPullImage")]
public sealed class InvokePodmanPullImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Reference { get; set; } = null!;
[Parameter]
public bool TlsVerify { get; set; } = true;
[Parameter]
public SwitchParameter Quiet { get; set; }
[Parameter]
public string Policy { get; set; } = "always";
[Parameter]
public string? Arch { get; set; }
[Parameter]
public string? Os { get; set; }
[Parameter]
public string? Variant { get; set; }
[Parameter]
public SwitchParameter AllTags { get; set; }
[Parameter]
public string? AuthHeader { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PullImageAsync(
Reference,
TlsVerify,
Quiet.IsPresent,
Policy,
Arch,
Os,
Variant,
AllTags.IsPresent,
AuthHeader).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanTagImage")]
public sealed class InvokePodmanTagImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Image { get; set; } = null!;
[Parameter(Mandatory = true, Position = 1)]
public string Repo { get; set; } = null!;
[Parameter(Mandatory = true, Position = 2)]
public string Tag { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.TagImageAsync(Image, Repo, Tag).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanImageList")]
[OutputType(typeof(ImageListEntryDto))]
public sealed class GetPodmanImageListCmdlet : PodmanCmdletBase {
[Parameter]
public SwitchParameter All { get; set; }
[Parameter]
public string? Filters { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ListImagesAsync(All.IsPresent, Filters).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanImage")]
[OutputType(typeof(ImageInspectDto))]
public sealed class GetPodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectImageAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsDiagnostic.Test, "PodmanImage")]
[OutputType(typeof(bool))]
public sealed class TestPodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ImageExistsAsync(Name).GetAwaiter().GetResult();
WriteObject(result.IsSuccess);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanImage")]
[OutputType(typeof(ImageDeleteDto))]
public sealed class RemovePodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter]
public SwitchParameter Force { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.DeleteImageAsync(Name, Force.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanImageBatch")]
[OutputType(typeof(ImageDeleteDto))]
public sealed class RemovePodmanImageBatchCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true)]
public string[] Image { get; set; } = null!;
[Parameter]
public SwitchParameter All { get; set; }
[Parameter]
public SwitchParameter Force { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.RemoveImagesAsync(Image, All.IsPresent, Force.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPruneImage")]
[OutputType(typeof(PruneReportEntryDto))]
public sealed class InvokePodmanPruneImageCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PruneImagesAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Search, "PodmanImage")]
[OutputType(typeof(ImageSearchResultDto))]
public sealed class SearchPodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Term { get; set; } = null!;
[Parameter]
public int? Limit { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.SearchImagesAsync(Term, Limit).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPushImage")]
public sealed class InvokePodmanPushImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter]
public string? Destination { get; set; }
[Parameter]
public bool TlsVerify { get; set; } = true;
[Parameter]
public SwitchParameter Compress { get; set; }
[Parameter]
public string? AuthHeader { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PushImageAsync(Name, Destination, TlsVerify, Compress.IsPresent, AuthHeader)
.GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanUntagImage")]
public sealed class InvokePodmanUntagImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter]
public string? Repo { get; set; }
[Parameter]
public string? Tag { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.UntagImageAsync(Name, Repo, Tag).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanImageHistory")]
[OutputType(typeof(ImageHistoryEntryDto))]
public sealed class GetPodmanImageHistoryCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetImageHistoryAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanImageTree")]
[OutputType(typeof(ImageTreeDto))]
public sealed class GetPodmanImageTreeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetImageTreeAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanImageChange")]
[OutputType(typeof(ImageChangesDto))]
public sealed class GetPodmanImageChangeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetImageChangesAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsData.Import, "PodmanImage")]
[OutputType(typeof(ImageImportDto))]
public sealed class ImportPodmanImageCmdlet : PodmanCmdletBase {
[Parameter]
public string? Path { get; set; }
[Parameter]
public Stream? InputStream { get; set; }
[Parameter]
public string? Changes { get; set; }
[Parameter]
public string? Message { get; set; }
[Parameter]
public string? Reference { get; set; }
[Parameter]
public string? Url { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
Stream? stream = null;
var ownsStream = false;
if (InputStream is not null || !string.IsNullOrWhiteSpace(Path)) {
stream = OpenInputStream(Path, InputStream);
ownsStream = InputStream is null;
}
try {
var result = client.ImportImageAsync(stream, Changes, Message, Reference, Url)
.GetAwaiter().GetResult();
WritePodmanResult(result);
}
finally {
if (ownsStream)
stream?.Dispose();
}
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsData.Import, "PodmanImageArchive")]
[OutputType(typeof(ImageLoadDto))]
public sealed class ImportPodmanImageArchiveCmdlet : PodmanCmdletBase {
[Parameter]
public string? Path { get; set; }
[Parameter]
public Stream? InputStream { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var stream = OpenInputStream(Path, InputStream);
var ownsStream = InputStream is null;
try {
var result = client.LoadImageAsync(stream).GetAwaiter().GetResult();
WritePodmanResult(result);
}
finally {
if (ownsStream)
stream.Dispose();
}
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsData.Export, "PodmanImage")]
[OutputType(typeof(Stream))]
[OutputType(typeof(string))]
public sealed class ExportPodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true)]
public string[] Reference { get; set; } = null!;
[Parameter]
public string? Format { get; set; }
[Parameter]
public SwitchParameter Compress { get; set; }
[Parameter]
public string? OutFile { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ExportImagesAsync(Reference, Format, Compress.IsPresent).GetAwaiter().GetResult();
WritePodmanStream(result, OutFile);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsData.Save, "PodmanImage")]
[OutputType(typeof(Stream))]
[OutputType(typeof(string))]
public sealed class SavePodmanImageCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter]
public string? Format { get; set; }
[Parameter]
public SwitchParameter Compress { get; set; }
[Parameter]
public string? OutFile { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetImageAsync(Name, Format, Compress.IsPresent).GetAwaiter().GetResult();
WritePodmanStream(result, OutFile);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPullImageProgress")]
[OutputType(typeof(PullImageResponseDto))]
[OutputType(typeof(IPodmanProgressSession<PullImageResponseDto>))]
public sealed class InvokePodmanPullImageProgressCmdlet : PodmanCmdletBase {
public InvokePodmanPullImageProgressCmdlet() {
Wait = true;
}
[Parameter(Mandatory = true, Position = 0)]
public string Reference { get; set; } = null!;
[Parameter]
public bool TlsVerify { get; set; } = true;
[Parameter]
public SwitchParameter Quiet { get; set; }
[Parameter]
public string Policy { get; set; } = "always";
[Parameter]
public string? Arch { get; set; }
[Parameter]
public string? Os { get; set; }
[Parameter]
public string? Variant { get; set; }
[Parameter]
public SwitchParameter AllTags { get; set; }
[Parameter]
public string? AuthHeader { get; set; }
[Parameter]
public SwitchParameter Wait { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PullImageWithProgressAsync(
Reference,
TlsVerify,
Quiet.IsPresent,
Policy,
Arch,
Os,
Variant,
AllTags.IsPresent,
AuthHeader).GetAwaiter().GetResult();
if (!Wait) {
WritePodmanResult(result);
return;
}
if (!result.IsSuccess) {
WritePodmanResult(result);
return;
}
if (result.Value is null)
return;
try {
foreach (var item in CollectProgress(result.Value))
WriteObject(item);
}
finally {
result.Value.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,125 @@
@{
RootModule = 'MaksIT.PodmanClientDotNet.PowerShell.dll'
ModuleVersion = '1.3.0'
GUID = 'b7e4c2a1-9f3d-4e8b-a6c5-1d2e3f4a5b6c'
Author = 'Maksym Sadovnychyy'
CompanyName = 'MAKS-IT'
Description = 'PowerShell cmdlets for PodmanClient.DotNet. Use Connect-Podman -BaseAddress, then domain cmdlets mirroring IPodmanClient.'
PowerShellVersion = '7.0'
RequiredModules = @()
RequiredAssemblies = @('MaksIT.PodmanClientDotNet.PowerShell.dll')
FunctionsToExport = @()
CmdletsToExport = @(
'Add-PodmanManifest',
'Checkpoint-PodmanContainer',
'Connect-Podman',
'Connect-PodmanNetwork',
'Disconnect-Podman',
'Disconnect-PodmanNetwork',
'Dismount-PodmanContainer',
'Export-PodmanContainer',
'Export-PodmanImage',
'Get-PodmanContainer',
'Get-PodmanContainerArchive',
'Get-PodmanContainerChange',
'Get-PodmanContainerList',
'Get-PodmanContainerLog',
'Get-PodmanContainerProcess',
'Get-PodmanContainerStat',
'Get-PodmanContainerStatBatch',
'Get-PodmanContainerTop',
'Get-PodmanEvent',
'Get-PodmanExec',
'Get-PodmanImage',
'Get-PodmanImageChange',
'Get-PodmanImageHistory',
'Get-PodmanImageList',
'Get-PodmanImageTree',
'Get-PodmanInfo',
'Get-PodmanManifest',
'Get-PodmanMountedContainer',
'Get-PodmanNetwork',
'Get-PodmanNetworkList',
'Get-PodmanPod',
'Get-PodmanPodList',
'Get-PodmanPodStat',
'Get-PodmanPodTop',
'Get-PodmanSystemDiskUsage',
'Get-PodmanVersion',
'Get-PodmanVolume',
'Get-PodmanVolumeList',
'Import-PodmanImage',
'Import-PodmanImageArchive',
'Initialize-PodmanContainer',
'Invoke-PodmanBuildImage',
'Invoke-PodmanBuildImageProgress',
'Invoke-PodmanCommitContainer',
'Invoke-PodmanContainerAttach',
'Invoke-PodmanContainerHealthCheck',
'Invoke-PodmanContainerSession',
'Invoke-PodmanExecSession',
'Invoke-PodmanExtractArchive',
'Invoke-PodmanGenerateKube',
'Invoke-PodmanGenerateSystemd',
'Invoke-PodmanPlayKube',
'Invoke-PodmanPruneContainer',
'Invoke-PodmanPruneImage',
'Invoke-PodmanPrunePod',
'Invoke-PodmanPruneSystem',
'Invoke-PodmanPruneVolume',
'Invoke-PodmanPullImage',
'Invoke-PodmanPullImageProgress',
'Invoke-PodmanPushImage',
'Invoke-PodmanPushManifest',
'Invoke-PodmanPutContainerArchive',
'Invoke-PodmanTagImage',
'Invoke-PodmanUntagImage',
'Kill-PodmanContainer',
'Kill-PodmanPod',
'Mount-PodmanContainer',
'New-PodmanContainer',
'New-PodmanExec',
'New-PodmanManifest',
'New-PodmanNetwork',
'New-PodmanPod',
'New-PodmanVolume',
'Publish-PodmanManifest',
'Remove-PodmanContainer',
'Remove-PodmanImage',
'Remove-PodmanImageBatch',
'Remove-PodmanManifest',
'Remove-PodmanNetwork',
'Remove-PodmanPod',
'Remove-PodmanVolume',
'Rename-PodmanContainer',
'Resize-PodmanExec',
'Restart-PodmanContainer',
'Restart-PodmanPod',
'Restore-PodmanContainer',
'Resume-PodmanContainer',
'Resume-PodmanPod',
'Save-PodmanImage',
'Search-PodmanImage',
'Set-PodmanContainerArchive',
'Start-PodmanContainer',
'Start-PodmanExec',
'Start-PodmanPod',
'Stop-PodmanContainer',
'Stop-PodmanPod',
'Suspend-PodmanContainer',
'Suspend-PodmanPod',
'Test-PodmanConnection',
'Test-PodmanContainer',
'Test-PodmanImage',
'Test-PodmanPod',
'Wait-PodmanContainer'
)
VariablesToExport = @()
AliasesToExport = @()
PrivateData = @{
PSData = @{
Tags = @('Podman', 'Container', 'MaksIT', 'API')
ProjectUri = 'https://github.com/MAKS-IT-COM/podman-client-dotnet'
}
}
}

View File

@ -0,0 +1,164 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Manifest;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommon.New, "PodmanManifest")]
[OutputType(typeof(ManifestCreateDto))]
public sealed class NewPodmanManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter]
public string? Image { get; set; }
[Parameter]
public SwitchParameter All { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.CreateManifestAsync(Name, Image, All.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanManifest", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class RemovePodmanManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public string? Digest { get; set; }
protected override void ProcessRecord() {
try {
if (!ShouldProcess(Name, "Remove Podman manifest"))
return;
var client = RequireClient();
var result = client.DeleteManifestAsync(Name, Digest).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanManifest")]
[OutputType(typeof(ManifestInspectDto))]
public sealed class GetPodmanManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectManifestAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Add, "PodmanManifest")]
[OutputType(typeof(void))]
public sealed class AddPodmanManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter(Position = 1)]
public string? Image { get; set; }
[Parameter]
public SwitchParameter All { get; set; }
[Parameter]
public string? Operation { get; set; }
[Parameter]
public ManifestAddRequestDto? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new ManifestAddRequestDto {
Images = string.IsNullOrWhiteSpace(Image) ? null : [Image],
Image = Image,
All = All.IsPresent,
Operation = Operation ?? "update",
};
if (Request is null && (request.Images is null || request.Images.Count == 0) && string.IsNullOrWhiteSpace(request.Image))
throw new ArgumentException("Specify -Image or -Request.");
if (request.Images is null && !string.IsNullOrWhiteSpace(request.Image))
request.Images = [request.Image];
if (string.IsNullOrWhiteSpace(request.Operation))
request.Operation = "update";
var result = client.AddToManifestAsync(Name, request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsData.Publish, "PodmanManifest")]
[OutputType(typeof(void))]
public sealed class PublishPodmanManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter(Mandatory = true, Position = 1)]
public string Destination { get; set; } = null!;
[Parameter]
public SwitchParameter All { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PushManifestAsync(Name, Destination, All.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPushManifest")]
[OutputType(typeof(void))]
public sealed class InvokePodmanPushManifestCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter(Mandatory = true, Position = 1)]
public string Destination { get; set; } = null!;
[Parameter]
public SwitchParameter All { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PushManifestAsync(Name, Destination, All.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,147 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Network;
using MaksIT.PodmanClientDotNet.Models.Network;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommon.New, "PodmanNetwork")]
[OutputType(typeof(NetworkListEntryDto))]
public sealed class NewPodmanNetworkCmdlet : PodmanCmdletBase {
[Parameter(Position = 0)]
public string? Name { get; set; }
[Parameter]
public NetworkCreateRequest? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new NetworkCreateRequest();
if (!string.IsNullOrWhiteSpace(Name))
request.Name = Name;
var result = client.CreateNetworkAsync(request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanNetworkList")]
[OutputType(typeof(NetworkListEntryDto))]
public sealed class GetPodmanNetworkListCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ListNetworksAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanNetwork")]
[OutputType(typeof(NetworkInspectDto))]
public sealed class GetPodmanNetworkCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectNetworkAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanNetwork", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class RemovePodmanNetworkCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
if (!ShouldProcess(Name, "Remove Podman network"))
return;
var client = RequireClient();
var result = client.DeleteNetworkAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommunications.Connect, "PodmanNetwork")]
[OutputType(typeof(void))]
public sealed class ConnectPodmanNetworkCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter(Position = 1)]
public string? Container { get; set; }
[Parameter]
public NetworkConnectRequest? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new NetworkConnectRequest();
if (!string.IsNullOrWhiteSpace(Container))
request.Container = Container;
var result = client.ConnectNetworkAsync(Name, request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommunications.Disconnect, "PodmanNetwork")]
[OutputType(typeof(void))]
public sealed class DisconnectPodmanNetworkCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; } = null!;
[Parameter(Position = 1)]
public string? Container { get; set; }
[Parameter]
public SwitchParameter Force { get; set; }
[Parameter]
public NetworkDisconnectRequest? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new NetworkDisconnectRequest();
if (!string.IsNullOrWhiteSpace(Container))
request.Container = Container;
if (Force.IsPresent)
request.Force = true;
var result = client.DisconnectNetworkAsync(Name, request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>MaksIT.PodmanClientDotNet.PowerShell</RootNamespace>
<AssemblyName>MaksIT.PodmanClientDotNet.PowerShell</AssemblyName>
<Description>PowerShell module with cmdlets for PodmanClient.DotNet.</Description>
<Version>1.3.0</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<EnableDynamicLoading>true</EnableDynamicLoading>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Management.Automation" Version="7.6.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PodmanClient\PodmanClientDotNet.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="MaksIT.PodmanClientDotNet.PowerShell.psd1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,111 @@
using System.Management.Automation;
using System.Text;
using MaksIT.PodmanClientDotNet.Streaming;
using MaksIT.Results;
namespace MaksIT.PodmanClientDotNet.PowerShell;
public abstract class PodmanCmdletBase : PSCmdlet {
protected IPodmanClient RequireClient() {
var client = PodmanConnectionState.Client;
if (client is null) {
ThrowTerminatingError(new ErrorRecord(
new InvalidOperationException("Not connected. Run Connect-Podman -BaseAddress <url> first."),
"NotConnected",
ErrorCategory.InvalidOperation,
null));
}
return client;
}
protected void WritePodmanResult(Result result) {
if (result.IsSuccess)
return;
WriteError(new ErrorRecord(
new InvalidOperationException(string.Join("; ", result.Messages)),
"PodmanApiError",
ErrorCategory.InvalidOperation,
null));
}
protected void WritePodmanResult<T>(Result<T?> result) {
if (!result.IsSuccess) {
WriteError(new ErrorRecord(
new InvalidOperationException(string.Join("; ", result.Messages)),
"PodmanApiError",
ErrorCategory.InvalidOperation,
null));
return;
}
if (result.Value is null)
return;
// Avoid PowerShell enumerating list results onto the host unexpectedly.
if (result.Value is System.Collections.IEnumerable and not string and not System.Collections.IDictionary)
WriteObject(result.Value, enumerateCollection: false);
else
WriteObject(result.Value);
}
protected void WritePodmanStream(Result<Stream?> result, string? outFile) {
if (!result.IsSuccess) {
WriteError(new ErrorRecord(
new InvalidOperationException(string.Join("; ", result.Messages)),
"PodmanApiError",
ErrorCategory.InvalidOperation,
null));
return;
}
if (result.Value is null)
return;
if (!string.IsNullOrWhiteSpace(outFile)) {
using (result.Value)
using (var fs = File.Create(outFile))
result.Value.CopyTo(fs);
WriteObject(outFile);
return;
}
WriteObject(result.Value);
}
protected static Stream OpenInputStream(string? path, Stream? inputStream) {
if (inputStream is not null)
return inputStream;
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Specify -Path or -InputStream.");
return File.OpenRead(path);
}
protected static string CollectAttachOutput(IPodmanAttachSession session) {
var output = new StringBuilder();
while (true) {
var frame = session.ReadFrameAsync().GetAwaiter().GetResult();
if (frame is null)
break;
output.Append(Encoding.UTF8.GetString(frame.Data));
}
return output.ToString();
}
protected static List<T> CollectProgress<T>(IPodmanProgressSession<T> session) {
var items = new List<T>();
var enumerator = session.ReadProgressAsync().GetAsyncEnumerator();
try {
while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())
items.Add(enumerator.Current);
}
finally {
enumerator.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
return items;
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.Extensions.Logging.Abstractions;
namespace MaksIT.PodmanClientDotNet.PowerShell;
/// <summary>Holds the current Podman client for the PowerShell session.</summary>
internal static class PodmanConnectionState {
private static readonly Lock Lock = new();
private static HttpClient? _httpClient;
public static IPodmanClient? Client { get; private set; }
public static void SetConnection(string baseAddress, int timeoutMinutes = 60, string? apiVersion = null) {
lock (Lock) {
_httpClient?.Dispose();
_httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(Math.Max(1, timeoutMinutes)) };
var configuration = new PodmanClientSessionConfiguration {
ServerUrl = baseAddress,
ApiVersion = string.IsNullOrWhiteSpace(apiVersion) ? "v5.4.0" : apiVersion,
TimeoutMinutes = timeoutMinutes
};
Client = new PodmanClient(_httpClient, NullLogger<PodmanClient>.Instance, configuration);
}
}
public static void ClearConnection() {
lock (Lock) {
Client = null;
_httpClient?.Dispose();
_httpClient = null;
}
}
private sealed class PodmanClientSessionConfiguration : IPodmanClientConfiguration {
public string ServerUrl { get; set; } = "";
public string ApiVersion { get; set; } = "v5.4.0";
public int TimeoutMinutes { get; set; } = 60;
}
}

View File

@ -0,0 +1,276 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.Pod;
using MaksIT.PodmanClientDotNet.Models.Pod;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommon.New, "PodmanPod")]
[OutputType(typeof(PodListEntryDto))]
public sealed class NewPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Position = 0)]
public string? Name { get; set; }
[Parameter]
public PodCreateRequest? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new PodCreateRequest();
if (!string.IsNullOrWhiteSpace(Name))
request.Name = Name;
var result = client.CreatePodAsync(request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanPodList")]
[OutputType(typeof(PodListEntryDto))]
public sealed class GetPodmanPodListCmdlet : PodmanCmdletBase {
[Parameter]
public SwitchParameter All { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ListPodsAsync(All.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanPod")]
[OutputType(typeof(PodInspectDto))]
public sealed class GetPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectPodAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsDiagnostic.Test, "PodmanPod")]
[OutputType(typeof(bool))]
public sealed class TestPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PodExistsAsync(Name).GetAwaiter().GetResult();
WriteObject(result.IsSuccess);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanPod", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class RemovePodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public SwitchParameter Force { get; set; }
protected override void ProcessRecord() {
try {
if (!ShouldProcess(Name, "Remove Podman pod"))
return;
var client = RequireClient();
var result = client.DeletePodAsync(Name, Force.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Start, "PodmanPod")]
[OutputType(typeof(void))]
public sealed class StartPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.StartPodAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Stop, "PodmanPod")]
[OutputType(typeof(void))]
public sealed class StopPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public int Timeout { get; set; } = 10;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.StopPodAsync(Name, Timeout).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Restart, "PodmanPod")]
[OutputType(typeof(void))]
public sealed class RestartPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public int Timeout { get; set; } = 10;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.RestartPodAsync(Name, Timeout).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet("Kill", "PodmanPod")]
[OutputType(typeof(void))]
public sealed class KillPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public string? Signal { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.KillPodAsync(Name, Signal).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Suspend, "PodmanPod")]
[OutputType(typeof(void))]
public sealed class SuspendPodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PausePodAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Resume, "PodmanPod")]
[OutputType(typeof(void))]
public sealed class ResumePodmanPodCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.UnpausePodAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPrunePod")]
[OutputType(typeof(PruneReportEntryDto))]
public sealed class InvokePodmanPrunePodCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PrunePodsAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanPodTop")]
[OutputType(typeof(PodTopDto))]
public sealed class GetPodmanPodTopCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.TopPodAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanPodStat")]
[OutputType(typeof(PodStatsDto))]
public sealed class GetPodmanPodStatCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetPodsStatsAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,21 @@
# MaksIT.PodmanClientDotNet.PowerShell
Binary PowerShell module wrapping **PodmanClient.DotNet** (`IPodmanClient`).
## Requirements
- PowerShell 7 hosted on **.NET 10**
- Reachable Podman REST API (`podman system service`) — E2E baseline **Podman 5.4.0** (`Connect-Podman` default `ApiVersion` = `v5.4.0`)
## Quick start
```powershell
dotnet build src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj
Import-Module .\src\PodmanClient.PowerShell\bin\Debug\net10.0\MaksIT.PodmanClientDotNet.PowerShell.psd1 -Force
Connect-Podman -BaseAddress 'http://192.168.2.128:8080' -ApiVersion 'v5.4.0'
Test-PodmanConnection
Get-PodmanVersion
Disconnect-Podman
```
Cmdlets mirror the .NET client domains (system, images, containers, exec, volumes, networks, pods, build, manifests, generate). See `CmdletsToExport` in the `.psd1` and live E2E under `src/e2e-tests/`.

View File

@ -0,0 +1,135 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.System;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsDiagnostic.Test, "PodmanConnection")]
[OutputType(typeof(LibpodPingDto))]
public sealed class TestPodmanConnectionCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PingAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanVersion")]
[OutputType(typeof(LibpodVersionDto))]
public sealed class GetPodmanVersionCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetVersionAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanInfo")]
[OutputType(typeof(InfoDto))]
public sealed class GetPodmanInfoCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetInfoAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanSystemDiskUsage")]
[OutputType(typeof(SystemDfDto))]
public sealed class GetPodmanSystemDiskUsageCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetSystemDiskUsageAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPruneSystem")]
[OutputType(typeof(SystemPruneReportDto))]
public sealed class InvokePodmanPruneSystemCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PruneSystemAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanEvent")]
[OutputType(typeof(Stream))]
[OutputType(typeof(string))]
public sealed class GetPodmanEventCmdlet : PodmanCmdletBase {
[Parameter]
public string? OutFile { get; set; }
/// <summary>Max time to read from the events stream when <see cref="OutFile"/> is set (stream is otherwise open-ended).</summary>
[Parameter]
public int ReadTimeoutSeconds { get; set; } = 2;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.GetEventsAsync().GetAwaiter().GetResult();
if (!result.IsSuccess) {
WritePodmanResult(result);
return;
}
if (result.Value is null)
return;
if (string.IsNullOrWhiteSpace(OutFile)) {
WriteObject(result.Value);
return;
}
using (result.Value)
using (var fs = File.Create(OutFile)) {
var buffer = new byte[8192];
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(Math.Max(1, ReadTimeoutSeconds)));
try {
while (true) {
var read = result.Value.ReadAsync(buffer.AsMemory(0, buffer.Length), cts.Token).AsTask().GetAwaiter().GetResult();
if (read <= 0)
break;
fs.Write(buffer, 0, read);
}
}
catch (OperationCanceledException) {
// Timed sample of the open-ended events stream.
}
}
WriteObject(OutFile);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -0,0 +1,108 @@
using System.Management.Automation;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.Volume;
using MaksIT.PodmanClientDotNet.Models.Volume;
namespace MaksIT.PodmanClientDotNet.PowerShell;
[Cmdlet(VerbsCommon.New, "PodmanVolume")]
[OutputType(typeof(VolumeInspectResponseDto))]
public sealed class NewPodmanVolumeCmdlet : PodmanCmdletBase {
[Parameter(Position = 0)]
public string? Name { get; set; }
[Parameter]
public string? Driver { get; set; }
[Parameter]
public CreateVolumeRequest? Request { get; set; }
protected override void ProcessRecord() {
try {
var client = RequireClient();
var request = Request ?? new CreateVolumeRequest {
Name = Name,
Driver = Driver,
};
var result = client.CreateVolumeAsync(request).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanVolumeList")]
[OutputType(typeof(VolumeListEntryDto))]
public sealed class GetPodmanVolumeListCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.ListVolumesAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Get, "PodmanVolume")]
[OutputType(typeof(VolumeInspectResponseDto))]
public sealed class GetPodmanVolumeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.InspectVolumeAsync(Name).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsCommon.Remove, "PodmanVolume", SupportsShouldProcess = true)]
[OutputType(typeof(void))]
public sealed class RemovePodmanVolumeCmdlet : PodmanCmdletBase {
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Name { get; set; } = null!;
[Parameter]
public SwitchParameter Force { get; set; }
protected override void ProcessRecord() {
try {
if (!ShouldProcess(Name, "Remove Podman volume"))
return;
var client = RequireClient();
var result = client.DeleteVolumeAsync(Name, Force.IsPresent).GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}
[Cmdlet(VerbsLifecycle.Invoke, "PodmanPruneVolume")]
[OutputType(typeof(PruneReportEntryDto))]
public sealed class InvokePodmanPruneVolumeCmdlet : PodmanCmdletBase {
protected override void ProcessRecord() {
try {
var client = RequireClient();
var result = client.PruneVolumesAsync().GetAwaiter().GetResult();
WritePodmanResult(result);
}
catch (Exception ex) {
WriteError(new ErrorRecord(ex, "PodmanApiError", ErrorCategory.NotSpecified, null));
}
}
}

View File

@ -1,7 +1,6 @@
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.Container;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Models.Container;
using MaksIT.PodmanClientDotNet.Streaming;
using MaksIT.Results;
@ -163,13 +162,13 @@ public interface IPodmanContainersClient {
);
Task<Result<ContainerStatsDto?>> GetContainerStatsAsync(string name, bool stream = false, CancellationToken cancellationToken = default);
Task<Result<Dictionary<string, ContainerStatsDto>?>> GetContainersStatsAsync(
Task<Result<ContainersStatsResponseDto?>> GetContainersStatsAsync(
IEnumerable<string>? containers = null,
bool stream = false,
CancellationToken cancellationToken = default
);
Task<Result<PruneReportDto?>> PruneContainersAsync(string? filters = null, CancellationToken cancellationToken = default);
Task<Result<List<PruneReportEntryDto>?>> PruneContainersAsync(string? filters = null, CancellationToken cancellationToken = default);
Task<Result> RenameContainerAsync(string name, string newName, CancellationToken cancellationToken = default);
Task<Result> InitContainerAsync(string name, CancellationToken cancellationToken = default);
Task<Result<Stream?>> CheckpointContainerAsync(
@ -239,6 +238,6 @@ public interface IPodmanContainersClient {
);
Task<Result<ContainerHealthCheckDto?>> HealthCheckContainerAsync(string name, CancellationToken cancellationToken = default);
Task<Result<MountedContainersResponseDto?>> GetMountedContainersAsync(CancellationToken cancellationToken = default);
Task<Result<List<Dictionary<string, string>>?>> GetMountedContainersAsync(CancellationToken cancellationToken = default);
Task<Result<ContainerTopDto?>> TopContainerAsync(string name, string psArgs = "-ef", bool stream = true, CancellationToken cancellationToken = default);
}

View File

@ -29,15 +29,15 @@ public interface IPodmanImagesClient {
Task<Result<ImageInspectDto?>> InspectImageAsync(string name, CancellationToken cancellationToken = default);
Task<Result> ImageExistsAsync(string name, CancellationToken cancellationToken = default);
Task<Result<ImageDeleteDto[]?>> DeleteImageAsync(string name, bool force = false, CancellationToken cancellationToken = default);
Task<Result<ImageDeleteDto[]?>> RemoveImagesAsync(
Task<Result<ImageDeleteDto?>> DeleteImageAsync(string name, bool force = false, CancellationToken cancellationToken = default);
Task<Result<ImageDeleteDto?>> RemoveImagesAsync(
IEnumerable<string> images,
bool all = false,
bool force = false,
CancellationToken cancellationToken = default
);
Task<Result<PruneReportDto?>> PruneImagesAsync(CancellationToken cancellationToken = default);
Task<Result<List<PruneReportEntryDto>?>> PruneImagesAsync(CancellationToken cancellationToken = default);
Task<Result<List<ImageSearchResultDto>?>> SearchImagesAsync(string term, int? limit = null, CancellationToken cancellationToken = default);
Task<Result> PushImageAsync(
string name,

View File

@ -18,7 +18,7 @@ public interface IPodmanPodsClient {
Task<Result> KillPodAsync(string name, string? signal = null, CancellationToken cancellationToken = default);
Task<Result> PausePodAsync(string name, CancellationToken cancellationToken = default);
Task<Result> UnpausePodAsync(string name, CancellationToken cancellationToken = default);
Task<Result<PruneReportDto?>> PrunePodsAsync(CancellationToken cancellationToken = default);
Task<Result<List<PruneReportEntryDto>?>> PrunePodsAsync(CancellationToken cancellationToken = default);
Task<Result<PodTopDto?>> TopPodAsync(string name, CancellationToken cancellationToken = default);
Task<Result<PodStatsResponseDto?>> GetPodsStatsAsync(CancellationToken cancellationToken = default);
Task<Result<List<PodStatsDto>?>> GetPodsStatsAsync(CancellationToken cancellationToken = default);
}

View File

@ -10,6 +10,6 @@ public interface IPodmanSystemClient {
Task<Result<LibpodVersionDto?>> GetVersionAsync(CancellationToken cancellationToken = default);
Task<Result<InfoDto?>> GetInfoAsync(CancellationToken cancellationToken = default);
Task<Result<SystemDfDto?>> GetSystemDiskUsageAsync(CancellationToken cancellationToken = default);
Task<Result<PruneReportDto?>> PruneSystemAsync(CancellationToken cancellationToken = default);
Task<Result<SystemPruneReportDto?>> PruneSystemAsync(CancellationToken cancellationToken = default);
Task<Result<Stream?>> GetEventsAsync(CancellationToken cancellationToken = default);
}

View File

@ -11,5 +11,5 @@ public interface IPodmanVolumesClient {
Task<Result<List<VolumeListEntryDto>?>> ListVolumesAsync(CancellationToken cancellationToken = default);
Task<Result<VolumeInspectResponseDto?>> InspectVolumeAsync(string name, CancellationToken cancellationToken = default);
Task<Result> DeleteVolumeAsync(string name, bool force = false, CancellationToken cancellationToken = default);
Task<Result<PruneReportDto?>> PruneVolumesAsync(CancellationToken cancellationToken = default);
Task<Result<List<PruneReportEntryDto>?>> PruneVolumesAsync(CancellationToken cancellationToken = default);
}

View File

@ -1,6 +1,13 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Common;
/// <summary>Prune operation report (containers, images, volumes, pods, networks).</summary>
/// <summary>Single entry from libpod prune endpoints that return a JSON array.</summary>
public sealed class PruneReportEntryDto {
public string? Id { get; set; }
public long Size { get; set; }
public string? Err { get; set; }
}
/// <summary>Legacy aggregate prune fields (kept for compatibility where applicable).</summary>
public sealed class PruneReportDto {
public string[]? Id { get; set; }
public string[]? IdDeleted { get; set; }
@ -12,3 +19,13 @@ public sealed class PruneReportDto {
public string[]? ImagesDeleted { get; set; }
public string[]? ContainersDeleted { get; set; }
}
/// <summary>Response from <c>POST /libpod/system/prune</c>.</summary>
public sealed class SystemPruneReportDto {
public List<PruneReportEntryDto>? PodPruneReport { get; set; }
public List<PruneReportEntryDto>? ContainerPruneReports { get; set; }
public List<PruneReportEntryDto>? ImagePruneReports { get; set; }
public List<PruneReportEntryDto>? NetworkPruneReports { get; set; }
public List<PruneReportEntryDto>? VolumePruneReports { get; set; }
public long ReclaimedSpace { get; set; }
}

View File

@ -1,5 +1,15 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>Podman returns a JSON array of filesystem change paths.</summary>
public sealed class ContainerChangesDto : List<string> {
/// <summary>
/// Single filesystem change entry from container changes.
/// </summary>
public sealed class ContainerChangeEntryDto {
public string? Path { get; set; }
public int Kind { get; set; }
}
/// <summary>
/// Podman returns a JSON array of path/kind change objects.
/// </summary>
public sealed class ContainerChangesDto : List<ContainerChangeEntryDto> {
}

View File

@ -1,8 +1,8 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>
/// Deserialized Podman libpod API payload (Container Inspect).
/// </summary>
public sealed class ContainerInspectDto {
public string? Id { get; set; }
public string? Name { get; set; }
@ -21,12 +21,12 @@ public sealed class ContainerInspectDto {
public string? Driver { get; set; }
public string? OCIConfigPath { get; set; }
public string? OCIRuntime { get; set; }
public long Created { get; set; }
public string? Created { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Container State).
/// </summary>
public sealed class ContainerStateDto {
public string? Status { get; set; }
public bool Running { get; set; }
@ -39,12 +39,11 @@ public sealed class ContainerStateDto {
public string? Error { get; set; }
public string? StartedAt { get; set; }
public string? FinishedAt { get; set; }
public string? Health { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Container Config).
/// </summary>
public sealed class ContainerConfigDto {
public string? Hostname { get; set; }
public string? Domainname { get; set; }
@ -52,7 +51,7 @@ public sealed class ContainerConfigDto {
public bool AttachStdin { get; set; }
public bool AttachStdout { get; set; }
public bool AttachStderr { get; set; }
public string? Tty { get; set; }
public bool Tty { get; set; }
public bool OpenStdin { get; set; }
public bool StdinOnce { get; set; }
public string[]? Env { get; set; }

View File

@ -1,18 +1,20 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>
/// Deserialized Podman libpod API payload (Container List Entry).
/// </summary>
public sealed class ContainerListEntryDto {
public string? Id { get; set; }
public string[]? Names { get; set; }
public string? Image { get; set; }
public string? ImageID { get; set; }
public string? Command { get; set; }
public long Created { get; set; }
public string[]? Command { get; set; }
public string? Created { get; set; }
public string? State { get; set; }
public string? Status { get; set; }
public string? Pod { get; set; }
public string? PodName { get; set; }
public bool AutoRemove { get; set; }
public long Pid { get; set; }
public string[]? Networks { get; set; }
}

View File

@ -1,8 +1,8 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>
/// Deserialized Podman libpod API payload (Container Mount).
/// </summary>
/// <summary>
/// Result of mounting a container's root filesystem (libpod returns a plain path string).
/// </summary>
public sealed class ContainerMountDto {
public string? Id { get; set; }
public string? Path { get; set; }
}

View File

@ -1,41 +1,96 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>
/// Deserialized Podman libpod API payload (Container Stats).
/// </summary>
public sealed class ContainerStatsDto {
public string? Name { get; set; }
public string? Id { get; set; }
public ContainerStatsCpuDto? CpuStats { get; set; }
public ContainerStatsMemoryDto? MemoryStats { get; set; }
public ContainerStatsNetworkDto[]? Networks { get; set; }
public string? Read { get; set; }
public string? Preread { get; set; }
public ContainerStatsCpuBlockDto? CpuStats { get; set; }
public ContainerStatsCpuBlockDto? PrecpuStats { get; set; }
public ContainerStatsMemoryDto? MemoryStats { get; set; }
public Dictionary<string, ContainerStatsNetworkDto>? Networks { get; set; }
public ContainerStatsPidsDto? PidsStats { get; set; }
public int NumProcs { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Container Stats Cpu).
/// </summary>
public sealed class ContainerStatsCpuDto {
/// <summary>
/// CPU stats block from container stats.
/// </summary>
public sealed class ContainerStatsCpuBlockDto {
public ContainerStatsCpuUsageDto? CpuUsage { get; set; }
public ulong SystemCpuUsage { get; set; }
public int OnlineCpus { get; set; }
public double Cpu { get; set; }
}
/// <summary>
/// Nested CPU usage counters.
/// </summary>
public sealed class ContainerStatsCpuUsageDto {
public ulong TotalUsage { get; set; }
public ulong SystemUsage { get; set; }
public ulong KernelMode { get; set; }
public ulong UserMode { get; set; }
public ulong UsageInKernelmode { get; set; }
public ulong UsageInUsermode { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Container Stats Memory).
/// </summary>
/// <summary>
/// Memory stats from container stats.
/// </summary>
public sealed class ContainerStatsMemoryDto {
public ulong Usage { get; set; }
public ulong MaxUsage { get; set; }
public ulong Limit { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Container Stats Network).
/// </summary>
/// <summary>
/// Per-interface network counters.
/// </summary>
public sealed class ContainerStatsNetworkDto {
public ulong RxBytes { get; set; }
public ulong RxPackets { get; set; }
public ulong RxErrors { get; set; }
public ulong RxDropped { get; set; }
public ulong TxBytes { get; set; }
public ulong TxPackets { get; set; }
public ulong TxErrors { get; set; }
public ulong TxDropped { get; set; }
}
/// <summary>
/// PID stats from container stats.
/// </summary>
public sealed class ContainerStatsPidsDto {
public long Current { get; set; }
}
/// <summary>
/// Response from <c>GET /libpod/containers/stats</c> (multi-container).
/// </summary>
public sealed class ContainersStatsResponseDto {
public string? Error { get; set; }
public List<ContainerLibpodStatsDto>? Stats { get; set; }
}
/// <summary>
/// Libpod multi-container stats entry.
/// </summary>
public sealed class ContainerLibpodStatsDto {
public double AvgCPU { get; set; }
public string? ContainerID { get; set; }
public string? Name { get; set; }
public double CPU { get; set; }
public long CPUNano { get; set; }
public long CPUSystemNano { get; set; }
public long SystemNano { get; set; }
public long MemUsage { get; set; }
public long MemLimit { get; set; }
public double MemPerc { get; set; }
public Dictionary<string, ContainerStatsNetworkDto>? Network { get; set; }
public long BlockInput { get; set; }
public long BlockOutput { get; set; }
public long PIDs { get; set; }
public long UpTime { get; set; }
public long Duration { get; set; }
}

View File

@ -1,17 +1,7 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Container;
/// <summary>
/// Deserialized Podman libpod API payload (Mounted Container).
/// </summary>
public sealed class MountedContainerDto {
public string? Id { get; set; }
public string? Name { get; set; }
public string? Mountpoint { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Mounted Containers response).
/// Libpod <c>showmounted</c> returns a JSON array of single-entry maps (container id → mount path).
/// </summary>
public sealed class MountedContainersResponseDto {
public List<MountedContainerDto>? Containers { get; set; }
public sealed class MountedContainersResponseDto : List<Dictionary<string, string>> {
}

View File

@ -7,11 +7,30 @@ public sealed class CreateExecResponseDto {
public string? Id { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Inspect Exec process config).
/// </summary>
public sealed class InspectExecProcessDto {
public string[]? Arguments { get; set; }
public string? Entrypoint { get; set; }
public bool Privileged { get; set; }
public bool Tty { get; set; }
public string? User { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Inspect Exec response).
/// </summary>
public sealed class InspectExecResponseDto {
public bool Running { get; set; }
public bool CanRemove { get; set; }
public string? ContainerID { get; set; }
public string? DetachKeys { get; set; }
public int ExitCode { get; set; }
public string? ProcessConfig { get; set; }
public string? ID { get; set; }
public bool OpenStderr { get; set; }
public bool OpenStdin { get; set; }
public bool OpenStdout { get; set; }
public bool Running { get; set; }
public int Pid { get; set; }
public InspectExecProcessDto? ProcessConfig { get; set; }
}

View File

@ -1,5 +1,15 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Image;
/// <summary>Podman returns a JSON array of filesystem change paths.</summary>
public sealed class ImageChangesDto : List<string> {
/// <summary>
/// Single filesystem change entry from image/container changes.
/// </summary>
public sealed class ImageChangeEntryDto {
public string? Path { get; set; }
public int Kind { get; set; }
}
/// <summary>
/// Podman returns a JSON array of path/kind change objects.
/// </summary>
public sealed class ImageChangesDto : List<ImageChangeEntryDto> {
}

View File

@ -1,10 +1,11 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Image;
/// <summary>
/// Deserialized Podman libpod API payload (Image Delete).
/// </summary>
/// <summary>
/// Deserialized Podman libpod API payload (Image Delete / Remove).
/// </summary>
public sealed class ImageDeleteDto {
public string? Deleted { get; set; }
public string[]? Deleted { get; set; }
public string[]? Untagged { get; set; }
public int ExitCode { get; set; }
public string[]? Errors { get; set; }
}

View File

@ -1,23 +1,30 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Image;
/// <summary>
/// Deserialized Podman libpod API payload (Image List Entry).
/// </summary>
public sealed class ImageListEntryDto {
public string? Id { get; set; }
public string? ParentId { get; set; }
public string[]? RepoTags { get; set; }
public string[]? RepoDigests { get; set; }
public string[]? Names { get; set; }
public string[]? History { get; set; }
public string? Digest { get; set; }
public long Created { get; set; }
public long Size { get; set; }
public long SharedSize { get; set; }
public string? ParentId { get; set; }
public string? RepoTags { get; set; }
public string? RepoDigests { get; set; }
public long VirtualSize { get; set; }
public long Containers { get; set; }
public string? Arch { get; set; }
public string? Os { get; set; }
public bool IsManifestList { get; set; }
public Dictionary<string, string>? Labels { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Image Search Result).
/// </summary>
public sealed class ImageSearchResultDto {
public string? Name { get; set; }
public string? Description { get; set; }

View File

@ -1,18 +1,8 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Image;
/// <summary>
/// Deserialized Podman libpod API payload (Image Tree).
/// </summary>
public sealed class ImageTreeDto {
public string? Id { get; set; }
public ImageTreeLayerDto[]? Layers { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Image Tree Layer).
/// </summary>
public sealed class ImageTreeLayerDto {
public string? Id { get; set; }
public string? Parent { get; set; }
public string[]? Tags { get; set; }
public string? Tree { get; set; }
}

View File

@ -1,36 +1,50 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Manifest;
/// <summary>
/// Deserialized Podman libpod API payload (Manifest Create).
/// </summary>
public sealed class ManifestCreateDto {
public string? Id { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Manifest Inspect).
/// </summary>
public sealed class ManifestInspectDto {
public int? SchemaVersion { get; set; }
public string? MediaType { get; set; }
public string? Name { get; set; }
public ManifestListSpecDto[]? Manifests { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Manifest List Spec).
/// </summary>
/// <summary>
/// Deserialized Podman libpod API payload (Manifest List Spec entry).
/// </summary>
public sealed class ManifestListSpecDto {
public string? Digest { get; set; }
public string? MediaType { get; set; }
public long? Size { get; set; }
public string? Image { get; set; }
public string? Platform { get; set; }
public ManifestPlatformDto? Platform { get; set; }
public string? Os { get; set; }
public string? Arch { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Manifest Add request).
/// </summary>
/// <summary>
/// Platform object nested in a manifest list entry.
/// </summary>
public sealed class ManifestPlatformDto {
public string? Architecture { get; set; }
public string? Os { get; set; }
public string? Variant { get; set; }
}
/// <summary>
/// Libpod ManifestModifyOptions body (v4+ PUT /libpod/manifests/{name}).
/// </summary>
public sealed class ManifestAddRequestDto {
public string Image { get; set; } = "";
public List<string>? Images { get; set; }
public string? Image { get; set; }
public bool All { get; set; }
public string? Operation { get; set; }
}

View File

@ -1,70 +1,94 @@
namespace MaksIT.PodmanClientDotNet.Dtos.Pod;
/// <summary>
/// Container summary nested in pod list/inspect responses.
/// </summary>
public sealed class PodContainerSummaryDto {
public string? Id { get; set; }
public string? Name { get; set; }
public string? Names { get; set; }
public string? State { get; set; }
public string? Status { get; set; }
public int? RestartCount { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod List Entry).
/// </summary>
public sealed class PodListEntryDto {
public string? Id { get; set; }
public string? Name { get; set; }
public string? Status { get; set; }
public string? Cgroup { get; set; }
public string? CgroupParent { get; set; }
public DateTime Created { get; set; }
public string? Created { get; set; }
public Dictionary<string, string>? Labels { get; set; }
public string? Namespace { get; set; }
public string? RestartPolicy { get; set; }
public ulong? StopTimeout { get; set; }
public string? InfraId { get; set; }
public string[]? Networks { get; set; }
public List<PodContainerSummaryDto>? Containers { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod Kill Report).
/// </summary>
public sealed class PodKillReportDto {
public string[]? Ids { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod Inspect).
/// </summary>
public sealed class PodInspectDto {
public string? Id { get; set; }
public string? Name { get; set; }
public string? State { get; set; }
public string? Status { get; set; }
public string? CgroupParent { get; set; }
public DateTime Created { get; set; }
public string? Created { get; set; }
public Dictionary<string, string>? Labels { get; set; }
public string? Namespace { get; set; }
public string? RestartPolicy { get; set; }
public ulong? StopTimeout { get; set; }
public string[]? Containers { get; set; }
public string? InfraContainerId { get; set; }
public List<PodContainerSummaryDto>? Containers { get; set; }
public string? InfraContainerID { get; set; }
public int? NumContainers { get; set; }
public string[]? SharedNamespaces { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod Top).
/// </summary>
public sealed class PodTopDto {
public string[]? Titles { get; set; }
public List<string[]>? Processes { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod Stats).
/// </summary>
/// <summary>
/// Deserialized Podman libpod API payload (Pod Stats entry).
/// </summary>
public sealed class PodStatsDto {
public string? Id { get; set; }
public string? CID { get; set; }
public string? Pod { get; set; }
public string? Name { get; set; }
public string? CPU { get; set; }
public string? MemUsage { get; set; }
public string? MemUsageBytes { get; set; }
public string? MemLimit { get; set; }
public string? Mem { get; set; }
public string? MemPercent { get; set; }
public string? NetIO { get; set; }
public string? BlockIO { get; set; }
public string? PIDs { get; set; }
public string? PIDS { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Pod Stats response).
/// </summary>
/// <summary>
/// Deserialized Podman libpod API payload (Pod Stats response wrapper).
/// Prefer <see cref="List{PodStatsDto}"/> from the API; this type remains for callers that expect a named bag.
/// </summary>
public sealed class PodStatsResponseDto {
public Dictionary<string, PodStatsDto>? Stats { get; set; }
public List<PodStatsDto>? Stats { get; set; }
}

View File

@ -1,49 +1,121 @@
namespace MaksIT.PodmanClientDotNet.Dtos.System;
/// <summary>
/// Deserialized Podman libpod API payload (Info).
/// </summary>
public sealed class InfoDto {
public InfoHostDto? Host { get; set; }
public InfoStoreDto? Store { get; set; }
public Dictionary<string, object>? Version { get; set; }
public InfoVersionDto? Version { get; set; }
public InfoPluginsDto? Plugins { get; set; }
}
/// <summary>
/// Version block under info.
/// </summary>
public sealed class InfoVersionDto {
public string? APIVersion { get; set; }
public string? Version { get; set; }
public string? GoVersion { get; set; }
public string? GitCommit { get; set; }
public string? BuiltTime { get; set; }
public long Built { get; set; }
public string? BuildOrigin { get; set; }
public string? OsArch { get; set; }
public string? Os { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Info Host).
/// </summary>
public sealed class InfoHostDto {
public string? Arch { get; set; }
public string? BuildahVersion { get; set; }
public long Containers { get; set; }
public string? Distribution { get; set; }
public string? Kernel { get; set; }
public string? MemTotal { get; set; }
public int MemFree { get; set; }
public string? OSType { get; set; }
public string? OS { get; set; }
public int CPUs { get; set; }
public string? PodmanVersion { get; set; }
public string? Machine { get; set; }
public string? CgroupManager { get; set; }
public string? CgroupVersion { get; set; }
public List<string>? CgroupControllers { get; set; }
public InfoConmonDto? Conmon { get; set; }
public int Cpus { get; set; }
public InfoCpuUtilizationDto? CpuUtilization { get; set; }
public string? DatabaseBackend { get; set; }
public InfoDistributionDto? Distribution { get; set; }
public string? EventLogger { get; set; }
public long FreeLocks { get; set; }
public string? Hostname { get; set; }
public string? Kernel { get; set; }
public string? LogDriver { get; set; }
public long MemFree { get; set; }
public long MemTotal { get; set; }
public string? NetworkBackend { get; set; }
public string? Os { get; set; }
public string? OSType { get; set; }
}
/// <summary>
/// Conmon info under host.
/// </summary>
public sealed class InfoConmonDto {
public string? Package { get; set; }
public string? Path { get; set; }
public string? Version { get; set; }
}
/// <summary>
/// CPU utilization percentages under host.
/// </summary>
public sealed class InfoCpuUtilizationDto {
public double UserPercent { get; set; }
public double SystemPercent { get; set; }
public double IdlePercent { get; set; }
}
/// <summary>
/// Distribution object under host.
/// </summary>
public sealed class InfoDistributionDto {
public string? Distribution { get; set; }
public string? Version { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Info Store).
/// </summary>
public sealed class InfoStoreDto {
public string? ConfigFile { get; set; }
public InfoContainerStoreDto? ContainerStore { get; set; }
public string? GraphRoot { get; set; }
public string? GraphDriverName { get; set; }
public Dictionary<string, string>? GraphOptions { get; set; }
public long ImageStoreNumber { get; set; }
public long RunRoot { get; set; }
public long VolumePath { get; set; }
public long GraphRootAllocated { get; set; }
public long GraphRootUsed { get; set; }
public Dictionary<string, string>? GraphStatus { get; set; }
public string? ImageCopyTmpDir { get; set; }
public InfoImageStoreDto? ImageStore { get; set; }
public string? RunRoot { get; set; }
public bool TransientStore { get; set; }
public string? VolumePath { get; set; }
}
/// <summary>
/// Container store counters under store.
/// </summary>
public sealed class InfoContainerStoreDto {
public long Number { get; set; }
public long Paused { get; set; }
public long Running { get; set; }
public long Stopped { get; set; }
}
/// <summary>
/// Image store counters under store.
/// </summary>
public sealed class InfoImageStoreDto {
public long Number { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Info Plugins).
/// </summary>
public sealed class InfoPluginsDto {
public string[]? Volume { get; set; }
public string[]? Network { get; set; }

View File

@ -1,18 +1,34 @@
namespace MaksIT.PodmanClientDotNet.Dtos.System;
/// <summary>
/// Deserialized Podman libpod API payload (Libpod Version).
/// </summary>
public sealed class LibpodVersionDto {
public VersionComponentsDto? Version { get; set; }
public string? Platform { get; set; }
public LibpodVersionPlatformDto? Platform { get; set; }
public List<LibpodVersionComponentDto>? Components { get; set; }
public string? Version { get; set; }
public string? ApiVersion { get; set; }
public string? MinAPIVersion { get; set; }
public string? GitCommit { get; set; }
public string? GoVersion { get; set; }
public string? Os { get; set; }
public string? Arch { get; set; }
public string? KernelVersion { get; set; }
public string? BuildTime { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (Version Components).
/// </summary>
public sealed class VersionComponentsDto {
public int Major { get; set; }
public int Minor { get; set; }
public int Micro { get; set; }
/// <summary>
/// Platform object from libpod version.
/// </summary>
public sealed class LibpodVersionPlatformDto {
public string? Name { get; set; }
}
/// <summary>
/// Component entry from libpod version.
/// </summary>
public sealed class LibpodVersionComponentDto {
public string? Name { get; set; }
public string? Version { get; set; }
public Dictionary<string, string>? Details { get; set; }
}

View File

@ -1,17 +1,18 @@
namespace MaksIT.PodmanClientDotNet.Dtos.System;
/// <summary>
/// Deserialized Podman libpod API payload (System Df).
/// </summary>
public sealed class SystemDfDto {
public long ImagesSize { get; set; }
public SystemDfEntryDto[]? Images { get; set; }
public SystemDfEntryDto[]? Containers { get; set; }
public SystemDfEntryDto[]? Volumes { get; set; }
}
/// <summary>
/// Deserialized Podman libpod API payload (System Df Entry).
/// </summary>
public sealed class SystemDfEntryDto {
public long Size { get; set; }
public long Reclaimable { get; set; }

View File

@ -14,7 +14,8 @@ public interface IPodmanClientConfiguration {
string ServerUrl { get; set; }
/// <summary>
/// Podman API version segment used in request paths. Defaults to <c>v1.41</c>.
/// Podman libpod API version segment used in request paths. Defaults to <c>v5.4.0</c>
/// (validated E2E target; use at least <c>v4.0.0</c> — Docker-compat <c>v1.41</c> is rejected by network endpoints).
/// </summary>
string ApiVersion { get; set; }

View File

@ -1,10 +1,7 @@
using System.Net;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.Results;
using Microsoft.Extensions.Logging;
namespace MaksIT.PodmanClientDotNet.Internal;

View File

@ -1,10 +1,7 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Dtos.Build;
using MaksIT.PodmanClientDotNet.Dtos.Image;
using MaksIT.Results;
using Microsoft.Extensions.Logging;
namespace MaksIT.PodmanClientDotNet.Internal;

View File

@ -12,4 +12,4 @@ public class AutoUserNsOptions {
public int InitialSize { get; set; }
public string? PasswdFile { get; set; }
public int Size { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class BindOptions {
public string? Propagation { get; set; }
public bool ReadOnlyForceRecursive { get; set; }
public bool ReadOnlyNonRecursive { get; set; }
}
}

View File

@ -13,4 +13,4 @@ public class BlockIO {
public List<ThrottleDevice>? ThrottleWriteIopsDevice { get; set; }
public int Weight { get; set; }
public List<WeightDevice>? WeightDevice { get; set; }
}
}

View File

@ -15,4 +15,4 @@ public class CPU {
public int RealtimePeriod { get; set; }
public int RealtimeRuntime { get; set; }
public int Shares { get; set; }
}
}

View File

@ -124,4 +124,4 @@ public class CreateContainerRequest {
public List<string>? VolumesFrom { get; set; }
public Dictionary<string, ulong>? WeightDevice { get; set; }
public string? WorkDir { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models.Container;
public class CreateContainerResponse {
public string? Id { get; set; }
public string[]? Warnings { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models.Container;
public class DeleteContainerResponse {
public string? Err { get; set; }
public string? Id { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class DriverConfig {
public string? Name { get; set; }
public Dictionary<string, string>? Options { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class ErrorResponse {
public string? Cause { get; set; }
public string? Message { get; set; }
public int Response { get; set; }
}
}

View File

@ -16,4 +16,4 @@ public class CreateExecRequest {
public bool Tty { get; set; }
public string? User { get; set; }
public string? WorkingDir { get; set; }
}
}

View File

@ -7,4 +7,4 @@ namespace MaksIT.PodmanClientDotNet.Models.Exec;
public class CreateExecResponse {
public string? Id { get; set; }
}
}

View File

@ -1,12 +1,32 @@
namespace MaksIT.PodmanClientDotNet.Models.Exec;
/// <summary>
/// Libpod API response body for Inspect Exec process config.
/// </summary>
public class InspectExecProcess {
public string[]? Arguments { get; set; }
public string? Entrypoint { get; set; }
public bool Privileged { get; set; }
public bool Tty { get; set; }
public string? User { get; set; }
}
/// <summary>
/// Libpod API response body for Inspect Exec response.
/// </summary>
public class InspectExecResponse {
public bool Running { get; set; }
public bool CanRemove { get; set; }
public string? ContainerID { get; set; }
public string? DetachKeys { get; set; }
public int ExitCode { get; set; }
public string? ProcessConfig { get; set; }
}
public string? ID { get; set; }
public bool OpenStderr { get; set; }
public bool OpenStdin { get; set; }
public bool OpenStdout { get; set; }
public bool Running { get; set; }
public int Pid { get; set; }
public InspectExecProcess? ProcessConfig { get; set; }
}

View File

@ -10,4 +10,4 @@ public class StartExecRequest {
public bool Tty { get; set; }
public int? Height { get; set; } // Optional, nullable if not provided
public int? Width { get; set; } // Optional, nullable if not provided
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class HugepageLimit {
public long Limit { get; set; }
public string? PageSize { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class IDMapping {
public int ContainerId { get; set; }
public int HostId { get; set; }
public int Size { get; set; }
}
}

View File

@ -12,4 +12,4 @@ public class IDMappingOptions {
public bool HostGIDMapping { get; set; }
public bool HostUIDMapping { get; set; }
public List<IDMapping>? UIDMap { get; set; }
}
}

View File

@ -1,26 +1,25 @@
namespace MaksIT.PodmanClientDotNet.Models.Image;
public class PullImageResponse
{
public class PullImageResponse {
/// <summary>
/// Error contains text of errors from c/image.
/// </summary>
public string? Error { get; set; }
/// <summary>
/// Error contains text of errors from c/image.
/// </summary>
public string? Error { get; set; }
/// <summary>
/// ID contains image ID (retained for backwards compatibility).
/// </summary>
public string? Id { get; set; }
/// <summary>
/// ID contains image ID (retained for backwards compatibility).
/// </summary>
public string? Id { get; set; }
/// <summary>
/// Images contains the IDs of the images pulled.
/// </summary>
public List<string>? Images { get; set; }
/// <summary>
/// Images contains the IDs of the images pulled.
/// </summary>
public List<string>? Images { get; set; }
/// <summary>
/// Stream used to provide output from c/image.
/// </summary>
public string? Stream { get; set; }
}
/// <summary>
/// Stream used to provide output from c/image.
/// </summary>
public string? Stream { get; set; }
}

View File

@ -10,4 +10,4 @@ public class ImageVolume {
public bool ReadWrite { get; set; }
public string? Source { get; set; }
public string? SubPath { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class IntelRdt {
public bool EnableMBM { get; set; }
public string? L3CacheSchema { get; set; }
public string? MemBwSchema { get; set; }
}
}

View File

@ -13,4 +13,4 @@ public class LinuxDevice {
public string? Path { get; set; }
public string? Type { get; set; }
public int Uid { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class LinuxDeviceCgroup {
public int Major { get; set; }
public int Minor { get; set; }
public string? Type { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class LinuxIntelRdt {
public bool EnableMBM { get; set; }
public string? L3CacheSchema { get; set; }
public string? MemBwSchema { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class LinuxPersonality {
public string? Domain { get; set; }
public List<string>? Flags { get; set; }
}
}

View File

@ -15,4 +15,4 @@ public class LinuxResources {
public Pids? Pids { get; set; }
public Dictionary<string, RdmaResource>? Rdma { get; set; }
public Dictionary<string, string>? Unified { get; set; }
}
}

View File

@ -10,4 +10,4 @@ public class LogConfigLibpod {
public Dictionary<string, string>? Options { get; set; }
public string? Path { get; set; }
public long Size { get; set; }
}
}

View File

@ -15,4 +15,4 @@ public class Memory {
public long Swap { get; set; }
public int Swappiness { get; set; }
public bool UseHierarchy { get; set; }
}
}

View File

@ -14,4 +14,4 @@ public class Mount {
public TmpfsOptions? TmpfsOptions { get; set; }
public string? Type { get; set; }
public VolumeOptions? VolumeOptions { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class NamedVolume {
public string? Name { get; set; }
public List<string>? Options { get; set; }
public string? SubPath { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class Namespace {
public string? Nsmode { get; set; }
public string? Value { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class NetworkPriority {
public string? Name { get; set; }
public int Priority { get; set; }
}
}

View File

@ -10,4 +10,4 @@ public class NetworkSettings {
public string? InterfaceName { get; set; }
public List<string>? StaticIps { get; set; }
public string? StaticMac { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class OverlayVolume {
public string? Destination { get; set; }
public List<string>? Options { get; set; }
public string? Source { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class POSIXRlimit {
public long Hard { get; set; }
public long Soft { get; set; }
public string? Type { get; set; }
}
}

View File

@ -7,4 +7,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class Pids {
public int Limit { get; set; }
}
}

View File

@ -11,4 +11,4 @@ public class PortMapping {
public int HostPort { get; set; }
public string? Protocol { get; set; }
public int Range { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class ProgressDetail {
public long? Current { get; set; }
public long? Total { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class RdmaResource {
public int HcaHandles { get; set; }
public int HcaObjects { get; set; }
}
}

View File

@ -12,4 +12,4 @@ public class Schema2HealthConfig {
public long StartPeriod { get; set; }
public List<string>? Test { get; set; }
public long Timeout { get; set; }
}
}

View File

@ -8,4 +8,4 @@ namespace MaksIT.PodmanClientDotNet.Models;
public class SecretProp {
public string? Key { get; set; }
public string? Secret { get; set; }
}
}

View File

@ -13,4 +13,4 @@ public class StartupHealthConfig {
public int Successes { get; set; }
public List<string>? Test { get; set; }
public long Timeout { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class ThrottleDevice {
public int Major { get; set; }
public int Minor { get; set; }
public long Rate { get; set; }
}
}

View File

@ -9,4 +9,4 @@ public class TmpfsOptions {
public int Mode { get; set; }
public List<string>? Options { get; set; }
public long SizeBytes { get; set; }
}
}

View File

@ -10,4 +10,4 @@ public class VolumeOptions {
public Dictionary<string, string>? Labels { get; set; }
public bool NoCopy { get; set; }
public string? Subpath { get; set; }
}
}

View File

@ -10,4 +10,4 @@ public class WeightDevice {
public int Major { get; set; }
public int Minor { get; set; }
public int Weight { get; set; }
}
}

View File

@ -1,15 +1,12 @@
using MaksIT.PodmanClientDotNet;
using System.Net;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet;
using MaksIT.PodmanClientDotNet.Dtos.Container;
using MaksIT.PodmanClientDotNet.Internal;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Dtos.Container;
using MaksIT.PodmanClientDotNet.Models.Container;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.Results;
using Microsoft.Extensions.Logging;
public partial class PodmanClient {
public async Task<Result<CreateContainerResponseDto?>> CreateContainerAsync(

View File

@ -57,12 +57,21 @@ public partial class PodmanClient {
PostWithoutBodyAsync($"{ContainerPath(name)}/unpause", "Unpause container", cancellationToken: cancellationToken);
public Task<Result<ContainerWaitDto?>> WaitContainerAsync(string name, string? condition = null, CancellationToken cancellationToken = default) =>
PostLibpodAsync<ContainerWaitDto>(
$"{ContainerPath(name)}/wait",
SendAsync(
() => _httpClient.PostAsync(
LibpodPath($"{ContainerPath(name)}/wait") + BuildQuery(condition is null ? [] : [("condition", condition)]),
content: null,
cancellationToken),
"Wait container",
PodmanJsonContext.Default.ContainerWaitDto,
query: condition is null ? null : [("condition", condition)],
cancellationToken: cancellationToken
// Libpod may return a bare exit-code integer or a JSON object.
body => {
var trimmed = body.Trim();
if (long.TryParse(trimmed, out var code))
return new ContainerWaitDto { StatusCode = code };
return System.Text.Json.JsonSerializer.Deserialize(trimmed, PodmanJsonContext.Default.ContainerWaitDto);
},
cancellationToken
);
public Task<Result<Stream?>> GetContainerLogsAsync(
@ -100,7 +109,7 @@ public partial class PodmanClient {
cancellationToken
);
public Task<Result<Dictionary<string, ContainerStatsDto>?>> GetContainersStatsAsync(
public Task<Result<ContainersStatsResponseDto?>> GetContainersStatsAsync(
IEnumerable<string>? containers = null,
bool stream = false,
CancellationToken cancellationToken = default
@ -111,14 +120,14 @@ public partial class PodmanClient {
query.Add(("containers", c));
}
return GetJsonAsync<Dictionary<string, ContainerStatsDto>>("/libpod/containers/stats", "Get containers stats", PodmanJsonContext.Default.DictionaryStringContainerStatsDto, query, cancellationToken);
return GetJsonAsync<ContainersStatsResponseDto>("/libpod/containers/stats", "Get containers stats", PodmanJsonContext.Default.ContainersStatsResponseDto, query, cancellationToken);
}
public Task<Result<PruneReportDto?>> PruneContainersAsync(string? filters = null, CancellationToken cancellationToken = default) =>
PostLibpodAsync<PruneReportDto>(
public Task<Result<List<PruneReportEntryDto>?>> PruneContainersAsync(string? filters = null, CancellationToken cancellationToken = default) =>
PostLibpodAsync<List<PruneReportEntryDto>>(
"/libpod/containers/prune",
"Prune containers",
PodmanJsonContext.Default.PruneReportDto,
PodmanJsonContext.Default.ListPruneReportEntryDto,
query: filters is null ? null : [("filters", filters)],
cancellationToken: cancellationToken
);
@ -183,7 +192,13 @@ public partial class PodmanClient {
);
public Task<Result<ContainerMountDto?>> MountContainerAsync(string name, CancellationToken cancellationToken = default) =>
PostLibpodAsync<ContainerMountDto>($"{ContainerPath(name)}/mount", "Mount container", PodmanJsonContext.Default.ContainerMountDto, cancellationToken: cancellationToken);
SendAsync(
() => _httpClient.PostAsync(LibpodPath($"{ContainerPath(name)}/mount"), content: null, cancellationToken),
"Mount container",
// Podman returns a plain filesystem path, not JSON.
body => new ContainerMountDto { Path = body.Trim().Trim('"') },
cancellationToken
);
public Task<Result> UnmountContainerAsync(string name, CancellationToken cancellationToken = default) =>
PostWithoutBodyAsync($"{ContainerPath(name)}/unmount", "Unmount container", cancellationToken: cancellationToken);
@ -266,8 +281,32 @@ public partial class PodmanClient {
public Task<Result<ContainerHealthCheckDto?>> HealthCheckContainerAsync(string name, CancellationToken cancellationToken = default) =>
GetJsonAsync<ContainerHealthCheckDto>($"{ContainerPath(name)}/healthcheck", "Health check container", PodmanJsonContext.Default.ContainerHealthCheckDto, cancellationToken: cancellationToken);
public Task<Result<MountedContainersResponseDto?>> GetMountedContainersAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<MountedContainersResponseDto>("/libpod/containers/showmounted", "Get mounted containers", PodmanJsonContext.Default.MountedContainersResponseDto, cancellationToken: cancellationToken);
public Task<Result<List<Dictionary<string, string>>?>> GetMountedContainersAsync(CancellationToken cancellationToken = default) =>
SendAsync(
() => _httpClient.GetAsync(LibpodPath("/libpod/containers/showmounted"), cancellationToken),
"Get mounted containers",
body => {
// Libpod returns [{"<id>":"<path>"}, ...] (and [{}}] when empty).
using var doc = System.Text.Json.JsonDocument.Parse(string.IsNullOrWhiteSpace(body) ? "[]" : body);
var list = new List<Dictionary<string, string>>();
if (doc.RootElement.ValueKind != System.Text.Json.JsonValueKind.Array)
return list;
foreach (var el in doc.RootElement.EnumerateArray()) {
var dict = new Dictionary<string, string>();
if (el.ValueKind == System.Text.Json.JsonValueKind.Object) {
foreach (var prop in el.EnumerateObject())
dict[prop.Name] = prop.Value.ValueKind == System.Text.Json.JsonValueKind.String
? (prop.Value.GetString() ?? string.Empty)
: prop.Value.ToString();
}
list.Add(dict);
}
return list;
},
cancellationToken
);
public Task<Result<ContainerTopDto?>> TopContainerAsync(
string name,

View File

@ -1,13 +1,8 @@
using MaksIT.PodmanClientDotNet;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Internal;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Dtos.Exec;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.Results;
using Microsoft.Extensions.Logging;
public partial class PodmanClient {
public async Task<Result<CreateExecResponseDto?>> CreateExecAsync(

View File

@ -1,6 +1,5 @@
using MaksIT.PodmanClientDotNet;
using System.Net.Http.Headers;
using MaksIT.PodmanClientDotNet;
using MaksIT.PodmanClientDotNet.Dtos.Generate;
using MaksIT.Results;

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