Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parsing changes for podman support #303

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System;
using System.Linq;
using System.Reflection;
using Ductus.FluentDocker.Executors;
using Ductus.FluentDocker.Executors.Parsers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Ductus.FluentDocker.Tests.ProcessResponseParsersTests
{
[TestClass]
public class ClientTopResponseParserTests
{
[TestMethod]
public void ProcessShallParseResponse()
{
// Arrange
var stdOut =
@"UID PID PPID C STIME TTY TIME CMD
999 7765 7735 0 20:23 ? 00:00:00 postgres
999 7838 7765 0 20:23 ? 00:00:00 postgres: checkpointer
999 7839 7765 0 20:23 ? 00:00:00 postgres: background writer
999 7841 7765 0 20:23 ? 00:00:00 postgres: walwriter
999 7842 7765 0 20:23 ? 00:00:00 postgres: autovacuum launcher
999 7843 7765 0 20:23 ? 00:00:00 postgres: logical replication launcher";
var ctorArgs = new object[] { "command", stdOut, "", 0 };
var executionResult = (ProcessExecutionResult)Activator.CreateInstance(typeof(ProcessExecutionResult),
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, ctorArgs, null, null);

var parser = new ClientTopResponseParser();

// Act
var result = parser.Process(executionResult);

// Assert
Assert.IsTrue(result.Response.Success);
var processes = result.Response.Data;
Assert.IsTrue(processes.Rows.All(row => row.User == "999"));
Assert.IsTrue(processes.Rows.All(row => row.Started == new TimeSpan(20, 23, 0)));
Assert.IsTrue(processes.Rows.All(row => row.Time == TimeSpan.Zero));
Assert.IsTrue(processes.Rows.All(row => row.Tty == "?"));
Assert.AreEqual(7765, processes.Rows[0].Pid);
Assert.AreEqual(7838, processes.Rows[1].Pid);
Assert.AreEqual(7839, processes.Rows[2].Pid);
Assert.AreEqual(7841, processes.Rows[3].Pid);
Assert.AreEqual(7842, processes.Rows[4].Pid);
Assert.AreEqual(7843, processes.Rows[5].Pid);

Assert.AreEqual(7735, processes.Rows[0].ProcessPid);
Assert.AreEqual(7765, processes.Rows[1].ProcessPid);
Assert.AreEqual(7765, processes.Rows[2].ProcessPid);
Assert.AreEqual(7765, processes.Rows[3].ProcessPid);
Assert.AreEqual(7765, processes.Rows[4].ProcessPid);
Assert.AreEqual(7765, processes.Rows[5].ProcessPid);

Assert.AreEqual("postgres", processes.Rows[0].Command);
Assert.AreEqual("postgres: checkpointer", processes.Rows[1].Command);
Assert.AreEqual("postgres: background writer", processes.Rows[2].Command);
Assert.AreEqual("postgres: walwriter", processes.Rows[3].Command);
Assert.AreEqual("postgres: autovacuum launcher", processes.Rows[4].Command);
Assert.AreEqual("postgres: logical replication launcher", processes.Rows[5].Command);
}


[TestMethod]
public void ProcessShallParsePodmanOutput()
{
// Arrange
var stdOut =
@"USER PID PPID %CPU ELAPSED TTY TIME COMMAND
postgres 1 0 0.000 12h0m5.863267473s ? 0s postgres
postgres 55 1 0.000 12h0m4.863350723s ? 0s postgres: checkpointer
postgres 56 1 0.000 12h0m4.863378515s ? 0s postgres: background writer
postgres 58 1 0.000 12h0m4.863404598s ? 0s postgres: walwriter
postgres 59 1 0.000 12h0m4.86343039s ? 0s postgres: autovacuum launcher
postgres 60 1 0.000 12h0m4.863453973s ? 0s postgres: logical replication launcher";
var ctorArgs = new object[] { "command", stdOut, "", 0 };
var executionResult = (ProcessExecutionResult)Activator.CreateInstance(typeof(ProcessExecutionResult),
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, ctorArgs, null, null);

// Act
var parser = new ClientTopResponseParser();
var result = parser.Process(executionResult);

// Assert
Assert.IsTrue(result.Response.Success);
var processes = result.Response.Data;
Assert.IsTrue(processes.Rows.All(row => row.User == "postgres"));
Assert.IsTrue(processes.Rows.All(row => row.PercentCpuUtilization == 0f));
Assert.IsTrue(processes.Rows.All(row => row.Tty == "?"));
Assert.IsTrue(processes.Rows.All(row => row.Time == TimeSpan.Zero));
Assert.AreEqual(1, processes.Rows[0].Pid);
Assert.AreEqual(55, processes.Rows[1].Pid);
Assert.AreEqual(56, processes.Rows[2].Pid);
Assert.AreEqual(58, processes.Rows[3].Pid);
Assert.AreEqual(59, processes.Rows[4].Pid);
Assert.AreEqual(60, processes.Rows[5].Pid);

Assert.AreEqual(0, processes.Rows[0].ProcessPid);
Assert.AreEqual(1, processes.Rows[1].ProcessPid);
Assert.AreEqual(1, processes.Rows[2].ProcessPid);
Assert.AreEqual(1, processes.Rows[3].ProcessPid);
Assert.AreEqual(1, processes.Rows[4].ProcessPid);
Assert.AreEqual(1, processes.Rows[5].ProcessPid);

Assert.AreEqual("postgres", processes.Rows[0].Command);
Assert.AreEqual("postgres: checkpointer", processes.Rows[1].Command);
Assert.AreEqual("postgres: background writer", processes.Rows[2].Command);
Assert.AreEqual("postgres: walwriter", processes.Rows[3].Command);
Assert.AreEqual("postgres: autovacuum launcher", processes.Rows[4].Command);
Assert.AreEqual("postgres: logical replication launcher", processes.Rows[5].Command);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Reflection;
using Ductus.FluentDocker.Executors;
using Ductus.FluentDocker.Executors.Parsers;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Ductus.FluentDocker.Tests.ProcessResponseParsersTests
{
[TestClass]
public class MinimalNetworkLsResponseParserTests
{
[TestMethod]
public void ProcessShallParseResponse()
{
// Arrange
var id = Guid.NewGuid().ToString();
var name = Guid.NewGuid().ToString();

var stdOut = $"{id};{name}";

var ctorArgs = new object[] { "command", stdOut, "", 0 };
var executionResult = (ProcessExecutionResult)Activator.CreateInstance(typeof(ProcessExecutionResult),
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.CreateInstance,
null, ctorArgs, null, null);

var parser = new MinimalNetworkLsResponseParser();

// Act
var result = parser.Process(executionResult).Response.Data[0];

// Assert
Assert.AreEqual(id, result.Id);
Assert.AreEqual(name, result.Name);
}
}
}
14 changes: 13 additions & 1 deletion Ductus.FluentDocker/Commands/Network.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ public static CommandResponse<IList<NetworkRow>> NetworkLs(this DockerUri host,
if (null != filters && 0 != filters.Length)
options = filters.Aggregate(options, (current, filter) => current + $" --filter={filter}");

return
var fullNetworkDetails =
new ProcessExecutor<NetworkLsResponseParser, IList<NetworkRow>>(
"docker".ResolveBinary(),
$"{args} network ls {options}").Execute();
if (fullNetworkDetails.Success)
return fullNetworkDetails;

options = $" --no-trunc --format \"{MinimalNetworkLsResponseParser.Format}\"";

if (null != filters && 0 != filters.Length)
options = filters.Aggregate(options, (current, filter) => current + $" --filter={filter}");

return
new ProcessExecutor<MinimalNetworkLsResponseParser, IList<NetworkRow>>(
"docker".ResolveBinary(),
$"{args} network ls {options}").Execute();
}

public static CommandResponse<IList<string>> NetworkConnect(this DockerUri host, string container, string network,
Expand Down
34 changes: 34 additions & 0 deletions Ductus.FluentDocker/Common/JsonArrayOrSingleConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Ductus.FluentDocker.Common
{
public class JsonArrayOrSingleConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T[]);
}

public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
var token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
return token.ToObject<T[]>();
}

return new[] {token.ToObject<T>()};
}

public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using Ductus.FluentDocker.Model.Containers;
using Ductus.FluentDocker.Model.Networks;

namespace Ductus.FluentDocker.Executors.Parsers
{
public sealed class MinimalNetworkLsResponseParser : IProcessResponseParser<IList<NetworkRow>>
{
public const string Format = "{{.ID}};{{.Name}}";

public CommandResponse<IList<NetworkRow>> Response { get; private set; }

public IProcessResponse<IList<NetworkRow>> Process(ProcessExecutionResult response)
{
if (response.ExitCode != 0)
{
Response = response.ToErrorResponse((IList<NetworkRow>)new List<NetworkRow>());
return this;
}

if (string.IsNullOrEmpty(response.StdOut))
{
Response = response.ToResponse(false, "No response", (IList<NetworkRow>)new List<NetworkRow>());
return this;
}

var result = new List<NetworkRow>();

foreach (var row in response.StdOutAsArray)
{
var items = row.Split(';');
if (items.Length < 2)
continue;

result.Add(new NetworkRow
{
Id = items[0],
Name = items[1],
});
}

Response = response.ToResponse(true, string.Empty, (IList<NetworkRow>)result);
return this;
}
}
}
3 changes: 3 additions & 0 deletions Ductus.FluentDocker/Model/Containers/ContainerConfig.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Ductus.FluentDocker.Common;
using Newtonsoft.Json;

namespace Ductus.FluentDocker.Model.Containers
{
Expand All @@ -26,6 +28,7 @@ public string Domainname
public string Image { get; set; }
public IDictionary<string, VolumeMount> Volumes { get; set; }
public string WorkingDir { get; set; }
[JsonConverter(typeof(JsonArrayOrSingleConverter<string>))]
public string[] EntryPoint { get; set; }
public IDictionary<string, string> Labels { get; set; }
public string StopSignal { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Ductus.FluentDocker/Model/Containers/Health.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Ductus.FluentDocker.Model.Containers
{
public class Health
{
public HealthState Status { get; set; }
public HealthState? Status { get; set; }
public int FailingStreak { get; set; }
}
}
30 changes: 27 additions & 3 deletions Ductus.FluentDocker/Model/Containers/ProcessRow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ internal static ProcessRow ToRow(IList<string> columns, IList<string> fullRow)
break;
case StartConst:
case StartTimeConst:
row.Started = TimeSpan.Parse(fullRow[i]);
row.Started = Parse(fullRow[i]);
break;
case TimeConst:
row.Time = TimeSpan.Parse(fullRow[i]);
row.Time = Parse(fullRow[i]);
break;
case TerminalConst:
row.Tty = fullRow[i];
Expand All @@ -77,7 +77,7 @@ internal static ProcessRow ToRow(IList<string> columns, IList<string> fullRow)
row.Status = fullRow[i];
break;
case CpuTime:
if (TimeSpan.TryParse(fullRow[i], out var cpuTime))
if (TryParse(fullRow[i], out var cpuTime))
row.Cpu = cpuTime;
break;
case PercentCpuConst:
Expand All @@ -91,5 +91,29 @@ internal static ProcessRow ToRow(IList<string> columns, IList<string> fullRow)

return row;
}

private static TimeSpan Parse(string value)
{
if (TimeSpan.TryParse(value, out var result))
return result;
if (TimeSpan.TryParseExact(value, @"%s\s", CultureInfo.InvariantCulture, out result)) // E.G. 0s or 12s
return result;
if (TimeSpan.TryParseExact(value, @"%m\m%s\s", CultureInfo.InvariantCulture, out result)) // E.G. 0m0s or 12m34s
return result;
return TimeSpan.ParseExact(value, @"%h\h%m\m%s\s", CultureInfo.InvariantCulture); // E.G. 0h0m0s or 12h34m56s
}

private static bool TryParse(string value, out TimeSpan result)
{
if (TimeSpan.TryParse(value, out result))
return true;
if (TimeSpan.TryParseExact(value, @"%s\s", CultureInfo.InvariantCulture, out result)) // E.G. 0s or 12s
return true;
if (TimeSpan.TryParseExact(value, @"%m\m%s\s", CultureInfo.InvariantCulture, out result)) // E.G. 0m0s or 12m34s
return true;
if (TimeSpan.TryParseExact(value, @"%h\h%m\m%s\s", CultureInfo.InvariantCulture, out result)) // E.G. 0h0m0s or 12h34m56s
return true;
return false;
}
}
}