Compare commits

...

4 Commits

Author SHA1 Message Date
Maksym Sadovnychyy
1d14db7a4a (refactor): LF and readme 2024-08-18 15:40:22 +02:00
Maksym Sadovnychyy
4bb2edcd77 (refactor): exec tests 2024-08-18 15:19:43 +02:00
Maksym Sadovnychyy
b68d8c1bca (refactor): containers tests 2024-08-18 14:49:24 +02:00
Maksym Sadovnychyy
acdcb1300d (refactor): code clean up, image pull and tag test 2024-08-18 14:24:14 +02:00
21 changed files with 959 additions and 357 deletions

Binary file not shown.

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MaksIT.PodmanClientDotNet.Models {
public class AutoUserNsOptions {
public List<IDMapping> AdditionalGIDMappings { get; set; }

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MaksIT.PodmanClientDotNet.Models.Container {
public class CreateContainerRequest {
public Dictionary<string, string> Annotations { get; set; }

View File

@ -1,14 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.PodmanClientDotNet.Models.Image {
public class ImagePullStatusResponse {
public string Status { get; set; }
public string Id { get; set; }
public string Progress { get; set; }
public ProgressDetail ProgressDetail { get; set; }
}
}

View File

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.PodmanClientDotNet.Models.Image {
public class ImagePullStreamResponse {
public string Stream { get; set; }
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.PodmanClientDotNet.Models.Image
{
public class PullImageResponse
{
/// <summary>
/// Error contains text of errors from c/image.
/// </summary>
public string Error { get; set; }
/// <summary>
/// ID contains image ID (retained for backwards compatibility).
/// </summary>
public string Id { get; set; }
/// <summary>
/// Images contains the IDs of the images pulled.
/// </summary>
public List<string> Images { get; set; }
/// <summary>
/// Stream used to provide output from c/image.
/// </summary>
public string Stream { get; set; }
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.Extensions.Logging;
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
private readonly ILogger<PodmanClient> _logger;
private readonly HttpClient _httpClient;
private const string _apiVersion = "v1.41";
/// <summary>
/// Initializes a new instance of the <see cref="PodmanClient"/> class using the specified base address and timeout.
/// </summary>
/// <param name="logger">The logger instance used for logging within the client.</param>
/// <param name="baseAddress">The base address of the Podman service.</param>
/// <param name="timeOut">The timeout period in minutes for HTTP requests.</param>
public PodmanClient(
ILogger<PodmanClient> logger,
string baseAddress,
int timeOut = 60
) : this(
logger,
baseAddress,
new HttpClient {
Timeout = TimeSpan.FromMinutes(timeOut)
}
) { }
/// <summary>
/// Initializes a new instance of the <see cref="PodmanClient"/> class using the provided <see cref="HttpClient"/> instance.
/// </summary>
/// <param name="logger">The logger instance used for logging within the client.</param>
/// <param name="httpClient">An existing <see cref="HttpClient"/> instance configured for use with the Podman service.</param>
/// <exception cref="ArgumentNullException">Thrown when the logger or httpClient parameter is null.</exception>
public PodmanClient(
ILogger<PodmanClient> logger,
string serverUrl,
HttpClient httpClient
) {
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
if (serverUrl == null)
throw new ArgumentNullException(nameof(serverUrl));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
ConfigureHttpClient(serverUrl);
}
/// <summary>
/// Configures the default settings for the <see cref="HttpClient"/> used by this instance.
/// Ensures that the "Accept" header is set to "application/json".
/// </summary>
private void ConfigureHttpClient(string baseAddress) {
_httpClient.BaseAddress = new Uri(baseAddress);
if (_httpClient.DefaultRequestHeaders.Contains("Accept"))
_httpClient.DefaultRequestHeaders.Remove("Accept");
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
}
}

View File

@ -1,10 +1,12 @@
using System.Text;
using System.Text.Json;
using MaksIT.PodmanClientDotNet.Extensions;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Models.Container;
using MaksIT.PodmanClientDotNet.Extensions;
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
@ -246,31 +248,31 @@ namespace MaksIT.PodmanClientDotNet {
};
var jsonContent = new StringContent(JsonSerializer.Serialize(createContainerParameters), Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/v1.41/libpod/containers/create", jsonContent);
var response = await _httpClient.PostAsync($"/{_apiVersion}/libpod/containers/create", jsonContent);
if (response.IsSuccessStatusCode) {
var jsonResponse = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<CreateContainerResponse>(jsonResponse);
return jsonResponse.ToObject<CreateContainerResponse>();
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = JsonSerializer.Deserialize<ErrorResponse>(errorContent);
var errorDetails = errorContent.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad parameter in request: {errorDetails?.Message}");
_logger.LogError($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorDetails?.Message}");
_logger.LogError($"No such container: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error in operation: {errorDetails?.Message}");
_logger.LogError($"Conflict error in operation: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error creating container: {errorDetails?.Message}");
_logger.LogError($"Error creating container: {errorDetails?.Message}");
break;
}
@ -284,34 +286,34 @@ namespace MaksIT.PodmanClientDotNet {
public async Task StartContainerAsync(string containerId, string detachKeys = "ctrl-p,ctrl-q") {
var response = await _httpClient.PostAsync(
$"/v1.41/libpod/containers/{containerId}/start?detachKeys={Uri.EscapeDataString(detachKeys)}", null);
$"/{_apiVersion}/libpod/containers/{containerId}/start?detachKeys={Uri.EscapeDataString(detachKeys)}", null);
switch (response.StatusCode) {
case System.Net.HttpStatusCode.NoContent:
Console.WriteLine("Container started successfully.");
_logger.LogInformation("Container started successfully.");
break;
case System.Net.HttpStatusCode.NotModified:
Console.WriteLine("Container was already started.");
_logger.LogWarning("Container was already started.");
break;
case System.Net.HttpStatusCode.NotFound:
var errorContent404 = await response.Content.ReadAsStringAsync();
var errorDetails404 = errorContent404.ToObject<ErrorResponse>();
Console.WriteLine($"Container not found: {errorDetails404?.Message}");
_logger.LogError($"Container not found: {errorDetails404?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
var errorContent500 = await response.Content.ReadAsStringAsync();
var errorDetails500 = errorContent500.ToObject<ErrorResponse>();
Console.WriteLine($"Internal server error: {errorDetails500?.Message}");
_logger.LogError($"Internal server error: {errorDetails500?.Message}");
break;
default:
if ((int)response.StatusCode >= 400) {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = errorContent.ToObject<ErrorResponse>();
Console.WriteLine($"Error starting container: {errorDetails?.Message}");
_logger.LogError($"Error starting container: {errorDetails?.Message}");
}
break;
}
@ -321,14 +323,14 @@ namespace MaksIT.PodmanClientDotNet {
public async Task StopContainerAsync(string containerId, int timeout = 10, bool ignoreAlreadyStopped = false) {
var queryParams = $"?timeout={timeout}&Ignore={ignoreAlreadyStopped.ToString().ToLower()}";
var response = await _httpClient.PostAsync($"/v1.41/libpod/containers/{containerId}/stop{queryParams}", null);
var response = await _httpClient.PostAsync($"/{_apiVersion}/libpod/containers/{containerId}/stop{queryParams}", null);
if (response.IsSuccessStatusCode) {
if (response.StatusCode == System.Net.HttpStatusCode.NoContent) {
Console.WriteLine("Container stopped successfully.");
_logger.LogInformation("Container stopped successfully.");
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotModified) {
Console.WriteLine("Container was already stopped.");
_logger.LogWarning("Container was already stopped.");
}
}
else {
@ -337,15 +339,15 @@ namespace MaksIT.PodmanClientDotNet {
switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorDetails?.Message}");
_logger.LogError($"No such container: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error stopping container: {errorDetails?.Message}");
_logger.LogError($"Error stopping container: {errorDetails?.Message}");
break;
}
@ -355,49 +357,49 @@ namespace MaksIT.PodmanClientDotNet {
public async Task ForceDeleteContainerAsync(string containerId, bool deleteVolumes = false, int timeout = 10) {
var queryParams = $"?force=true&v={deleteVolumes.ToString().ToLower()}&timeout={timeout}";
var response = await _httpClient.DeleteAsync($"/v1.41/libpod/containers/{containerId}{queryParams}");
var response = await _httpClient.DeleteAsync($"/{_apiVersion}/libpod/containers/{containerId}{queryParams}");
if (response.IsSuccessStatusCode) {
if (response.StatusCode == System.Net.HttpStatusCode.NoContent) {
Console.WriteLine("Container force deleted successfully.");
_logger.LogInformation("Container force deleted successfully.");
}
else if (response.StatusCode == System.Net.HttpStatusCode.OK) {
var responseContent = await response.Content.ReadAsStringAsync();
var deleteResponses = JsonSerializer.Deserialize<DeleteContainerResponse[]>(responseContent);
var deleteResponses = responseContent.ToObject<DeleteContainerResponse[]>();
foreach (var deleteResponse in deleteResponses) {
if (string.IsNullOrEmpty(deleteResponse.Err)) {
Console.WriteLine($"Container {deleteResponse.Id} deleted successfully.");
_logger.LogInformation($"Container {deleteResponse.Id} deleted successfully.");
}
else {
Console.WriteLine($"Error deleting container {deleteResponse.Id}: {deleteResponse.Err}");
_logger.LogError($"Error deleting container {deleteResponse.Id}: {deleteResponse.Err}");
}
}
}
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = JsonSerializer.Deserialize<ErrorResponse>(errorContent);
var errorDetails = errorContent.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad parameter in request: {errorDetails?.Message}");
_logger.LogError($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorDetails?.Message}");
_logger.LogError($"No such container: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorDetails?.Message}");
_logger.LogError($"Conflict error: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error deleting container: {errorDetails?.Message}");
_logger.LogError($"Error deleting container: {errorDetails?.Message}");
break;
}
@ -407,11 +409,11 @@ namespace MaksIT.PodmanClientDotNet {
public async Task DeleteContainerAsync(string containerId, bool depend = false, bool ignore = false, int timeout = 10) {
var queryParams = $"?depend={depend.ToString().ToLower()}&ignore={ignore.ToString().ToLower()}&timeout={timeout}";
var response = await _httpClient.DeleteAsync($"/v1.41/libpod/containers/{containerId}{queryParams}");
var response = await _httpClient.DeleteAsync($"/libpod/containers/{containerId}{queryParams}");
if (response.IsSuccessStatusCode) {
if (response.StatusCode == System.Net.HttpStatusCode.NoContent) {
Console.WriteLine("Container deleted successfully.");
_logger.LogInformation("Container deleted successfully.");
}
else if (response.StatusCode == System.Net.HttpStatusCode.OK) {
var responseContent = await response.Content.ReadAsStringAsync();
@ -419,10 +421,10 @@ namespace MaksIT.PodmanClientDotNet {
foreach (var deleteResponse in deleteResponses) {
if (string.IsNullOrEmpty(deleteResponse.Err)) {
Console.WriteLine($"Container {deleteResponse.Id} deleted successfully.");
_logger.LogInformation($"Container {deleteResponse.Id} deleted successfully.");
}
else {
Console.WriteLine($"Error deleting container {deleteResponse.Id}: {deleteResponse.Err}");
_logger.LogInformation($"Error deleting container {deleteResponse.Id}: {deleteResponse.Err}");
}
}
}
@ -433,23 +435,23 @@ namespace MaksIT.PodmanClientDotNet {
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad parameter in request: {errorDetails?.Message}");
_logger.LogInformation($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorDetails?.Message}");
_logger.LogInformation($"No such container: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorDetails?.Message}");
_logger.LogInformation($"Conflict error: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
_logger.LogInformation($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error deleting container: {errorDetails?.Message}");
_logger.LogInformation($"Error deleting container: {errorDetails?.Message}");
break;
}
@ -464,34 +466,34 @@ namespace MaksIT.PodmanClientDotNet {
content.Headers.Add("Content-Type", "application/x-tar");
var queryParams = $"?path={Uri.EscapeDataString(path)}&pause={pause.ToString().ToLower()}";
var response = await _httpClient.PutAsync($"/v1.41/libpod/containers/{containerId}/archive{queryParams}", content);
var response = await _httpClient.PutAsync($"/{_apiVersion}/libpod/containers/{containerId}/archive{queryParams}", content);
if (response.IsSuccessStatusCode) {
Console.WriteLine("Files copied successfully to the container.");
_logger.LogInformation("Files copied successfully to the container.");
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = JsonSerializer.Deserialize<ErrorResponse>(errorContent);
var errorDetails = errorContent.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad parameter in request: {errorDetails?.Message}");
_logger.LogError($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Forbidden:
Console.WriteLine($"The container root filesystem is read-only: {errorDetails?.Message}");
_logger.LogError($"The container root filesystem is read-only: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorDetails?.Message}");
_logger.LogError($"No such container: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error copying files: {errorDetails?.Message}");
_logger.LogError($"Error copying files: {errorDetails?.Message}");
break;
}

View File

@ -8,15 +8,21 @@
<!-- NuGet package metadata -->
<PackageId>PodmanClient.DotNet</PackageId>
<Version>1.0.2</Version>
<Version>1.0.3</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>PodmanClient</Product>
<Product>PodmanClient.DotNet</Product>
<Description>Podman API client .NET implementation</Description>
<PackageTags>podman client dotnet</PackageTags>
<RepositoryUrl>https://github.com/MAKS-IT-COM/podman-client-dotnet</RepositoryUrl>
<License>MIT</License>
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<None Include="README.md" Pack="true" />
</ItemGroup>
</Project>

View File

@ -1,9 +1,11 @@
using System.Text;
using System.Text.Json;
using MaksIT.PodmanClientDotNet.Extensions;
using MaksIT.PodmanClientDotNet.Models.Exec;
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.PodmanClientDotNet.Extensions;
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
@ -40,7 +42,7 @@ namespace MaksIT.PodmanClientDotNet {
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Create the request URL
var requestUrl = $"/containers/{Uri.EscapeDataString(containerName)}/exec";
var requestUrl = $"/{_apiVersion}/containers/{Uri.EscapeDataString(containerName)}/exec";
// Send the POST request
var response = await _httpClient.PostAsync(requestUrl, content);
@ -51,21 +53,21 @@ namespace MaksIT.PodmanClientDotNet {
}
else {
var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse);
var errorResponse = jsonResponse.ToObject<ErrorResponse>();
// Handle different response codes
switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorResponse?.Message}");
_logger.LogInformation($"No such container: {errorResponse?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorResponse?.Message}");
_logger.LogInformation($"Conflict error: {errorResponse?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}");
_logger.LogInformation($"Internal server error: {errorResponse?.Message}");
break;
default:
Console.WriteLine($"Error creating exec instance: {errorResponse?.Message}");
_logger.LogInformation($"Error creating exec instance: {errorResponse?.Message}");
break;
}
@ -79,12 +81,8 @@ namespace MaksIT.PodmanClientDotNet {
bool detach = false,
bool tty = false,
int? height = null,
int? width = null,
string outputFilePath = "exec_output.log"
int? width = null
) {
outputFilePath = Path.Combine(Path.GetTempPath(), outputFilePath);
// Construct the request object
var startExecRequest = new StartExecRequest {
Detach = detach,
@ -94,41 +92,37 @@ namespace MaksIT.PodmanClientDotNet {
};
// Serialize the request object to JSON
var jsonRequest = JsonSerializer.Serialize(startExecRequest);
var jsonRequest = startExecRequest.ToJson();
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Create the request URL
var requestUrl = $"/exec/{Uri.EscapeDataString(execId)}/start";
var requestUrl = $"/{_apiVersion}/exec/{Uri.EscapeDataString(execId)}/start";
// Send the POST request
var response = await _httpClient.PostAsync(requestUrl, content);
if (response.IsSuccessStatusCode) {
// Write the response stream directly to a file
using (var responseStream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write)) {
await responseStream.CopyToAsync(fileStream);
}
var test = File.ReadAllText(outputFilePath);
Console.WriteLine($"Exec instance started and output written to {outputFilePath}");
var stringResponse = await response.Content.ReadAsStringAsync();
_logger.LogInformation(stringResponse);
}
else {
var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse);
var errorResponse = jsonResponse.ToObject<ErrorResponse>();
// Handle different response codes
switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such exec instance: {errorResponse?.Message}");
_logger.LogWarning($"No such exec instance: {errorResponse?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorResponse?.Message}");
_logger.LogError($"Conflict error: {errorResponse?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}");
_logger.LogError($"Internal server error: {errorResponse?.Message}");
break;
default:
Console.WriteLine($"Error starting exec instance: {errorResponse?.Message}");
_logger.LogError($"Error starting exec instance: {errorResponse?.Message}");
break;
}
@ -136,8 +130,9 @@ namespace MaksIT.PodmanClientDotNet {
}
}
public async Task<InspectExecResponse?> InspectExecAsync(string execId) {
var requestUrl = $"/exec/{Uri.EscapeDataString(execId)}/json";
var requestUrl = $"/{_apiVersion}/exec/{Uri.EscapeDataString(execId)}/json";
var response = await _httpClient.GetAsync(requestUrl);
if (response.IsSuccessStatusCode) {
@ -146,19 +141,19 @@ namespace MaksIT.PodmanClientDotNet {
}
else {
var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse);
var errorResponse = jsonResponse.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such exec instance: {errorResponse?.Message}");
_logger.LogWarning($"No such exec instance: {errorResponse?.Message}");
return null;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}");
_logger.LogError($"Internal server error: {errorResponse?.Message}");
break;
default:
Console.WriteLine($"Error inspecting exec instance: {errorResponse?.Message}");
_logger.LogError($"Error inspecting exec instance: {errorResponse?.Message}");
break;
}

View File

@ -0,0 +1,97 @@
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Models.Image;
using MaksIT.PodmanClientDotNet.Extensions;
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
public async Task PullImageAsync(string reference, bool tlsVerify = true, bool quiet = false, string policy = "always", string arch = null, string os = null, string variant = null, bool allTags = false, string authHeader = null) {
var query = $"reference={Uri.EscapeDataString(reference)}&tlsVerify={tlsVerify}&quiet={quiet}&policy={Uri.EscapeDataString(policy)}";
if (!string.IsNullOrEmpty(arch)) query += $"&Arch={Uri.EscapeDataString(arch)}";
if (!string.IsNullOrEmpty(os)) query += $"&OS={Uri.EscapeDataString(os)}";
if (!string.IsNullOrEmpty(variant)) query += $"&Variant={Uri.EscapeDataString(variant)}";
if (allTags) query += "&allTags=true";
if (!string.IsNullOrEmpty(authHeader)) {
_httpClient.DefaultRequestHeaders.Add("X-Registry-Auth", authHeader);
}
var response = await _httpClient.PostAsync($"/{_apiVersion}/libpod/images/pull?{query}", null);
if (response.IsSuccessStatusCode) {
var responseStream = await response.Content.ReadAsStreamAsync();
using (var reader = new StreamReader(responseStream)) {
string line;
while ((line = await reader.ReadLineAsync()) != null) {
if (line.StartsWith("{\"error\"")) {
var errorDetails = line.ToObject<PullImageResponse>();
_logger.LogError($"Error pulling image: {errorDetails?.Error}");
throw new HttpRequestException($"Forced exception: {response.StatusCode} - {errorDetails?.Error}");
}
}
}
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = errorContent.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
_logger.LogError($"Bad request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
_logger.LogError($"Error pulling image: {errorDetails?.Message}");
break;
}
response.EnsureSuccessStatusCode();
}
}
public async Task TagImageAsync(string image, string repo, string tag) {
var response = await _httpClient.PostAsync($"/{_apiVersion}/libpod/images/{image}/tag?repo={Uri.EscapeDataString(repo)}&tag={Uri.EscapeDataString(tag)}", null);
if (response.IsSuccessStatusCode) {
if (response.StatusCode == System.Net.HttpStatusCode.Created) {
_logger.LogInformation("Image tagged successfully.");
}
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = errorContent.ToObject<ErrorResponse>();
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
_logger.LogError($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
_logger.LogWarning($"No such image: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
_logger.LogError($"Conflict error in operation: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
_logger.LogError($"Internal server error: {errorDetails?.Message}");
break;
default:
_logger.LogError($"Error tagging image: {errorDetails?.Message}");
break;
}
response.EnsureSuccessStatusCode();
}
}
}
}

View File

@ -1,19 +0,0 @@
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
private readonly HttpClient _httpClient;
public PodmanClient(string baseAddress, int timeOut) {
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(baseAddress);
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
_httpClient.Timeout = TimeSpan.FromMinutes(timeOut);
}
public PodmanClient(HttpClient httpClient) {
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
_httpClient.Timeout = TimeSpan.FromMinutes(60);
}
}
}

View File

@ -1,107 +0,0 @@
using System.Text.Json;
using MaksIT.PodmanClientDotNet.Models.Image;
using MaksIT.PodmanClientDotNet.Models;
namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient {
public async Task<List<ImagePullStatusResponse>> PullImageAsync(string reference, bool tlsVerify = true, bool quiet = false, string policy = "always", string arch = null, string os = null, string variant = null, bool allTags = false, string authHeader = null) {
var query = $"reference={Uri.EscapeDataString(reference)}&tlsVerify={tlsVerify}&quiet={quiet}&policy={Uri.EscapeDataString(policy)}";
if (!string.IsNullOrEmpty(arch)) query += $"&Arch={Uri.EscapeDataString(arch)}";
if (!string.IsNullOrEmpty(os)) query += $"&OS={Uri.EscapeDataString(os)}";
if (!string.IsNullOrEmpty(variant)) query += $"&Variant={Uri.EscapeDataString(variant)}";
if (allTags) query += "&allTags=true";
if (!string.IsNullOrEmpty(authHeader)) {
_httpClient.DefaultRequestHeaders.Add("X-Registry-Auth", authHeader);
}
var response = await _httpClient.PostAsync($"/v1.41/libpod/images/pull?{query}", null);
var imagePullResponses = new List<ImagePullStatusResponse>();
if (response.IsSuccessStatusCode) {
using var responseStream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(responseStream);
string line;
while ((line = await reader.ReadLineAsync()) != null) {
if (line.Contains("\"status\"")) {
// The line contains status information
var statusResponse = JsonSerializer.Deserialize<ImagePullStatusResponse>(line);
Console.WriteLine($"Status: {statusResponse.Status}");
}
else if (line.Contains("\"id\"") || line.Contains("\"images\"")) {
// The line contains image ID information
var imageResponse = JsonSerializer.Deserialize<ImagePullStatusResponse>(line);
if (imageResponse != null) {
imagePullResponses.Add(imageResponse);
}
}
}
return imagePullResponses;
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = JsonSerializer.Deserialize<ErrorResponse>(errorContent);
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error pulling image: {errorDetails?.Message}");
break;
}
response.EnsureSuccessStatusCode();
return null;
}
}
public async Task TagImageAsync(string image, string repo, string tag) {
var response = await _httpClient.PostAsync($"/v1.41/libpod/images/{image}/tag?repo={Uri.EscapeDataString(repo)}&tag={Uri.EscapeDataString(tag)}", null);
if (response.IsSuccessStatusCode) {
if (response.StatusCode == System.Net.HttpStatusCode.Created) {
Console.WriteLine("Image tagged successfully.");
}
}
else {
var errorContent = await response.Content.ReadAsStringAsync();
var errorDetails = JsonSerializer.Deserialize<ErrorResponse>(errorContent);
switch (response.StatusCode) {
case System.Net.HttpStatusCode.BadRequest:
Console.WriteLine($"Bad parameter in request: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such image: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error in operation: {errorDetails?.Message}");
break;
case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorDetails?.Message}");
break;
default:
Console.WriteLine($"Error tagging image: {errorDetails?.Message}");
break;
}
response.EnsureSuccessStatusCode();
}
}
}
}

139
src/PodmanClient/README.md Normal file
View File

@ -0,0 +1,139 @@
# PodmanClient.DotNet
## Description
`PodmanClient.DotNet` is a .NET library designed to provide seamless interaction with the Podman API, allowing developers to manage and control containers directly from their .NET applications. This client library wraps the Podman API endpoints, offering a .NET-friendly interface to perform common container operations such as creating, starting, stopping, deleting containers, and more.
## Purpose
The primary goal of `PodmanClient.DotNet` is to simplify the integration of Podman into .NET applications by providing a comprehensive, easy-to-use client library. Whether you're managing container lifecycles, executing commands inside containers, or manipulating container images, this library allows developers to interface with Podman using the familiar .NET development environment.
## Key Features
- **Container Management:** Create, start, stop, and delete containers with straightforward methods.
- **Image Operations:** Pull, tag, and manage images using the Podman API.
- **Exec Support:** Execute commands inside running containers, with support for attaching input/output streams.
- **Volume and Network Management:** Manage container volumes and networks as needed.
- **Streamlined Error Handling:** Provides detailed error handling, with informative responses based on HTTP status codes.
- **Customizable HTTP Client:** Easily configure and inject your own `HttpClient` instance for extended control and customization.
- **Logging Support:** Integrated logging support via `Microsoft.Extensions.Logging` for better observability.
## Installation
To include `PodmanClient.DotNet` in your .NET project, you can add the package via NuGet:
```shell
dotnet add package PodmanClient.DotNet
```
## Usage Examples
### Initialize the PodmanClient
```csharp
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClient.DotNet;
var logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<PodmanClient>();
var podmanClient = new PodmanClient(logger, "http://localhost:8080", 5);
```
### Create and Start a Container
```csharp
var createResponse = await podmanClient.CreateContainerAsync(
name: "my-container",
image: "alpine:latest",
command: new List<string> { "/bin/sh" },
env: new Dictionary<string, string> { { "ENV_VAR", "value" } },
remove: true
);
await podmanClient.StartContainerAsync(createResponse.Id);
```
### Execute a Command in a Container
```csharp
var execResponse = await podmanClient.CreateExecAsync(createResponse.Id, new[] { "echo", "Hello, World!" });
await podmanClient.StartExecAsync(execResponse.Id);
```
### Pull an Image
```csharp
await podmanClient.PullImageAsync("alpine:latest");
```
### Tag an Image
```csharp
await podmanClient.TagImageAsync("alpine:latest", "myrepo", "mytag");
```
## Available Methods
### `PodmanClient`
- **Container Management**
- `Task<CreateContainerResponse> CreateContainerAsync(...)`: Creates a new container.
- `Task StartContainerAsync(string containerId, string detachKeys = "ctrl-p,ctrl-q")`: Starts an existing container.
- `Task StopContainerAsync(string containerId, int timeout = 10, bool ignoreAlreadyStopped = false)`: Stops a running container.
- `Task DeleteContainerAsync(string containerId, bool depend = false, bool ignore = false, int timeout = 10)`: Deletes a container.
- `Task ForceDeleteContainerAsync(string containerId, bool deleteVolumes = false, int timeout = 10)`: Forcefully deletes a container, optionally removing associated volumes.
- **Exec Management**
- `Task<CreateExecResponse> CreateExecAsync(...)`: Creates an exec instance in a running container.
- `Task StartExecAsync(string execId, bool detach = false, bool tty = false, int? height = null, int? width = null)`: Starts an exec instance.
- `Task<InspectExecResponse?> InspectExecAsync(string execId)`: Inspects an exec instance to retrieve its details.
- **Image Operations**
- `Task PullImageAsync(...)`: Pulls an image from a container registry.
- `Task TagImageAsync(string image, string repo, string tag)`: Tags an existing image with a new repository and tag.
- **File Operations**
- `Task ExtractArchiveToContainerAsync(string containerId, Stream tarStream, string path, bool pause = true)`: Extracts files from a tar stream into a container.
## Documentation (TODO: Agile)
For detailed documentation on each method, including parameter descriptions and example usage, please refer to the official documentation (link to be provided).
## Contribution
Contributions to this project are welcome! Please fork the repository and submit a pull request with your changes. If you encounter any issues or have feature requests, feel free to open an issue on GitHub.
## License
This project is licensed under the MIT License. See the full license text below.
---
### MIT License
```
MIT License
Copyright (c) 2024 Maksym Sadovnychyy (MAKS-IT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Contact
For any questions or inquiries, please reach out via GitHub or [email](mailto:maksym.sadovnychyy@gmail.com).

View File

@ -0,0 +1,54 @@
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))
{
tarOutputStream.IsStreamOwner = false;
AddDirectoryFilesToTar(tarOutputStream, sourceDirectory, true);
}
}
static void AddDirectoryFilesToTar(TarOutputStream tarOutputStream, string sourceDirectory, bool recursive, string baseDirectory = null)
{
// If baseDirectory is null, set it to the sourceDirectory to start with
if (baseDirectory == null)
{
baseDirectory = sourceDirectory;
}
var directoryInfo = new DirectoryInfo(sourceDirectory);
foreach (var fileInfo in directoryInfo.GetFiles())
{
// Calculate the relative path for the file within the base directory
string relativePath = Path.GetRelativePath(baseDirectory, fileInfo.FullName);
// Create tar entry with the relative path
var entry = TarEntry.CreateEntryFromFile(fileInfo.FullName);
entry.Name = relativePath.Replace(Path.DirectorySeparatorChar, '/'); // Use Unix-style path separators
tarOutputStream.PutNextEntry(entry);
using (var fileStream = fileInfo.OpenRead())
{
fileStream.CopyTo(tarOutputStream);
}
tarOutputStream.CloseEntry();
}
if (recursive)
{
foreach (var subDirectory in directoryInfo.GetDirectories())
{
// Recurse into subdirectories, passing the base directory
AddDirectoryFilesToTar(tarOutputStream, subDirectory.FullName, true, baseDirectory);
}
}
}
}
}

View File

@ -0,0 +1,149 @@
using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Tests.Archives;
namespace MaksIT.PodmanClientDotNet.Tests {
public class PodmanClientContainersTests {
private readonly PodmanClient _client;
public PodmanClientContainersTests() {
// Initialize the logger
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<PodmanClient>();
// Initialize PodmanClient with real HttpClient
_client = new PodmanClient(logger, "http://wks0002.corp.maks-it.com:8080", 60);
}
#region Success Cases
[Fact]
public async Task PodmanClient_ContainerLifecycle_Success() {
// Arrange
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
// Act & Assert
await PullImageAsync(image);
var containerId = await CreateContainerAsync(containerName, image);
await StartContainerAsync(containerId);
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
[Fact]
public async Task CopyFilesToContainer_Success() {
// Arrange
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
string pathInContainer = "/podman-test-copy";
// Create temporary folder with random files
string tempFolderPath = CreateTemporaryFolderWithFiles();
try {
// Act
await PullImageAsync(image);
var containerId = await CreateContainerAsync(containerName, image);
await StartContainerAsync(containerId);
// Archive the folder and copy to container
using (var tarStream = CreateTarStream(tempFolderPath)) {
await CopyToContainerAsync(containerId, tarStream, pathInContainer);
}
// Stop and delete the container
await StopContainerAsync(containerId);
await ForceDeleteContainerAsync(containerId);
}
finally {
// Cleanup: Delete temporary folder
if (Directory.Exists(tempFolderPath)) {
Directory.Delete(tempFolderPath, true);
}
}
}
#endregion
#region Helper Methods
private async Task PullImageAsync(string image) {
var exception = await Record.ExceptionAsync(() => _client.PullImageAsync(image));
Assert.Null(exception); // Expect no exceptions if the pull was successful
}
private async Task<string> CreateContainerAsync(string containerName, string image) {
var createResponse = await _client.CreateContainerAsync(
name: containerName,
image: image,
command: new List<string> {
"sh", "-c",
"sleep infinity"
});
Assert.NotNull(createResponse);
Assert.False(string.IsNullOrEmpty(createResponse.Id)); // Ensure a valid container ID is returned
return createResponse.Id;
}
private async Task StartContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.StartContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was started successfully
}
private async Task StopContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.StopContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was stopped successfully
}
private async Task ForceDeleteContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.ForceDeleteContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was deleted successfully
}
private async Task CopyToContainerAsync(string containerId, Stream tarStream, string path) {
var exception = await Record.ExceptionAsync(() => _client.ExtractArchiveToContainerAsync(containerId, tarStream, path));
Assert.Null(exception); // Expect no exceptions if the copy was successful
}
private string CreateTemporaryFolderWithFiles() {
string tempFolder = Path.Combine(Path.GetTempPath(), $"podman-test-{Guid.NewGuid()}");
Directory.CreateDirectory(tempFolder);
// Create some random files
for (int i = 0; i < 5; i++) {
File.WriteAllText(Path.Combine(tempFolder, $"test-file-{i}.txt"), $"This is test file {i}");
}
return tempFolder;
}
private Stream CreateTarStream(string folderPath) {
var memoryStream = new MemoryStream();
Tar.CreateTarFromDirectory(folderPath, memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin); // Reset the stream position for reading
return memoryStream;
}
#endregion
#region Fail Cases
[Fact]
public async Task StartContainerAsync_Should_HandleErrors() {
string invalidContainerId = "invalid-container-id";
var exception = await Record.ExceptionAsync(() => _client.StartContainerAsync(invalidContainerId));
Assert.NotNull(exception); // Expect an exception due to invalid container ID
}
[Fact]
public async Task StopContainerAsync_Should_HandleErrors() {
string invalidContainerId = "invalid-container-id";
var exception = await Record.ExceptionAsync(() => _client.StopContainerAsync(invalidContainerId));
Assert.NotNull(exception); // Expect an exception due to invalid container ID
}
[Fact]
public async Task ForceDeleteContainerAsync_Should_HandleErrors() {
string invalidContainerId = "invalid-container-id";
var exception = await Record.ExceptionAsync(() => _client.ForceDeleteContainerAsync(invalidContainerId));
Assert.NotNull(exception); // Expect an exception due to invalid container ID
}
#endregion
}
}

View File

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PodmanClient\PodmanClientDotNet.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,193 @@
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit;
namespace MaksIT.PodmanClientDotNet.Tests {
public class PodmanClientExecTests {
private readonly PodmanClient _client;
public PodmanClientExecTests() {
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<PodmanClient>();
_client = new PodmanClient(logger, "http://wks0002.corp.maks-it.com:8080", 60);
}
#region Success Cases
[Fact]
public async Task Full_ContainerLifecycle_With_Exec_Should_Succeed() {
// Arrange
string containerName = $"podman-client-test-{Guid.NewGuid()}";
string image = "alpine:latest";
// Act & Assert
// 1. Pull the image
await PullImageAsync(image);
// 2. Create the container with sleep infinity command
var containerId = await CreateContainerAsync(containerName, image);
// 3. Start the container
await StartContainerAsync(containerId);
// 4. Execute a command in the container to install a package (e.g., "apk add curl")
var execId = await CreateExecAsync(containerName, new[] { "apk", "add", "--no-cache", "curl" });
await StartExecAsync(execId);
// 5. Stop the container
await StopContainerAsync(containerId);
// 6. Delete the container
await ForceDeleteContainerAsync(containerId);
}
#endregion
#region Helper Methods
private async Task PullImageAsync(string image) {
var exception = await Record.ExceptionAsync(() => _client.PullImageAsync(image));
Assert.Null(exception); // Expect no exceptions if the pull was successful
}
private async Task<string> CreateContainerAsync(string containerName, string image) {
var createResponse = await _client.CreateContainerAsync(
name: containerName,
image: image,
command: new List<string> {
"sh", "-c",
"sleep infinity"
});
Assert.NotNull(createResponse);
Assert.False(string.IsNullOrEmpty(createResponse.Id)); // Ensure a valid container ID is returned
return createResponse.Id;
}
private async Task StartContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.StartContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was started successfully
}
private async Task<string> CreateExecAsync(string containerName, string[] cmd) {
var execResponse = await _client.CreateExecAsync(containerName, cmd);
Assert.NotNull(execResponse);
Assert.False(string.IsNullOrEmpty(execResponse.Id)); // Ensure a valid exec ID is returned
return execResponse.Id;
}
private async Task StartExecAsync(string execId) {
var exception = await Record.ExceptionAsync(() => _client.StartExecAsync(execId));
Assert.Null(exception); // Expect no exceptions if the exec command was started successfully
}
private async Task StopContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.StopContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was stopped successfully
}
private async Task ForceDeleteContainerAsync(string containerId) {
var exception = await Record.ExceptionAsync(() => _client.ForceDeleteContainerAsync(containerId));
Assert.Null(exception); // Expect no exceptions if the container was deleted successfully
}
#endregion
#region Fail Cases
[Fact]
public async Task PullImageAsync_Should_HandleErrors() {
// Arrange
string invalidImageReference = "invalidimage:latest"; // Intentionally wrong image
// Act
var exception = await Record.ExceptionAsync(() => _client.PullImageAsync(invalidImageReference));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task CreateContainerAsync_Should_HandleErrors() {
// Arrange
string invalidImage = "invalidimage:latest"; // Intentionally wrong image
string containerName = "test-container";
// Act
var exception = await Record.ExceptionAsync(() => _client.CreateContainerAsync(containerName, invalidImage, new List<string> { "sh", "-c", "sleep infinity" }));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task StartContainerAsync_Should_HandleErrors() {
// Arrange
string invalidContainerId = "invalid-container-id"; // Intentionally wrong container ID
// Act
var exception = await Record.ExceptionAsync(() => _client.StartContainerAsync(invalidContainerId));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task CreateExecAsync_Should_HandleErrors() {
// Arrange
string containerName = "invalid-container"; // Intentionally wrong container name
var cmd = new[] { "apk", "add", "--no-cache", "curl" };
// Act
var exception = await Record.ExceptionAsync(() => _client.CreateExecAsync(containerName, cmd));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task StartExecAsync_Should_HandleErrors() {
// Arrange
string invalidExecId = "invalid-exec-id"; // Intentionally wrong exec ID
// Act
var exception = await Record.ExceptionAsync(() => _client.StartExecAsync(invalidExecId));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task StopContainerAsync_Should_HandleErrors() {
// Arrange
string invalidContainerId = "invalid-container-id"; // Intentionally wrong container ID
// Act
var exception = await Record.ExceptionAsync(() => _client.StopContainerAsync(invalidContainerId));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task ForceDeleteContainerAsync_Should_HandleErrors() {
// Arrange
string invalidContainerId = "invalid-container-id"; // Intentionally wrong container ID
// Act
var exception = await Record.ExceptionAsync(() => _client.ForceDeleteContainerAsync(invalidContainerId));
// Assert
Assert.NotNull(exception);
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
#endregion
}
}

View File

@ -0,0 +1,87 @@

using Microsoft.Extensions.Logging;
namespace MaksIT.PodmanClientDotNet.Tests;
public class PodmanClientImagesTests {
private readonly PodmanClient _client;
public PodmanClientImagesTests() {
// Initialize the logger
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<PodmanClient>();
// Initialize PodmanClient with real HttpClient
_client = new PodmanClient(logger, "http://wks0002.corp.maks-it.com:8080", 60);
}
#region Success Cases
[Fact]
public async Task PodmanClient_IntegrationTests() {
// Test 1: Pull Image - Success
await PullImageAsync_Should_Succeed();
// Test 2: Tag Image - Success
await TagImageAsync_Should_Succeed();
}
#endregion
#region Helper Methods
private async Task PullImageAsync_Should_Succeed() {
// Arrange
string imageReference = "alpine:latest"; // Example image
// Act
var exception = await Record.ExceptionAsync(() => _client.PullImageAsync(imageReference));
// Assert
Assert.Null(exception); // Expect no exceptions if the pull was successful
}
private async Task TagImageAsync_Should_Succeed() {
// Arrange
string image = "alpine:latest"; // Example image
string repo = "myrepo";
string tag = "v1";
// Act
var exception = await Record.ExceptionAsync(() => _client.TagImageAsync(image, repo, tag));
// Assert
Assert.Null(exception); // Expect no exceptions if the tagging was successful
}
#endregion
#region Fail Cases
[Fact]
public async Task PodmanClient_PullImage_Errors() {
// Arrange
string imageReference = "dghdfdghmhgn:latest"; // Intentionally wrong image
// Act
var exception = await Record.ExceptionAsync(() => _client.PullImageAsync(imageReference));
// Assert
Assert.NotNull(exception); // Expect an exception due to nonexistent image
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
[Fact]
public async Task PodmanClient_TagImage_Errors() {
// Arrange
string image = "dghdfdghmhgn:latest"; // Intentionally wrong image
string repo = "myrepo";
string tag = "v1";
// Act
var exception = await Record.ExceptionAsync(() => _client.TagImageAsync(image, repo, tag));
// Assert
Assert.NotNull(exception); // Expect an exception due to nonexistent image
Assert.IsType<HttpRequestException>(exception); // Ensure it's the expected type
}
#endregion
}

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 17.9.34902.65
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PodmanClientDotNet", "PodmanClient\PodmanClientDotNet.csproj", "{0833C90F-6BF3-40E4-A035-B6D6C81DB9D7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PodmanClientDotNet.Tests", "PodmanClientDotNet.Tests\PodmanClientDotNet.Tests.csproj", "{657C39AC-BF63-4678-9A35-A782FE9D4D7E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,6 +17,10 @@ Global
{0833C90F-6BF3-40E4-A035-B6D6C81DB9D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0833C90F-6BF3-40E4-A035-B6D6C81DB9D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0833C90F-6BF3-40E4-A035-B6D6C81DB9D7}.Release|Any CPU.Build.0 = Release|Any CPU
{657C39AC-BF63-4678-9A35-A782FE9D4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{657C39AC-BF63-4678-9A35-A782FE9D4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{657C39AC-BF63-4678-9A35-A782FE9D4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{657C39AC-BF63-4678-9A35-A782FE9D4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,92 +0,0 @@
# PodmanClientDotNet
## Description
`PodmanClientDotNet` is a .NET library designed to provide seamless interaction with the Podman API, allowing developers to manage and control containers directly from their .NET applications. This client library wraps the Podman API endpoints, offering a .NET-friendly interface to perform common container operations such as creating, starting, stopping, deleting containers, and more.
## Purpose
The primary goal of `PodmanClientDotNet` is to simplify the integration of Podman into .NET applications by providing a comprehensive, easy-to-use client library. Whether you're managing container lifecycles, executing commands inside containers, or manipulating container images, this library allows developers to interface with Podman using the familiar .NET development environment.
## Key Features
- **Container Management:** Create, start, stop, and delete containers with straightforward methods.
- **Image Operations:** Pull, tag, and manage images using the Podman API.
- **Exec Support:** Execute commands inside running containers, with support for attaching input/output streams.
- **Volume and Network Management:** Manage container volumes and networks as needed.
- **Streamlined Error Handling:** Provides detailed error handling, with informative responses based on HTTP status codes.
## Usage Example
```csharp
// Initialize the PodmanClient with base URL and timeout
var podmanClient = new PodmanClient("http://localhost:8080", 5);
// Create a new container
var createResponse = await podmanClient.CreateContainerAsync(
name: "my-container",
image: "alpine:latest",
command: new List<string> { "/bin/sh" },
env: new Dictionary<string, string> { { "ENV_VAR", "value" } },
remove: true
);
// Start the container
await podmanClient.StartContainerAsync(createResponse.Id);
// Execute a command inside the container
var execResponse = await podmanClient.CreateExecAsync(createResponse.Id, new[] { "echo", "Hello, World!" });
await podmanClient.StartExecAsync(execResponse.Id);
```
## Installation
To include `PodmanClientDotNet` in your .NET project, you can add the package via NuGet:
```shell
dotnet add package PodmanClientDotNet
```
## Documentation
For detailed documentation on each method, including parameter descriptions and example usage, please refer to the official documentation (link to be provided).
## Contribution
Contributions to this project are welcome! Please fork the repository and submit a pull request with your changes. If you encounter any issues or have feature requests, feel free to open an issue on GitHub.
## License
This project is licensed under the MIT License. See the full license text below.
---
### MIT License
```
MIT License
Copyright (c) 2024 Maksym Sadovnychyy (MAKS-IT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Contact
For any questions or inquiries, please reach out via GitHub or [email](mailto:maksym.sadovnychyy@gmail.com).