mirror of
https://github.com/MAKS-IT-COM/podman-client-dotnet.git
synced 2026-08-15 06:38:10 +02:00
(feature): add powershell client and e2e
This commit is contained in:
parent
755f7b2341
commit
9f6284ba6f
@ -4,6 +4,7 @@
|
||||
"skills": [
|
||||
"common/csharp",
|
||||
"common/maksit-repo-maintenance",
|
||||
"local-ollama"
|
||||
"local-ollama",
|
||||
"gh-cli"
|
||||
]
|
||||
}
|
||||
|
||||
@ -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
118
.editorconfig
Normal 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 ...; } (IDE0022–IDE0027)
|
||||
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
1
.gitignore
vendored
@ -29,6 +29,7 @@ bld/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
test-results/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
|
||||
@ -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`.
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
32
README.md
32
README.md
@ -1,6 +1,8 @@
|
||||
# PodmanClient.DotNet
|
||||
|
||||
  
|
||||

|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@ -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 |
@ -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 |
@ -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 |
186
src/PodmanClient.PowerShell/Build/BuildCmdlets.cs
Normal file
186
src/PodmanClient.PowerShell/Build/BuildCmdlets.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
30
src/PodmanClient.PowerShell/ConnectDisconnectCmdlets.cs
Normal file
30
src/PodmanClient.PowerShell/ConnectDisconnectCmdlets.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
1026
src/PodmanClient.PowerShell/Containers/ContainersCmdlets.cs
Normal file
1026
src/PodmanClient.PowerShell/Containers/ContainersCmdlets.cs
Normal file
File diff suppressed because it is too large
Load Diff
192
src/PodmanClient.PowerShell/Exec/ExecCmdlets.cs
Normal file
192
src/PodmanClient.PowerShell/Exec/ExecCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/PodmanClient.PowerShell/Generate/GenerateCmdlets.cs
Normal file
122
src/PodmanClient.PowerShell/Generate/GenerateCmdlets.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
537
src/PodmanClient.PowerShell/Images/ImagesCmdlets.cs
Normal file
537
src/PodmanClient.PowerShell/Images/ImagesCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
164
src/PodmanClient.PowerShell/Manifests/ManifestsCmdlets.cs
Normal file
164
src/PodmanClient.PowerShell/Manifests/ManifestsCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
147
src/PodmanClient.PowerShell/Networks/NetworksCmdlets.cs
Normal file
147
src/PodmanClient.PowerShell/Networks/NetworksCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj
Normal file
33
src/PodmanClient.PowerShell/PodmanClient.PowerShell.csproj
Normal 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>
|
||||
111
src/PodmanClient.PowerShell/PodmanCmdletBase.cs
Normal file
111
src/PodmanClient.PowerShell/PodmanCmdletBase.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
38
src/PodmanClient.PowerShell/PodmanConnectionState.cs
Normal file
38
src/PodmanClient.PowerShell/PodmanConnectionState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
276
src/PodmanClient.PowerShell/Pods/PodsCmdlets.cs
generated
Normal file
276
src/PodmanClient.PowerShell/Pods/PodsCmdlets.cs
generated
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/PodmanClient.PowerShell/README.md
Normal file
21
src/PodmanClient.PowerShell/README.md
Normal 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/`.
|
||||
135
src/PodmanClient.PowerShell/System/SystemCmdlets.cs
Normal file
135
src/PodmanClient.PowerShell/System/SystemCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
108
src/PodmanClient.PowerShell/Volumes/VolumesCmdlets.cs
Normal file
108
src/PodmanClient.PowerShell/Volumes/VolumesCmdlets.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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>> {
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
|
||||
namespace MaksIT.PodmanClientDotNet.Models.Image;
|
||||
|
||||
public class PullImageResponse
|
||||
{
|
||||
public class PullImageResponse {
|
||||
|
||||
/// <summary>
|
||||
/// Error contains text of errors from c/image.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using MaksIT.PodmanClientDotNet;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
using MaksIT.PodmanClientDotNet;
|
||||
using MaksIT.PodmanClientDotNet.Dtos.Generate;
|
||||
using MaksIT.Results;
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
40
src/PodmanClientDotNet.Tests/InspectExecResponseDtoTests.cs
Normal file
40
src/PodmanClientDotNet.Tests/InspectExecResponseDtoTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
using System.Text;
|
||||
|
||||
using MaksIT.PodmanClientDotNet.Dtos.Build;
|
||||
using MaksIT.PodmanClientDotNet.Internal;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -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>
|
||||
|
||||
77
src/e2e-tests/Podman.E2E.Common.ps1
Normal file
77
src/e2e-tests/Podman.E2E.Common.ps1
Normal 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
|
||||
}
|
||||
18
src/e2e-tests/Test-PodmanE2E.bat
Normal file
18
src/e2e-tests/Test-PodmanE2E.bat
Normal 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%
|
||||
172
src/e2e-tests/Test-PodmanE2E.ps1
Normal file
172
src/e2e-tests/Test-PodmanE2E.ps1
Normal 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'
|
||||
}
|
||||
40
src/e2e-tests/scenarios/Scenario-01-System.ps1
Normal file
40
src/e2e-tests/scenarios/Scenario-01-System.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/e2e-tests/scenarios/Scenario-02-Images.ps1
Normal file
139
src/e2e-tests/scenarios/Scenario-02-Images.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
229
src/e2e-tests/scenarios/Scenario-03-ContainersLifecycle.ps1
Normal file
229
src/e2e-tests/scenarios/Scenario-03-ContainersLifecycle.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/e2e-tests/scenarios/Scenario-05-Exec.ps1
Normal file
76
src/e2e-tests/scenarios/Scenario-05-Exec.ps1
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/e2e-tests/scenarios/Scenario-06-Volumes.ps1
Normal file
48
src/e2e-tests/scenarios/Scenario-06-Volumes.ps1
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/e2e-tests/scenarios/Scenario-07-Networks.ps1
Normal file
73
src/e2e-tests/scenarios/Scenario-07-Networks.ps1
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/e2e-tests/scenarios/Scenario-08-Pods.ps1
Normal file
138
src/e2e-tests/scenarios/Scenario-08-Pods.ps1
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/e2e-tests/scenarios/Scenario-09-Build.ps1
Normal file
52
src/e2e-tests/scenarios/Scenario-09-Build.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/e2e-tests/scenarios/Scenario-10-Manifests.ps1
Normal file
63
src/e2e-tests/scenarios/Scenario-10-Manifests.ps1
Normal 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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/e2e-tests/scenarios/Scenario-11-Generate.ps1
Normal file
94
src/e2e-tests/scenarios/Scenario-11-Generate.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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%
|
||||
|
||||
@ -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%
|
||||
|
||||
@ -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'
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user