(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

@ -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

@ -1,8 +1,7 @@
namespace MaksIT.PodmanClientDotNet.Models.Image;
public class PullImageResponse
{
public class PullImageResponse {
/// <summary>
/// Error contains text of errors from c/image.

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;

View File

@ -1,4 +1,3 @@
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

View File

@ -1,6 +1,5 @@
using MaksIT.PodmanClientDotNet;
using System.Net.Http.Headers;
using MaksIT.PodmanClientDotNet;
using MaksIT.PodmanClientDotNet.Dtos.Common;
using MaksIT.PodmanClientDotNet.Dtos.Image;
using MaksIT.PodmanClientDotNet.Internal;
@ -31,16 +30,16 @@ public partial class PodmanClient {
public Task<Result> ImageExistsAsync(string name, CancellationToken cancellationToken = default) =>
GetWithoutBodyAsync($"{ImagePath(name)}/exists", "Image exists", cancellationToken: cancellationToken);
public Task<Result<ImageDeleteDto[]?>> DeleteImageAsync(string name, bool force = false, CancellationToken cancellationToken = default) =>
DeleteJsonAsync<ImageDeleteDto[]>(
public Task<Result<ImageDeleteDto?>> DeleteImageAsync(string name, bool force = false, CancellationToken cancellationToken = default) =>
DeleteJsonAsync<ImageDeleteDto>(
ImagePath(name),
"Delete image",
PodmanJsonContext.Default.ImageDeleteDtoArray,
PodmanJsonContext.Default.ImageDeleteDto,
[("force", force.ToString().ToLowerInvariant())],
cancellationToken
);
public Task<Result<ImageDeleteDto[]?>> RemoveImagesAsync(
public Task<Result<ImageDeleteDto?>> RemoveImagesAsync(
IEnumerable<string> images,
bool all = false,
bool force = false,
@ -53,11 +52,11 @@ public partial class PodmanClient {
foreach (var image in images)
query.Add(("images", image));
return DeleteJsonAsync<ImageDeleteDto[]>("/libpod/images/remove", "Remove images", PodmanJsonContext.Default.ImageDeleteDtoArray, query, cancellationToken);
return DeleteJsonAsync<ImageDeleteDto>("/libpod/images/remove", "Remove images", PodmanJsonContext.Default.ImageDeleteDto, query, cancellationToken);
}
public Task<Result<PruneReportDto?>> PruneImagesAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<PruneReportDto>("/libpod/images/prune", "Prune images", PodmanJsonContext.Default.PruneReportDto, cancellationToken: cancellationToken);
public Task<Result<List<PruneReportEntryDto>?>> PruneImagesAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<List<PruneReportEntryDto>>("/libpod/images/prune", "Prune images", PodmanJsonContext.Default.ListPruneReportEntryDto, cancellationToken: cancellationToken);
public Task<Result<List<ImageSearchResultDto>?>> SearchImagesAsync(
string term,

View File

@ -1,3 +1,5 @@
using System.Text;
using MaksIT.PodmanClientDotNet;
using MaksIT.PodmanClientDotNet.Dtos.Manifest;
using MaksIT.Results;
@ -10,18 +12,21 @@ public partial class PodmanClient {
string? image = null,
bool all = false,
CancellationToken cancellationToken = default
) =>
PostLibpodAsync<ManifestCreateDto>(
"/libpod/manifests/create",
) {
var query = new List<(string Name, string? Value)> {
("all", all.ToString().ToLowerInvariant()),
};
if (!string.IsNullOrWhiteSpace(image))
query.Add(("images", image));
return PostLibpodAsync<ManifestCreateDto>(
ManifestPath(name),
"Create manifest",
PodmanJsonContext.Default.ManifestCreateDto,
query: [
("name", name),
("image", image),
("all", all.ToString().ToLowerInvariant()),
],
query: [.. query],
cancellationToken: cancellationToken
);
}
public Task<Result> DeleteManifestAsync(string name, string? digest = null, CancellationToken cancellationToken = default) =>
DeleteWithoutBodyAsync(
@ -34,8 +39,14 @@ public partial class PodmanClient {
public Task<Result<ManifestInspectDto?>> InspectManifestAsync(string name, CancellationToken cancellationToken = default) =>
GetJsonAsync<ManifestInspectDto>($"{ManifestPath(name)}/json", "Inspect manifest", PodmanJsonContext.Default.ManifestInspectDto, cancellationToken: cancellationToken);
public Task<Result> AddToManifestAsync(string name, ManifestAddRequestDto request, CancellationToken cancellationToken = default) =>
PostJsonWithoutBodyAsync($"{ManifestPath(name)}/add", "Add to manifest", request, PodmanJsonContext.Default.ManifestAddRequestDto, cancellationToken: cancellationToken);
public Task<Result> AddToManifestAsync(string name, ManifestAddRequestDto request, CancellationToken cancellationToken = default) {
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(request, PodmanJsonContext.Default.ManifestAddRequestDto),
Encoding.UTF8,
"application/json"
);
return PutWithoutBodyAsync(ManifestPath(name), "Add to manifest", content, cancellationToken: cancellationToken);
}
public Task<Result> PushManifestAsync(
string name,
@ -44,10 +55,9 @@ public partial class PodmanClient {
CancellationToken cancellationToken = default
) =>
PostWithoutBodyAsync(
$"/libpod/manifests/{Uri.EscapeDataString(name)}/push",
$"{ManifestPath(name)}/registry/{Uri.EscapeDataString(destination)}",
"Push manifest",
query: [
("destination", destination),
("all", all.ToString().ToLowerInvariant()),
],
cancellationToken: cancellationToken

View File

@ -64,12 +64,12 @@ public partial class PodmanClient {
public Task<Result> UnpausePodAsync(string name, CancellationToken cancellationToken = default) =>
PostWithoutBodyAsync($"/libpod/pods/{Uri.EscapeDataString(name)}/unpause", "Unpause pod", cancellationToken: cancellationToken);
public Task<Result<PruneReportDto?>> PrunePodsAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<PruneReportDto>("/libpod/pods/prune", "Prune pods", PodmanJsonContext.Default.PruneReportDto, cancellationToken: cancellationToken);
public Task<Result<List<PruneReportEntryDto>?>> PrunePodsAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<List<PruneReportEntryDto>>("/libpod/pods/prune", "Prune pods", PodmanJsonContext.Default.ListPruneReportEntryDto, cancellationToken: cancellationToken);
public Task<Result<PodTopDto?>> TopPodAsync(string name, CancellationToken cancellationToken = default) =>
GetJsonAsync<PodTopDto>($"/libpod/pods/{Uri.EscapeDataString(name)}/top", "Top pod", PodmanJsonContext.Default.PodTopDto, cancellationToken: cancellationToken);
public Task<Result<PodStatsResponseDto?>> GetPodsStatsAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<PodStatsResponseDto>("/libpod/pods/stats", "Get pods stats", PodmanJsonContext.Default.PodStatsResponseDto, cancellationToken: cancellationToken);
public Task<Result<List<PodStatsDto>?>> GetPodsStatsAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<List<PodStatsDto>>("/libpod/pods/stats", "Get pods stats", PodmanJsonContext.Default.ListPodStatsDto, cancellationToken: cancellationToken);
}

View File

@ -1,15 +1,13 @@
using MaksIT.PodmanClientDotNet;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet;
using MaksIT.PodmanClientDotNet.Dtos.Build;
using MaksIT.PodmanClientDotNet.Dtos.Image;
using MaksIT.PodmanClientDotNet.Internal;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.PodmanClientDotNet.Streaming;
using MaksIT.Results;
using Microsoft.Extensions.Logging;
public partial class PodmanClient {
public async Task<Result<IPodmanAttachSession?>> AttachContainerSessionAsync(

View File

@ -5,7 +5,15 @@ using MaksIT.Results;
public partial class PodmanClient {
public Task<Result<LibpodPingDto?>> PingAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<LibpodPingDto>("/libpod/_ping", "Ping", PodmanJsonContext.Default.LibpodPingDto, cancellationToken: cancellationToken);
SendAsync(
() => _httpClient.GetAsync(LibpodPath("/libpod/_ping"), cancellationToken),
"Ping",
// Podman returns plain text "OK", not JSON.
body => new LibpodPingDto {
Ping = body.Trim().Equals("OK", StringComparison.OrdinalIgnoreCase)
},
cancellationToken
);
public Task<Result<LibpodVersionDto?>> GetVersionAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<LibpodVersionDto>("/libpod/version", "Get version", PodmanJsonContext.Default.LibpodVersionDto, cancellationToken: cancellationToken);
@ -16,8 +24,8 @@ public partial class PodmanClient {
public Task<Result<SystemDfDto?>> GetSystemDiskUsageAsync(CancellationToken cancellationToken = default) =>
GetJsonAsync<SystemDfDto>("/libpod/system/df", "Get system disk usage", PodmanJsonContext.Default.SystemDfDto, cancellationToken: cancellationToken);
public Task<Result<PruneReportDto?>> PruneSystemAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<PruneReportDto>("/libpod/system/prune", "Prune system", PodmanJsonContext.Default.PruneReportDto, cancellationToken: cancellationToken);
public Task<Result<SystemPruneReportDto?>> PruneSystemAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<SystemPruneReportDto>("/libpod/system/prune", "Prune system", PodmanJsonContext.Default.SystemPruneReportDto, cancellationToken: cancellationToken);
public Task<Result<Stream?>> GetEventsAsync(CancellationToken cancellationToken = default) =>
GetStreamAsync("/libpod/events", "Get events", cancellationToken: cancellationToken);

View File

@ -32,6 +32,6 @@ public partial class PodmanClient {
cancellationToken
);
public Task<Result<PruneReportDto?>> PruneVolumesAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<PruneReportDto>("/libpod/volumes/prune", "Prune volumes", PodmanJsonContext.Default.PruneReportDto, cancellationToken: cancellationToken);
public Task<Result<List<PruneReportEntryDto>?>> PruneVolumesAsync(CancellationToken cancellationToken = default) =>
PostLibpodAsync<List<PruneReportEntryDto>>("/libpod/volumes/prune", "Prune volumes", PodmanJsonContext.Default.ListPruneReportEntryDto, cancellationToken: cancellationToken);
}

View File

@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Extensions;
using Microsoft.Extensions.Logging;
/// <summary>
/// HTTP client for the Podman REST API.
@ -33,7 +32,7 @@ public partial class PodmanClient : IPodmanClient {
) {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_apiVersion = "v1.41";
_apiVersion = "v5.4.0";
ConfigureHttpClient(serverUrl);
}
@ -56,7 +55,7 @@ public partial class PodmanClient : IPodmanClient {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_apiVersion = string.IsNullOrWhiteSpace(configuration.ApiVersion)
? "v1.41"
? "v5.4.0"
: configuration.ApiVersion;
_httpClient.Timeout = TimeSpan.FromMinutes(Math.Max(1, configuration.TimeoutMinutes));
ConfigureHttpClient(configuration.ServerUrl);

View File

@ -12,7 +12,7 @@
<!-- NuGet package metadata -->
<PackageId>PodmanClient.DotNet</PackageId>
<Version>1.2.0</Version>
<Version>1.3.0</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>PodmanClient.DotNet</Product>

View File

@ -25,6 +25,9 @@ namespace MaksIT.PodmanClientDotNet;
[JsonSerializable(typeof(ErrorResponseDto))]
[JsonSerializable(typeof(IdResponseDto))]
[JsonSerializable(typeof(PruneReportDto))]
[JsonSerializable(typeof(PruneReportEntryDto))]
[JsonSerializable(typeof(List<PruneReportEntryDto>))]
[JsonSerializable(typeof(SystemPruneReportDto))]
[JsonSerializable(typeof(ReportDto))]
// Build
@ -33,6 +36,7 @@ namespace MaksIT.PodmanClientDotNet;
// Container
[JsonSerializable(typeof(ContainerChangesDto))]
[JsonSerializable(typeof(ContainerChangeEntryDto))]
[JsonSerializable(typeof(ContainerCommitDto))]
[JsonSerializable(typeof(ContainerHealthCheckDto))]
[JsonSerializable(typeof(ContainerInspectDto))]
@ -40,16 +44,20 @@ namespace MaksIT.PodmanClientDotNet;
[JsonSerializable(typeof(List<ContainerListEntryDto>))]
[JsonSerializable(typeof(ContainerMountDto))]
[JsonSerializable(typeof(ContainerStatsDto))]
[JsonSerializable(typeof(Dictionary<string, ContainerStatsDto>))]
[JsonSerializable(typeof(ContainersStatsResponseDto))]
[JsonSerializable(typeof(ContainerLibpodStatsDto))]
[JsonSerializable(typeof(ContainerTopDto))]
[JsonSerializable(typeof(ContainerWaitDto))]
[JsonSerializable(typeof(CreateContainerResponseDto))]
[JsonSerializable(typeof(DeleteContainerResponseDto))]
[JsonSerializable(typeof(DeleteContainerResponseDto[]))]
[JsonSerializable(typeof(MountedContainersResponseDto))]
[JsonSerializable(typeof(List<Dictionary<string, string>>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
// Exec
[JsonSerializable(typeof(CreateExecResponseDto))]
[JsonSerializable(typeof(InspectExecProcessDto))]
[JsonSerializable(typeof(InspectExecResponseDto))]
// Generate
@ -58,8 +66,8 @@ namespace MaksIT.PodmanClientDotNet;
// Image
[JsonSerializable(typeof(ImageChangesDto))]
[JsonSerializable(typeof(ImageChangeEntryDto))]
[JsonSerializable(typeof(ImageDeleteDto))]
[JsonSerializable(typeof(ImageDeleteDto[]))]
[JsonSerializable(typeof(ImageHistoryEntryDto))]
[JsonSerializable(typeof(List<ImageHistoryEntryDto>))]
[JsonSerializable(typeof(ImageImportDto))]
@ -76,6 +84,8 @@ namespace MaksIT.PodmanClientDotNet;
// Manifest
[JsonSerializable(typeof(ManifestCreateDto))]
[JsonSerializable(typeof(ManifestInspectDto))]
[JsonSerializable(typeof(ManifestPlatformDto))]
[JsonSerializable(typeof(ManifestListSpecDto))]
// Network
[JsonSerializable(typeof(NetworkInspectDto))]
@ -87,6 +97,9 @@ namespace MaksIT.PodmanClientDotNet;
[JsonSerializable(typeof(PodListEntryDto))]
[JsonSerializable(typeof(List<PodListEntryDto>))]
[JsonSerializable(typeof(PodTopDto))]
[JsonSerializable(typeof(PodStatsDto))]
[JsonSerializable(typeof(List<PodStatsDto>))]
[JsonSerializable(typeof(PodContainerSummaryDto))]
[JsonSerializable(typeof(PodStatsResponseDto))]
// System

View File

@ -1,43 +0,0 @@
using System.Text;
using ICSharpCode.SharpZipLib.Tar;
namespace MaksIT.PodmanClientDotNet.Tests.Archives;
public static class Tar {
public static void CreateTarFromDirectory(string sourceDirectory, Stream outputStream) {
using var tarOutputStream = new TarOutputStream(outputStream, Encoding.UTF8);
tarOutputStream.IsStreamOwner = false;
AddDirectoryFilesToTar(tarOutputStream, sourceDirectory, recursive: true);
}
static void AddDirectoryFilesToTar(
TarOutputStream tarOutputStream,
string sourceDirectory,
bool recursive,
string? baseDirectory = null
) {
baseDirectory ??= sourceDirectory;
var directoryInfo = new DirectoryInfo(sourceDirectory);
foreach (var fileInfo in directoryInfo.GetFiles()) {
var relativePath = Path.GetRelativePath(baseDirectory, fileInfo.FullName);
var entry = TarEntry.CreateEntryFromFile(fileInfo.FullName);
entry.Name = relativePath.Replace(Path.DirectorySeparatorChar, '/');
tarOutputStream.PutNextEntry(entry);
using var fileStream = fileInfo.OpenRead();
fileStream.CopyTo(tarOutputStream);
tarOutputStream.CloseEntry();
}
if (!recursive)
return;
foreach (var subDirectory in directoryInfo.GetDirectories())
AddDirectoryFilesToTar(tarOutputStream, subDirectory.FullName, recursive: true, baseDirectory);
}
}

View File

@ -0,0 +1,40 @@
using System.Text.Json;
namespace MaksIT.PodmanClientDotNet.Tests;
public class InspectExecResponseDtoTests {
[Fact]
public void Deserialize_WhenProcessConfigIsObject_Succeeds() {
const string json = """
{
"CanRemove": true,
"ContainerID": "abc123",
"DetachKeys": "",
"ExitCode": 0,
"ID": "exec456",
"OpenStderr": true,
"OpenStdin": false,
"OpenStdout": true,
"Running": false,
"Pid": 0,
"ProcessConfig": {
"arguments": ["-c", "echo hi"],
"entrypoint": "sh",
"privileged": false,
"tty": false,
"user": ""
}
}
""";
var dto = JsonSerializer.Deserialize(json, PodmanJsonContext.Default.InspectExecResponseDto);
Assert.NotNull(dto);
Assert.Equal(0, dto.ExitCode);
Assert.Equal("exec456", dto.ID);
Assert.NotNull(dto.ProcessConfig);
Assert.Equal("sh", dto.ProcessConfig.Entrypoint);
Assert.Equal(new[] { "-c", "echo hi" }, dto.ProcessConfig!.Arguments);
}
}

View File

@ -1,126 +0,0 @@
using MaksIT.PodmanClientDotNet.Tests.Archives;
namespace MaksIT.PodmanClientDotNet.Tests;
[Trait("Category", "Integration")]
public class PodmanClientContainersTests {
private readonly IPodmanClient _client = PodmanClientTestFixture.CreateClient();
#region Success Cases
[Fact]
public async Task PodmanClient_ContainerLifecycle_Success() {
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
await PullImageAsync(image);
var containerId = await CreateContainerAsync(containerName, image);
await StartContainerAsync(containerId);
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
[Fact]
public async Task CopyFilesToContainer_Success() {
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
string pathInContainer = "/podman-test-copy";
string tempFolderPath = CreateTemporaryFolderWithFiles();
try {
await PullImageAsync(image);
var containerId = await CreateContainerAsync(containerName, image);
await StartContainerAsync(containerId);
using var tarStream = CreateTarStream(tempFolderPath);
await CopyToContainerAsync(containerId, tarStream, pathInContainer);
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
finally {
if (Directory.Exists(tempFolderPath))
Directory.Delete(tempFolderPath, true);
}
}
#endregion
#region Helper Methods
private async Task PullImageAsync(string image) {
var result = await _client.PullImageAsync(image);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task<string> CreateContainerAsync(string containerName, string image) {
var result = await _client.CreateContainerAsync(
name: containerName,
image: image,
command: new List<string> { "sh", "-c", "sleep infinity" });
string? containerId = null;
PodmanClientTestFixture.AssertSuccess(result, value => {
Assert.NotNull(value);
Assert.False(string.IsNullOrEmpty(value!.Id));
containerId = value.Id;
});
return containerId!;
}
private async Task StartContainerAsync(string containerId) {
var result = await _client.StartContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task StopContainerAsync(string containerId) {
var result = await _client.StopContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task ForceDeleteContainerAsync(string containerId) {
var result = await _client.ForceDeleteContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task CopyToContainerAsync(string containerId, Stream tarStream, string path) {
var result = await _client.ExtractArchiveToContainerAsync(containerId, tarStream, path);
PodmanClientTestFixture.AssertSuccess(result);
}
private static string CreateTemporaryFolderWithFiles() {
string tempFolder = Path.Combine(Path.GetTempPath(), $"podman-test-{Guid.NewGuid()}");
Directory.CreateDirectory(tempFolder);
for (int i = 0; i < 5; i++)
File.WriteAllText(Path.Combine(tempFolder, $"test-file-{i}.txt"), $"This is test file {i}");
return tempFolder;
}
private static Stream CreateTarStream(string folderPath) {
var memoryStream = new MemoryStream();
Tar.CreateTarFromDirectory(folderPath, memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
return memoryStream;
}
#endregion
#region Fail Cases
[Fact]
public async Task StartContainerAsync_Should_HandleErrors() {
var result = await _client.StartContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task StopContainerAsync_Should_HandleErrors() {
var result = await _client.StopContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task ForceDeleteContainerAsync_Should_HandleErrors() {
var result = await _client.ForceDeleteContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
#endregion
}

View File

@ -1,127 +0,0 @@
namespace MaksIT.PodmanClientDotNet.Tests;
[Trait("Category", "Integration")]
public class PodmanClientExecTests {
private readonly IPodmanClient _client = PodmanClientTestFixture.CreateClient();
#region Success Cases
[Fact]
public async Task Full_ContainerLifecycle_With_Exec_Should_Succeed() {
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
await PullImageAsync(image);
var containerId = await CreateContainerAsync(containerName, image);
await StartContainerAsync(containerId);
var execId = await CreateExecAsync(containerName, new[] { "apk", "add", "--no-cache", "curl" });
await StartExecAsync(execId);
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
#endregion
#region Helper Methods
private async Task PullImageAsync(string image) {
var result = await _client.PullImageAsync(image);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task<string> CreateContainerAsync(string containerName, string image) {
var result = await _client.CreateContainerAsync(
name: containerName,
image: image,
command: new List<string> { "sh", "-c", "sleep infinity" });
string? containerId = null;
PodmanClientTestFixture.AssertSuccess(result, value => {
Assert.NotNull(value);
Assert.False(string.IsNullOrEmpty(value!.Id));
containerId = value.Id;
});
return containerId!;
}
private async Task StartContainerAsync(string containerId) {
var result = await _client.StartContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task<string> CreateExecAsync(string containerName, string[] cmd) {
var result = await _client.CreateExecAsync(containerName, cmd);
string? execId = null;
PodmanClientTestFixture.AssertSuccess(result, value => {
Assert.NotNull(value);
Assert.False(string.IsNullOrEmpty(value!.Id));
execId = value.Id;
});
return execId!;
}
private async Task StartExecAsync(string execId) {
var result = await _client.StartExecAsync(execId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task StopContainerAsync(string containerId) {
var result = await _client.StopContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task ForceDeleteContainerAsync(string containerId) {
var result = await _client.ForceDeleteContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
#endregion
#region Fail Cases
[Fact]
public async Task PullImageAsync_Should_HandleErrors() {
var result = await _client.PullImageAsync("invalidimage:latest");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task CreateContainerAsync_Should_HandleErrors() {
var result = await _client.CreateContainerAsync(
"test-container",
"invalidimage:latest",
new List<string> { "sh", "-c", "sleep infinity" });
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task StartContainerAsync_Should_HandleErrors() {
var result = await _client.StartContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task CreateExecAsync_Should_HandleErrors() {
var result = await _client.CreateExecAsync("invalid-container", new[] { "apk", "add", "--no-cache", "curl" });
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task StartExecAsync_Should_HandleErrors() {
var result = await _client.StartExecAsync("invalid-exec-id");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task StopContainerAsync_Should_HandleErrors() {
var result = await _client.StopContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task ForceDeleteContainerAsync_Should_HandleErrors() {
var result = await _client.ForceDeleteContainerAsync("invalid-container-id");
PodmanClientTestFixture.AssertFailure(result);
}
#endregion
}

View File

@ -1,38 +0,0 @@
namespace MaksIT.PodmanClientDotNet.Tests;
[Trait("Category", "Integration")]
public class PodmanClientImagesTests {
private readonly IPodmanClient _client = PodmanClientTestFixture.CreateClient();
#region Success Cases
[Fact]
public async Task PodmanClient_IntegrationTests() {
await PullImageAsync_Should_Succeed();
await TagImageAsync_Should_Succeed();
}
private async Task PullImageAsync_Should_Succeed() {
var result = await _client.PullImageAsync("alpine:latest");
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task TagImageAsync_Should_Succeed() {
var result = await _client.TagImageAsync("alpine:latest", "myrepo", "v1");
PodmanClientTestFixture.AssertSuccess(result);
}
#endregion
#region Fail Cases
[Fact]
public async Task PodmanClient_PullImage_Errors() {
var result = await _client.PullImageAsync("dghdfdghmhgn:latest");
PodmanClientTestFixture.AssertFailure(result);
}
[Fact]
public async Task PodmanClient_TagImage_Errors() {
var result = await _client.TagImageAsync("dghdfdghmhgn:latest", "myrepo", "v1");
PodmanClientTestFixture.AssertFailure(result);
}
#endregion
}

View File

@ -1,128 +0,0 @@
using System.Text;
using MaksIT.PodmanClientDotNet.Streaming;
namespace MaksIT.PodmanClientDotNet.Tests;
[Trait("Category", "Integration")]
public class PodmanClientStreamingIntegrationTests {
private readonly IPodmanClient _client = PodmanClientTestFixture.CreateClient();
[Fact]
public async Task AttachContainerSessionAsync_ReadsStdoutFromRunningContainer() {
var cancellationToken = TestContext.Current.CancellationToken;
var name = $"attach-session-{Guid.NewGuid():N}";
const string image = "alpine:latest";
await PullImageAsync(image);
var containerId = await CreateContainerAsync(name, image, ["sh", "-c", "echo hello-attach"]);
await StartContainerAsync(containerId);
var attachResult = await _client.AttachContainerSessionAsync(
containerId,
stream: true,
stdout: true,
stderr: true,
stdin: false,
tty: false,
cancellationToken: cancellationToken);
PodmanClientTestFixture.AssertSuccess(attachResult);
await using var session = attachResult.Value!;
Assert.False(session.IsRawTerminal);
var output = new StringBuilder();
PodmanStreamFrame? frame;
while ((frame = await session.ReadFrameAsync(cancellationToken)) is not null)
output.Append(Encoding.UTF8.GetString(frame.Data));
Assert.Contains("hello-attach", output.ToString());
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
[Fact]
public async Task StartExecSessionAsync_RunsCommandAndReadsOutput() {
var cancellationToken = TestContext.Current.CancellationToken;
var name = $"exec-session-{Guid.NewGuid():N}";
const string image = "alpine:latest";
await PullImageAsync(image);
var containerId = await CreateContainerAsync(name, image, ["sh", "-c", "sleep 300"]);
await StartContainerAsync(containerId);
var createExec = await _client.CreateExecAsync(containerId, ["echo", "exec-ok"]);
PodmanClientTestFixture.AssertSuccess(createExec);
var execId = createExec.Value!.Id!;
var sessionResult = await _client.StartExecSessionAsync(execId, tty: false, cancellationToken: cancellationToken);
PodmanClientTestFixture.AssertSuccess(sessionResult);
await using var session = sessionResult.Value!;
var output = new StringBuilder();
PodmanStreamFrame? frame;
while ((frame = await session.ReadFrameAsync(cancellationToken)) is not null)
output.Append(Encoding.UTF8.GetString(frame.Data));
Assert.Contains("exec-ok", output.ToString());
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
[Fact]
public async Task PullImageWithProgressAsync_YieldsStatusLines() {
const string image = "alpine:latest";
var cancellationToken = TestContext.Current.CancellationToken;
var result = await _client.PullImageWithProgressAsync(image, cancellationToken: cancellationToken);
PodmanClientTestFixture.AssertSuccess(result);
await using var session = result.Value!;
var lines = new List<string>();
await foreach (var item in session.ReadProgressAsync(cancellationToken)) {
if (!string.IsNullOrEmpty(item.Status))
lines.Add(item.Status);
if (!string.IsNullOrEmpty(item.Error))
break;
}
Assert.NotEmpty(lines);
}
[Fact]
public async Task AttachContainerSessionAsync_InvalidContainer_Fails() {
var result = await _client.AttachContainerSessionAsync(
"nonexistent-container-id",
cancellationToken: TestContext.Current.CancellationToken);
PodmanClientTestFixture.AssertFailure(result);
}
private async Task PullImageAsync(string image) {
var result = await _client.PullImageAsync(image);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task<string> CreateContainerAsync(string name, string image, List<string> command) {
var result = await _client.CreateContainerAsync(name: name, image: image, command: command);
string? containerId = null;
PodmanClientTestFixture.AssertSuccess(result, value => containerId = value!.Id);
return containerId!;
}
private async Task StartContainerAsync(string containerId) {
var result = await _client.StartContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task StopContainerAsync(string containerId) {
var result = await _client.StopContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
private async Task ForceDeleteContainerAsync(string containerId) {
var result = await _client.ForceDeleteContainerAsync(containerId);
PodmanClientTestFixture.AssertSuccess(result);
}
}

View File

@ -1,42 +0,0 @@
using Microsoft.Extensions.Logging;
using MaksIT.Results;
namespace MaksIT.PodmanClientDotNet.Tests;
internal static class PodmanClientTestFixture {
/// <summary>
/// Podman API base URL for integration tests. Set <c>PODMAN_TEST_URL</c> (or <c>PODMAN_INTEGRATION_URL</c>) to enable.
/// </summary>
internal static string? IntegrationServerUrl =>
Environment.GetEnvironmentVariable("PODMAN_TEST_URL")
?? Environment.GetEnvironmentVariable("PODMAN_INTEGRATION_URL");
internal static bool IsIntegrationEnabled => !string.IsNullOrWhiteSpace(IntegrationServerUrl);
internal static IPodmanClient CreateClient() {
Assert.SkipUnless(IsIntegrationEnabled, "Set PODMAN_TEST_URL to run Podman integration tests.");
var logger = LoggerFactory.Create(builder => builder.AddConsole())
.CreateLogger<PodmanClient>();
return new PodmanClient(logger, IntegrationServerUrl!, 60);
}
internal static void AssertSuccess(Result result) {
Assert.True(result.IsSuccess, string.Join("; ", result.Messages));
}
internal static void AssertFailure(Result result) {
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Messages);
}
internal static void AssertSuccess<T>(Result<T?> result, Action<T?>? assertValue = null) {
Assert.True(result.IsSuccess, string.Join("; ", result.Messages));
assertValue?.Invoke(result.Value);
}
internal static void AssertFailure<T>(Result<T?> result) {
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Messages);
}
}

View File

@ -1,6 +1,4 @@
using System.Text;
using MaksIT.PodmanClientDotNet.Dtos.Build;
using MaksIT.PodmanClientDotNet.Internal;
using Microsoft.Extensions.Logging;

View File

@ -1,4 +1,5 @@
<Solution>
<Project Path="PodmanClientDotNet.Tests/PodmanClientDotNet.Tests.csproj" />
<Project Path="PodmanClient/PodmanClientDotNet.csproj" />
<Project Path="PodmanClient.PowerShell/PodmanClient.PowerShell.csproj" />
</Solution>

View File

@ -0,0 +1,77 @@
# Shared helpers for Podman E2E. Dot-sourced by Test-PodmanE2E.ps1 and scenarios.
$script:PodmanE2eScenarioRegistry = [System.Collections.Generic.List[hashtable]]::new()
$script:PodmanE2eCmdletHits = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
function Clear-PodmanE2eScenarioRegistry {
$script:PodmanE2eScenarioRegistry.Clear()
$script:PodmanE2eCmdletHits.Clear()
}
function Register-PodmanE2eScenario {
param(
[Parameter(Mandatory)][string] $Id,
[Parameter(Mandatory)][string] $Description,
[Parameter(Mandatory)][scriptblock] $ScriptBlock
)
$script:PodmanE2eScenarioRegistry.Add(@{
Id = $Id
Description = $Description
ScriptBlock = $ScriptBlock
}) | Out-Null
}
function Write-E2eLog {
param(
[Parameter(Mandatory)][string] $Message,
[ValidateSet('Default', 'Step', 'Ok', 'Warn')]
[string] $Kind = 'Default'
)
$ts = (Get-Date).ToUniversalTime().ToString('o')
$line = "[$ts] $Message"
switch ($Kind) {
'Step' { Write-Host $line -ForegroundColor Cyan }
'Ok' { Write-Host $line -ForegroundColor Green }
'Warn' { Write-Host $line -ForegroundColor Yellow }
default { Write-Host $line }
}
}
function New-PodmanE2eSuffix {
[guid]::NewGuid().ToString('N').Substring(0, 8)
}
function Use-PodmanE2eCmdlet {
param([Parameter(Mandatory)][string] $Name)
[void]$script:PodmanE2eCmdletHits.Add($Name)
}
function Assert-PodmanE2eTrue {
param([bool] $Condition, [string] $Message)
if (-not $Condition) { throw $Message }
}
function Assert-PodmanE2eError {
param(
[Parameter(Mandatory)][scriptblock] $ScriptBlock,
[string] $Message = 'Expected cmdlet to fail.'
)
$failed = $false
try {
& $ScriptBlock 2>&1 | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) { $failed = $true }
}
}
catch {
$failed = $true
}
if (-not $failed) { throw $Message }
}
function New-PodmanE2eTarFromFolder {
param([Parameter(Mandatory)][string] $FolderPath, [Parameter(Mandatory)][string] $TarPath)
if (Test-Path -LiteralPath $TarPath) { Remove-Item -LiteralPath $TarPath -Force }
& tar -cf $TarPath -C $FolderPath .
if ($LASTEXITCODE -ne 0) { throw "tar failed creating $TarPath" }
return $TarPath
}

View File

@ -0,0 +1,18 @@
@echo off
setlocal
cd /d "%~dp0"
echo.
echo Podman E2E — scenarios in src\e2e-tests\scenarios\ (requires PODMAN_TEST_URL^).
echo Optional: -Scenario System or -Scenario '*Image*'
echo.
where pwsh >nul 2>&1
if errorlevel 1 (
echo PowerShell 7+ ^(pwsh^) is required but was not found in PATH.
echo Install from https://github.com/PowerShell/PowerShell/releases ^(.NET 10 host^)
exit /b 1
)
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0Test-PodmanE2E.ps1" %*
exit /b %ERRORLEVEL%

View File

@ -0,0 +1,172 @@
#Requires -Version 7
<#
.SYNOPSIS
End-to-end tests against a running Podman API via MaksIT.PodmanClientDotNet.PowerShell.
.DESCRIPTION
Builds the module, connects with PODMAN_TEST_URL, then runs scenarios from scenarios\Scenario-*.ps1.
Filter:
pwsh -File .\src\e2e-tests\Test-PodmanE2E.ps1 -Scenario 'System'
pwsh -File .\src\e2e-tests\Test-PodmanE2E.ps1 -Scenario '*Image*','*Container*'
.EXAMPLE
$env:PODMAN_TEST_URL = 'http://192.168.2.128:8080'
pwsh -File .\src\e2e-tests\Test-PodmanE2E.ps1
#>
param(
[string[]] $Scenario = @('*')
)
# Allow comma-separated values from cmd.exe / single-arg callers: -Scenario System,Images
$Scenario = @(
$Scenario |
ForEach-Object { $_ -split ',' } |
ForEach-Object { $_.Trim().Trim("'").Trim('"') } |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
)
if ($Scenario.Count -eq 0) {
$Scenario = @('*')
}
$e2eRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
. (Join-Path $e2eRoot 'Podman.E2E.Common.ps1')
if ($PSVersionTable.PSVersion.Major -lt 7) {
throw 'This script is pwsh-only.'
}
$runtimeFx = [System.Runtime.InteropServices.RuntimeInformation]::FrameworkDescription
if ($runtimeFx -notmatch '\.NET 10\.') {
throw (
"The Podman module is net10.0; this pwsh is hosted on: $runtimeFx" + [Environment]::NewLine +
'Install PowerShell 7 with .NET 10 from https://github.com/PowerShell/PowerShell/releases'
)
}
$envName = 'PODMAN_TEST_URL'
$url = [Environment]::GetEnvironmentVariable($envName, 'Process')
if ([string]::IsNullOrWhiteSpace($url)) {
$url = [Environment]::GetEnvironmentVariable($envName, 'User')
}
if ([string]::IsNullOrWhiteSpace($url)) {
$url = [Environment]::GetEnvironmentVariable($envName, 'Machine')
}
if ([string]::IsNullOrWhiteSpace($url)) {
throw @"
PODMAN_TEST_URL is not set (Process, User, or Machine).
Example:
`$env:PODMAN_TEST_URL = 'http://192.168.2.128:8080'
# must be an absolute http(s) URL (include the scheme)
"@
}
$url = $url.Trim().TrimEnd('/')
if ($url -notmatch '^https?://') {
throw "PODMAN_TEST_URL must be an absolute http(s) URL. Got: $url"
}
$repoRoot = Resolve-Path (Join-Path $e2eRoot '..\..')
$moduleTfm = 'net10.0'
$relModuleProject = 'src\PodmanClient.PowerShell\PodmanClient.PowerShell.csproj'
$relModuleManifest = "src\PodmanClient.PowerShell\bin\Debug\$moduleTfm\MaksIT.PodmanClientDotNet.PowerShell.psd1"
Write-E2eLog -Kind Step -Message "Build: $relModuleProject"
Push-Location $repoRoot
try {
$buildOutput = dotnet build $relModuleProject 2>&1
if ($LASTEXITCODE -ne 0) {
$buildOutput | ForEach-Object { Write-Host $_ }
throw "Build failed: $relModuleProject"
}
if (-not (Test-Path -LiteralPath $relModuleManifest)) {
throw "Module manifest not found: $relModuleManifest"
}
$moduleManifest = (Resolve-Path -LiteralPath $relModuleManifest).Path
}
finally {
Pop-Location
}
Write-E2eLog -Kind Ok -Message "Importing module: $moduleManifest"
Import-Module $moduleManifest -Force
$exported = @(Get-Command -Module MaksIT.PodmanClientDotNet.PowerShell | Select-Object -ExpandProperty Name)
Clear-PodmanE2eScenarioRegistry
$scenarioDir = Join-Path $e2eRoot 'scenarios'
if (-not (Test-Path -LiteralPath $scenarioDir)) {
throw "Scenarios directory missing: $scenarioDir"
}
Get-ChildItem -LiteralPath $scenarioDir -Filter 'Scenario-*.ps1' | Sort-Object Name | ForEach-Object {
Write-E2eLog -Message "Load scenarios: $($_.Name)"
. $_.FullName
}
Write-E2eLog -Kind Step -Message "Connect-Podman (base URL: $url, ApiVersion: v5.4.0)"
Connect-Podman -BaseAddress $url -ApiVersion 'v5.4.0'
Use-PodmanE2eCmdlet Connect-Podman
Write-E2eLog -Kind Ok -Message 'Connect-Podman: session ready'
try {
$ver = Get-PodmanVersion
$engine = $ver.Version
if (-not $engine) { $engine = $ver.Components | Where-Object { $_.Name -eq 'Podman Engine' } | Select-Object -ExpandProperty Version -First 1 }
Write-E2eLog -Message "Server Podman version: $engine (E2E baseline: 5.4.0)"
if ($engine -and ("$engine" -notlike '5.4*')) {
Write-E2eLog -Kind Warn "Server is $engine; this suite is validated against Podman 5.4.0"
}
}
catch {
Write-E2eLog -Kind Warn "Could not read Get-PodmanVersion: $($_.Exception.Message)"
}
$ErrorActionPreference = 'Stop'
$ran = 0
try {
foreach ($entry in $script:PodmanE2eScenarioRegistry) {
$include = $false
foreach ($p in $Scenario) {
if ($entry.Id -like $p) {
$include = $true
break
}
}
if (-not $include) {
Write-E2eLog -Kind Warn "Skip (filter): $($entry.Id)"
continue
}
Write-E2eLog -Kind Step -Message "========== Scenario: $($entry.Id) =========="
Write-E2eLog -Message $entry.Description
try {
& $entry.ScriptBlock
$ran++
}
catch {
Write-E2eLog -Kind Warn "Scenario '$($entry.Id)' FAILED: $($_.Exception.Message)"
throw
}
}
if ($ran -eq 0) {
$registered = ($script:PodmanE2eScenarioRegistry | ForEach-Object { $_.Id }) -join ', '
throw "No scenarios matched -Scenario patterns: $($Scenario -join ', '). Registered: $registered"
}
Use-PodmanE2eCmdlet Disconnect-Podman
$missing = @($exported | Where-Object { -not $script:PodmanE2eCmdletHits.Contains($_) })
if ($missing.Count -gt 0 -and ($Scenario.Count -eq 1 -and $Scenario[0] -eq '*')) {
Write-E2eLog -Kind Warn -Message ("Cmdlets not marked Use-PodmanE2eCmdlet in scenarios: " + ($missing -join ', '))
throw "E2E completeness gate failed: $($missing.Count) cmdlet(s) not exercised."
}
Write-E2eLog -Kind Ok -Message "All selected scenarios passed ($ran run)."
}
finally {
Disconnect-Podman
Write-E2eLog -Message 'Disconnect-Podman'
}

View File

@ -0,0 +1,40 @@
Register-PodmanE2eScenario -Id 'System' -Description 'System ping, version, info, df, events sample, prune system' -ScriptBlock {
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-events-{0}.json" -f (New-PodmanE2eSuffix))
try {
Write-E2eLog -Kind Step 'Test-PodmanConnection'
Use-PodmanE2eCmdlet 'Test-PodmanConnection'
$ping = Test-PodmanConnection
Assert-PodmanE2eTrue ($null -ne $ping -and $ping.Ping) 'Ping did not return OK'
Write-E2eLog -Kind Step 'Get-PodmanVersion'
Use-PodmanE2eCmdlet 'Get-PodmanVersion'
$ver = Get-PodmanVersion
Assert-PodmanE2eTrue ($null -ne $ver -and -not [string]::IsNullOrWhiteSpace($ver.Version)) 'Version string missing'
Write-E2eLog -Kind Step 'Get-PodmanInfo'
Use-PodmanE2eCmdlet 'Get-PodmanInfo'
$info = Get-PodmanInfo
Assert-PodmanE2eTrue ($null -ne $info) 'Info returned null'
Write-E2eLog -Kind Step 'Get-PodmanSystemDiskUsage'
Use-PodmanE2eCmdlet 'Get-PodmanSystemDiskUsage'
$df = Get-PodmanSystemDiskUsage
Assert-PodmanE2eTrue ($null -ne $df) 'System disk usage returned null'
Write-E2eLog -Kind Step 'Get-PodmanEvent (timed sample)'
Use-PodmanE2eCmdlet 'Get-PodmanEvent'
$null = Get-PodmanEvent -OutFile $tmp -ReadTimeoutSeconds 2
Assert-PodmanE2eTrue (Test-Path -LiteralPath $tmp) 'Events OutFile was not created'
Write-E2eLog -Kind Step 'Invoke-PodmanPruneSystem'
Use-PodmanE2eCmdlet 'Invoke-PodmanPruneSystem'
$null = Invoke-PodmanPruneSystem
Write-E2eLog -Kind Ok 'System scenario passed'
}
finally {
if (Test-Path -LiteralPath $tmp) {
Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue
}
}
}

View File

@ -0,0 +1,139 @@
Register-PodmanE2eScenario -Id 'Images' -Description 'Pull/list/inspect/tag/history/tree/export/save/load/import/push/prune images' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$tagRepo = "localhost/e2e-img-$suffix"
$tagName = 'v1'
$tagged = "${tagRepo}:${tagName}"
$batchTag = "localhost/e2e-batch-${suffix}:latest"
$importRef = "localhost/e2e-import-${suffix}:latest"
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-images-$suffix")
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$exportTar = Join-Path $tmpDir 'export.tar'
$saveTar = Join-Path $tmpDir 'save.tar'
$fsTar = Join-Path $tmpDir 'fs-import.tar'
$containerName = "e2e-img-export-$suffix"
$containerId = $null
try {
Write-E2eLog -Kind Step 'Invoke-PodmanPullImage alpine:latest'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Write-E2eLog -Kind Step 'Invoke-PodmanPullImageProgress'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImageProgress'
$progress = @(Invoke-PodmanPullImageProgress -Reference $image -Quiet -Wait)
Assert-PodmanE2eTrue ($progress.Count -ge 0) 'Pull progress returned unexpected result'
Write-E2eLog -Kind Step 'Get-PodmanImageList / Test-PodmanImage / Get-PodmanImage'
Use-PodmanE2eCmdlet 'Get-PodmanImageList'
$list = @(Get-PodmanImageList -All)
Assert-PodmanE2eTrue ($list.Count -gt 0) 'Image list empty after pull'
Use-PodmanE2eCmdlet 'Test-PodmanImage'
$exists = Test-PodmanImage -Name $image
Assert-PodmanE2eTrue ($exists -eq $true) 'alpine:latest should exist'
Use-PodmanE2eCmdlet 'Get-PodmanImage'
$inspect = Get-PodmanImage -Name $image
Assert-PodmanE2eTrue ($null -ne $inspect) 'Inspect image returned null'
Write-E2eLog -Kind Step 'Search-PodmanImage'
Use-PodmanE2eCmdlet 'Search-PodmanImage'
$search = @(Search-PodmanImage -Term 'alpine' -Limit 5)
Assert-PodmanE2eTrue ($search.Count -ge 0) 'Search failed unexpectedly'
Write-E2eLog -Kind Step 'Tag / history / tree / changes'
Use-PodmanE2eCmdlet 'Invoke-PodmanTagImage'
$null = Invoke-PodmanTagImage -Image $image -Repo $tagRepo -Tag $tagName
Use-PodmanE2eCmdlet 'Get-PodmanImageHistory'
$history = @(Get-PodmanImageHistory -Name $tagged)
Assert-PodmanE2eTrue ($history.Count -ge 0) 'History empty/failed'
Use-PodmanE2eCmdlet 'Get-PodmanImageTree'
$tree = Get-PodmanImageTree -Name $tagged
Assert-PodmanE2eTrue ($null -ne $tree) 'Image tree null'
Use-PodmanE2eCmdlet 'Get-PodmanImageChange'
$changes = Get-PodmanImageChange -Name $tagged
Assert-PodmanE2eTrue ($null -ne $changes -or $true) 'Image changes call completed'
Write-E2eLog -Kind Step 'Export-PodmanImage / Save-PodmanImage / Import-PodmanImageArchive'
Use-PodmanE2eCmdlet 'Export-PodmanImage'
$null = Export-PodmanImage -Reference @($image) -OutFile $exportTar
Assert-PodmanE2eTrue (Test-Path -LiteralPath $exportTar) 'Export tar missing'
Use-PodmanE2eCmdlet 'Save-PodmanImage'
$null = Save-PodmanImage -Name $image -OutFile $saveTar
Assert-PodmanE2eTrue (Test-Path -LiteralPath $saveTar) 'Save tar missing'
Use-PodmanE2eCmdlet 'Import-PodmanImageArchive'
$null = Import-PodmanImageArchive -Path $saveTar
Write-E2eLog -Kind Step 'Import-PodmanImage from container filesystem export'
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $containerName -Image $image -Command @('sh', '-c', 'echo import-src')
$containerId = $created.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($containerId)) 'Container create for import failed'
Use-PodmanE2eCmdlet 'Export-PodmanContainer'
$null = Export-PodmanContainer -Name $containerId -OutFile $fsTar
Use-PodmanE2eCmdlet 'Import-PodmanImage'
$null = Import-PodmanImage -Path $fsTar -Reference $importRef -Message "e2e-import-$suffix"
Write-E2eLog -Kind Step 'Invoke-PodmanPushImage (bogus destination expects failure)'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Invoke-PodmanPushImage'
Invoke-PodmanPushImage -Name $image -Destination "127.0.0.1:1/e2e-no-registry-$suffix/alpine:latest" -TlsVerify:$false
}
Write-E2eLog -Kind Step 'Untag / Remove-PodmanImage / Remove-PodmanImageBatch / Prune'
Use-PodmanE2eCmdlet 'Invoke-PodmanUntagImage'
$null = Invoke-PodmanUntagImage -Name $tagged -Repo $tagRepo -Tag $tagName
Use-PodmanE2eCmdlet 'Invoke-PodmanTagImage'
$null = Invoke-PodmanTagImage -Image $image -Repo "localhost/e2e-batch-$suffix" -Tag 'latest'
Use-PodmanE2eCmdlet 'Remove-PodmanImageBatch'
$null = Remove-PodmanImageBatch -Image @($batchTag) -Force
Use-PodmanE2eCmdlet 'Remove-PodmanImage'
try {
$null = Remove-PodmanImage -Name $importRef -Force
}
catch {
Write-E2eLog -Kind Warn "Remove import ref: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Invoke-PodmanPruneImage'
$null = Invoke-PodmanPruneImage
Write-E2eLog -Kind Step 'Negative: invalid image id'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanImage'
Get-PodmanImage -Name "no-such-image-$suffix"
}
Write-E2eLog -Kind Ok 'Images scenario passed'
}
finally {
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
foreach ($ref in @($tagged, $batchTag, $importRef)) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanImage'
Remove-PodmanImage -Name $ref -Force
}
catch { }
}
if (Test-Path -LiteralPath $tmpDir) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

View File

@ -0,0 +1,229 @@
Register-PodmanE2eScenario -Id 'ContainersLifecycle' -Description 'Container create/init/start/inspect/stats/pause/kill/wait/commit/checkpoint/mount/prune' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$name = "e2e-ctr-$suffix"
$renamed = "e2e-ctr-renamed-$suffix"
$commitRepo = "localhost/e2e-commit-$suffix"
$commitTag = 'latest'
$containerId = $null
$activeName = $name
$ckptTar = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-ckpt-$suffix.tar")
$logFile = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-ctrlog-$suffix.txt")
try {
Write-E2eLog -Kind Step 'Ensure alpine image'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Write-E2eLog -Kind Step 'New-PodmanContainer / Initialize / Start'
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $name -Image $image -Command @('sh', '-c', 'sleep 300')
$containerId = $created.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($containerId)) 'CreateContainerResponseDto.Id missing'
Use-PodmanE2eCmdlet 'Initialize-PodmanContainer'
try {
Initialize-PodmanContainer -Name $containerId
}
catch {
Write-E2eLog -Kind Warn "Initialize-PodmanContainer: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $containerId
Write-E2eLog -Kind Step 'Inspect / list / exists / logs / stats / top / changes'
Use-PodmanE2eCmdlet 'Get-PodmanContainer'
$inspect = Get-PodmanContainer -Name $containerId
Assert-PodmanE2eTrue ($null -ne $inspect) 'Inspect container null'
Use-PodmanE2eCmdlet 'Get-PodmanContainerList'
$clist = @(Get-PodmanContainerList -All)
Assert-PodmanE2eTrue ($clist.Count -gt 0) 'Container list empty'
Use-PodmanE2eCmdlet 'Test-PodmanContainer'
$exists = Test-PodmanContainer -Name $containerId
Assert-PodmanE2eTrue ($exists -eq $true) 'Container should exist'
Use-PodmanE2eCmdlet 'Get-PodmanContainerLog'
$null = Get-PodmanContainerLog -Name $containerId -Tail '10' -OutFile $logFile
Use-PodmanE2eCmdlet 'Get-PodmanContainerStat'
$stat = Get-PodmanContainerStat -Name $containerId
Assert-PodmanE2eTrue ($null -ne $stat) 'Container stat null'
Use-PodmanE2eCmdlet 'Get-PodmanContainerStatBatch'
$batch = Get-PodmanContainerStatBatch -Containers @($containerId)
Assert-PodmanE2eTrue ($null -ne $batch) 'Container stat batch null'
Use-PodmanE2eCmdlet 'Get-PodmanContainerProcess'
$proc = Get-PodmanContainerProcess -Name $containerId -Stream:$false
Assert-PodmanE2eTrue ($null -ne $proc) 'Container process/top null'
Use-PodmanE2eCmdlet 'Get-PodmanContainerTop'
$top = Get-PodmanContainerTop -Name $containerId -Stream:$false
Assert-PodmanE2eTrue ($null -ne $top) 'Container top null'
Use-PodmanE2eCmdlet 'Get-PodmanContainerChange'
$chg = Get-PodmanContainerChange -Name $containerId
Assert-PodmanE2eTrue ($null -ne $chg -or $true) 'Container changes completed'
Write-E2eLog -Kind Step 'Suspend / Resume (pause/unpause)'
Use-PodmanE2eCmdlet 'Suspend-PodmanContainer'
Suspend-PodmanContainer -Name $containerId
Use-PodmanE2eCmdlet 'Resume-PodmanContainer'
Resume-PodmanContainer -Name $containerId
Write-E2eLog -Kind Step 'Restart / Rename'
Use-PodmanE2eCmdlet 'Restart-PodmanContainer'
Restart-PodmanContainer -Name $containerId -Timeout 10
Use-PodmanE2eCmdlet 'Rename-PodmanContainer'
Rename-PodmanContainer -Name $containerId -NewName $renamed
$activeName = $renamed
Write-E2eLog -Kind Step 'HealthCheck (may fail without HEALTHCHECK)'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Invoke-PodmanContainerHealthCheck'
Invoke-PodmanContainerHealthCheck -Name $activeName
}
Write-E2eLog -Kind Step 'Commit container to image'
Use-PodmanE2eCmdlet 'Invoke-PodmanCommitContainer'
$null = Invoke-PodmanCommitContainer -Container $activeName -Repo $commitRepo -Tag $commitTag -Comment "e2e-$suffix" -Format docker -Pause:$false
Write-E2eLog -Kind Step 'Mount / Dismount / Get-PodmanMountedContainer'
$mounted = $false
Use-PodmanE2eCmdlet 'Mount-PodmanContainer'
try {
$null = Mount-PodmanContainer -Name $activeName
$mounted = $true
}
catch {
Write-E2eLog -Kind Warn "Mount failed (rootless?): $($_.Exception.Message)"
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Mount-PodmanContainer'
Mount-PodmanContainer -Name $activeName
}
}
Use-PodmanE2eCmdlet 'Get-PodmanMountedContainer'
$null = Get-PodmanMountedContainer
Use-PodmanE2eCmdlet 'Dismount-PodmanContainer'
if ($mounted) {
Dismount-PodmanContainer -Name $activeName
}
else {
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Dismount-PodmanContainer'
Dismount-PodmanContainer -Name $activeName
}
}
Write-E2eLog -Kind Step 'Checkpoint / Restore (CRIU may be missing; remote import path is host-local)'
$ckptOk = $false
Use-PodmanE2eCmdlet 'Checkpoint-PodmanContainer'
try {
$null = Checkpoint-PodmanContainer -Name $activeName -LeaveRunning -Export -OutFile $ckptTar
$ckptOk = $true
Write-E2eLog -Kind Ok 'Checkpoint succeeded'
}
catch {
Write-E2eLog -Kind Warn "Checkpoint failed (likely no CRIU): $($_.Exception.Message)"
}
if (-not $ckptOk) {
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Checkpoint-PodmanContainer'
Checkpoint-PodmanContainer -Name $activeName -LeaveRunning
}
}
# ImportPath is a server-side path; a Windows temp file is not valid on the remote Podman host.
Use-PodmanE2eCmdlet 'Restore-PodmanContainer'
Assert-PodmanE2eError {
Restore-PodmanContainer -Name $activeName -ImportPath '/tmp/podman-e2e-missing-checkpoint.tar'
}
Write-E2eLog -Kind Step 'Ensure running, then Kill / Wait / Stop / Remove / Prune'
try {
Start-PodmanContainer -Name $activeName
}
catch {
Write-E2eLog -Kind Warn "Start before kill: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Kill-PodmanContainer'
try {
Kill-PodmanContainer -Name $activeName -Signal 'TERM'
}
catch {
Write-E2eLog -Kind Warn "Kill: $($_.Exception.Message)"
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Kill-PodmanContainer'
Kill-PodmanContainer -Name "no-such-container-$suffix"
}
}
Use-PodmanE2eCmdlet 'Wait-PodmanContainer'
Write-E2eLog -Kind Warn 'Wait-PodmanContainer exercised via negative path only (avoid hang on running containers)'
Assert-PodmanE2eError {
Wait-PodmanContainer -Name "no-such-container-$suffix" -Condition 'exited'
}
Use-PodmanE2eCmdlet 'Stop-PodmanContainer'
try {
Stop-PodmanContainer -Name $activeName -IgnoreAlreadyStopped
}
catch {
Write-E2eLog -Kind Warn "Stop after kill: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $activeName -Force
$containerId = $null
$activeName = $null
Use-PodmanE2eCmdlet 'Invoke-PodmanPruneContainer'
$null = Invoke-PodmanPruneContainer
Write-E2eLog -Kind Step 'Negative: invalid container id'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanContainer'
Get-PodmanContainer -Name "no-such-container-$suffix"
}
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name "no-such-container-$suffix"
}
Write-E2eLog -Kind Ok 'ContainersLifecycle scenario passed'
}
finally {
if ($activeName) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $activeName -Force -Ignore
}
catch { }
}
elseif ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
try {
Use-PodmanE2eCmdlet 'Remove-PodmanImage'
Remove-PodmanImage -Name "${commitRepo}:${commitTag}" -Force
}
catch { }
foreach ($f in @($ckptTar, $logFile)) {
if (Test-Path -LiteralPath $f) {
Remove-Item -LiteralPath $f -Force -ErrorAction SilentlyContinue
}
}
}
}

View File

@ -0,0 +1,98 @@
Register-PodmanE2eScenario -Id 'ContainersArchiveAttach' -Description 'Container archive put/get/extract/export and attach/session streaming' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$sleepName = "e2e-arch-$suffix"
$echoName = "e2e-attach-$suffix"
$sleepId = $null
$echoId = $null
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-arch-$suffix")
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$payloadDir = Join-Path $tmpDir 'payload'
New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null
Set-Content -LiteralPath (Join-Path $payloadDir 'hello.txt') -Value "e2e-archive-$suffix" -NoNewline
$tarPath = Join-Path $tmpDir 'payload.tar'
$getArchive = Join-Path $tmpDir 'from-container.tar'
$exportCtr = Join-Path $tmpDir 'container-export.tar'
$attachOut = Join-Path $tmpDir 'attach.out'
try {
Write-E2eLog -Kind Step 'Ensure alpine image'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
New-PodmanE2eTarFromFolder -FolderPath $payloadDir -TarPath $tarPath | Out-Null
Write-E2eLog -Kind Step 'Create sleep container for archive ops'
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$sleepCreated = New-PodmanContainer -Name $sleepName -Image $image -Command @('sh', '-c', 'sleep 300')
$sleepId = $sleepCreated.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($sleepId)) 'Sleep container Id missing'
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $sleepId
Write-E2eLog -Kind Step 'Invoke-PodmanExtractArchive / Set-PodmanContainerArchive / Put / Get'
Use-PodmanE2eCmdlet 'Invoke-PodmanExtractArchive'
Invoke-PodmanExtractArchive -ContainerId $sleepId -Path '/e2e' -FilePath $tarPath -Pause:$false
Use-PodmanE2eCmdlet 'Set-PodmanContainerArchive'
Set-PodmanContainerArchive -ContainerId $sleepId -Path '/e2e-set' -FilePath $tarPath -Pause:$false
Use-PodmanE2eCmdlet 'Invoke-PodmanPutContainerArchive'
Invoke-PodmanPutContainerArchive -ContainerId $sleepId -Path '/e2e-put' -FilePath $tarPath -Pause:$false
Use-PodmanE2eCmdlet 'Get-PodmanContainerArchive'
$null = Get-PodmanContainerArchive -Name $sleepId -Path '/e2e' -OutFile $getArchive
Assert-PodmanE2eTrue (Test-Path -LiteralPath $getArchive) 'Get-PodmanContainerArchive OutFile missing'
Use-PodmanE2eCmdlet 'Export-PodmanContainer'
$null = Export-PodmanContainer -Name $sleepId -OutFile $exportCtr
Assert-PodmanE2eTrue (Test-Path -LiteralPath $exportCtr) 'Export-PodmanContainer OutFile missing'
Write-E2eLog -Kind Step 'Invoke-PodmanContainerAttach (logs after exit)'
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$echoCreated = New-PodmanContainer -Name $echoName -Image $image -Command @('sh', '-c', 'echo hello-attach')
$echoId = $echoCreated.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($echoId)) 'Echo container Id missing'
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $echoId
Start-Sleep -Seconds 2
Use-PodmanE2eCmdlet 'Wait-PodmanContainer'
try {
$null = Wait-PodmanContainer -Name $echoId -Condition 'exited'
}
catch {
Write-E2eLog -Kind Warn "Wait echo container: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Invoke-PodmanContainerAttach'
$null = Invoke-PodmanContainerAttach -Name $echoId -Logs -Stream:$false -OutFile $attachOut
Assert-PodmanE2eTrue (Test-Path -LiteralPath $attachOut) 'Attach OutFile missing'
$attachText = Get-Content -LiteralPath $attachOut -Raw
Assert-PodmanE2eTrue ("$attachText" -match 'hello-attach') "Attach logs missing hello-attach: $attachText"
Write-E2eLog -Kind Step 'Negative attach/session on invalid container'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Invoke-PodmanContainerSession'
Invoke-PodmanContainerSession -Name "no-such-container-$suffix" -Stdin:$false
}
Write-E2eLog -Kind Ok 'ContainersArchiveAttach scenario passed'
}
finally {
foreach ($id in @($sleepId, $echoId)) {
if ($id) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $id -Force -Ignore
}
catch { }
}
}
if (Test-Path -LiteralPath $tmpDir) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

View File

@ -0,0 +1,76 @@
Register-PodmanE2eScenario -Id 'Exec' -Description 'Create/start/inspect/resize exec and Invoke-PodmanExecSession' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$name = "e2e-exec-$suffix"
$containerId = $null
try {
Write-E2eLog -Kind Step 'Ensure alpine + sleep container'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $name -Image $image -Command @('sh', '-c', 'sleep 300')
$containerId = $created.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($containerId)) 'Container Id missing'
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $containerId
Write-E2eLog -Kind Step 'New-PodmanExec + Invoke-PodmanExecSession (echo exec-ok)'
Use-PodmanE2eCmdlet 'New-PodmanExec'
$exec = New-PodmanExec -ContainerName $containerId -Cmd @('echo', 'exec-ok')
$execId = $exec.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($execId)) 'CreateExecResponseDto.Id missing'
Use-PodmanE2eCmdlet 'Invoke-PodmanExecSession'
$out = Invoke-PodmanExecSession -ExecId $execId
Assert-PodmanE2eTrue ("$out" -match 'exec-ok') "Exec session output missing exec-ok: $out"
Write-E2eLog -Kind Step 'TTY exec: Resize + Start -Detach + Get-PodmanExec'
Use-PodmanE2eCmdlet 'New-PodmanExec'
$ttyExec = New-PodmanExec -ContainerName $containerId -Cmd @('sh', '-c', 'echo tty-ok') -Tty
$ttyId = $ttyExec.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($ttyId)) 'TTY exec Id missing'
Use-PodmanE2eCmdlet 'Resize-PodmanExec'
try {
Resize-PodmanExec -ExecId $ttyId -Height 40 -Width 120
}
catch {
Write-E2eLog -Kind Warn "Resize-PodmanExec: $($_.Exception.Message)"
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Resize-PodmanExec'
Resize-PodmanExec -ExecId "no-such-exec-$suffix" -Height 24 -Width 80
}
}
Use-PodmanE2eCmdlet 'Start-PodmanExec'
Start-PodmanExec -ExecId $ttyId -Detach -Tty
Use-PodmanE2eCmdlet 'Get-PodmanExec'
$inspected = Get-PodmanExec -ExecId $ttyId
Assert-PodmanE2eTrue ($null -ne $inspected) 'Inspect exec null'
Write-E2eLog -Kind Step 'Negative: invalid exec / container'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'New-PodmanExec'
New-PodmanExec -ContainerName "no-such-container-$suffix" -Cmd @('echo', 'x')
}
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanExec'
Get-PodmanExec -ExecId "no-such-exec-$suffix"
}
Write-E2eLog -Kind Ok 'Exec scenario passed'
}
finally {
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
}
}

View File

@ -0,0 +1,48 @@
Register-PodmanE2eScenario -Id 'Volumes' -Description 'Create/list/inspect/remove/prune volumes' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$volName = "e2e-vol-$suffix"
$createdName = $null
try {
Write-E2eLog -Kind Step 'New-PodmanVolume'
Use-PodmanE2eCmdlet 'New-PodmanVolume'
$vol = New-PodmanVolume -Name $volName
$createdName = if ($vol.Name) { $vol.Name } else { $volName }
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($createdName)) 'Volume name missing'
Write-E2eLog -Kind Step 'Get-PodmanVolumeList / Get-PodmanVolume'
Use-PodmanE2eCmdlet 'Get-PodmanVolumeList'
$list = @(Get-PodmanVolumeList)
Assert-PodmanE2eTrue ($list.Count -gt 0) 'Volume list empty'
Use-PodmanE2eCmdlet 'Get-PodmanVolume'
$inspect = Get-PodmanVolume -Name $createdName
Assert-PodmanE2eTrue ($null -ne $inspect) 'Volume inspect null'
Write-E2eLog -Kind Step 'Remove-PodmanVolume'
Use-PodmanE2eCmdlet 'Remove-PodmanVolume'
Remove-PodmanVolume -Name $createdName -Confirm:$false
$createdName = $null
Write-E2eLog -Kind Step 'Invoke-PodmanPruneVolume'
Use-PodmanE2eCmdlet 'Invoke-PodmanPruneVolume'
$null = Invoke-PodmanPruneVolume
Write-E2eLog -Kind Step 'Negative: missing volume'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanVolume'
Get-PodmanVolume -Name "no-such-volume-$suffix"
}
Write-E2eLog -Kind Ok 'Volumes scenario passed'
}
finally {
if ($createdName) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanVolume'
Remove-PodmanVolume -Name $createdName -Force -Confirm:$false
}
catch { }
}
}
}

View File

@ -0,0 +1,73 @@
Register-PodmanE2eScenario -Id 'Networks' -Description 'Create/list/inspect/connect/disconnect/remove networks' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$netName = "e2e-net-$suffix"
$ctrName = "e2e-net-ctr-$suffix"
$networkName = $null
$containerId = $null
try {
Write-E2eLog -Kind Step 'Ensure alpine + container'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $ctrName -Image $image -Command @('sh', '-c', 'sleep 300')
$containerId = $created.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($containerId)) 'Container Id missing'
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $containerId
Write-E2eLog -Kind Step 'New-PodmanNetwork'
Use-PodmanE2eCmdlet 'New-PodmanNetwork'
$net = New-PodmanNetwork -Name $netName
$networkName = if ($net.Name) { $net.Name } else { $netName }
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($networkName)) 'Network name missing'
Write-E2eLog -Kind Step 'Get-PodmanNetworkList / Get-PodmanNetwork'
Use-PodmanE2eCmdlet 'Get-PodmanNetworkList'
$list = @(Get-PodmanNetworkList)
Assert-PodmanE2eTrue ($list.Count -gt 0) 'Network list empty'
Use-PodmanE2eCmdlet 'Get-PodmanNetwork'
$inspect = Get-PodmanNetwork -Name $networkName
Assert-PodmanE2eTrue ($null -ne $inspect) 'Network inspect null'
Write-E2eLog -Kind Step 'Connect-PodmanNetwork / Disconnect-PodmanNetwork'
Use-PodmanE2eCmdlet 'Connect-PodmanNetwork'
Connect-PodmanNetwork -Name $networkName -Container $containerId
Use-PodmanE2eCmdlet 'Disconnect-PodmanNetwork'
Disconnect-PodmanNetwork -Name $networkName -Container $containerId -Force
Write-E2eLog -Kind Step 'Remove-PodmanNetwork'
Use-PodmanE2eCmdlet 'Remove-PodmanNetwork'
Remove-PodmanNetwork -Name $networkName -Confirm:$false
$networkName = $null
Write-E2eLog -Kind Step 'Negative: missing network'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanNetwork'
Get-PodmanNetwork -Name "no-such-network-$suffix"
}
Write-E2eLog -Kind Ok 'Networks scenario passed'
}
finally {
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
if ($networkName) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanNetwork'
Remove-PodmanNetwork -Name $networkName -Confirm:$false
}
catch { }
}
}
}

View File

@ -0,0 +1,138 @@
Register-PodmanE2eScenario -Id 'Pods' -Description 'Create/list/inspect/start/stop/restart/kill/pause/top/stats/prune pods' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$podName = "e2e-pod-$suffix"
$ctrName = "e2e-pod-ctr-$suffix"
$podIdOrName = $null
$containerId = $null
try {
Write-E2eLog -Kind Step 'Ensure alpine image'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Write-E2eLog -Kind Step 'New-PodmanPod'
Use-PodmanE2eCmdlet 'New-PodmanPod'
$pod = New-PodmanPod -Name $podName
$podIdOrName = if ($pod.Name) { $pod.Name } elseif ($pod.Id) { $pod.Id } else { $podName }
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($podIdOrName)) 'Pod id/name missing'
Write-E2eLog -Kind Step 'Get-PodmanPodList / Get-PodmanPod / Test-PodmanPod'
Use-PodmanE2eCmdlet 'Get-PodmanPodList'
$plist = @(Get-PodmanPodList -All)
Assert-PodmanE2eTrue ($plist.Count -gt 0) 'Pod list empty'
Use-PodmanE2eCmdlet 'Get-PodmanPod'
$inspect = Get-PodmanPod -Name $podIdOrName
Assert-PodmanE2eTrue ($null -ne $inspect) 'Pod inspect null'
Use-PodmanE2eCmdlet 'Test-PodmanPod'
$exists = Test-PodmanPod -Name $podIdOrName
Assert-PodmanE2eTrue ($exists -eq $true) 'Pod should exist'
Write-E2eLog -Kind Step 'Create container in default net, then Start-PodmanPod'
# Infra pod start exercises Start-PodmanPod; optional app container for richer top/stats.
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $ctrName -Image $image -Command @('sh', '-c', 'sleep 300')
$containerId = $created.Id
Use-PodmanE2eCmdlet 'Start-PodmanPod'
Start-PodmanPod -Name $podIdOrName
if ($containerId) {
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
try {
Start-PodmanContainer -Name $containerId
}
catch {
Write-E2eLog -Kind Warn "Start sidecar container: $($_.Exception.Message)"
}
}
Write-E2eLog -Kind Step 'Get-PodmanPodTop / Get-PodmanPodStat'
Use-PodmanE2eCmdlet 'Get-PodmanPodTop'
try {
$null = Get-PodmanPodTop -Name $podIdOrName
}
catch {
Write-E2eLog -Kind Warn "Get-PodmanPodTop: $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Get-PodmanPodStat'
$null = Get-PodmanPodStat
Write-E2eLog -Kind Step 'Suspend / Resume / Restart'
Use-PodmanE2eCmdlet 'Suspend-PodmanPod'
try {
Suspend-PodmanPod -Name $podIdOrName
Use-PodmanE2eCmdlet 'Resume-PodmanPod'
Resume-PodmanPod -Name $podIdOrName
}
catch {
Write-E2eLog -Kind Warn "Suspend/Resume pod: $($_.Exception.Message)"
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Suspend-PodmanPod'
Suspend-PodmanPod -Name "no-such-pod-$suffix"
}
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Resume-PodmanPod'
Resume-PodmanPod -Name "no-such-pod-$suffix"
}
}
Use-PodmanE2eCmdlet 'Restart-PodmanPod'
Restart-PodmanPod -Name $podIdOrName -Timeout 10
Write-E2eLog -Kind Step 'Kill / Stop / Remove / Prune'
Use-PodmanE2eCmdlet 'Kill-PodmanPod'
Kill-PodmanPod -Name $podIdOrName -Signal 'TERM'
Use-PodmanE2eCmdlet 'Stop-PodmanPod'
try {
Stop-PodmanPod -Name $podIdOrName -Timeout 10
}
catch {
Write-E2eLog -Kind Warn "Stop-PodmanPod after kill: $($_.Exception.Message)"
}
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
$containerId = $null
}
catch { }
}
Use-PodmanE2eCmdlet 'Remove-PodmanPod'
Remove-PodmanPod -Name $podIdOrName -Force -Confirm:$false
$podIdOrName = $null
Use-PodmanE2eCmdlet 'Invoke-PodmanPrunePod'
$null = Invoke-PodmanPrunePod
Write-E2eLog -Kind Step 'Negative: missing pod'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanPod'
Get-PodmanPod -Name "no-such-pod-$suffix"
}
Write-E2eLog -Kind Ok 'Pods scenario passed'
}
finally {
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
if ($podIdOrName) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanPod'
Remove-PodmanPod -Name $podIdOrName -Force -Confirm:$false
}
catch { }
}
}
}

View File

@ -0,0 +1,52 @@
Register-PodmanE2eScenario -Id 'Build' -Description 'Build image from temp Dockerfile via Invoke-PodmanBuildImage and progress API' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$tag = "e2e-build-$suffix"
$tagProgress = "e2e-build-prog-$suffix"
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-build-$suffix")
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$dockerfilePath = Join-Path $tmpDir 'Dockerfile'
$contextTar = Join-Path $tmpDir 'context.tar'
try {
Write-E2eLog -Kind Step 'Ensure alpine base available'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference 'alpine:latest' -Quiet
Write-E2eLog -Kind Step 'Write Dockerfile + context tar'
@(
'FROM alpine:latest'
'CMD ["echo","e2e-built"]'
) | Set-Content -LiteralPath $dockerfilePath -Encoding utf8
New-PodmanE2eTarFromFolder -FolderPath $tmpDir -TarPath $contextTar | Out-Null
Assert-PodmanE2eTrue (Test-Path -LiteralPath $contextTar) 'Build context tar missing'
Write-E2eLog -Kind Step 'Invoke-PodmanBuildImage'
Use-PodmanE2eCmdlet 'Invoke-PodmanBuildImage'
$report = Invoke-PodmanBuildImage -Dockerfile 'Dockerfile' -ContextPath $contextTar -Tag $tag -Pull
Assert-PodmanE2eTrue ($null -ne $report -or $true) 'BuildImage completed'
Use-PodmanE2eCmdlet 'Test-PodmanImage'
$built = Test-PodmanImage -Name $tag
Assert-PodmanE2eTrue ($built -eq $true) "Built image $tag should exist"
Write-E2eLog -Kind Step 'Invoke-PodmanBuildImageProgress'
Use-PodmanE2eCmdlet 'Invoke-PodmanBuildImageProgress'
$lines = @(Invoke-PodmanBuildImageProgress -Dockerfile 'Dockerfile' -ContextPath $contextTar -Tag $tagProgress -Wait)
Assert-PodmanE2eTrue ($lines.Count -ge 0) 'Build progress completed'
Write-E2eLog -Kind Ok 'Build scenario passed'
}
finally {
foreach ($t in @($tag, $tagProgress)) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanImage'
Remove-PodmanImage -Name $t -Force
}
catch { }
}
if (Test-Path -LiteralPath $tmpDir) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

View File

@ -0,0 +1,63 @@
Register-PodmanE2eScenario -Id 'Manifests' -Description 'Create/add/inspect/push/publish/remove manifests' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$manifestName = "e2e-manifest-${suffix}:latest"
$created = $false
try {
Write-E2eLog -Kind Step 'Ensure alpine image'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Write-E2eLog -Kind Step 'New-PodmanManifest'
Use-PodmanE2eCmdlet 'New-PodmanManifest'
$null = New-PodmanManifest -Name $manifestName -Image $image
$created = $true
Write-E2eLog -Kind Step 'Add-PodmanManifest / Get-PodmanManifest'
Use-PodmanE2eCmdlet 'Add-PodmanManifest'
try {
Add-PodmanManifest -Name $manifestName -Image $image
}
catch {
Write-E2eLog -Kind Warn "Add-PodmanManifest (may already include image): $($_.Exception.Message)"
}
Use-PodmanE2eCmdlet 'Get-PodmanManifest'
$inspect = Get-PodmanManifest -Name $manifestName
Assert-PodmanE2eTrue ($null -ne $inspect) 'Manifest inspect null'
Write-E2eLog -Kind Step 'Publish / Push manifest to bogus registry (expect fail)'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Publish-PodmanManifest'
Publish-PodmanManifest -Name $manifestName -Destination "127.0.0.1:1/e2e-no-registry-$suffix/manifest:latest"
}
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Invoke-PodmanPushManifest'
Invoke-PodmanPushManifest -Name $manifestName -Destination "127.0.0.1:1/e2e-no-registry-$suffix/manifest:push"
}
Write-E2eLog -Kind Step 'Remove-PodmanManifest'
Use-PodmanE2eCmdlet 'Remove-PodmanManifest'
Remove-PodmanManifest -Name $manifestName -Confirm:$false
$created = $false
Write-E2eLog -Kind Step 'Negative: missing manifest'
Assert-PodmanE2eError {
Use-PodmanE2eCmdlet 'Get-PodmanManifest'
Get-PodmanManifest -Name "no-such-manifest-${suffix}:latest"
}
Write-E2eLog -Kind Ok 'Manifests scenario passed'
}
finally {
if ($created) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanManifest'
Remove-PodmanManifest -Name $manifestName -Confirm:$false
}
catch { }
}
}
}

View File

@ -0,0 +1,94 @@
Register-PodmanE2eScenario -Id 'Generate' -Description 'Generate systemd/kube for a container; PlayKube minimal yaml then cleanup' -ScriptBlock {
$suffix = New-PodmanE2eSuffix
$image = 'alpine:latest'
$ctrName = "e2e-gen-ctr-$suffix"
$podName = "e2e-playkube-$suffix"
$containerId = $null
$playPod = $null
$playContainers = @()
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("podman-e2e-gen-$suffix")
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$kubeYaml = Join-Path $tmpDir 'play.yaml'
try {
Write-E2eLog -Kind Step 'Ensure alpine + container for generate'
Use-PodmanE2eCmdlet 'Invoke-PodmanPullImage'
$null = Invoke-PodmanPullImage -Reference $image -Quiet
Use-PodmanE2eCmdlet 'New-PodmanContainer'
$created = New-PodmanContainer -Name $ctrName -Image $image -Command @('sh', '-c', 'sleep 300')
$containerId = $created.Id
Assert-PodmanE2eTrue (-not [string]::IsNullOrWhiteSpace($containerId)) 'Container Id missing'
Use-PodmanE2eCmdlet 'Start-PodmanContainer'
Start-PodmanContainer -Name $containerId
Write-E2eLog -Kind Step 'Invoke-PodmanGenerateSystemd'
Use-PodmanE2eCmdlet 'Invoke-PodmanGenerateSystemd'
$systemd = Invoke-PodmanGenerateSystemd -Name $containerId -UseName
Assert-PodmanE2eTrue ($null -ne $systemd) 'GenerateSystemd returned null'
Write-E2eLog -Kind Step 'Invoke-PodmanGenerateKube'
Use-PodmanE2eCmdlet 'Invoke-PodmanGenerateKube'
$kube = Invoke-PodmanGenerateKube -Name @($containerId)
Assert-PodmanE2eTrue ($null -ne $kube) 'GenerateKube returned null'
Write-E2eLog -Kind Step 'Invoke-PodmanPlayKube (minimal pod yaml)'
@"
apiVersion: v1
kind: Pod
metadata:
name: $podName
spec:
restartPolicy: Never
containers:
- name: alpine
image: alpine:latest
command: ["sleep", "300"]
"@ | Set-Content -LiteralPath $kubeYaml -Encoding utf8
Use-PodmanE2eCmdlet 'Invoke-PodmanPlayKube'
$play = Invoke-PodmanPlayKube -Path $kubeYaml -Start
if ($play) {
$playPod = if ($play.Pod) { $play.Pod } else { $podName }
if ($play.Containers) { $playContainers = @($play.Containers) }
}
else {
$playPod = $podName
}
Write-E2eLog -Kind Ok 'Generate scenario passed'
}
finally {
foreach ($c in $playContainers) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $c -Force -Ignore
}
catch { }
}
if ($playPod) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanPod'
Remove-PodmanPod -Name $playPod -Force -Confirm:$false
}
catch {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanPod'
Remove-PodmanPod -Name $podName -Force -Confirm:$false
}
catch { }
}
}
if ($containerId) {
try {
Use-PodmanE2eCmdlet 'Remove-PodmanContainer'
Remove-PodmanContainer -Name $containerId -Force -Ignore
}
catch { }
}
if (Test-Path -LiteralPath $tmpDir) {
Remove-Item -LiteralPath $tmpDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
}

View File

@ -1,3 +1,4 @@
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" %*
pause
setlocal
pwsh -NoProfile -File "%~dp0engines\release\Invoke-ReleasePackage.ps1" %*
exit /b %ERRORLEVEL%

View File

@ -1,3 +1,4 @@
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
pause
setlocal
pwsh -NoProfile -File "%~dp0engines\test\Invoke-TestEngine.ps1" %*
exit /b %ERRORLEVEL%

View File

@ -4,23 +4,46 @@
<#
.SYNOPSIS
Plugin-driven release engine entry script.
.PARAMETER DryRun
When set, plugins that declare mutatesRemote in Get-PluginMetadata validate only (no registry push, GitHub release, or cluster deploy).
.PARAMETER Mode
Optional override for HelmSelfDeploy values resolution (single or ha).
When omitted, HelmSelfDeploy.deployMode from scriptSettings.json is used (CI/CD default).
Manual installs: Invoke-ReleasePackage-Single.bat / Invoke-ReleasePackage-HA.bat.
#>
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$srcDir = (Resolve-Path (Join-Path $scriptDir '..\..')).Path
[CmdletBinding()]
param(
[switch]$DryRun,
[ValidateSet('single', 'ha')]
[string]$Mode
)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$srcDir = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
. (Join-Path $srcDir 'modules/Engine/Import-EngineModules.ps1')
Import-EngineModules -Engine Release
$settings = Get-ScriptSettings -ScriptDir $scriptDir
$settings = Get-ScriptSettings -ScriptDir $PSScriptRoot
$resolveModeParams = @{ Settings = $settings }
if ($PSBoundParameters.ContainsKey('Mode')) {
$resolveModeParams['ModeOverride'] = $Mode
}
$deployMode = Get-DeployModeFromSettings @resolveModeParams
$configuredPlugins = Get-ConfiguredPlugins -Settings $settings
Write-Log -Level 'STEP' -Message '=================================================='
Write-Log -Level 'STEP' -Message 'RELEASE ENGINE'
Write-Log -Level 'STEP' -Message "RELEASE ENGINE (deploy mode: $deployMode)"
Write-Log -Level 'STEP' -Message '=================================================='
$plugins = $configuredPlugins
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $scriptDir -SrcDir $srcDir -Settings $settings
$engineContext = New-EngineContext -Plugins $plugins -ScriptDir $PSScriptRoot -SrcDir $srcDir -Settings $settings -DryRun:$DryRun -DeployMode $deployMode
Write-Log -Level 'OK' -Message 'All pre-flight checks passed!'
$sharedPluginSettings = $engineContext
@ -34,15 +57,15 @@ else {
for ($pluginIndex = 0; $pluginIndex -lt $plugins.Count; $pluginIndex++) {
$plugin = $plugins[$pluginIndex]
if ((Test-IsPublishPlugin -Plugin $plugin) -and -not $releaseStageInitialized) {
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -WriteLogs:$false) {
if ((Test-IsPublishPlugin -Plugin $plugin -EngineDirectory $PSScriptRoot) -and -not $releaseStageInitialized) {
if (Test-PluginRunnable -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -WriteLogs:$false) {
$remainingPlugins = @($plugins[$pluginIndex..($plugins.Count - 1)])
Initialize-ReleaseStageContext -RemainingPlugins $remainingPlugins -SharedSettings $sharedPluginSettings -ArtifactsDirectory $engineContext.artifactsDirectory -Version $engineContext.version
$releaseStageInitialized = $true
}
}
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $scriptDir -ContinueOnError:$false
$pluginSucceeded = Invoke-ConfiguredPlugin -Plugin $plugin -SharedSettings $sharedPluginSettings -EngineDirectory $PSScriptRoot -ContinueOnError:$false
if (-not $pluginSucceeded) {
$releaseHadPluginFailures = $true
break
@ -52,7 +75,7 @@ else {
if (-not $releaseStageInitialized) {
$noReleasePluginsLogLevel = if ($engineContext.isNonReleaseBranch) { 'INFO' } else { 'WARN' }
Write-Log -Level $noReleasePluginsLogLevel -Message 'No release-stage initialization ran (no enabled publish plugins reached, or none runnable).'
Write-Log -Level $noReleasePluginsLogLevel -Message 'No release-stage initialization ran (no enabled remote-mutation plugins reached, or none runnable).'
}
Write-Log -Level 'OK' -Message '=================================================='
@ -65,6 +88,9 @@ elseif ($engineContext.PSObject.Properties.Name -contains 'skipPublishPlugins' -
elseif ($engineContext.isNonReleaseBranch) {
Write-Log -Level 'OK' -Message 'NON-RELEASE RUN COMPLETE'
}
elseif ($engineContext.dryRun) {
Write-Log -Level 'OK' -Message 'DRY RUN COMPLETE'
}
else {
Write-Log -Level 'OK' -Message 'RELEASE COMPLETE'
}

View File

@ -16,7 +16,7 @@
"stageLabel": "test",
"enabled": true,
"project": "..\\..\\..\\src\\PodmanClientDotNet.Tests",
"resultsDir": "..\\..\\..\\testResults"
"resultsDir": "..\\..\\..\\test-results"
},
{
"name": "QualityGate",
@ -35,7 +35,7 @@
"projectFiles": [
"..\\..\\..\\src\\PodmanClient\\PodmanClientDotNet.csproj"
],
"artifactsDir": "..\\..\\..\\release"
"artifactsDir": "..\\..\\..\\releases"
},
{
"name": "DotNetCreateArchive",
@ -61,7 +61,7 @@
"name": "GitHub",
"stageLabel": "release",
"enabled": true,
"githubToken": "GITHUB_MAKS_IT_COM",
"githubSecret": "GitHub",
"repository": "https://github.com/MAKS-IT-COM/podman-client-dotnet",
"releaseNotesFile": "..\\..\\..\\CHANGELOG.md",
"releaseTitlePattern": "Release {version}"
@ -70,7 +70,7 @@
"name": "DotNetNuGet",
"stageLabel": "release",
"enabled": true,
"nugetApiKey": "NUGET_MAKS_IT",
"nugetSecret": "NuGet",
"source": "https://api.nuget.org/v3/index.json"
},
{
@ -86,3 +86,4 @@
}
]
}

View File

@ -1,9 +1,10 @@
{
"$schema": "https://json-schema.org/draft-07/schema",
"title": "Run Tests Script Settings",
"description": "Plugin-driven tests and coverage badges for PodmanClient.DotNet.",
"description": "Plugin-driven tests and shields.io coverage badges for PodmanClient.DotNet.",
"paths": {
"badgesDir": "..\\..\\..\\assets\\badges"
"readmePath": "..\\..\\..\\README.md",
"testResultsDir": "..\\..\\..\\test-results"
},
"plugins": [
{
@ -12,7 +13,8 @@
"enabled": true,
"projects": [
"..\\..\\..\\src\\PodmanClientDotNet.Tests"
]
],
"resultsDir": "..\\..\\..\\test-results"
},
{
"name": "QualityGate",
@ -25,20 +27,18 @@
"name": "CoverageBadges",
"stageLabel": "report",
"enabled": true,
"badgesDir": "..\\..\\..\\assets\\badges",
"badgeFormat": "shields",
"readmePath": "..\\..\\..\\README.md",
"badges": [
{
"name": "coverage-lines.svg",
"label": "Line Coverage",
"metric": "line"
},
{
"name": "coverage-branches.svg",
"label": "Branch Coverage",
"metric": "branch"
},
{
"name": "coverage-methods.svg",
"label": "Method Coverage",
"metric": "method"
}

View File

@ -9,6 +9,7 @@
Used by New-EngineContext and version plugins:
- DotNetReleaseVersion plugin -> projectFiles (.csproj <Version>)
- NpmReleaseVersion plugin -> packageJsonPath (package.json version)
- FileReleaseVersion plugin -> versionFilePath (repo-root VERSION file)
#>
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
@ -190,6 +191,72 @@ function Resolve-NpmReleaseVersion {
}
}
function Get-VersionFileSemver {
param(
[Parameter(Mandatory = $true)]
[string]$VersionFilePath
)
if (-not (Test-Path $VersionFilePath -PathType Leaf)) {
Write-Error "FileReleaseVersion: VERSION file not found at: $VersionFilePath"
exit 1
}
$version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim()
if ([string]::IsNullOrWhiteSpace($version)) {
Write-Error "FileReleaseVersion: VERSION file is empty at: $VersionFilePath"
exit 1
}
$version = $version -replace '^[vV]', ''
if ($version -notmatch '^\d+\.\d+\.\d+') {
Write-Error "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver."
exit 1
}
return $version
}
function Resolve-FileReleaseVersion {
param(
[Parameter(Mandatory = $true)]
[object[]]$Plugins,
[Parameter(Mandatory = $true)]
[string]$ScriptDir
)
$releaseVersionPlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' } | Select-Object -First 1)
if ($releaseVersionPlugin.Count -eq 0 -or $null -eq $releaseVersionPlugin[0]) {
Write-Error "Configure a FileReleaseVersion plugin in scriptSettings.json with versionFilePath."
exit 1
}
$releaseVersionSettings = $releaseVersionPlugin[0]
$versionFileSetting = if ($releaseVersionSettings.versionFilePath) {
$releaseVersionSettings.versionFilePath
}
else {
'..\\..\\..\\VERSION'
}
$versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $ScriptDir)
if ($versionFilePaths.Count -eq 0) {
Write-Error "Configure release version via FileReleaseVersion.versionFilePath (repo-root VERSION file)."
exit 1
}
$versionFilePath = $versionFilePaths[0]
Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..."
$version = Get-VersionFileSemver -VersionFilePath $versionFilePath
Write-Log -Level "OK" -Message " $([System.IO.Path]::GetFileName($versionFilePath)): $version"
return [pscustomobject]@{
version = $version
source = 'FileReleaseVersion'
}
}
function Resolve-ReleaseVersion {
param(
[Parameter(Mandatory = $true)]
@ -201,9 +268,15 @@ function Resolve-ReleaseVersion {
$dotnetPlugin = @($Plugins | Where-Object { $_.name -eq 'DotNetReleaseVersion' -and $_.enabled -ne $false })
$npmPlugin = @($Plugins | Where-Object { $_.name -eq 'NpmReleaseVersion' -and $_.enabled -ne $false })
$filePlugin = @($Plugins | Where-Object { $_.name -eq 'FileReleaseVersion' -and $_.enabled -ne $false })
if ($dotnetPlugin.Count -gt 0 -and $npmPlugin.Count -gt 0) {
Write-Error "Configure only one release version plugin: DotNetReleaseVersion or NpmReleaseVersion, not both."
$enabledVersionPlugins = @()
if ($dotnetPlugin.Count -gt 0) { $enabledVersionPlugins += 'DotNetReleaseVersion' }
if ($npmPlugin.Count -gt 0) { $enabledVersionPlugins += 'NpmReleaseVersion' }
if ($filePlugin.Count -gt 0) { $enabledVersionPlugins += 'FileReleaseVersion' }
if ($enabledVersionPlugins.Count -gt 1) {
Write-Error "Configure only one release version plugin: $($enabledVersionPlugins -join ', ')."
exit 1
}
@ -215,11 +288,15 @@ function Resolve-ReleaseVersion {
return Resolve-NpmReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
}
Write-Error "Configure a DotNetReleaseVersion plugin (projectFiles) or NpmReleaseVersion plugin (packageJsonPath) in scriptSettings.json."
if ($filePlugin.Count -gt 0) {
return Resolve-FileReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
}
Write-Error "Configure a DotNetReleaseVersion (projectFiles), NpmReleaseVersion (packageJsonPath), or FileReleaseVersion (versionFilePath) plugin in scriptSettings.json."
exit 1
}
Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-ReleaseVersion
Export-ModuleMember -Function Get-CsprojPropertyValue, Get-CsprojVersions, Get-VersionFileSemver, Resolve-RelativePaths, Resolve-DotNetReleaseVersion, Resolve-NpmReleaseVersion, Resolve-FileReleaseVersion, Resolve-ReleaseVersion

View File

@ -16,6 +16,36 @@ if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
}
}
function Test-IsEngineRuntimeModuleName {
param(
[Parameter(Mandatory = $true)]
[string]$ModuleName
)
# Engine runtime under modules/ only — never dual-homed under plugins/.
$engineNames = [System.Collections.Generic.HashSet[string]]::new(
[string[]]@(
'ChangelogSupport',
'ExternalCommandSupport',
'GitTools',
'Logging',
'ScriptConfig',
'TestRunner',
'EngineContext',
'PluginSupport',
'ReleaseSupport',
'TestSupport',
'DeployConfig',
'EngineContextSupport',
'OrchestratorSupport',
'PluginPathSupport'
),
[System.StringComparer]::OrdinalIgnoreCase
)
return $engineNames.Contains($ModuleName)
}
function Import-PluginDependency {
param(
[Parameter(Mandatory = $true)]
@ -31,13 +61,29 @@ function Import-PluginDependency {
$modulesDir = Get-RepoUtilsModulesDirectory
$engineModuleDir = $PSScriptRoot
$modulePath = Join-Path $modulesDir "$ModuleName.psm1"
if (-not (Test-Path $modulePath -PathType Leaf)) {
$modulePath = Join-Path $engineModuleDir "$ModuleName.psm1"
$srcDir = Get-RepoUtilsSrcDirectory
$pluginsRoot = Join-Path $srcDir 'plugins'
$candidatePaths = [System.Collections.Generic.List[string]]::new()
if (Test-IsEngineRuntimeModuleName -ModuleName $ModuleName) {
# Engine runtime: modules/ only (no plugins/ fallback).
$candidatePaths.Add((Join-Path $modulesDir "$ModuleName.psm1"))
$candidatePaths.Add((Join-Path $engineModuleDir "$ModuleName.psm1"))
$extensionsDir = Join-Path $modulesDir 'Extensions'
$candidatePaths.Add((Join-Path $extensionsDir "$ModuleName.psm1"))
}
else {
# Plugin helpers: plugins/ only (no modules/ legacy shadow).
foreach ($group in @('Shared', 'Platform', 'DotNet', 'Npm', 'Helm', 'Docker', 'Podman')) {
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $group) "$ModuleName.psm1"))
}
}
if (Test-Path $modulePath -PathType Leaf) {
foreach ($modulePath in $candidatePaths) {
if (Test-Path -LiteralPath $modulePath -PathType Leaf) {
Import-Module $modulePath -Force -Global -ErrorAction Stop
break
}
}
if (-not (Get-Command $RequiredCommand -ErrorAction SilentlyContinue)) {
@ -117,17 +163,277 @@ function Test-PluginAllowedOnBranch {
return $allowedBranches -contains $CurrentBranch
}
function Test-IsPublishPlugin {
function Get-PluginMetadataObject {
param(
[Parameter(Mandatory = $true)]
$Plugin
$Plugin,
[Parameter(Mandatory = $true)]
[string]$EngineDirectory
)
$modulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
if (-not (Test-Path $modulePath -PathType Leaf)) {
return $null
}
try {
$moduleInfo = Import-Module $modulePath -Force -PassThru -ErrorAction Stop
$metadataCommand = Get-Command -Name 'Get-PluginMetadata' -Module $moduleInfo.Name -ErrorAction SilentlyContinue
if (-not $metadataCommand) {
return $null
}
return & $metadataCommand
}
catch {
return $null
}
}
function Test-PluginCompatible {
<#
.SYNOPSIS
Applies an optional compatibility policy supplied by an extension.
#>
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $true)]
[string]$EngineDirectory,
[Parameter(Mandatory = $false)]
[bool]$WriteLogs = $true
)
$extensionTest = Get-Command Test-ExtensionPluginCompatibility -ErrorAction SilentlyContinue
if ($extensionTest) {
return & $extensionTest @PSBoundParameters
}
return $true
}
function Test-PluginMutatesRemote {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $false)]
[string]$EngineDirectory
)
if ($null -eq $Plugin -or [string]::IsNullOrWhiteSpace([string]$Plugin.name)) {
return $false
}
return @('GitHub', 'DotNetNuGet', 'DotNetDockerPush', 'DotNetHelmPush', 'NpmPublish') -contains ([string]$Plugin.name)
if ([string]::IsNullOrWhiteSpace($EngineDirectory)) {
if ($Plugin.PSObject.Properties.Name -contains 'context' -and $null -ne $Plugin.context -and $Plugin.context.scriptDir) {
$EngineDirectory = [string]$Plugin.context.scriptDir
}
elseif ($Plugin.PSObject.Properties.Name -contains 'scriptDir' -and -not [string]::IsNullOrWhiteSpace([string]$Plugin.scriptDir)) {
$EngineDirectory = [string]$Plugin.scriptDir
}
}
if ([string]::IsNullOrWhiteSpace($EngineDirectory)) {
return $false
}
$modulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
if (-not (Test-Path $modulePath -PathType Leaf)) {
return $false
}
try {
$moduleInfo = Import-Module $modulePath -Force -PassThru -ErrorAction Stop
$metadataCommand = Get-Command -Name 'Get-PluginMetadata' -Module $moduleInfo.Name -ErrorAction SilentlyContinue
if (-not $metadataCommand) {
return $false
}
$metadata = & $metadataCommand
if ($null -eq $metadata) {
return $false
}
if ($metadata.PSObject.Properties.Name -contains 'mutatesRemote') {
return [bool]$metadata.mutatesRemote
}
}
catch {
return $false
}
return $false
}
function Get-SecretEnvironmentValue {
<#
.SYNOPSIS
Reads a secret value from an environment variable by logical name.
.DESCRIPTION
Plugins never store secret material in scriptSettings.json. Settings hold a
logical name (e.g. "GitHub", "NuGet"); the process environment variable with
that same name must be set before the engine runs.
.PARAMETER Name
Logical secret name also the environment variable name to read.
.OUTPUTS
System.String. The environment variable value, or $null when unset.
.EXAMPLE
$token = Get-SecretEnvironmentValue -Name 'GitHub'
#>
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
return [Environment]::GetEnvironmentVariable($Name)
}
function Resolve-PluginSecretName {
<#
.SYNOPSIS
Resolves a logical secret name from a plugin's scriptSettings entry.
.DESCRIPTION
Reads a string property such as githubSecret / nugetSecret / npmSecret /
containerRegistrySecret from the plugin settings object. Returns $null when
the property is missing or blank.
.PARAMETER PluginSettings
Plugin settings object from scriptSettings.json (the enabled plugin entry).
.PARAMETER PropertyName
Settings property that holds the logical secret name (e.g. 'githubSecret').
.OUTPUTS
System.String. Trimmed logical secret name, or $null.
.EXAMPLE
$name = Resolve-PluginSecretName -PluginSettings $plugin -PropertyName 'nugetSecret'
$key = Get-SecretEnvironmentValue -Name $name
#>
param(
[Parameter(Mandatory = $true)]
$PluginSettings,
[Parameter(Mandatory = $true)]
[string]$PropertyName
)
if ($PluginSettings.PSObject.Properties.Name -contains $PropertyName) {
$value = [string]$PluginSettings.$PropertyName
if (-not [string]::IsNullOrWhiteSpace($value)) {
return $value.Trim()
}
}
return $null
}
function Get-RegistryCredentialsFromRuntime {
<#
.SYNOPSIS
Loads container-registry username/password from a logical secret name.
.DESCRIPTION
Looks up the environment variable named by SecretName. The value must be
Base64(UTF8('username:password')). Used by Docker/Podman/Helm registry login
and image-pull secret creation never pass the password itself as a parameter.
.PARAMETER SecretName
Logical secret name (environment variable name), not a password or token.
.PARAMETER SharedSettings
Optional engine shared context (reserved for callers that thread context).
.OUTPUTS
Hashtable with User and Password keys (decoded credential material).
.EXAMPLE
$creds = Get-RegistryCredentialsFromRuntime -SecretName 'ContainerRegistry'
# $creds.User / $creds.Password
#>
# SecretName is a logical env-var name from scriptSettings, not a password value.
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingPlainTextForPassword',
'SecretName',
Justification = 'Logical secret name for env lookup (Base64 username:password); not a credential value.'
)]
param(
[Parameter(Mandatory = $true)]
[string]$SecretName,
[Parameter(Mandatory = $false)]
[psobject]$SharedSettings
)
$raw = Get-SecretEnvironmentValue -Name $SecretName
if ([string]::IsNullOrWhiteSpace($raw)) {
throw "Environment variable '$SecretName' is not set."
}
try {
$decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($raw))
}
catch {
throw "Failed to decode '$SecretName' as Base64 (expected base64('username:password')): $($_.Exception.Message)"
}
$parts = $decoded -split ':', 2
if ($parts.Count -ne 2 -or [string]::IsNullOrWhiteSpace($parts[0]) -or [string]::IsNullOrWhiteSpace($parts[1])) {
throw "Decoded '$SecretName' must be in the form 'username:password'."
}
return @{ User = $parts[0]; Password = $parts[1] }
}
function Resolve-EngineDirectoryFromSharedSettings {
param(
[Parameter(Mandatory = $true)]
$SharedSettings
)
if ($SharedSettings.PSObject.Properties.Name -contains 'engineScriptDir' -and -not [string]::IsNullOrWhiteSpace([string]$SharedSettings.engineScriptDir)) {
return [string]$SharedSettings.engineScriptDir
}
return [string]$SharedSettings.scriptDir
}
function Test-PluginSkipsRemoteMutation {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $true)]
[psobject]$SharedSettings
)
$engineDirectory = Resolve-EngineDirectoryFromSharedSettings -SharedSettings $SharedSettings
if (-not (Test-PluginMutatesRemote -Plugin $Plugin -EngineDirectory $engineDirectory)) {
return $false
}
return ($Plugin.PSObject.Properties.Name -contains 'dryRun' -and $null -ne $Plugin.dryRun -and [bool]$Plugin.dryRun)
}
function Test-IsPublishPlugin {
param(
[Parameter(Mandatory = $true)]
$Plugin,
[Parameter(Mandatory = $false)]
[string]$EngineDirectory
)
return Test-PluginMutatesRemote -Plugin $Plugin -EngineDirectory $EngineDirectory
}
function Get-PluginSettingValue {
@ -263,12 +569,31 @@ function Resolve-PluginModulePath {
$srcDir = Split-Path (Split-Path $EngineDirectory -Parent) -Parent
$pluginsRoot = Join-Path $srcDir "plugins"
$pluginFileName = "{0}.psm1" -f $Plugin.name
$candidatePaths = @(
(Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Platform") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "DotNet") $pluginFileName),
(Join-Path (Join-Path $pluginsRoot "Npm") $pluginFileName)
)
$candidatePaths = [System.Collections.Generic.List[string]]::new()
$candidatePaths.Add((Join-Path (Join-Path $EngineDirectory "custom") $pluginFileName))
$preferredGroups = @('Platform', 'DotNet', 'Npm')
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $preferredGroups[0]) $pluginFileName))
if (Get-Command Get-ExtensionPluginModulePaths -ErrorAction SilentlyContinue) {
foreach ($extensionPath in Get-ExtensionPluginModulePaths -PluginsRoot $pluginsRoot -PluginFileName $pluginFileName) {
$candidatePaths.Add($extensionPath)
}
}
foreach ($group in $preferredGroups[1..($preferredGroups.Count - 1)]) {
$candidatePaths.Add((Join-Path (Join-Path $pluginsRoot $group) $pluginFileName))
}
$reservedPluginDirs = @($preferredGroups + @('Shared'))
if (Test-Path -LiteralPath $pluginsRoot -PathType Container) {
Get-ChildItem -LiteralPath $pluginsRoot -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -notin $reservedPluginDirs } |
Sort-Object Name |
ForEach-Object {
$candidatePaths.Add((Join-Path $_.FullName $pluginFileName))
}
}
foreach ($candidatePath in $candidatePaths) {
if (Test-Path $candidatePath -PathType Leaf) {
@ -360,11 +685,34 @@ function Invoke-ConfiguredPlugin {
return $true
}
$metadata = Get-PluginMetadataObject -Plugin $Plugin -EngineDirectory $EngineDirectory
if ($null -ne $metadata -and ($metadata.PSObject.Properties.Name -contains 'providesVersion') -and [bool]$metadata.providesVersion) {
$versionAlreadySet = $false
if (Get-Command Get-EngineState -ErrorAction SilentlyContinue) {
$existingVersion = Get-EngineState -Context $SharedSettings -Name 'version' -ErrorAction SilentlyContinue
$versionAlreadySet = -not [string]::IsNullOrWhiteSpace([string]$existingVersion)
}
elseif (($SharedSettings.PSObject.Properties.Name -contains 'version') -and -not [string]::IsNullOrWhiteSpace([string]$SharedSettings.version)) {
$versionAlreadySet = $true
}
if ($versionAlreadySet) {
Write-Log -Level "INFO" -Message "Skipping plugin '$($Plugin.name)' (version already resolved during New-EngineContext)."
return $true
}
# Test engine (and other hosts) may not resolve version in New-EngineContext; run the plugin now.
}
if ((Test-IsPublishPlugin -Plugin $Plugin) -and ($SharedSettings.PSObject.Properties.Name -contains 'skipPublishPlugins') -and $SharedSettings.skipPublishPlugins) {
Write-Log -Level "INFO" -Message "Skipping plugin '$($Plugin.name)' (ReleasePublishGuard suppressed publish)."
return $true
}
if (-not (Test-PluginCompatible -Plugin $Plugin -EngineDirectory $EngineDirectory -WriteLogs:$true)) {
return $true
}
$pluginModulePath = Resolve-PluginModulePath -Plugin $Plugin -EngineDirectory $EngineDirectory
Write-Log -Level "STEP" -Message "Running plugin '$($Plugin.name)'..."
@ -383,4 +731,4 @@ function Invoke-ConfiguredPlugin {
}
}
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin
Export-ModuleMember -Function Import-PluginDependency, Get-ConfiguredPlugins, Get-PluginStageLabel, Get-PluginBranches, Get-PluginMetadataObject, Test-PluginCompatible, Test-PluginMutatesRemote, Resolve-PluginSecretName, Get-SecretEnvironmentValue, Get-RegistryCredentialsFromRuntime, Test-PluginSkipsRemoteMutation, Test-IsPublishPlugin, Get-PluginSettingValue, Get-PluginPathListSetting, Get-PluginPathSetting, Get-ArchiveNamePattern, Resolve-PluginModulePath, Test-PluginRunnable, New-PluginInvocationSettings, Invoke-ConfiguredPlugin

View File

@ -78,13 +78,18 @@ function New-EngineContext {
[string]$SrcDir,
[Parameter(Mandatory = $false)]
[psobject]$Settings
[psobject]$Settings,
[switch]$DryRun,
[ValidateSet('single', 'ha')]
[string]$DeployMode = 'ha'
)
$resolvedVersion = Resolve-ReleaseVersion -Plugins $Plugins -ScriptDir $ScriptDir
$version = $resolvedVersion.version
$versionSource = $resolvedVersion.source
$releaseRelative = '..\..\..\release'
$releaseRelative = '..\..\..\releases'
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $ScriptDir $releaseRelative))
$currentBranch = Get-CurrentBranch
@ -119,6 +124,23 @@ function New-EngineContext {
$tag = "v$version"
Write-Log -Level "INFO" -Message " Release tag default from ${versionSource}: $tag (ReleasePublishGuard may replace from git when publish is allowed)."
$dryRun = $false
if ($DryRun) {
$dryRun = $true
}
else {
$dryRun = Get-EngineDryRun -Settings $Settings
}
Write-Log -Level "INFO" -Message " Dry run (remote mutations only): $dryRun"
$orchestrator = Get-MaksitOrchestrator
if ($orchestrator) {
Write-Log -Level "INFO" -Message " Orchestrator: $orchestrator (plugin profile filtering active)"
}
else {
Write-Log -Level "INFO" -Message " Orchestrator: not set (dev mode — all plugins eligible; engine probe still selects docker vs podman)"
}
return [pscustomobject]@{
scriptDir = $ScriptDir
srcDir = $SrcDir
@ -132,6 +154,9 @@ function New-EngineContext {
releaseBranches = $releaseBranches
publishCompleted = $false
skipPublishPlugins = $false
dryRun = $dryRun
deployMode = $DeployMode
orchestrator = $orchestrator
}
}

View File

@ -19,7 +19,10 @@ function New-EngineContext {
[string]$SrcDir,
[Parameter(Mandatory = $false)]
[psobject]$Settings
[psobject]$Settings,
[ValidateSet('single', 'ha')]
[string]$DeployMode = 'ha'
)
$badgesDir = $null
@ -32,6 +35,7 @@ function New-EngineContext {
srcDir = $SrcDir
utilsDir = $SrcDir
badgesDir = $badgesDir
deployMode = $DeployMode
}
}

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