diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ab2f925a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/IronFoundry.Warden.Protocol/warden"] + path = src/IronFoundry.Warden.Protocol/warden + url = https://github.com/cloudfoundry/warden.git diff --git a/src/IronFoundry.Warden.Protocol/CopyAndFixUpProto.ps1 b/src/IronFoundry.Warden.Protocol/CopyAndFixUpProto.ps1 new file mode 100644 index 00000000..cc056227 --- /dev/null +++ b/src/IronFoundry.Warden.Protocol/CopyAndFixUpProto.ps1 @@ -0,0 +1,49 @@ +Set-StrictMode -Version Latest + +$warden_proto_dir = '.\warden\warden-protocol\lib\warden\protocol\pb' +$target_proto_dir = '.\pb' + +if (!(Test-Path $warden_proto_dir)) +{ + Write-Error "Directory '$warden_proto_dir' containing *.proto files does not exist, exiting." + exit 1 +} + +mkdir -ErrorAction SilentlyContinue $target_proto_dir + +$std_replace_text = "package IronFoundry.Warden.Protocol;`r`nimport `"info.proto`";`r`n" + +$warden_proto_files = Get-ChildItem $warden_proto_dir -File -Filter '*.proto' +foreach ($proto_file in $warden_proto_files) +{ + $repl_text = $std_replace_text + if ($proto_file.Name -eq 'info.proto') + { + $repl_text = 'package IronFoundry.Warden.Protocol;' + } + elseif (($proto_file.Name -eq 'run.proto') -or ($proto_file.Name -eq 'spawn.proto')) + { + $repl_text = "package IronFoundry.Warden.Protocol;`r`nimport `"info.proto`";`r`nimport `"resource_limits.proto`";`r`n" + } + (Get-Content -Path $proto_file.FullName) | ForEach-Object { + $_ -replace 'package warden;', $repl_text + } | Set-Content (Join-Path $target_proto_dir $proto_file.Name) +} + +# Per-file tweaks +# info.proto +(Get-Content -Path .\pb\info.proto) | ForEach-Object { + $_ -replace 'memory_stat', 'memory_stat_info' ` + -replace 'cpu_stat', 'cpu_stat_info' ` + -replace 'disk_stat', 'disk_stat_info' ` + -replace 'bandwidth_stat', 'bandwidth_stat_info' +} | Set-Content .\pb\info.proto + +# create.proto +(Get-Content -Path .\pb\create.proto) | ForEach-Object { $_ -replace 'Mode mode', 'Mode bind_mount_mode' } | Set-Content .\pb\create.proto + +# message.proto +(Get-Content -Path .\pb\message.proto) | ForEach-Object { $_ -replace 'Type type', 'Type message_type' } | Set-Content .\pb\message.proto + +# run.proto +(Get-Content -Path .\pb\run.proto) | ForEach-Object { $_ -replace 'optional uint32 exit_status', 'required uint32 exit_status' } | Set-Content .\pb\run.proto diff --git a/src/IronFoundry.Warden.Protocol/Messages.cs b/src/IronFoundry.Warden.Protocol/Messages.cs index aac46455..54e3ae04 100644 --- a/src/IronFoundry.Warden.Protocol/Messages.cs +++ b/src/IronFoundry.Warden.Protocol/Messages.cs @@ -8,6 +8,7 @@ //------------------------------------------------------------------------------ // Generated from: copy_in.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"CopyInRequest")] @@ -53,6 +54,7 @@ public partial class CopyInResponse : global::ProtoBuf.IExtensible } // Generated from: copy_out.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"CopyOutRequest")] @@ -107,6 +109,7 @@ public partial class CopyOutResponse : global::ProtoBuf.IExtensible } // Generated from: create.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"CreateRequest")] @@ -223,6 +226,7 @@ public string Handle } // Generated from: destroy.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"DestroyRequest")] @@ -254,6 +258,7 @@ public partial class DestroyResponse : global::ProtoBuf.IExtensible } // Generated from: echo.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"EchoRequest")] @@ -292,6 +297,7 @@ public string Message } // Generated from: error.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"ErrorResponse")] @@ -821,6 +827,7 @@ public ulong OutBurst } // Generated from: limit_bandwidth.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"LimitBandwidthRequest")] @@ -880,6 +887,7 @@ public ulong Burst } // Generated from: limit_disk.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"LimitDiskRequest")] @@ -1127,6 +1135,7 @@ public ulong ByteHard } // Generated from: limit_memory.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"LimitMemoryRequest")] @@ -1208,8 +1217,10 @@ public partial class LinkResponse : global::ProtoBuf.IExtensible { public LinkResponse() {} - private uint _exitStatus; - [global::ProtoBuf.ProtoMember(1, IsRequired = true, Name=@"exit_status", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] + + private uint _exitStatus = default(uint); + [global::ProtoBuf.ProtoMember(1, IsRequired = false, Name=@"exit_status", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] + [global::System.ComponentModel.DefaultValue(default(uint))] public uint ExitStatus { get { return _exitStatus; } @@ -1249,6 +1260,7 @@ public IronFoundry.Warden.Protocol.InfoResponse Info } // Generated from: list.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"ListRequest")] @@ -1280,6 +1292,7 @@ public partial class ListResponse : global::ProtoBuf.IExtensible } // Generated from: message.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"Message")] @@ -1370,6 +1383,7 @@ public enum Type } // Generated from: net_in.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"NetInRequest")] @@ -1433,6 +1447,7 @@ public uint ContainerPort } // Generated from: net_out.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"NetOutRequest")] @@ -1482,6 +1497,7 @@ public partial class NetOutResponse : global::ProtoBuf.IExtensible } // Generated from: ping.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"PingRequest")] @@ -1506,6 +1522,7 @@ public partial class PingResponse : global::ProtoBuf.IExtensible } // Generated from: resource_limits.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"ResourceLimits")] @@ -1655,8 +1672,8 @@ public ulong Stack } // Generated from: run.proto -// Note: requires additional types generated from: resource_limits.proto // Note: requires additional types generated from: info.proto +// Note: requires additional types generated from: resource_limits.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"RunRequest")] @@ -1747,6 +1764,7 @@ public IronFoundry.Warden.Protocol.InfoResponse Info } // Generated from: spawn.proto +// Note: requires additional types generated from: info.proto // Note: requires additional types generated from: resource_limits.proto namespace IronFoundry.Warden.Protocol { @@ -1811,6 +1829,7 @@ public uint JobId } // Generated from: stop.proto +// Note: requires additional types generated from: info.proto namespace IronFoundry.Warden.Protocol { [global::System.Serializable, global::ProtoBuf.ProtoContract(Name=@"StopRequest")] @@ -1910,8 +1929,10 @@ public string Data get { return _data; } set { _data = value; } } - private uint _exitStatus; - [global::ProtoBuf.ProtoMember(3, IsRequired = true, Name=@"exit_status", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] + + private uint _exitStatus = default(uint); + [global::ProtoBuf.ProtoMember(3, IsRequired = false, Name=@"exit_status", DataFormat = global::ProtoBuf.DataFormat.TwosComplement)] + [global::System.ComponentModel.DefaultValue(default(uint))] public uint ExitStatus { get { return _exitStatus; } diff --git a/src/IronFoundry.Warden.Protocol/ParseProto.ps1 b/src/IronFoundry.Warden.Protocol/ParseProto.ps1 index c3a12451..bc48b18f 100644 --- a/src/IronFoundry.Warden.Protocol/ParseProto.ps1 +++ b/src/IronFoundry.Warden.Protocol/ParseProto.ps1 @@ -23,7 +23,6 @@ foreach ($proto_file in $proto_files) $protogen_args += "-i:$in_name" } -# NB: we can't use -p:fixCase due to type clashes $protogen_output = & $protogen_exe -p:fixCase -q $protogen_args Pop-Location -Verbose diff --git a/src/IronFoundry.Warden.Protocol/pb/copy_in.proto b/src/IronFoundry.Warden.Protocol/pb/copy_in.proto index 9e02af94..6e76f9b5 100644 --- a/src/IronFoundry.Warden.Protocol/pb/copy_in.proto +++ b/src/IronFoundry.Warden.Protocol/pb/copy_in.proto @@ -24,6 +24,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message CopyInRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/copy_out.proto b/src/IronFoundry.Warden.Protocol/pb/copy_out.proto index b92e93de..43e49849 100644 --- a/src/IronFoundry.Warden.Protocol/pb/copy_out.proto +++ b/src/IronFoundry.Warden.Protocol/pb/copy_out.proto @@ -27,6 +27,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message CopyOutRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/create.proto b/src/IronFoundry.Warden.Protocol/pb/create.proto index 7d94b06b..5aac6b60 100644 --- a/src/IronFoundry.Warden.Protocol/pb/create.proto +++ b/src/IronFoundry.Warden.Protocol/pb/create.proto @@ -36,6 +36,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message CreateRequest { message BindMount { diff --git a/src/IronFoundry.Warden.Protocol/pb/destroy.proto b/src/IronFoundry.Warden.Protocol/pb/destroy.proto index 9eee835e..95881e74 100644 --- a/src/IronFoundry.Warden.Protocol/pb/destroy.proto +++ b/src/IronFoundry.Warden.Protocol/pb/destroy.proto @@ -24,6 +24,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message DestroyRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/echo.proto b/src/IronFoundry.Warden.Protocol/pb/echo.proto index 4c07372e..d3ba6e64 100644 --- a/src/IronFoundry.Warden.Protocol/pb/echo.proto +++ b/src/IronFoundry.Warden.Protocol/pb/echo.proto @@ -16,6 +16,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message EchoRequest { required string message = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/error.proto b/src/IronFoundry.Warden.Protocol/pb/error.proto index fa31300a..31f5e066 100644 --- a/src/IronFoundry.Warden.Protocol/pb/error.proto +++ b/src/IronFoundry.Warden.Protocol/pb/error.proto @@ -11,6 +11,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message ErrorResponse { optional string message = 2; diff --git a/src/IronFoundry.Warden.Protocol/pb/limit_bandwidth.proto b/src/IronFoundry.Warden.Protocol/pb/limit_bandwidth.proto index bd01e481..374a3c4c 100644 --- a/src/IronFoundry.Warden.Protocol/pb/limit_bandwidth.proto +++ b/src/IronFoundry.Warden.Protocol/pb/limit_bandwidth.proto @@ -16,6 +16,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message LimitBandwidthRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/limit_disk.proto b/src/IronFoundry.Warden.Protocol/pb/limit_disk.proto index 36b68b91..49a9d6c4 100644 --- a/src/IronFoundry.Warden.Protocol/pb/limit_disk.proto +++ b/src/IronFoundry.Warden.Protocol/pb/limit_disk.proto @@ -32,6 +32,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message LimitDiskRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/limit_memory.proto b/src/IronFoundry.Warden.Protocol/pb/limit_memory.proto index ed75ef12..64fce19a 100644 --- a/src/IronFoundry.Warden.Protocol/pb/limit_memory.proto +++ b/src/IronFoundry.Warden.Protocol/pb/limit_memory.proto @@ -22,6 +22,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message LimitMemoryRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/link.proto b/src/IronFoundry.Warden.Protocol/pb/link.proto index 9e7f34d4..6f1a46ab 100644 --- a/src/IronFoundry.Warden.Protocol/pb/link.proto +++ b/src/IronFoundry.Warden.Protocol/pb/link.proto @@ -25,16 +25,17 @@ // package IronFoundry.Warden.Protocol; - import "info.proto"; + message LinkRequest { required string handle = 1; + required uint32 job_id = 2; } message LinkResponse { - required uint32 exit_status = 1; + optional uint32 exit_status = 1; optional string stdout = 2; optional string stderr = 3; optional InfoResponse info = 4; diff --git a/src/IronFoundry.Warden.Protocol/pb/list.proto b/src/IronFoundry.Warden.Protocol/pb/list.proto index 1a134e90..91429a94 100644 --- a/src/IronFoundry.Warden.Protocol/pb/list.proto +++ b/src/IronFoundry.Warden.Protocol/pb/list.proto @@ -16,6 +16,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message ListRequest { } diff --git a/src/IronFoundry.Warden.Protocol/pb/message.proto b/src/IronFoundry.Warden.Protocol/pb/message.proto index 97414162..a8181a82 100644 --- a/src/IronFoundry.Warden.Protocol/pb/message.proto +++ b/src/IronFoundry.Warden.Protocol/pb/message.proto @@ -1,6 +1,8 @@ // nodoc package IronFoundry.Warden.Protocol; +import "info.proto"; + message Message { enum Type { diff --git a/src/IronFoundry.Warden.Protocol/pb/net_in.proto b/src/IronFoundry.Warden.Protocol/pb/net_in.proto index f7ba3f29..0fba50a6 100644 --- a/src/IronFoundry.Warden.Protocol/pb/net_in.proto +++ b/src/IronFoundry.Warden.Protocol/pb/net_in.proto @@ -25,6 +25,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message NetInRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/net_out.proto b/src/IronFoundry.Warden.Protocol/pb/net_out.proto index eb108ebe..5c18c32d 100644 --- a/src/IronFoundry.Warden.Protocol/pb/net_out.proto +++ b/src/IronFoundry.Warden.Protocol/pb/net_out.proto @@ -23,6 +23,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message NetOutRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/ping.proto b/src/IronFoundry.Warden.Protocol/pb/ping.proto index 697fd24d..448c8650 100644 --- a/src/IronFoundry.Warden.Protocol/pb/ping.proto +++ b/src/IronFoundry.Warden.Protocol/pb/ping.proto @@ -16,6 +16,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message PingRequest { } diff --git a/src/IronFoundry.Warden.Protocol/pb/resource_limits.proto b/src/IronFoundry.Warden.Protocol/pb/resource_limits.proto index 85f927c5..685d8ac5 100644 --- a/src/IronFoundry.Warden.Protocol/pb/resource_limits.proto +++ b/src/IronFoundry.Warden.Protocol/pb/resource_limits.proto @@ -10,6 +10,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message ResourceLimits { optional uint64 as = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/run.proto b/src/IronFoundry.Warden.Protocol/pb/run.proto index 0161f164..1ecf02be 100644 --- a/src/IronFoundry.Warden.Protocol/pb/run.proto +++ b/src/IronFoundry.Warden.Protocol/pb/run.proto @@ -12,12 +12,13 @@ // package IronFoundry.Warden.Protocol; - -import "resource_limits.proto"; import "info.proto"; +import "resource_limits.proto"; + message RunRequest { required string handle = 1; + required string script = 2; optional bool privileged = 3 [default = false]; optional ResourceLimits rlimits = 4; diff --git a/src/IronFoundry.Warden.Protocol/pb/spawn.proto b/src/IronFoundry.Warden.Protocol/pb/spawn.proto index 4bfa899c..0f7d6990 100644 --- a/src/IronFoundry.Warden.Protocol/pb/spawn.proto +++ b/src/IronFoundry.Warden.Protocol/pb/spawn.proto @@ -23,9 +23,10 @@ // package IronFoundry.Warden.Protocol; - +import "info.proto"; import "resource_limits.proto"; + message SpawnRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/stop.proto b/src/IronFoundry.Warden.Protocol/pb/stop.proto index 6d54140b..6271eb46 100644 --- a/src/IronFoundry.Warden.Protocol/pb/stop.proto +++ b/src/IronFoundry.Warden.Protocol/pb/stop.proto @@ -28,6 +28,8 @@ // package IronFoundry.Warden.Protocol; +import "info.proto"; + message StopRequest { required string handle = 1; diff --git a/src/IronFoundry.Warden.Protocol/pb/stream.proto b/src/IronFoundry.Warden.Protocol/pb/stream.proto index 4f1e6518..adc0ee1d 100644 --- a/src/IronFoundry.Warden.Protocol/pb/stream.proto +++ b/src/IronFoundry.Warden.Protocol/pb/stream.proto @@ -26,17 +26,18 @@ // package IronFoundry.Warden.Protocol; - import "info.proto"; + message StreamRequest { required string handle = 1; + required uint32 job_id = 2; } message StreamResponse { optional string name = 1; optional string data = 2; - required uint32 exit_status = 3; + optional uint32 exit_status = 3; optional InfoResponse info = 4; } diff --git a/src/IronFoundry.Warden.Protocol/warden b/src/IronFoundry.Warden.Protocol/warden new file mode 160000 index 00000000..4db1fe33 --- /dev/null +++ b/src/IronFoundry.Warden.Protocol/warden @@ -0,0 +1 @@ +Subproject commit 4db1fe332f7698157cba021ee57123cae6b11bea diff --git a/src/IronFoundry.Warden.Service/WinService.cs b/src/IronFoundry.Warden.Service/WinService.cs index 772ab377..ae306f80 100644 --- a/src/IronFoundry.Warden.Service/WinService.cs +++ b/src/IronFoundry.Warden.Service/WinService.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Jobs; using IronFoundry.Warden.Server; using NLog; using Topshelf; @@ -13,13 +14,16 @@ public class WinService : ServiceControl private readonly Logger log = LogManager.GetCurrentClassLogger(); private readonly CancellationTokenSource cts = new CancellationTokenSource(); + // TODO IoC ? private readonly IContainerManager containerManager = new ContainerManager(); + private readonly IJobManager jobManager = new JobManager(); private readonly TcpServer wardenServer; + private readonly Task wardenServerTask; public WinService() { - this.wardenServer = new TcpServer(this.containerManager, cts.Token); + this.wardenServer = new TcpServer(this.containerManager, this.jobManager, cts.Token); this.wardenServerTask = new Task(wardenServer.RunServer, cts.Token); } diff --git a/src/IronFoundry.Warden/Containers/ContainerUser.cs b/src/IronFoundry.Warden/Containers/ContainerUser.cs index 0fa1988e..7c86eeb3 100644 --- a/src/IronFoundry.Warden/Containers/ContainerUser.cs +++ b/src/IronFoundry.Warden/Containers/ContainerUser.cs @@ -9,6 +9,7 @@ public class ContainerUser : IEquatable { private const string userPrefix = "warden_"; private static readonly Regex uniqueIdValidator = new Regex(@"^\w{8,}$", RegexOptions.CultureInvariant | RegexOptions.Compiled); + private readonly string uniqueId; private readonly string userName; public ContainerUser(string uniqueId, bool shouldCreate = false) @@ -17,6 +18,7 @@ public ContainerUser(string uniqueId, bool shouldCreate = false) { throw new ArgumentNullException("uniqueId"); } + this.uniqueId = uniqueId; if (uniqueIdValidator.IsMatch(uniqueId)) { @@ -30,7 +32,7 @@ public ContainerUser(string uniqueId, bool shouldCreate = false) var principalManager = new LocalPrincipalManager(); if (shouldCreate) { - principalManager.CreateUser(this.userName); + principalManager.CreateUser(this.userName, this.uniqueId); } else { diff --git a/src/IronFoundry.Warden/Containers/InfoBuilder.cs b/src/IronFoundry.Warden/Containers/InfoBuilder.cs index aca63987..cdeffae2 100644 --- a/src/IronFoundry.Warden/Containers/InfoBuilder.cs +++ b/src/IronFoundry.Warden/Containers/InfoBuilder.cs @@ -23,10 +23,18 @@ public InfoResponse GetInfoResponseFor(string handle) { throw new ArgumentNullException("handle"); } - Container c = containerManager.GetContainer(handle); + return GetInfoResponseFor(c); + } + + private InfoResponse GetInfoResponseFor(Container container) + { + if (container == null) + { + throw new ArgumentNullException("container"); + } var hostIp = Utility.GetLocalIPAddress().ToString(); - return new InfoResponse(hostIp, hostIp, c.Path); + return new InfoResponse(hostIp, hostIp, container.Path); } } } diff --git a/src/IronFoundry.Warden/Handlers/RequestHandlerFactory.cs b/src/IronFoundry.Warden/Handlers/RequestHandlerFactory.cs index 6c0ac379..e79ed54e 100644 --- a/src/IronFoundry.Warden/Handlers/RequestHandlerFactory.cs +++ b/src/IronFoundry.Warden/Handlers/RequestHandlerFactory.cs @@ -2,20 +2,26 @@ { using System; using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Jobs; using IronFoundry.Warden.Protocol; public class RequestHandlerFactory { private readonly IContainerManager containerManager; + private readonly IJobManager jobManager; private readonly Message.Type requestType; private readonly Request request; - public RequestHandlerFactory(IContainerManager containerManager, Message.Type requestType, Request request) + public RequestHandlerFactory(IContainerManager containerManager, IJobManager jobManager, Message.Type requestType, Request request) { if (containerManager == null) { throw new ArgumentNullException("containerManager"); } + if (jobManager == null) + { + throw new ArgumentNullException("jobManager"); + } if (requestType == default(Message.Type)) { throw new ArgumentNullException("requestType"); @@ -25,6 +31,7 @@ public RequestHandlerFactory(IContainerManager containerManager, Message.Type re throw new ArgumentNullException("message"); } this.containerManager = containerManager; + this.jobManager = jobManager; this.requestType = requestType; this.request = request; } @@ -81,13 +88,13 @@ public RequestHandler GetHandler() handler = new RunRequestHandler(containerManager, request); break; case Message.Type.Spawn: - handler = new SpawnRequestHandler(request); + handler = new SpawnRequestHandler(containerManager, jobManager, request); break; case Message.Type.Stop: handler = new StopRequestHandler(request); break; case Message.Type.Stream: - handler = new StreamRequestHandler(containerManager, request); + handler = new StreamRequestHandler(containerManager, jobManager, request); break; default: throw new WardenException("Unknown request type '{0}' passed to handler factory.", requestType); diff --git a/src/IronFoundry.Warden/Handlers/RunRequestHandler.cs b/src/IronFoundry.Warden/Handlers/RunRequestHandler.cs index 8cb98e3f..069d6964 100644 --- a/src/IronFoundry.Warden/Handlers/RunRequestHandler.cs +++ b/src/IronFoundry.Warden/Handlers/RunRequestHandler.cs @@ -1,40 +1,40 @@ namespace IronFoundry.Warden.Handlers { - using System; using IronFoundry.Warden.Containers; using IronFoundry.Warden.Protocol; - using IronFoundry.Warden.Utilities; + using IronFoundry.Warden.Run; using NLog; - public class RunRequestHandler : RequestHandler + public class RunRequestHandler : TaskRequestHandler { private readonly Logger log = LogManager.GetCurrentClassLogger(); private readonly RunRequest request; private readonly InfoBuilder infoBuilder; public RunRequestHandler(IContainerManager containerManager, Request request) - : base(request) + : base(containerManager, request) { - if (containerManager == null) - { - throw new ArgumentNullException("containerManager"); - } this.infoBuilder = new InfoBuilder(containerManager); this.request = (RunRequest)request; } public override Response Handle() { - // TODO do work! log.Trace("Handle: '{0}' Script: '{1}'", request.Handle, request.Script); - var runner = new ScriptRunner(); - return new RunResponse + + ScriptRunner runner = base.GetScriptRunnerFor(request.Handle, request.Script); + var result = runner.Run(); + + unchecked { - ExitStatus = 0, - Stderr = "TODO STDERR", - Stdout = "TODO STDOUT", - Info = infoBuilder.GetInfoResponseFor(request.Handle) - }; + return new RunResponse + { + ExitStatus = (uint)result.ExitCode, + Stdout = result.Stdout, + Stderr = result.Stderr, + Info = infoBuilder.GetInfoResponseFor(request.Handle) + }; + } } } } diff --git a/src/IronFoundry.Warden/Handlers/SpawnRequestHandler.cs b/src/IronFoundry.Warden/Handlers/SpawnRequestHandler.cs index 50dbd4fa..6d30955c 100644 --- a/src/IronFoundry.Warden/Handlers/SpawnRequestHandler.cs +++ b/src/IronFoundry.Warden/Handlers/SpawnRequestHandler.cs @@ -1,24 +1,40 @@ namespace IronFoundry.Warden.Handlers { + using System; + using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Jobs; using IronFoundry.Warden.Protocol; + using IronFoundry.Warden.Run; using NLog; - public class SpawnRequestHandler : RequestHandler + /// + /// This request will spawn the requested script in the background and + /// create a job ID that can be used to retrieve results later on with a + /// stream or link request. + /// + public class SpawnRequestHandler : TaskRequestHandler { private readonly Logger log = LogManager.GetCurrentClassLogger(); + private readonly IJobManager jobManager; private readonly SpawnRequest request; - public SpawnRequestHandler(Request request) - : base(request) + public SpawnRequestHandler(IContainerManager containerManager, IJobManager jobManager, Request request) + : base(containerManager, request) { + if (jobManager == null) + { + throw new ArgumentNullException("jobManager"); + } + this.jobManager = jobManager; this.request = (SpawnRequest)request; } public override Response Handle() { - // TODO do work log.Trace("Handle: '{0}' Script: '{1}'", request.Handle, request.Script); - return new SpawnResponse { JobId = 1 }; + ScriptRunner scriptRunner = base.GetScriptRunnerFor(request.Handle, request.Script); + uint jobId = jobManager.StartJobFor(scriptRunner); + return new SpawnResponse { JobId = jobId }; } } } diff --git a/src/IronFoundry.Warden/Handlers/StreamRequestHandler.cs b/src/IronFoundry.Warden/Handlers/StreamRequestHandler.cs index a5d5bf98..09c9e05e 100644 --- a/src/IronFoundry.Warden/Handlers/StreamRequestHandler.cs +++ b/src/IronFoundry.Warden/Handlers/StreamRequestHandler.cs @@ -2,30 +2,37 @@ { using System; using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Jobs; using IronFoundry.Warden.Protocol; using NLog; public class StreamRequestHandler : RequestHandler { private readonly Logger log = LogManager.GetCurrentClassLogger(); + private readonly IJobManager jobManager; private readonly StreamRequest request; private readonly InfoBuilder infoBuilder; - public StreamRequestHandler(IContainerManager containerManager, Request request) + public StreamRequestHandler(IContainerManager containerManager, IJobManager jobManager, Request request) : base(request) { if (containerManager == null) { throw new ArgumentNullException("containerManager"); } + if (jobManager == null) + { + throw new ArgumentNullException("jobManager"); + } + this.jobManager = jobManager; this.infoBuilder = new InfoBuilder(containerManager); this.request = (StreamRequest)request; } public override Response Handle() { - // TODO do work! log.Trace("Handle: '{0}' JobId: '{1}''", request.Handle, request.JobId); + var job = jobManager.GetJob(request.JobId); return new StreamResponse { Data = "DATA TODO\n\n", diff --git a/src/IronFoundry.Warden/Handlers/TaskRequestHandler.cs b/src/IronFoundry.Warden/Handlers/TaskRequestHandler.cs new file mode 100644 index 00000000..d658bf33 --- /dev/null +++ b/src/IronFoundry.Warden/Handlers/TaskRequestHandler.cs @@ -0,0 +1,28 @@ +namespace IronFoundry.Warden.Handlers +{ + using System; + using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Run; + using IronFoundry.Warden.Protocol; + + public abstract class TaskRequestHandler : RequestHandler + { + protected readonly IContainerManager containerManager; + + public TaskRequestHandler(IContainerManager containerManager, Request request) + : base(request) + { + if (containerManager == null) + { + throw new ArgumentNullException("containerManager"); + } + this.containerManager = containerManager; + } + + protected ScriptRunner GetScriptRunnerFor(string handle, string script) + { + Container c = containerManager.GetContainer(handle); + return new ScriptRunner(c, script); + } + } +} diff --git a/src/IronFoundry.Warden/IronFoundry.Warden.csproj b/src/IronFoundry.Warden/IronFoundry.Warden.csproj index 7125bfcb..d292b272 100644 --- a/src/IronFoundry.Warden/IronFoundry.Warden.csproj +++ b/src/IronFoundry.Warden/IronFoundry.Warden.csproj @@ -65,6 +65,14 @@ + + + + + + + + @@ -79,7 +87,7 @@ - + @@ -95,6 +103,9 @@ + + ..\..\packages\Newtonsoft.Json.5.0.5\lib\net45\Newtonsoft.Json.dll + ..\..\packages\NLog.2.0.1.2\lib\net40\NLog.dll diff --git a/src/IronFoundry.Warden/Jobs/IJobManager.cs b/src/IronFoundry.Warden/Jobs/IJobManager.cs new file mode 100644 index 00000000..1ad408ea --- /dev/null +++ b/src/IronFoundry.Warden/Jobs/IJobManager.cs @@ -0,0 +1,8 @@ +namespace IronFoundry.Warden.Jobs +{ + public interface IJobManager + { + uint StartJobFor(IJobRunnable runnable); + Job GetJob(uint jobId); + } +} diff --git a/src/IronFoundry.Warden/Jobs/IJobResult.cs b/src/IronFoundry.Warden/Jobs/IJobResult.cs new file mode 100644 index 00000000..7787ddae --- /dev/null +++ b/src/IronFoundry.Warden/Jobs/IJobResult.cs @@ -0,0 +1,9 @@ +namespace IronFoundry.Warden.Jobs +{ + public interface IJobResult + { + int ExitCode { get; } + string Stdout { get; } + string Stderr { get; } + } +} diff --git a/src/IronFoundry.Warden/Jobs/IJobRunnable.cs b/src/IronFoundry.Warden/Jobs/IJobRunnable.cs new file mode 100644 index 00000000..f20ad882 --- /dev/null +++ b/src/IronFoundry.Warden/Jobs/IJobRunnable.cs @@ -0,0 +1,7 @@ +namespace IronFoundry.Warden.Jobs +{ + public interface IJobRunnable + { + IJobResult Run(); + } +} diff --git a/src/IronFoundry.Warden/Jobs/JobManager.cs b/src/IronFoundry.Warden/Jobs/JobManager.cs new file mode 100644 index 00000000..de2e7bb9 --- /dev/null +++ b/src/IronFoundry.Warden/Jobs/JobManager.cs @@ -0,0 +1,49 @@ +namespace IronFoundry.Warden.Jobs +{ + using System.Collections.Generic; + using System.Threading; + + public class JobManager : IJobManager + { + private uint jobIds = 0; + + private readonly IDictionary jobs = new Dictionary(); + private readonly ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim(); + + public uint StartJobFor(IJobRunnable runnable) + { + try + { + rwlock.EnterWriteLock(); + uint jobId = GetNextJobID(); + var job = new Job(jobId, runnable); + jobs.Add(jobId, job); + job.Run(); + return jobId; + } + finally + { + rwlock.ExitWriteLock(); + } + } + + public Job GetJob(uint jobId) + { + try + { + rwlock.EnterReadLock(); + return jobs[jobId]; + } + finally + { + rwlock.ExitReadLock(); + } + } + + private uint GetNextJobID() + { + ++jobIds; + return jobIds; + } + } +} diff --git a/src/IronFoundry.Warden/Jobs/Jobs.cs b/src/IronFoundry.Warden/Jobs/Jobs.cs new file mode 100644 index 00000000..140ac255 --- /dev/null +++ b/src/IronFoundry.Warden/Jobs/Jobs.cs @@ -0,0 +1,50 @@ +namespace IronFoundry.Warden.Jobs +{ + using System; + using System.Threading.Tasks; + + public class Job + { + private readonly uint jobId; + private readonly Task runnableTask; + + public Job(uint jobId, IJobRunnable runnable) // TODO use something other than Func to save pre-run state for recovery. + { + if (jobId == default(uint)) + { + throw new ArgumentException("jobId must be > 0"); + } + if (runnable == null) + { + throw new ArgumentNullException("runnable"); + } + this.jobId = jobId; + this.runnableTask = new Task(runnable.Run); + } + + public void Run() + { + runnableTask.Start(); + } + + public bool IsCompleted + { + get { return runnableTask.IsCompleted; } + } + + public IJobResult Result + { + get + { + IJobResult rslt = null; + + if (runnableTask.IsCompleted) + { + rslt = runnableTask.Result; + } + + return rslt; + } + } + } +} diff --git a/src/IronFoundry.Warden/Run/RunCommand.cs b/src/IronFoundry.Warden/Run/RunCommand.cs new file mode 100644 index 00000000..551cba93 --- /dev/null +++ b/src/IronFoundry.Warden/Run/RunCommand.cs @@ -0,0 +1,13 @@ +namespace IronFoundry.Warden.Run +{ + using Newtonsoft.Json; + + public class RunCommand + { + [JsonProperty(PropertyName="cmd")] + public string Command { get; set; } + + [JsonProperty(PropertyName="args")] + public string[] Args { get; set; } + } +} diff --git a/src/IronFoundry.Warden/Run/ScriptResult.cs b/src/IronFoundry.Warden/Run/ScriptResult.cs new file mode 100644 index 00000000..a101dc7c --- /dev/null +++ b/src/IronFoundry.Warden/Run/ScriptResult.cs @@ -0,0 +1,55 @@ +namespace IronFoundry.Warden.Run +{ + using System.Collections.Generic; + using System.Text; + using IronFoundry.Warden.Jobs; + + public class ScriptResult : IJobResult + { + private readonly int exitCode; + private readonly string stdout; + private readonly string stderr; + + public ScriptResult(int exitCode, string stdout, string stderr) + { + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + + public int ExitCode + { + get { return exitCode; } + } + + public string Stdout + { + get { return stdout; } + } + + public string Stderr + { + get { return stderr; } + } + + public static IJobResult Flatten(IEnumerable results) + { + var stdout = new StringBuilder(); + var stderr = new StringBuilder(); + + int lastExitCode = 0; + foreach (ScriptResult rslt in results) + { + stdout.SmartAppendLine(rslt.Stdout); + stderr.SmartAppendLine(rslt.Stderr); + if (rslt.ExitCode != 0) + { + lastExitCode = rslt.ExitCode; + break; + } + } + + return new ScriptResult(lastExitCode, stdout.ToString(), stderr.ToString()); + } + } +} diff --git a/src/IronFoundry.Warden/Run/ScriptRunner.cs b/src/IronFoundry.Warden/Run/ScriptRunner.cs new file mode 100644 index 00000000..2aec54b8 --- /dev/null +++ b/src/IronFoundry.Warden/Run/ScriptRunner.cs @@ -0,0 +1,101 @@ +namespace IronFoundry.Warden.Run +{ + using System; + using System.Collections.Generic; + using System.IO; + using IronFoundry.Warden.Configuration; + using IronFoundry.Warden.Containers; + using IronFoundry.Warden.Jobs; + using Newtonsoft.Json; + + public class ScriptRunner : IJobRunnable + { + private static readonly WardenConfig config = new WardenConfig(); + private static readonly IDictionary> commandHandlers = + new Dictionary> + { + { + "mkdir", (container, args) => { + var results = new List(); + foreach (string dir in args) + { + try + { + string toCreate = dir; + if (dir.StartsWith("CROOT")) + { + toCreate = dir.Replace("CROOT", Path.Combine(config.ContainerBasePath, container.Handle)); + } + Directory.CreateDirectory(toCreate); + results.Add(new ScriptResult(0, String.Format("mkdir: created directory '{0}'", toCreate), null)); + } + catch (Exception ex) + { + results.Add(new ScriptResult(1, null, ex.Message)); + } + } + return results.ToArray(); + } + }, // mkdir + }; + + private readonly Container container; + private readonly RunCommand[] commands; + + /* + * Find container + * parse JSON + commands = [ + { :cmd => 'mkdir', :args => [ 'CROOT/app' ] }, + { :cmd => 'touch', :args => [ 'CROOT/app/support_heroku_buildpacks' ] }, + # NB: chown not necessary as /app will inherit perms + ] + * create handlers for :cmd objects + * execute as the user if necessary and ensure all succeed + * return output + */ + public ScriptRunner(Container container, string script) + { + if (container == null) + { + throw new ArgumentNullException("container"); + } + this.container = container; + + if (script.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException("script"); + } + + commands = JsonConvert.DeserializeObject(script); + if (commands.IsNullOrEmpty()) + { + throw new ArgumentException("Expected to run at least one command."); + } + } + + public IJobResult Run() + { + var results = new List(); + + foreach (RunCommand cmd in commands) + { + if (commandHandlers.ContainsKey(cmd.Command)) + { + var handler = commandHandlers[cmd.Command]; + try + { + // TODO impersonation vs. privileged + results.AddRange(handler(container, cmd.Args)); + } + catch (Exception ex) + { + results.Add(new ScriptResult(1, null, ex.Message)); + } + } + } + + return ScriptResult.Flatten(results); + } + } +} diff --git a/src/IronFoundry.Warden/Server/TcpServer.cs b/src/IronFoundry.Warden/Server/TcpServer.cs index 4eeca389..2cf1a3cf 100644 --- a/src/IronFoundry.Warden/Server/TcpServer.cs +++ b/src/IronFoundry.Warden/Server/TcpServer.cs @@ -8,6 +8,7 @@ using System.Threading; using IronFoundry.Warden.Containers; using IronFoundry.Warden.Handlers; + using IronFoundry.Warden.Jobs; using IronFoundry.Warden.Protocol; using IronFoundry.Warden.Utilities; using NLog; @@ -20,18 +21,24 @@ public class TcpServer private readonly ServiceHelper serviceHelper = new ServiceHelper(); private readonly IContainerManager containerManager; + private readonly IJobManager jobManager; - public TcpServer(IContainerManager containerManager, CancellationToken cancellationToken) + public TcpServer(IContainerManager containerManager, IJobManager jobManager, CancellationToken cancellationToken) { if (containerManager == null) { throw new ArgumentNullException("containerManager"); } + if (jobManager == null) + { + throw new ArgumentNullException("jobManager"); + } if (cancellationToken == null) { throw new ArgumentNullException("cancellationToken"); } this.containerManager = containerManager; + this.jobManager = jobManager; this.cancellationToken = cancellationToken; } @@ -181,7 +188,7 @@ private Message HandleRequest(Message msg) var unwrapper = new MessageUnwrapper(msg); Request request = unwrapper.GetRequest(); - var factory = new RequestHandlerFactory(containerManager, msg.MessageType, request); + var factory = new RequestHandlerFactory(containerManager, jobManager, msg.MessageType, request); RequestHandler handler = factory.GetHandler(); Response response; diff --git a/src/IronFoundry.Warden/Utilities/LocalPrincipalManager.cs b/src/IronFoundry.Warden/Utilities/LocalPrincipalManager.cs index 5e733e68..880a396a 100644 --- a/src/IronFoundry.Warden/Utilities/LocalPrincipalManager.cs +++ b/src/IronFoundry.Warden/Utilities/LocalPrincipalManager.cs @@ -27,10 +27,9 @@ public string FindUser(string userName) return rvUserName; } - public string CreateUser(string userName) + public string CreateUser(string userName, string password) { string rvUserName; - string password = Guid.NewGuid().ToString("N").Substring(0, 16); using (var context = new PrincipalContext(ContextType.Machine)) { diff --git a/src/IronFoundry.Warden/Utilities/ScriptRunner.cs b/src/IronFoundry.Warden/Utilities/ScriptRunner.cs deleted file mode 100644 index c4eafedc..00000000 --- a/src/IronFoundry.Warden/Utilities/ScriptRunner.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace IronFoundry.Warden.Utilities -{ - public class ScriptRunner - { - } -} diff --git a/src/IronFoundry.Warden/packages.config b/src/IronFoundry.Warden/packages.config index ef2656ac..52e2e6d7 100644 --- a/src/IronFoundry.Warden/packages.config +++ b/src/IronFoundry.Warden/packages.config @@ -1,5 +1,6 @@  + \ No newline at end of file diff --git a/src/shared/ExtensionMethods.cs b/src/shared/ExtensionMethods.cs index 35a94a25..c3666e9e 100644 --- a/src/shared/ExtensionMethods.cs +++ b/src/shared/ExtensionMethods.cs @@ -82,6 +82,21 @@ public static string Hexdigest(this FileInfo argThis) } } +namespace System.Text +{ + internal static class StringBuilderExtensionMethods + { + public static StringBuilder SmartAppendLine(this StringBuilder argThis, string toAppend) + { + if (!toAppend.IsNullOrWhiteSpace()) + { + argThis.AppendLine(toAppend); + } + return argThis; + } + } +} + namespace System.Text.RegularExpressions { internal static class RegexExtensionMethods