(refactor): code clean up, image pull and tag test

This commit is contained in:
Maksym Sadovnychyy 2024-08-18 14:20:54 +02:00
parent de06e407c5
commit a0efb59c7c
13 changed files with 423 additions and 250 deletions

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

View File

@ -19,4 +19,8 @@
<RequireLicenseAcceptance>false</RequireLicenseAcceptance> <RequireLicenseAcceptance>false</RequireLicenseAcceptance>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
</Project> </Project>

View File

@ -1,26 +1,28 @@
using System.Text; using System.Text;
using System.Text.Json;
using MaksIT.PodmanClientDotNet.Extensions; using Microsoft.Extensions.Logging;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.PodmanClientDotNet.Models; using MaksIT.PodmanClientDotNet.Models;
using MaksIT.PodmanClientDotNet.Models.Exec;
using MaksIT.PodmanClientDotNet.Extensions;
namespace MaksIT.PodmanClientDotNet { namespace MaksIT.PodmanClientDotNet {
public partial class PodmanClient { public partial class PodmanClient {
public async Task<CreateExecResponse> CreateExecAsync( public async Task<CreateExecResponse> CreateExecAsync(
string containerName, string containerName,
string[] cmd, string[] cmd,
bool attachStderr = true, bool attachStderr = true,
bool attachStdin = false, bool attachStdin = false,
bool attachStdout = true, bool attachStdout = true,
string detachKeys = null, string detachKeys = null,
string[] env = null, string[] env = null,
bool privileged = false, bool privileged = false,
bool tty = false, bool tty = false,
string user = null, string user = null,
string workingDir = null string workingDir = null
) { ) {
// Construct the request object // Construct the request object
var execRequest = new CreateExecRequest { var execRequest = new CreateExecRequest {
AttachStderr = attachStderr, AttachStderr = attachStderr,
@ -40,7 +42,7 @@ namespace MaksIT.PodmanClientDotNet {
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Create the request URL // Create the request URL
var requestUrl = $"/containers/{Uri.EscapeDataString(containerName)}/exec"; var requestUrl = $"/{_apiVersion}/containers/{Uri.EscapeDataString(containerName)}/exec";
// Send the POST request // Send the POST request
var response = await _httpClient.PostAsync(requestUrl, content); var response = await _httpClient.PostAsync(requestUrl, content);
@ -51,21 +53,21 @@ namespace MaksIT.PodmanClientDotNet {
} }
else { else {
var jsonResponse = await response.Content.ReadAsStringAsync(); var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse); var errorResponse = jsonResponse.ToObject<ErrorResponse>();
// Handle different response codes // Handle different response codes
switch (response.StatusCode) { switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound: case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such container: {errorResponse?.Message}"); _logger.LogInformation($"No such container: {errorResponse?.Message}");
break; break;
case System.Net.HttpStatusCode.Conflict: case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorResponse?.Message}"); _logger.LogInformation($"Conflict error: {errorResponse?.Message}");
break; break;
case System.Net.HttpStatusCode.InternalServerError: case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}"); _logger.LogInformation($"Internal server error: {errorResponse?.Message}");
break; break;
default: default:
Console.WriteLine($"Error creating exec instance: {errorResponse?.Message}"); _logger.LogInformation($"Error creating exec instance: {errorResponse?.Message}");
break; break;
} }
@ -75,16 +77,12 @@ namespace MaksIT.PodmanClientDotNet {
} }
public async Task StartExecAsync( public async Task StartExecAsync(
string execId, string execId,
bool detach = false, bool detach = false,
bool tty = false, bool tty = false,
int? height = null, int? height = null,
int? width = null, int? width = null
string outputFilePath = "exec_output.log" ) {
) {
outputFilePath = Path.Combine(Path.GetTempPath(), outputFilePath);
// Construct the request object // Construct the request object
var startExecRequest = new StartExecRequest { var startExecRequest = new StartExecRequest {
Detach = detach, Detach = detach,
@ -94,41 +92,37 @@ namespace MaksIT.PodmanClientDotNet {
}; };
// Serialize the request object to JSON // Serialize the request object to JSON
var jsonRequest = JsonSerializer.Serialize(startExecRequest); var jsonRequest = startExecRequest.ToJson();
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json"); var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Create the request URL // Create the request URL
var requestUrl = $"/exec/{Uri.EscapeDataString(execId)}/start"; var requestUrl = $"/{_apiVersion}/exec/{Uri.EscapeDataString(execId)}/start";
// Send the POST request // Send the POST request
var response = await _httpClient.PostAsync(requestUrl, content); var response = await _httpClient.PostAsync(requestUrl, content);
if (response.IsSuccessStatusCode) { if (response.IsSuccessStatusCode) {
// Write the response stream directly to a file var stringResponse = await response.Content.ReadAsStringAsync();
using (var responseStream = await response.Content.ReadAsStreamAsync()) _logger.LogInformation(stringResponse);
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}");
} }
else { else {
var jsonResponse = await response.Content.ReadAsStringAsync(); var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse); var errorResponse = jsonResponse.ToObject<ErrorResponse>();
// Handle different response codes // Handle different response codes
switch (response.StatusCode) { switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound: case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such exec instance: {errorResponse?.Message}"); _logger.LogWarning($"No such exec instance: {errorResponse?.Message}");
break; break;
case System.Net.HttpStatusCode.Conflict: case System.Net.HttpStatusCode.Conflict:
Console.WriteLine($"Conflict error: {errorResponse?.Message}"); _logger.LogError($"Conflict error: {errorResponse?.Message}");
break; break;
case System.Net.HttpStatusCode.InternalServerError: case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}"); _logger.LogError($"Internal server error: {errorResponse?.Message}");
break; break;
default: default:
Console.WriteLine($"Error starting exec instance: {errorResponse?.Message}"); _logger.LogError($"Error starting exec instance: {errorResponse?.Message}");
break; break;
} }
@ -136,8 +130,9 @@ namespace MaksIT.PodmanClientDotNet {
} }
} }
public async Task<InspectExecResponse?> InspectExecAsync(string execId) { 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); var response = await _httpClient.GetAsync(requestUrl);
if (response.IsSuccessStatusCode) { if (response.IsSuccessStatusCode) {
@ -146,19 +141,19 @@ namespace MaksIT.PodmanClientDotNet {
} }
else { else {
var jsonResponse = await response.Content.ReadAsStringAsync(); var jsonResponse = await response.Content.ReadAsStringAsync();
var errorResponse = JsonSerializer.Deserialize<ErrorResponse>(jsonResponse); var errorResponse = jsonResponse.ToObject<ErrorResponse>();
switch (response.StatusCode) { switch (response.StatusCode) {
case System.Net.HttpStatusCode.NotFound: case System.Net.HttpStatusCode.NotFound:
Console.WriteLine($"No such exec instance: {errorResponse?.Message}"); _logger.LogWarning($"No such exec instance: {errorResponse?.Message}");
return null; return null;
case System.Net.HttpStatusCode.InternalServerError: case System.Net.HttpStatusCode.InternalServerError:
Console.WriteLine($"Internal server error: {errorResponse?.Message}"); _logger.LogError($"Internal server error: {errorResponse?.Message}");
break; break;
default: default:
Console.WriteLine($"Error inspecting exec instance: {errorResponse?.Message}"); _logger.LogError($"Error inspecting exec instance: {errorResponse?.Message}");
break; break;
} }

View File

@ -0,0 +1,100 @@
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) {
// Process each line as needed
// For example, you can accumulate the lines into a single string or parse them directly
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();
}
}
}
}

View File

@ -0,0 +1,34 @@
<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="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,90 @@

using Microsoft.Extensions.Logging;
using static System.Net.Mime.MediaTypeNames;
namespace MaksIT.PodmanClientDotNet.Tests {
public class PodmanClientIntegrationTests {
private readonly PodmanClient _client;
public PodmanClientIntegrationTests() {
// 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);
}
[Fact]
public async Task PodmanClient_IntegrationTests() {
// Test 1: Pull Image - Success
await PullImageAsync_Should_Succeed();
// Test 2: Tag Image - Success
await TagImageAsync_Should_Succeed();
}
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
}
[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
}
}
}

View File

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