From 8af70ab2ab8459c3fed15ea8f15ceccb47575a9e Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Sun, 14 Jan 2018 10:34:47 +1100 Subject: [PATCH 1/6] fix up examples and documentation --- dotnet/examples/simple-web/Program.cs | 3 ++- dotnet/examples/simple/Program.cs | 3 ++- tutorials/dotnet/tutorial.md | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dotnet/examples/simple-web/Program.cs b/dotnet/examples/simple-web/Program.cs index 0efa040..8f79c55 100644 --- a/dotnet/examples/simple-web/Program.cs +++ b/dotnet/examples/simple-web/Program.cs @@ -14,7 +14,8 @@ namespace web public class Program { const int port = 8080; - [Metaparticle.Package.Config(Repository = "brendanburns/dotnet-simple-web", Publish = true, Verbose = true)] + [Metaparticle.Runtime.Config(Ports = new int[] {8080})] + [Metaparticle.Package.Config(Repository = "docker.io/docker-user-goes-here/dotnet-simple-web", Publish = false, Verbose = true)] public static void Main(string[] args) => Containerize(args, () => { WebHost.CreateDefaultBuilder(args) diff --git a/dotnet/examples/simple/Program.cs b/dotnet/examples/simple/Program.cs index 5ecf8db..cd548dd 100644 --- a/dotnet/examples/simple/Program.cs +++ b/dotnet/examples/simple/Program.cs @@ -5,8 +5,9 @@ namespace simple { public class Program { + [Metaparticle.Runtime.Config] [Metaparticle.Package.Config(Verbose = true, - Publish = true, Repository = "brendanburns/dotnet-simple")] + Publish = false, Repository = "docker.io/docker-user-goes-here/dotnet-simple")] public static void Main(string[] args) => Containerize (args, () => { Console.Out.WriteLine(args); diff --git a/tutorials/dotnet/tutorial.md b/tutorials/dotnet/tutorial.md index 945e569..2c26a92 100644 --- a/tutorials/dotnet/tutorial.md +++ b/tutorials/dotnet/tutorial.md @@ -131,7 +131,7 @@ The code snippet to add is: ```cs ... - [Metaparticle.Runtime.Config(Ports = int[] {8080})] + [Metaparticle.Runtime.Config(Ports = new int[] {8080})] ... ``` @@ -172,6 +172,8 @@ namespace web Now if you run this with `dotnet run` your webserver will be successfully exposed on port 8080. +You can verify that it works by running `curl localhost:8080` + ## Replicating and exposing on the web. As a final step, consider the task of exposing a replicated service on the internet. To do this, we're going to expand our usage of the `Metaparticle.Runtime.Config` tag. First we will From bfab40b1c0e1225922100f06bb2c42d32e2f5577 Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Mon, 15 Jan 2018 22:41:59 +1100 Subject: [PATCH 2/6] fix up indenting --- dotnet/examples/simple/Program.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dotnet/examples/simple/Program.cs b/dotnet/examples/simple/Program.cs index cd548dd..da158f1 100644 --- a/dotnet/examples/simple/Program.cs +++ b/dotnet/examples/simple/Program.cs @@ -3,9 +3,11 @@ using static Metaparticle.Package.Driver; -namespace simple { - public class Program { - [Metaparticle.Runtime.Config] +namespace simple +{ + public class Program + { + [Metaparticle.Runtime.Config] [Metaparticle.Package.Config(Verbose = true, Publish = false, Repository = "docker.io/docker-user-goes-here/dotnet-simple")] public static void Main(string[] args) => Containerize (args, () => From eab13b5315f6b0a5314304feb584d34b6f660b43 Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Mon, 22 Jan 2018 21:34:40 +1100 Subject: [PATCH 3/6] renamed env vars to be conssitent and also moved braces to new lines --- dotnet/Metaparticle.Package/Config.cs | 13 +++-- .../Metaparticle.Package/ContainerExecutor.cs | 7 ++- dotnet/Metaparticle.Package/DockerExecutor.cs | 18 ++++--- dotnet/Metaparticle.Package/ImageBuilder.cs | 6 ++- dotnet/Metaparticle.Package/Metaparticle.cs | 48 ++++++++++++------- .../MetaparticleExecutor.cs | 6 ++- dotnet/Metaparticle.Package/Util.cs | 15 ++++-- dotnet/README.md | 2 +- 8 files changed, 76 insertions(+), 39 deletions(-) diff --git a/dotnet/Metaparticle.Package/Config.cs b/dotnet/Metaparticle.Package/Config.cs index 1a582ce..d08f8c9 100644 --- a/dotnet/Metaparticle.Package/Config.cs +++ b/dotnet/Metaparticle.Package/Config.cs @@ -1,7 +1,9 @@ using System; -namespace Metaparticle.Package { - public class Config : Attribute { +namespace Metaparticle.Package +{ + public class Config : Attribute + { public bool Verbose { get; set; } public bool Quiet { get; set; } @@ -16,15 +18,16 @@ public class Config : Attribute { public string Dockerfile { get; set; } - public Config() { + public Config() + { Builder = "docker"; LoadConfigVariablesFromEnvironment(); } private void LoadConfigVariablesFromEnvironment() { - Repository = TryGetEnvironmentVariable("MP_CONFIG_REPOSITORY"); - Publish = TryGetEnvironmentVariable("MP_CONFIG_PUBLISH").ToLower() == "true"; + Repository = TryGetEnvironmentVariable("METAPARTICLE_CONFIG_REPOSITORY"); + Publish = TryGetEnvironmentVariable("METAPARTICLE_CONFIG_PUBLISH").ToLower() == "true"; } private string TryGetEnvironmentVariable(string name) diff --git a/dotnet/Metaparticle.Package/ContainerExecutor.cs b/dotnet/Metaparticle.Package/ContainerExecutor.cs index 76e7dd9..f4b5adc 100644 --- a/dotnet/Metaparticle.Package/ContainerExecutor.cs +++ b/dotnet/Metaparticle.Package/ContainerExecutor.cs @@ -1,6 +1,9 @@ using System.IO; -namespace Metaparticle.Package { - public interface ContainerExecutor { + +namespace Metaparticle.Package +{ + public interface ContainerExecutor + { string Run(string image, Metaparticle.Runtime.Config config); void Cancel(string id); diff --git a/dotnet/Metaparticle.Package/DockerExecutor.cs b/dotnet/Metaparticle.Package/DockerExecutor.cs index 6026119..34408e1 100644 --- a/dotnet/Metaparticle.Package/DockerExecutor.cs +++ b/dotnet/Metaparticle.Package/DockerExecutor.cs @@ -1,7 +1,8 @@ using System.IO; using static Metaparticle.Package.Util; -namespace Metaparticle.Package { +namespace Metaparticle.Package +{ public class DockerExecutor : ContainerExecutor { public void Cancel(string id) @@ -17,20 +18,25 @@ public string Run(string image, Metaparticle.Runtime.Config config) return idWriter.ToString().Trim(); } - public void Logs(string id, TextWriter stdout, TextWriter stderr) { + public void Logs(string id, TextWriter stdout, TextWriter stderr) + { Exec("docker", string.Format("logs -f {0}", id), stdout, stderr); } - public bool PublishRequired() { + public bool PublishRequired() + { return false; } - private string portString(int[] ports) { - if (ports == null || ports.Length == 0) { + private string portString(int[] ports) + { + if (ports == null || ports.Length == 0) + { return ""; } var pieces = new string[ports.Length]; - for (int i = 0; i < ports.Length; i++) { + for (int i = 0; i < ports.Length; i++) + { pieces[i] = string.Format("-p {0}:{1}", ports[i], ports[i]); } return string.Join(",", pieces); diff --git a/dotnet/Metaparticle.Package/ImageBuilder.cs b/dotnet/Metaparticle.Package/ImageBuilder.cs index 8360454..81276f7 100644 --- a/dotnet/Metaparticle.Package/ImageBuilder.cs +++ b/dotnet/Metaparticle.Package/ImageBuilder.cs @@ -1,7 +1,9 @@ using System.IO; -namespace Metaparticle.Package { - public interface ImageBuilder { +namespace Metaparticle.Package +{ + public interface ImageBuilder + { bool Build(string configFile, string imageName, TextWriter stdout = null, TextWriter stderr = null); bool Push(string imageName, TextWriter stdout = null, TextWriter stderr = null); diff --git a/dotnet/Metaparticle.Package/Metaparticle.cs b/dotnet/Metaparticle.Package/Metaparticle.cs index 3305bb8..403a43d 100644 --- a/dotnet/Metaparticle.Package/Metaparticle.cs +++ b/dotnet/Metaparticle.Package/Metaparticle.cs @@ -14,13 +14,16 @@ public class Driver private Config config; private RuntimeConfig runtimeConfig; - public Driver(Config config, RuntimeConfig runtimeConfig) { + public Driver(Config config, RuntimeConfig runtimeConfig) + { this.config = config; this.runtimeConfig = runtimeConfig; } - private ImageBuilder getBuilder() { - switch (config.Builder.ToLowerInvariant()) { + private ImageBuilder getBuilder() + { + switch (config.Builder.ToLowerInvariant()) + { case "docker": return new DockerBuilder(); case "aci": @@ -30,11 +33,13 @@ public class Driver } } - private ContainerExecutor getExecutor() { + private ContainerExecutor getExecutor() + { if (runtimeConfig == null) { return null; } - switch (runtimeConfig.Executor.ToLowerInvariant()) { + switch (runtimeConfig.Executor.ToLowerInvariant()) + { case "docker": return new DockerExecutor(); case "aci": @@ -46,12 +51,15 @@ public class Driver } } - private static string getArgs(string[] args) { - if (args == null || args.Length == 0) { + private static string getArgs(string[] args) + { + if (args == null || args.Length == 0) + { return ""; } var b = new StringBuilder(); - foreach (var arg in args) { + foreach (var arg in args) + { b.Append(arg); b.Append(" "); } @@ -94,30 +102,36 @@ public void Build(string[] args) if (!string.IsNullOrEmpty(config.Version)) { imgName += ":" + config.Version; } - if (!builder.Build(dockerfilename, imgName, stdout: o, stderr: e)) { + if (!builder.Build(dockerfilename, imgName, stdout: o, stderr: e)) + { Console.Error.WriteLine("Image build failed."); return; } - if (config.Publish) { - if (!builder.Push(imgName, stdout: o, stderr: e)) { + if (config.Publish) + { + if (!builder.Push(imgName, stdout: o, stderr: e)) + { Console.Error.WriteLine("Image push failed."); return; } } - if (runtimeConfig == null) { + if (runtimeConfig == null) + { return; } var exec = getExecutor(); - if (exec.PublishRequired() && !config.Publish) { + if (exec.PublishRequired() && !config.Publish) + { Console.Error.WriteLine("Image publish is required, but image was not published. Set publish to true in the package config."); return; } var id = exec.Run(imgName, runtimeConfig); - Console.CancelKeyPress += delegate { + Console.CancelKeyPress += delegate + { exec.Cancel(id); }; @@ -146,7 +160,8 @@ private string writeDockerfile(string dir, string exe, string[] args, Config con public static bool InDockerContainer() { - switch (System.Environment.GetEnvironmentVariable("METAPARTICLE_IN_CONTAINER")) { + switch (System.Environment.GetEnvironmentVariable("METAPARTICLE_IN_CONTAINER")) + { case "true": case "1": return true; @@ -180,7 +195,8 @@ public static void Containerize(string[] args, Action main) { config = (Config) attribute; } - if (attribute is RuntimeConfig) { + if (attribute is RuntimeConfig) + { runtimeConfig = (RuntimeConfig) attribute; } } diff --git a/dotnet/Metaparticle.Package/MetaparticleExecutor.cs b/dotnet/Metaparticle.Package/MetaparticleExecutor.cs index f235b70..9bc367e 100644 --- a/dotnet/Metaparticle.Package/MetaparticleExecutor.cs +++ b/dotnet/Metaparticle.Package/MetaparticleExecutor.cs @@ -5,7 +5,8 @@ using Metaparticle.Runtime; using static Metaparticle.Package.Util; -namespace Metaparticle.Package { +namespace Metaparticle.Package +{ public class MetaparticleExecutor : ContainerExecutor { public void Cancel(string id) @@ -15,7 +16,8 @@ public void Cancel(string id) HandleErrorExec("mp-compiler", string.Format("-f {0} --delete", specFileName)); } - private void HandleErrorExec(string cmd, string args, TextWriter stdout=null) { + private void HandleErrorExec(string cmd, string args, TextWriter stdout=null) + { var err = new StringWriter(); var proc = Exec(cmd, args, stdout: stdout, stderr: err); if (proc.ExitCode != 0) { diff --git a/dotnet/Metaparticle.Package/Util.cs b/dotnet/Metaparticle.Package/Util.cs index 028df0f..a8c4262 100644 --- a/dotnet/Metaparticle.Package/Util.cs +++ b/dotnet/Metaparticle.Package/Util.cs @@ -3,8 +3,10 @@ using System.IO; using System.Diagnostics; -namespace Metaparticle.Package { - public class Util { +namespace Metaparticle.Package +{ + public class Util + { public static Process Exec(String file, String args, TextWriter stdout=null, TextWriter stderr=null) { var task = ExecAsync(file, args, stdout, stderr); @@ -12,7 +14,8 @@ public static Process Exec(String file, String args, TextWriter stdout=null, Tex return task.Result; } - public static async Task Copy(StreamReader reader, TextWriter writer) { + public static async Task Copy(StreamReader reader, TextWriter writer) + { if (reader == null || writer == null) { return; } @@ -34,8 +37,10 @@ public static async Task ExecAsync(String file, String args, TextWriter var outputTask = Copy(proc.StandardOutput, stdout); var errTask = Copy(proc.StandardError, stderr); - await Task.Run(() => { - Task.WaitAll(new []{ + await Task.Run(() => + { + Task.WaitAll(new [] + { runTask, outputTask, errTask, diff --git a/dotnet/README.md b/dotnet/README.md index d4afb6d..44db008 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -51,7 +51,7 @@ You can set some of the config attributes through environment variables so that E.g. ``` -set MP_CONFIG_REPOSITORY=docker.io/myrepo/myimagename:sometag +set METAPARTICLE_CONFIG_REPOSITORY=docker.io/myrepo/myimagename:sometag ``` This will set the `Repository` property that you would otherwise set in the attributes. See `Config.cs` for supported environment variable overrides. From 1e4f1f6ca27374c4e0a4a229a8e905c8f0cfccf5 Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Tue, 23 Jan 2018 22:46:31 +1100 Subject: [PATCH 4/6] Add travis for dotnet (#3) * add initial travis file * fix up missing dash * try again going into the right directory this time * no need for mono and we can set the dotnet core version * make build.sh executable * see what directory we are on * set to unix type * build the examples --- .travis.yml | 13 ++++++++++++- dotnet/build.sh | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100755 dotnet/build.sh diff --git a/.travis.yml b/.travis.yml index 788fd95..94b5604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,4 +14,15 @@ matrix: - cd javascript/metaparticle-package - npm install script: - - npm test \ No newline at end of file + - npm test + + include: + - language: csharp + sudo: required + dotnet: 2.0.0 + mono: none + before_script: + - cd dotnet + - ls + script: + - ./build.sh \ No newline at end of file diff --git a/dotnet/build.sh b/dotnet/build.sh new file mode 100755 index 0000000..1728772 --- /dev/null +++ b/dotnet/build.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e + +dotnet build Metaparticle.Package + +# Build examples +cd examples +for D in *; do + if [ -d "${D}" ]; then + dotnet build ${D} + fi +done From 299dadd77f9263f7096716563e3572e763709d5e Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Thu, 20 Sep 2018 07:06:20 +1000 Subject: [PATCH 5/6] sync fork (#4) * Updated Java package tutorial instructions (#110) * Correct the expected value of the @Package/repository value * Mention the need for the @Package/publish=true field * Removed use of the @Runtime/publicAddress field since the tutorial does not appear to be ACI specific * Correct the type of service created on the remote Kubernetes cluster * Added missing unit test dependencies (#109) * Update README.md (#106) Slight typo. "a nd language of choice" ===> "and language of choice" * Create dotnet testrunner (#87) * fix up examples and documentation * fix up indenting * add initial code for a dotnet test runner as part of metaparticle * add missed file * renamed env vars to be conssitent and also moved braces to new lines * Add travis for dotnet (#3) * add initial travis file * fix up missing dash * try again going into the right directory this time * no need for mono and we can set the dotnet core version * make build.sh executable * see what directory we are on * set to unix type * build the examples * move tests to attributes rather than using env var * Initial Set of Unit Tests For DotNet Metaparticle.Package (#111) * Create unit test project and added tests for Metaparticle.Package.Config * Added unit tests for Config and an initial set of tests for the Driver * Minor update to Java sharding tutorial instructions (#112) (#113) * Fix typo in tutorial (#114) * Support containerizing of Spring Boot apps (#115) * Necessary to upgrade PowerMock version in order to run tests * Additions to gitignore file * Support sharding. (#118) * Update definition of local functions with explicit const (#120) * Add some words on Spring Boot support to package Java tutorial (#121) * Follow up to previous PR for issue #6 * Lazy intialize the Docker client. (#119) * Fix missing comma, go fmt (#122) * Adding rust (#86) * learned a touch of rust * no decorator * base rust entrypoint * with bare docker builder and executor (#3) * Real traits jim (#4) * add placeholder functions for actual interface * move builder trait into builder module * move docker builder struct into correct module * move existing functions onto trait implementation * move executor trait to appropriate module * move executor struct into correct module * initial builder working * added docker run support; cleaned up command execs * refactor to str refs; added readme * We need strings here, I think The result of the format! call is a string, (as is the result of other ways of getting a u64 into a string). This change makes the method compile without changing anything's type signature. There might be other was to accomplish this, but I haven't found one. * added web example * use executable's path as Docker context * wrapped docker cmds in sh * write dockerfile to docker context explicitly * fix copy paste error in README * Python fix + cleanup (#123) * general cleanup * nested options should be processed into objects from dictionaries * comments on logging; pythonic json read * Drive-by cleanups (#126) * Let requirements match the latest version * Make print statements Python3-compatible * insulate more against Py2 -> Py3 changes * Add python additionalFiles option (#125) * add python additionalFiles option * in case a dir is given * added tests for additionalFiles * Lazy intialize the Docker client. (#127) * more efficient base image (#131) * more efficient base image * squashed layers * removed breaking character * adding certs * sized down to 6mb and tested with simple http server --- .gitignore | 2 + README.md | 3 +- .../Metaparticle.Package.Tests/DriverTests.cs | 39 ++++ .../Metaparticle.Package.Tests.csproj | 18 ++ .../MetaparticleTests.cs | 26 +++ .../Metaparticle.Tests/Config.cs | 9 + .../Metaparticle.Tests/DotnetTestRunner.cs | 23 +++ .../Metaparticle.Tests/TestRunner.cs | 9 + dotnet/Metaparticle.Package/Metaparticle.cs | 25 ++- dotnet/README.md | 14 ++ dotnet/examples/simple-test/SimpleTest.cs | 23 +++ .../examples/simple-test/simple-test.csproj | 20 +++ dotnet/examples/simple/Program.cs | 10 +- go/examples/sharded/main.go | 8 +- go/metaparticle/metaparticle.go | 20 +-- java/pom.xml | 29 ++- .../java/io/metaparticle/Metaparticle.java | 56 ++++-- .../{test => }/MetaparticleTest.java | 84 +++++++-- javascript/metaparticle-package/index.js | 8 +- python/.gitignore | 2 + python/Makefile | 5 +- python/examples/include/data.json | 4 + python/examples/include/example.py | 37 ++++ python/examples/include/get_data.sh | 4 + python/examples/include/requirements.txt | 2 + python/examples/simple/example.py | 40 ++--- python/examples/simple/requirements.txt | 2 +- python/examples/web/requirements.txt | 2 +- python/examples/web/web.py | 29 ++- python/metaparticle_pkg/__init__.py | 5 +- python/metaparticle_pkg/builder/__init__.py | 1 + .../builder/docker_builder.py | 11 +- .../builder/test/test_docker_builder.py | 1 + python/metaparticle_pkg/containerize.py | 26 ++- python/metaparticle_pkg/option.py | 13 +- python/metaparticle_pkg/runner/__init__.py | 1 + .../metaparticle_pkg/runner/docker_runner.py | 26 ++- .../metaparticle_pkg/runner/metaparticle.py | 1 + .../runner/test/test_docker_runner.py | 12 +- .../runner/test/test_metaparticle.py | 1 + .../test/test_containerize.py | 54 +++++- python/metaparticle_pkg/test/test_option.py | 1 + python/metaparticle_pkg/version.json | 2 +- python/setup.cfg | 3 + python/setup.py | 4 +- python/tox.ini | 13 -- rust/.gitignore | 3 + rust/Cargo.toml | 13 ++ rust/README.md | 52 ++++++ rust/examples/hello.rs | 17 ++ rust/examples/web.rs | 61 +++++++ rust/src/builder/docker.rs | 13 ++ rust/src/builder/mod.rs | 8 + rust/src/executor/docker.rs | 31 ++++ rust/src/executor/mod.rs | 9 + rust/src/lib.rs | 166 ++++++++++++++++++ tutorials/java/sharding-tutorial.md | 7 +- tutorials/java/tutorial.md | 129 ++++++++++++-- tutorials/python/tutorial.md | 2 +- tutorials/python/web.py | 4 +- 60 files changed, 1101 insertions(+), 142 deletions(-) create mode 100644 dotnet/Metaparticle.Package.Tests/DriverTests.cs create mode 100644 dotnet/Metaparticle.Package.Tests/Metaparticle.Package.Tests.csproj create mode 100644 dotnet/Metaparticle.Package.Tests/MetaparticleTests.cs create mode 100644 dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs create mode 100644 dotnet/Metaparticle.Package/Metaparticle.Tests/DotnetTestRunner.cs create mode 100644 dotnet/Metaparticle.Package/Metaparticle.Tests/TestRunner.cs create mode 100644 dotnet/examples/simple-test/SimpleTest.cs create mode 100644 dotnet/examples/simple-test/simple-test.csproj rename java/src/test/java/io/metaparticle/{test => }/MetaparticleTest.java (54%) create mode 100644 python/examples/include/data.json create mode 100755 python/examples/include/example.py create mode 100644 python/examples/include/get_data.sh create mode 100644 python/examples/include/requirements.txt delete mode 100644 python/tox.ini create mode 100644 rust/.gitignore create mode 100644 rust/Cargo.toml create mode 100644 rust/README.md create mode 100644 rust/examples/hello.rs create mode 100644 rust/examples/web.rs create mode 100644 rust/src/builder/docker.rs create mode 100644 rust/src/builder/mod.rs create mode 100644 rust/src/executor/docker.rs create mode 100644 rust/src/executor/mod.rs create mode 100644 rust/src/lib.rs diff --git a/.gitignore b/.gitignore index a8fc746..7460869 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ go/**/Dockerfile python/**/MANIFEST python/**/dist .cache/ +/.idea/ +/**/*.iml diff --git a/README.md b/README.md index 5abfc93..3cc1e3d 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,7 @@ func main(args []string) { } ``` -The net effect of this is that a developer can containerize, distribute and optionally deploy their application without ever leaving the syntax or confines of their development environment a -nd language of choice. +The net effect of this is that a developer can containerize, distribute and optionally deploy their application without ever leaving the syntax or confines of their development environment and language of choice. At the same time, metaparticle is not intended to be a platform. Under the hood, the libraries still write `Dockerfiles` and make calls to the same build and push code. So when a developer wants or needs diff --git a/dotnet/Metaparticle.Package.Tests/DriverTests.cs b/dotnet/Metaparticle.Package.Tests/DriverTests.cs new file mode 100644 index 0000000..e5082f2 --- /dev/null +++ b/dotnet/Metaparticle.Package.Tests/DriverTests.cs @@ -0,0 +1,39 @@ +using System; +using NSubstitute; +using Xunit; +using static Metaparticle.Package.Driver; +using RuntimeConfig = Metaparticle.Runtime.Config; + +namespace Metaparticle.Package.Tests +{ + public class DriverTests + { + static int testOutput = 0; + + [Metaparticle.Runtime.Config] + [Metaparticle.Package.Config(Verbose = true, + Publish = false, Repository = "testrepo")] + private static void TestActionInContainer(string[] args) => Containerize (args, () => + { + testOutput = 1; + }); + + [Fact] + public void Containerize_Executes_Action_When_In_Container_Equals_True() + { + testOutput = 0; + Environment.SetEnvironmentVariable("METAPARTICLE_IN_CONTAINER", "true"); + TestActionInContainer(null); + Assert.True(1 == testOutput); + } + + [Fact] + public void Containerize_Executes_Action_When_In_Container_Equals_1() + { + testOutput = 0; + Environment.SetEnvironmentVariable("METAPARTICLE_IN_CONTAINER", "1"); + TestActionInContainer(null); + Assert.True(1 == testOutput); + } + } +} diff --git a/dotnet/Metaparticle.Package.Tests/Metaparticle.Package.Tests.csproj b/dotnet/Metaparticle.Package.Tests/Metaparticle.Package.Tests.csproj new file mode 100644 index 0000000..3d298c8 --- /dev/null +++ b/dotnet/Metaparticle.Package.Tests/Metaparticle.Package.Tests.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + diff --git a/dotnet/Metaparticle.Package.Tests/MetaparticleTests.cs b/dotnet/Metaparticle.Package.Tests/MetaparticleTests.cs new file mode 100644 index 0000000..ab3e25d --- /dev/null +++ b/dotnet/Metaparticle.Package.Tests/MetaparticleTests.cs @@ -0,0 +1,26 @@ +using System; +using Xunit; +using Metaparticle.Package; + +namespace Metaparticle.Package.Tests +{ + public class ConfigTests + { + [Fact] + public void Config_Loads_Environment_Variables() + { + Environment.SetEnvironmentVariable("METAPARTICLE_CONFIG_REPOSITORY", "testRepo"); + Environment.SetEnvironmentVariable("METAPARTICLE_CONFIG_PUBLISH", "true"); + var config = new Config(); + Assert.True("testRepo" == config.Repository); + Assert.True(true == config.Publish); + } + + [Fact] + public void Config_Defaults_Builder_To_Docker() + { + var config = new Config(); + Assert.True("docker" == config.Builder); + } + } +} diff --git a/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs b/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs new file mode 100644 index 0000000..6db13af --- /dev/null +++ b/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs @@ -0,0 +1,9 @@ +using System; + +namespace Metaparticle.Tests +{ + public class Config : Attribute + { + public string[] Names { get;set; } + } +} \ No newline at end of file diff --git a/dotnet/Metaparticle.Package/Metaparticle.Tests/DotnetTestRunner.cs b/dotnet/Metaparticle.Package/Metaparticle.Tests/DotnetTestRunner.cs new file mode 100644 index 0000000..d58619e --- /dev/null +++ b/dotnet/Metaparticle.Package/Metaparticle.Tests/DotnetTestRunner.cs @@ -0,0 +1,23 @@ +using System; +using System.IO; +using static Metaparticle.Package.Util; + +namespace Metaparticle.Tests +{ + public class DotnetTestRunner : TestRunner + { + public bool Run(string[] tests) + { + foreach (var testFolder in tests) + { + Console.WriteLine($"Running Test {testFolder}"); + var result = Exec("dotnet", $"test {testFolder}", Console.Out, Console.Error); + + if (result.ExitCode != 0) + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/dotnet/Metaparticle.Package/Metaparticle.Tests/TestRunner.cs b/dotnet/Metaparticle.Package/Metaparticle.Tests/TestRunner.cs new file mode 100644 index 0000000..1992010 --- /dev/null +++ b/dotnet/Metaparticle.Package/Metaparticle.Tests/TestRunner.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Metaparticle.Tests +{ + public interface TestRunner + { + bool Run(string[] tests); + } +} \ No newline at end of file diff --git a/dotnet/Metaparticle.Package/Metaparticle.cs b/dotnet/Metaparticle.Package/Metaparticle.cs index 403a43d..2a26bfd 100644 --- a/dotnet/Metaparticle.Package/Metaparticle.cs +++ b/dotnet/Metaparticle.Package/Metaparticle.cs @@ -4,8 +4,10 @@ using System.IO; using System.Text; using dockerfile; +using Metaparticle.Tests; using static Metaparticle.Package.Util; using RuntimeConfig = Metaparticle.Runtime.Config; +using TestConfig = Metaparticle.Tests.Config; namespace Metaparticle.Package { @@ -13,11 +15,13 @@ public class Driver { private Config config; private RuntimeConfig runtimeConfig; + private TestConfig testConfig; - public Driver(Config config, RuntimeConfig runtimeConfig) + public Driver(Config config, RuntimeConfig runtimeConfig, TestConfig testConfig) { this.config = config; this.runtimeConfig = runtimeConfig; + this.testConfig = testConfig; } private ImageBuilder getBuilder() @@ -76,6 +80,8 @@ public void Build(string[] args) TextWriter e = config.Quiet ? Console.Error : null; if (procName == "dotnet") { + RunTests(); + dir = "bin/release/netcoreapp2.0/debian.8-x64/publish"; Exec("dotnet", "publish -r debian.8-x64 -c release", stdout: o, stderr: e); //var dirInfo = new UnixDirectoryInfo(dir); @@ -138,6 +144,15 @@ public void Build(string[] args) exec.Logs(id, Console.Out, Console.Error); } + private void RunTests() + { + var runTestsResult = new DotnetTestRunner().Run(testConfig.Names); + if (runTestsResult == false) + { + throw new Exception("Tests Failed."); + } + } + private string writeDockerfile(string dir, string exe, string[] args, Config config) { var dockerfilename = dir + "/Dockerfile"; @@ -188,6 +203,8 @@ public static void Containerize(string[] args, Action main) } Config config = new Config(); RuntimeConfig runtimeConfig = null; + TestConfig testConfig = null; + var trace = new StackTrace(); foreach (object attribute in trace.GetFrame(1).GetMethod().GetCustomAttributes(true)) { @@ -199,8 +216,12 @@ public static void Containerize(string[] args, Action main) { runtimeConfig = (RuntimeConfig) attribute; } + if (attribute is TestConfig) + { + testConfig = (TestConfig) attribute; + } } - var mp = new Driver(config, runtimeConfig); + var mp = new Driver(config, runtimeConfig, testConfig); mp.Build(args); } } diff --git a/dotnet/README.md b/dotnet/README.md index 44db008..5e281cf 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -56,5 +56,19 @@ set METAPARTICLE_CONFIG_REPOSITORY=docker.io/myrepo/myimagename:sometag This will set the `Repository` property that you would otherwise set in the attributes. See `Config.cs` for supported environment variable overrides. +### Tests +If you wish to add some test project to your metaparticle that get run as part of the build pipeline, you can add the tests projects (relative paths) to the `Tests.Config` attribute above `Main`, where you declare the runtime config. + +``` +[Metaparticle.Tests.Config(Names = new[] {"../my-test-folder1/", "../my-test-folder2/tests"})] +``` + +Sample project `simple-web` uses the test project `simple-test`. You can run the example to see it in action. + +``` +cd examples/simple +dotnet run +``` + ## Tutorial For a more complete exploration of the Metaparticle/Package for .NET Core, please see the [in-depth tutorial](../tutorials/dotnet/tutorial.md). diff --git a/dotnet/examples/simple-test/SimpleTest.cs b/dotnet/examples/simple-test/SimpleTest.cs new file mode 100644 index 0000000..55a1875 --- /dev/null +++ b/dotnet/examples/simple-test/SimpleTest.cs @@ -0,0 +1,23 @@ +using System; +using Xunit; +using simple; + +namespace simpletest +{ + public class SimpleTests + { + [Fact] + public void GivenTwoStrings_WhenConcatenateIsCalled_TwoStringsAreConcatenated() + { + // arrange + var stringOne = "Hello"; + var stringTwo = "World"; + + // act + var result = Program.Concatenate(stringOne, stringTwo); + + // assert + Assert.Equal("HelloWorld", result); + } + } +} diff --git a/dotnet/examples/simple-test/simple-test.csproj b/dotnet/examples/simple-test/simple-test.csproj new file mode 100644 index 0000000..99061c3 --- /dev/null +++ b/dotnet/examples/simple-test/simple-test.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + + false + + + + + + + + + + + + + + diff --git a/dotnet/examples/simple/Program.cs b/dotnet/examples/simple/Program.cs index a71d560..9724481 100644 --- a/dotnet/examples/simple/Program.cs +++ b/dotnet/examples/simple/Program.cs @@ -9,15 +9,21 @@ public class Program { [Metaparticle.Runtime.Config] [Metaparticle.Package.Config(Verbose = true, - Publish = false, Repository = "docker.io/docker-user-goes-here/dotnet-simple")] + Publish = false, Repository = "docker.io/docker-user-goes-here/dotnet-simple")] + [Metaparticle.Tests.Config(Names = new[] {"../simple-test"})] public static void Main(string[] args) => Containerize (args, () => { Console.Out.WriteLine(args); int i = 0; while (true) { - Console.WriteLine("Hello world " + (i++)); + Console.WriteLine($"{Concatenate("Hello", " world")} " + (i++)); Thread.Sleep(10 * 1000); } }); + + public static string Concatenate(string one, string two) + { + return $"{one}{two}"; + } } } diff --git a/go/examples/sharded/main.go b/go/examples/sharded/main.go index b2815c0..b43f214 100644 --- a/go/examples/sharded/main.go +++ b/go/examples/sharded/main.go @@ -20,9 +20,9 @@ func handler(w http.ResponseWriter, r *http.Request) { func main() { metaparticle.Containerize( &metaparticle.Runtime{ - Ports: []int32{port}, - Executor: "metaparticle", - Shards: 3, + Ports: []int32{port}, + Executor: "metaparticle", + Shards: 3, URLShardPattern: "^\\/users\\/([^\\/]*)\\/.*", }, &metaparticle.Package{ @@ -33,7 +33,7 @@ func main() { Publish: true, }, func() { - log.Printf("Starting server on :%d\n" port) + log.Printf("Starting server on :%d\n", port) http.HandleFunc("/", handler) err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) if err != nil { diff --git a/go/metaparticle/metaparticle.go b/go/metaparticle/metaparticle.go index c5db8c0..575df23 100644 --- a/go/metaparticle/metaparticle.go +++ b/go/metaparticle/metaparticle.go @@ -103,19 +103,13 @@ func builderFromPackage(pkg *Package) (Builder, error) { func writeDockerfile(name string) error { contents := `FROM golang:1.9 as builder -WORKDIR /go/src/app -COPY . . - -RUN go get -u github.com/golang/dep/cmd/dep -RUN dep init -RUN go-wrapper install - - -FROM ubuntu - -COPY --from=builder /go/bin/app . - -CMD ["./app"] + WORKDIR /go/src/app + COPY . . + RUN go get -u github.com/golang/dep/cmd/dep && dep init && CGO_ENABLED=0 GOOS=linux go-wrapper install + FROM alpine:3.7 + RUN apk add --no-cache --update ca-certificates + COPY --from=builder /go/bin/app . + CMD ["./app"] ` return ioutil.WriteFile("Dockerfile", []byte(contents), 0644) } diff --git a/java/pom.xml b/java/pom.xml index 7fc3c83..d7d7c2f 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -154,12 +154,39 @@ gson 2.8.2 + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.powermock + powermock-api-mockito2 + ${powermock.version} + test + + + org.powermock + powermock-module-junit4 + ${powermock.version} + test + 1.8 ${java.version} ${java.version} UTF-8 - 1.7.1 + 2.0.0-beta.5 + 2.15.0 + 5.1.0 \ No newline at end of file diff --git a/java/src/main/java/io/metaparticle/Metaparticle.java b/java/src/main/java/io/metaparticle/Metaparticle.java index cbadae8..8bfc847 100644 --- a/java/src/main/java/io/metaparticle/Metaparticle.java +++ b/java/src/main/java/io/metaparticle/Metaparticle.java @@ -1,20 +1,27 @@ package io.metaparticle; -import static io.metaparticle.Util.handleErrorExec; -import static io.metaparticle.Util.once; - +import io.metaparticle.annotations.Package; +import io.metaparticle.annotations.Runtime; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.logging.Logger; -import io.metaparticle.annotations.Package; -import io.metaparticle.annotations.Runtime; +import static io.metaparticle.Util.handleErrorExec; +import static io.metaparticle.Util.once; public class Metaparticle { + private static Logger logger = Logger.getLogger(Metaparticle.class.getName()); + + static final String SPRING_BOOT_APP_ANNOTATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; + + static final String SPRING_BOOT_LOADER_JAR_LAUNCHER_CLASS = "org.springframework.boot.loader.JarLauncher"; + public static boolean inDockerContainer() { String envFlag = System.getenv("METAPARTICLE_IN_CONTAINER"); if (envFlag != null) { @@ -32,7 +39,7 @@ public static boolean inDockerContainer() { if (f.exists()) { try { String s = new String(Files.readAllBytes(f.toPath()), "UTF-8"); - return (s.indexOf("docker") != -1 || s.indexOf("kubepods") != -1); + return (s.contains("docker") || s.contains("kubepods")); } catch (IOException ex) { throw new IllegalStateException(ex); } @@ -68,13 +75,13 @@ public static Builder getBuilder(Package pkg) { } } - public static void writeDockerfile(String className, Package p) throws IOException { + public static void writeDockerfile(String className, Package p, boolean runSpringBootJar) throws IOException { byte [] output; - if (p.dockerfile() == null || p.dockerfile().length() == 0) { - String contents = + if (p.dockerfile().isEmpty()) { + String contents = "FROM openjdk:8-jre-alpine\n" + "COPY %s /main.jar\n" + -"CMD java -classpath /main.jar %s"; +(runSpringBootJar ? "CMD java -jar /main.jar" : "CMD java -classpath /main.jar %s"); output = String.format(contents, p.jarFile(), className).getBytes(); } else { output = Files.readAllBytes(Paths.get(p.dockerfile())); @@ -98,7 +105,6 @@ public static void Containerize(Runnable fn) { StackTraceElement[] traces = Thread.currentThread().getStackTrace(); String className = traces[2].getClassName(); String methodName = traces[2].getMethodName(); - try { Class clazz = Class.forName(className); @@ -118,10 +124,10 @@ public static void Containerize(Runnable fn) { OutputStream stdout = packageAnnotation.verbose() ? System.out : null; OutputStream stderr = packageAnnotation.quiet() ? null : System.err; - writeDockerfile(className, packageAnnotation); + writeDockerfile(className, packageAnnotation, isSpringBootAppBeingContainerized(clazz)); if (packageAnnotation.build()) { - handleErrorExec(new String[] {"mvn", "package"}, System.out, System.err); + doPackage(isRunningAsSpringBootApplication(traces), targetJarExists(packageAnnotation.jarFile())); builder.build(".", image, stdout, stderr); if (packageAnnotation.publish()) { builder.push(image, stdout, stderr); @@ -140,4 +146,28 @@ public static void Containerize(Runnable fn) { } } } + + static boolean isSpringBootAppBeingContainerized(Class containerizedClass) { + return Arrays.stream(containerizedClass.getAnnotations()) + .anyMatch(annotation -> annotation.toString().contains(SPRING_BOOT_APP_ANNOTATION)); + } + + static boolean targetJarExists(String jarfileName) { + return Files.exists(Paths.get(jarfileName)); + } + + static boolean isRunningAsSpringBootApplication(StackTraceElement[] traces) { + String launchClass = traces[traces.length - 1].getClassName(); + String launchMethod = traces[traces.length - 1].getMethodName(); + return launchClass.equals(SPRING_BOOT_LOADER_JAR_LAUNCHER_CLASS) && launchMethod.equals("main"); + } + + static boolean doPackage(boolean inRunningSpringBootApp, boolean targetExists) { + if (inRunningSpringBootApp && targetExists) { + logger.info("Package step skipped when running as Spring Boot Jar and target already exists"); + return false; + } else { + return handleErrorExec(new String[]{"mvn", "package"}, System.out, System.err); + } + } } diff --git a/java/src/test/java/io/metaparticle/test/MetaparticleTest.java b/java/src/test/java/io/metaparticle/MetaparticleTest.java similarity index 54% rename from java/src/test/java/io/metaparticle/test/MetaparticleTest.java rename to java/src/test/java/io/metaparticle/MetaparticleTest.java index b90c19e..bfb27af 100644 --- a/java/src/test/java/io/metaparticle/test/MetaparticleTest.java +++ b/java/src/test/java/io/metaparticle/MetaparticleTest.java @@ -1,26 +1,34 @@ -package io.metaparticle.test; - -import static io.metaparticle.Metaparticle.Containerize; -import static org.junit.jupiter.api.Assertions.assertEquals; +package io.metaparticle; +import io.metaparticle.annotations.Package; +import io.metaparticle.annotations.Runtime; import java.io.IOException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Paths; - -import org.junit.jupiter.api.Test; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; -import io.metaparticle.AciExecutor; -import io.metaparticle.DockerImpl; -import io.metaparticle.Metaparticle; -import io.metaparticle.MetaparticleExecutor; -import io.metaparticle.annotations.Package; -import io.metaparticle.annotations.Runtime; +import static io.metaparticle.Metaparticle.Containerize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@RunWith(PowerMockRunner.class) +@PrepareForTest({Util.class}) public class MetaparticleTest { + @After + public void tearDown() throws IOException { + Files.deleteIfExists(Paths.get("Dockerfile")); + } + @Test public void inDockerContainerTest() { assertEquals(false, Metaparticle.inDockerContainer()); @@ -83,6 +91,58 @@ public void getBuilderTest() throws NoSuchMethodException, SecurityException { assertEquals(DockerImpl.class.getName(), Metaparticle.getBuilder(packageAnnotation).getClass().getName()); } + @Test + public void dockerFileCommandSetForSpringBootApplication() throws IOException { + Package mockPackage = mock(Package.class); + when(mockPackage.dockerfile()).thenReturn(""); + when(mockPackage.jarFile()).thenReturn("target/foo.jar"); + Metaparticle.writeDockerfile("foo.Bar", mockPackage, true); + + String dockerfileAsString = new String(Files.readAllBytes(Paths.get("Dockerfile"))); + assertTrue(dockerfileAsString.endsWith("\nCMD java -jar /main.jar")); + } + + @Test + public void dockerFileCommandSetWithDefaultCommand() throws IOException { + Package mockPackage = mock(Package.class); + when(mockPackage.dockerfile()).thenReturn(""); + when(mockPackage.jarFile()).thenReturn("target/foo.jar"); + Metaparticle.writeDockerfile("foo.Bar", mockPackage, false); + + String dockerfileAsString = new String(Files.readAllBytes(Paths.get("Dockerfile"))); + assertTrue(dockerfileAsString.endsWith("\nCMD java -classpath /main.jar foo.Bar")); + } + + @Test + public void packageStepSkippedWhenRunningInSpringBootAppAndTargetAlreadyExists() { + PowerMockito.mockStatic(Util.class); + + Metaparticle.doPackage(true, true); + + PowerMockito.verifyStatic(Util.class, Mockito.never()); + Util.handleErrorExec(new String[]{"mvn", "package"}, System.out, System.err); + } + + @Test + public void packageStepOccursWhenNotRunningInSpringBootAppAndTargetAlreadyExists() { + PowerMockito.mockStatic(Util.class); + + Metaparticle.doPackage(false, true); + + PowerMockito.verifyStatic(Util.class); + Util.handleErrorExec(new String[]{"mvn", "package"}, System.out, System.err); + } + + @Test + public void packageStepOccursWhenNotRunningInSpringBootAppAndTargetDoesNotExist() { + PowerMockito.mockStatic(Util.class); + + Metaparticle.doPackage(false, false); + + PowerMockito.verifyStatic(Util.class); + Util.handleErrorExec(new String[]{"mvn", "package"}, System.out, System.err); + } + @Test public void writeDockerFileTest() throws IOException, NoSuchMethodException, SecurityException { //TODO diff --git a/javascript/metaparticle-package/index.js b/javascript/metaparticle-package/index.js index 12ceff1..90ff159 100644 --- a/javascript/metaparticle-package/index.js +++ b/javascript/metaparticle-package/index.js @@ -2,7 +2,7 @@ var fs = require('fs'); var path = require('path'); - inDockerContainer = () => { + const inDockerContainer = () => { switch (process.env.METAPARTICLE_IN_CONTAINER) { case 'true': case '1': @@ -26,7 +26,7 @@ return false; }; - writeDockerfile = (options) => { + const writeDockerfile = (options) => { if (options.dockerfile) { fs.copyFileSync(options.dockerfile, 'Dockerfile'); return; @@ -43,7 +43,7 @@ fs.writeFileSync('Dockerfile', dockerfile); }; - selectBuilder = (buildSpec) => { + const selectBuilder = (buildSpec) => { switch (buildSpec) { case 'docker': return require('./docker-builder'); @@ -52,7 +52,7 @@ } } - selectRunner = (execSpec) => { + const selectRunner = (execSpec) => { switch (execSpec) { case 'docker': return require('./docker-runner'); diff --git a/python/.gitignore b/python/.gitignore index 4ab7448..c9e0ea5 100644 --- a/python/.gitignore +++ b/python/.gitignore @@ -1,3 +1,5 @@ +.pytest_cache/ + # This repo is generating etherial dockerfiles Dockerfile diff --git a/python/Makefile b/python/Makefile index baefb4b..ea3bae7 100644 --- a/python/Makefile +++ b/python/Makefile @@ -2,7 +2,8 @@ test: (. venv/bin/activate; \ - tox; \ + python3 -m pytest --cov=. --cov-report=term-missing:skip-covered ;\ + python3 -m flake8 --config setup.cfg ;\ ) venv: @@ -10,8 +11,8 @@ venv: venv/bin/pip install --upgrade pip venv/bin/pip install --upgrade setuptools . venv/bin/activate + venv/bin/pip install -r requirements.txt venv/bin/pip install -e . - venv/bin/pip install --upgrade tox publish: python setup.py sdist upload -r pypi diff --git a/python/examples/include/data.json b/python/examples/include/data.json new file mode 100644 index 0000000..7d2eb52 --- /dev/null +++ b/python/examples/include/data.json @@ -0,0 +1,4 @@ +{ + "important": "payload", + "goes": "here" +} \ No newline at end of file diff --git a/python/examples/include/example.py b/python/examples/include/example.py new file mode 100755 index 0000000..361160d --- /dev/null +++ b/python/examples/include/example.py @@ -0,0 +1,37 @@ +#!/usr/bin/python +from metaparticle_pkg import Containerize, PackageFile + +import os +import time +import logging + +# all metaparticle output is accessible through the stdlib logger (debug level) +logging.basicConfig(level=logging.INFO) +logging.getLogger('metaparticle_pkg.runner').setLevel(logging.DEBUG) +logging.getLogger('metaparticle_pkg.builder').setLevel(logging.DEBUG) + + +DATA_FILE = '/opt/some/random/spot/data1.json' +SCRIPT = '/opt/another/random/place/get_the_data.sh' + + +@Containerize( + package={ + 'name': 'file-example', + 'repository': 'docker.io/brendanburns', + 'publish': False, + 'additionalFiles': [ + PackageFile(src='./data.json', dest=DATA_FILE, mode='0400'), + PackageFile(src='./get_data.sh', dest=SCRIPT), + ] + } +) +def main(): + os.system(SCRIPT) + for i in range(5): + print('Sleeping ... {} sec'.format(i)) + time.sleep(1) + + +if __name__ == '__main__': + main() diff --git a/python/examples/include/get_data.sh b/python/examples/include/get_data.sh new file mode 100644 index 0000000..e7d991d --- /dev/null +++ b/python/examples/include/get_data.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo "Hello, world!" +ls -al /opt/some/random/spot/data1.json +cat /opt/some/random/spot/data1.json \ No newline at end of file diff --git a/python/examples/include/requirements.txt b/python/examples/include/requirements.txt new file mode 100644 index 0000000..eb3b229 --- /dev/null +++ b/python/examples/include/requirements.txt @@ -0,0 +1,2 @@ +six==1.10.0 +metaparticle_pkg \ No newline at end of file diff --git a/python/examples/simple/example.py b/python/examples/simple/example.py index a614433..7319eca 100755 --- a/python/examples/simple/example.py +++ b/python/examples/simple/example.py @@ -1,34 +1,32 @@ +#!/usr/bin/python +from __future__ import absolute_import +from __future__ import print_function from metaparticle_pkg import Containerize -import time - -package_repo = 'repo' -package_name = 'something' -sleep_time = 10 +import time +import logging +from six.moves import range -@Containerize( - package={'name': package_name, 'repository': package_repo}, - runtime={'ports': [80, 8080]} -) -def container_with_port(): - print('hello container_with_port') - - for i in range(sleep_time): - print('Sleeping ... {} sec'.format(i)) - time.sleep(1) +# all metaparticle output is accessible through the stdlib logger (debug level) +logging.basicConfig(level=logging.INFO) +logging.getLogger('metaparticle_pkg.runner').setLevel(logging.DEBUG) +logging.getLogger('metaparticle_pkg.builder').setLevel(logging.DEBUG) @Containerize( - package={'name': package_name, 'repository': package_repo, 'publish': True} + package={ + 'name': 'simple', + 'repository': 'docker.io/brendanburns', + 'publish': False + } ) -def hihi(): +def main(): print('hello world!') - for i in range(sleep_time): - print('Sleeping ... {} sec'.format(i)) + for i in range(5): + print(('Sleeping ... {} sec'.format(i))) time.sleep(1) if __name__ == '__main__': - hihi() - container_with_port() + main() diff --git a/python/examples/simple/requirements.txt b/python/examples/simple/requirements.txt index 81fefc0..19bf20f 100644 --- a/python/examples/simple/requirements.txt +++ b/python/examples/simple/requirements.txt @@ -1,2 +1,2 @@ six==1.10.0 -metaparticle_pkg==0.4.1 +metaparticle_pkg diff --git a/python/examples/web/requirements.txt b/python/examples/web/requirements.txt index 21bc28a..19bf20f 100644 --- a/python/examples/web/requirements.txt +++ b/python/examples/web/requirements.txt @@ -1,2 +1,2 @@ six==1.10.0 -metaparticle_pkg<=0.5.0 +metaparticle_pkg diff --git a/python/examples/web/web.py b/python/examples/web/web.py index 13bac51..4b3a85f 100755 --- a/python/examples/web/web.py +++ b/python/examples/web/web.py @@ -1,11 +1,19 @@ #!/usr/bin/python +from __future__ import absolute_import +from __future__ import print_function +from metaparticle_pkg import Containerize -from six.moves import SimpleHTTPServer, socketserver +import logging import socket -from metaparticle_pkg.containerize import Containerize +from six.moves import SimpleHTTPServer, socketserver -OK = 200 +# all metaparticle output is accessible through the stdlib logger (debug level) +logging.basicConfig(level=logging.INFO) +logging.getLogger('metaparticle_pkg.runner').setLevel(logging.DEBUG) +logging.getLogger('metaparticle_pkg.builder').setLevel(logging.DEBUG) + +OK = 200 port = 8080 @@ -15,7 +23,7 @@ def do_GET(self): self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write("Hello Metaparticle [{}] @ {}\n".format(self.path, socket.gethostname()).encode('UTF-8')) - print("request for {}".format(self.path)) + print(("request for {}".format(self.path))) def do_HEAD(self): self.send_response(OK) @@ -24,8 +32,17 @@ def do_HEAD(self): @Containerize( - package={'name': 'web', 'repository': 'docker.io/brendanburns'}, - runtime={'ports': [8080], 'executor': 'metaparticle', 'replicas': 3, 'public': True} + package={ + # to run this example you'll need to change these values + 'name': 'web', + 'repository': 'docker.io/brendanburns', + }, + runtime={ + 'ports': [8080], + 'executor': 'metaparticle', + 'replicas': 3, + 'public': True + } ) def main(): Handler = MyHandler diff --git a/python/metaparticle_pkg/__init__.py b/python/metaparticle_pkg/__init__.py index f8a2ade..26dc652 100644 --- a/python/metaparticle_pkg/__init__.py +++ b/python/metaparticle_pkg/__init__.py @@ -1,3 +1,4 @@ -from metaparticle_pkg.containerize import Containerize +from __future__ import absolute_import +from metaparticle_pkg.containerize import Containerize, PackageFile -__all__ = [Containerize] +__all__ = ['Containerize', 'PackageFile'] diff --git a/python/metaparticle_pkg/builder/__init__.py b/python/metaparticle_pkg/builder/__init__.py index 73c614e..6b74e08 100644 --- a/python/metaparticle_pkg/builder/__init__.py +++ b/python/metaparticle_pkg/builder/__init__.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from metaparticle_pkg.builder.docker_builder import DockerBuilder diff --git a/python/metaparticle_pkg/builder/docker_builder.py b/python/metaparticle_pkg/builder/docker_builder.py index 98e7378..b33b2d2 100644 --- a/python/metaparticle_pkg/builder/docker_builder.py +++ b/python/metaparticle_pkg/builder/docker_builder.py @@ -1,16 +1,20 @@ +from __future__ import absolute_import import json import logging from docker import APIClient -logger = logging.getLogger(__name__) +# use a generic logger name: metaparticle_pkg.builder +logger = logging.getLogger('.'.join(__name__.split('.')[:-1])) class DockerBuilder: def __init__(self): - self.docker_client = APIClient(version='auto') + self.docker_client = None def build(self, img, path='.'): + if self.docker_client is None: + self.docker_client = APIClient(version='auto') bld = self.docker_client.build( path=path, @@ -22,6 +26,9 @@ def build(self, img, path='.'): self._process_stream(line) def publish(self, img): + if self.docker_client is None: + self.docker_client = APIClient(version='auto') + # TODO: do we need to set tag? for line in self.docker_client.push(img, stream=True): self._process_stream(line) diff --git a/python/metaparticle_pkg/builder/test/test_docker_builder.py b/python/metaparticle_pkg/builder/test/test_docker_builder.py index c628a36..9325f23 100644 --- a/python/metaparticle_pkg/builder/test/test_docker_builder.py +++ b/python/metaparticle_pkg/builder/test/test_docker_builder.py @@ -2,6 +2,7 @@ '''Unit tests for DockerBuilder''' +from __future__ import absolute_import import unittest from unittest.mock import patch from metaparticle_pkg.builder import docker_builder diff --git a/python/metaparticle_pkg/containerize.py b/python/metaparticle_pkg/containerize.py index 00c556a..2bc4e20 100644 --- a/python/metaparticle_pkg/containerize.py +++ b/python/metaparticle_pkg/containerize.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import os import shutil import signal @@ -35,14 +36,17 @@ def write_dockerfile(package, exec_file): shutil.copy(package.dockerfile, 'Dockerfile') return + copy_files = "\n".join([addFile.render() for addFile in package.additionalFiles]) + with open('Dockerfile', 'w+t') as f: f.write("""FROM python:{version}-alpine - COPY ./ /app/ +{copy_files} RUN pip install --no-cache -r /app/requirements.txt - -CMD python /app/{exec_file} -""".format(version=package.py_version, exec_file=exec_file)) +CMD python -u /app/{exec_file} +""".format(version=package.py_version, + exec_file=exec_file, + copy_files=copy_files)) class Containerize(object): @@ -83,3 +87,17 @@ def signal_handler(signal, frame): return self.runner.logs(self.package.name) return wrapped + + +class PackageFile(object): + + def __init__(self, src, dest, mode=None): + self.src = src + self.dest = dest + self.mode = mode + + def render(self): + ret = "COPY {src} {dest}".format(src=self.src, dest=self.dest) + if self.mode: + ret += "\nRUN chmod -R {mode} {dest}".format(mode=self.mode, dest=self.dest) + return ret diff --git a/python/metaparticle_pkg/option.py b/python/metaparticle_pkg/option.py index 8a66ffb..b4f4d7f 100644 --- a/python/metaparticle_pkg/option.py +++ b/python/metaparticle_pkg/option.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from collections import namedtuple import sys import os @@ -5,7 +6,7 @@ def load(cls, options): if not isinstance(options, dict): - sys.stderr.write("Must provide an options dict.") + sys.stderr.write("Must provide an options dictionary. Given option: %s" % repr(options)) sys.exit(1) for option in cls.required_options: if option not in options: @@ -22,6 +23,10 @@ class RuntimeOptions(namedtuple('Runtime', 'executor replicas ports public shard required_options = [] def __new__(cls, executor='docker', replicas=0, ports=[], public=False, shardSpec=None, jobSpec=None): + if shardSpec: + shardSpec = load(ShardSpec, shardSpec) + if jobSpec: + jobSpec = load(JobSpec, jobSpec) return super(RuntimeOptions, cls).__new__(cls, executor, replicas, ports, public, shardSpec, jobSpec) @@ -39,9 +44,9 @@ def __new__(cls, iterations=0): return super(JobSpec, cls).__new__(cls, iterations) -class PackageOptions(namedtuple('Package', 'repository name builder publish verbose quiet py_version')): +class PackageOptions(namedtuple('Package', 'repository name builder publish py_version additionalFiles')): required_options = ['repository'] - def __new__(cls, repository, name, builder='docker', publish=False, verbose=True, quiet=False, py_version=3, dockerfile=None): + def __new__(cls, repository, name, builder='docker', publish=False, py_version=3, dockerfile=None, additionalFiles=tuple()): name = name if name else os.path.basename(os.getcwd()) - return super(PackageOptions, cls).__new__(cls, repository, name, builder, publish, verbose, quiet, py_version) + return super(PackageOptions, cls).__new__(cls, repository, name, builder, publish, py_version, additionalFiles) diff --git a/python/metaparticle_pkg/runner/__init__.py b/python/metaparticle_pkg/runner/__init__.py index 8bb2fa8..31aaded 100644 --- a/python/metaparticle_pkg/runner/__init__.py +++ b/python/metaparticle_pkg/runner/__init__.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from metaparticle_pkg.runner.docker_runner import DockerRunner from metaparticle_pkg.runner.metaparticle import MetaparticleRunner diff --git a/python/metaparticle_pkg/runner/docker_runner.py b/python/metaparticle_pkg/runner/docker_runner.py index da2763b..8228f83 100644 --- a/python/metaparticle_pkg/runner/docker_runner.py +++ b/python/metaparticle_pkg/runner/docker_runner.py @@ -1,15 +1,20 @@ +from __future__ import absolute_import import logging from docker import APIClient -logger = logging.getLogger(__name__) +# use a generic logger name: metaparticle_pkg.runner +logger = logging.getLogger('.'.join(__name__.split('.')[:-1])) class DockerRunner: def __init__(self): - self.docker_client = APIClient(version='auto') + self.docker_client = None def run(self, img, name, options): + if self.docker_client is None: + self.docker_client = APIClient(version='auto') + ports = [] host_config = None @@ -25,17 +30,20 @@ def run(self, img, name, options): # Launch docker container container = self.docker_client.create_container( img, - name=name, - ports=ports, host_config=host_config, + name=name, + ports=ports ) - self.docker_client.start(container=container.get('Id')) - - self.container = container logger.info('Starting container {}'.format(container)) + self.docker_client.start(container=container.get('Id')) + self.container = container + def logs(self, *args, **kwargs): + if self.docker_client is None: + self.docker_client = APIClient(version='auto') + # seems like we are hitting bug # https://github.com/docker/docker-py/issues/300 log_stream = self.docker_client.logs( @@ -45,8 +53,10 @@ def logs(self, *args, **kwargs): ) for line in log_stream: - logger.info(line) + logger.info(line.decode("utf-8").strip('\n')) def cancel(self, name): + if self.docker_client is None: + self.docker_client = APIClient(version='auto') self.docker_client.kill(self.container.get('Id')) self.docker_client.remove_container(self.container.get('Id')) diff --git a/python/metaparticle_pkg/runner/metaparticle.py b/python/metaparticle_pkg/runner/metaparticle.py index 50f58b9..1f2620d 100644 --- a/python/metaparticle_pkg/runner/metaparticle.py +++ b/python/metaparticle_pkg/runner/metaparticle.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import json import os import subprocess diff --git a/python/metaparticle_pkg/runner/test/test_docker_runner.py b/python/metaparticle_pkg/runner/test/test_docker_runner.py index bb47ee6..68f5976 100644 --- a/python/metaparticle_pkg/runner/test/test_docker_runner.py +++ b/python/metaparticle_pkg/runner/test/test_docker_runner.py @@ -2,6 +2,7 @@ '''Unit tests for DockerRunner''' +from __future__ import absolute_import import unittest from unittest.mock import patch, MagicMock from metaparticle_pkg.runner import docker_runner @@ -30,6 +31,11 @@ def test_run(self, mocked_start, mocked_create): options = MagicMock() options.ports = [port] + result = {'id': 'foo'} + mocked_container = MagicMock() + mocked_container.get = MagicMock(return_value=result) + mocked_create.return_value = mocked_container + # Expected argument called with self.runner.run(img, name, options) @@ -50,7 +56,11 @@ def test_run(self, mocked_start, mocked_create): ports=[port] ) - mocked_start.assert_called_once() + mocked_container.get.assert_called_once_with('Id') + + mocked_start.assert_called_with( + container=result + ) @patch('metaparticle_pkg.runner.docker_runner.APIClient.logs') def test_logs(self, mocked_logs): diff --git a/python/metaparticle_pkg/runner/test/test_metaparticle.py b/python/metaparticle_pkg/runner/test/test_metaparticle.py index 50d88b9..e78824f 100644 --- a/python/metaparticle_pkg/runner/test/test_metaparticle.py +++ b/python/metaparticle_pkg/runner/test/test_metaparticle.py @@ -1,6 +1,7 @@ #!/usr/bin/env python '''Unit tests for MetaparticleRunner''' +from __future__ import absolute_import import unittest from unittest.mock import patch, MagicMock, mock_open from metaparticle_pkg.runner import metaparticle diff --git a/python/metaparticle_pkg/test/test_containerize.py b/python/metaparticle_pkg/test/test_containerize.py index 90b41d7..8515c81 100644 --- a/python/metaparticle_pkg/test/test_containerize.py +++ b/python/metaparticle_pkg/test/test_containerize.py @@ -1,6 +1,7 @@ #!/usr/bin/env python '''Unit tests for containerize module''' +from __future__ import absolute_import import unittest from unittest.mock import patch, mock_open, MagicMock from metaparticle_pkg import containerize @@ -141,12 +142,47 @@ def test_write_dockerfile_with_dockerfile_absent(self): # Input parameters package = MagicMock() package.dockerfile = None + package.py_version = "3.7" exec_file = '/some/fake_exec_file_path' with patch("metaparticle_pkg.containerize.open", mocked_open_function) as mocked_open: containerize.write_dockerfile(package, exec_file) - self.assertEqual(mocked_open().write.call_count, 1) + mocked_open().write.assert_called_once_with("""\ +FROM python:3.7-alpine +COPY ./ /app/ + +RUN pip install --no-cache -r /app/requirements.txt +CMD python -u /app//some/fake_exec_file_path +""") + + def test_write_dockerfile_with_additional_files(self): + '''Test write_dockerfile method in case of Dockerfile is absent''' + + mocked_open_function = mock_open() + + # Input parameters + package = MagicMock() + package.dockerfile = None + package.py_version = "3.7" + package.additionalFiles = [ + containerize.PackageFile(src='/from/somewhere', dest='/to/somewhere'), + containerize.PackageFile(src='/from/somewhere/else', dest='/to/somewhere/else', mode=754) + ] + exec_file = '/some/fake_exec_file_path' + + with patch("metaparticle_pkg.containerize.open", + mocked_open_function) as mocked_open: + containerize.write_dockerfile(package, exec_file) + mocked_open().write.assert_called_once_with("""\ +FROM python:3.7-alpine +COPY ./ /app/ +COPY /from/somewhere /to/somewhere +COPY /from/somewhere/else /to/somewhere/else +RUN chmod -R 754 /to/somewhere/else +RUN pip install --no-cache -r /app/requirements.txt +CMD python -u /app//some/fake_exec_file_path +""") @patch("metaparticle_pkg.containerize.os") @patch("metaparticle_pkg.containerize.builder") @@ -198,6 +234,22 @@ def test_func(): pass self.assertEqual(mocked_runner.select().logs.call_count, 1) self.assertEqual(mocked_builder.select().build.call_count, 1) + def test_packagefile_render(self): + '''Test packagefile.render method go case''' + + package_file = containerize.PackageFile(src='thesrc', dest='thedst') + actual_value = package_file.render() + + self.assertEqual(actual_value, "COPY thesrc thedst") + + def test_packagefile_render_mode(self): + '''Test packagefile.render method with a mode''' + + package_file = containerize.PackageFile(src='thesrc', dest='thedst', mode=444) + actual_value = package_file.render() + + self.assertEqual(actual_value, "COPY thesrc thedst\nRUN chmod -R 444 thedst") + if __name__ == '__main__': unittest.main() diff --git a/python/metaparticle_pkg/test/test_option.py b/python/metaparticle_pkg/test/test_option.py index a6464fd..5d78348 100644 --- a/python/metaparticle_pkg/test/test_option.py +++ b/python/metaparticle_pkg/test/test_option.py @@ -1,6 +1,7 @@ #!/usr/bin/env python '''Unit tests for option module''' +from __future__ import absolute_import from sys import exit as real_exit import unittest from unittest.mock import patch, MagicMock diff --git a/python/metaparticle_pkg/version.json b/python/metaparticle_pkg/version.json index 831fc0e..3d61c51 100644 --- a/python/metaparticle_pkg/version.json +++ b/python/metaparticle_pkg/version.json @@ -1,5 +1,5 @@ { "version": "0.6.4", "license": "MIT", -"status": "Development", +"status": "Development" } diff --git a/python/setup.cfg b/python/setup.cfg index bd2bcf6..5e3a93d 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -3,3 +3,6 @@ description-file = README.md [flake8] max-line-length = 160 +exclude = + ./.git, + ./venv \ No newline at end of file diff --git a/python/setup.py b/python/setup.py index f444fa0..2370c15 100644 --- a/python/setup.py +++ b/python/setup.py @@ -1,7 +1,9 @@ +from __future__ import absolute_import import setuptools import json -config = json.loads('./metaparticle_pkg/version.json') +with open('./metaparticle_pkg/version.json', 'r') as json_file: + config = json.load(json_file) setuptools.setup( name='metaparticle_pkg', diff --git a/python/tox.ini b/python/tox.ini deleted file mode 100644 index 8cf2edd..0000000 --- a/python/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -envlist = py2 - py3 - -[testenv] -commands = coverage erase - coverage run --source metaparticle -m py.test - coverage report -deps= -r{toxinidir}/requirements.txt - pytest - coverage - mock - pytest-mock diff --git a/rust/.gitignore b/rust/.gitignore new file mode 100644 index 0000000..fa1050b --- /dev/null +++ b/rust/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +Dockerfile diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..f8697af --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "metaparticle" +version = "0.0.1" + +[[example]] +name = "hello" +path = "examples/hello.rs" + +[[example]] +name = "web" +path = "examples/web.rs" + +[dependencies] \ No newline at end of file diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..80f44d4 --- /dev/null +++ b/rust/README.md @@ -0,0 +1,52 @@ +# Metaparticle for Rust +Metaparticle/Package is a collection of libraries intended to +make building and deploying containers a seamless and idiomatic +experience for developers. + +This is the implementation for Rust. + +## Introduction +Metaparticle/Package simplifies and centralizes the task of +building and deploying a container image. + +Here is a quick example. + +Consider this simple Rust application: +```rust +fn main() { + println!("Hello World!"); +} +``` + +To containerize this application, you need to use the `metaparticle` crate and +the `containerize` wrapper function like this: + +```rust +fn run() { + println!("Hello World!"); +} + +fn main() { + let runtime = metaparticle::Runtime{ + ..Default::default() + }; + let package = metaparticle::Package{ + name: "hello".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} +``` + +When you run this application, instead of printing "Hello world", it first packages itself as a container, and +then (optionally) deploys itself inside that container. + +## Tutorial + +```bash +git clone https://github.com/metaparticle-io/package/ +cd package/rust + +cargo build --example hello +./target/debug/examples/hello +``` \ No newline at end of file diff --git a/rust/examples/hello.rs b/rust/examples/hello.rs new file mode 100644 index 0000000..8d0145b --- /dev/null +++ b/rust/examples/hello.rs @@ -0,0 +1,17 @@ +extern crate metaparticle; + +fn run() { + println!("Hello World!"); +} + +fn main() { + let runtime = metaparticle::Runtime{ + ..Default::default() + }; + let package = metaparticle::Package{ + name: "hello".to_string(), + repository: "brendanburns".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} \ No newline at end of file diff --git a/rust/examples/web.rs b/rust/examples/web.rs new file mode 100644 index 0000000..8a9645a --- /dev/null +++ b/rust/examples/web.rs @@ -0,0 +1,61 @@ +extern crate metaparticle; + +use std::net::{TcpStream, TcpListener}; +use std::io::{Read, Write}; +use std::thread; + + +fn handle_read(mut stream: &TcpStream) { + let mut buf = [0u8 ;4096]; + match stream.read(&mut buf) { + Ok(_) => { + let req_str = String::from_utf8_lossy(&buf); + println!("{}", req_str); + }, + Err(e) => println!("Unable to read stream: {}", e), + } +} + +fn handle_write(mut stream: TcpStream) { + let response = b"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\nHello world\r\n"; + match stream.write(response) { + Ok(_) => println!("Response sent"), + Err(e) => println!("Failed sending response: {}", e), + } +} + +fn handle_client(stream: TcpStream) { + handle_read(&stream); + handle_write(stream); +} + +fn run() { + let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); + println!("Listening for connections on port {}", 8080); + + for stream in listener.incoming() { + match stream { + Ok(stream) => { + thread::spawn(|| { + handle_client(stream) + }); + } + Err(e) => { + println!("Unable to connect: {}", e); + } + } + } +} + +fn main() { + let runtime = metaparticle::Runtime{ + ports: Some(8080), + ..Default::default() + }; + let package = metaparticle::Package{ + name: "web".to_string(), + repository: "brendanburns".to_string(), + ..Default::default() + }; + metaparticle::containerize(run, runtime, package) +} \ No newline at end of file diff --git a/rust/src/builder/docker.rs b/rust/src/builder/docker.rs new file mode 100644 index 0000000..83f6e7e --- /dev/null +++ b/rust/src/builder/docker.rs @@ -0,0 +1,13 @@ +use super::run_docker_process; +use Builder; + +pub struct DockerBuilder{} + +impl Builder for DockerBuilder { + fn build(&self, dir: &str, image: &str) { + run_docker_process(vec!["docker", "build", &*format!("-t{}", image), dir]); + } + fn push(&self, image: &str) { + run_docker_process(vec!["docker", "push", image]); + } +} \ No newline at end of file diff --git a/rust/src/builder/mod.rs b/rust/src/builder/mod.rs new file mode 100644 index 0000000..17e443f --- /dev/null +++ b/rust/src/builder/mod.rs @@ -0,0 +1,8 @@ +pub mod docker; +use super::run_docker_process; + + +pub trait Builder { + fn build(&self, dir: &str, image: &str); + fn push(&self, image: &str); +} \ No newline at end of file diff --git a/rust/src/executor/docker.rs b/rust/src/executor/docker.rs new file mode 100644 index 0000000..31610af --- /dev/null +++ b/rust/src/executor/docker.rs @@ -0,0 +1,31 @@ +use super::run_docker_process; +use super::Runtime; +use Executor; + +pub struct DockerExecutor{} + +impl Executor for DockerExecutor { + fn cancel(&self, name: &str) { + run_docker_process(vec!["docker", "stop", name]); + run_docker_process(vec!["docker", "rm", "-f", name]); + } + + fn logs(&self, name: &str) { + run_docker_process(vec!["docker", "logs", "-f", name]); + } + + fn run(&self, image: &str, name: &str, config: Runtime) { + let mut ports = String::new(); + let mut args = vec!["docker", "run", "-d", "--rm", "--name", name]; + + if let Some(port) = config.ports { + ports.push_str(&format!("-p {port}", port=port)); + args.push(&ports); + } + + args.push(image); + + run_docker_process(args); + } +} + diff --git a/rust/src/executor/mod.rs b/rust/src/executor/mod.rs new file mode 100644 index 0000000..9221027 --- /dev/null +++ b/rust/src/executor/mod.rs @@ -0,0 +1,9 @@ +pub mod docker; +use super::Runtime; +use super::run_docker_process; + +pub trait Executor { + fn cancel(&self, name: &str); + fn logs(&self, name: &str); + fn run(&self, image: &str, name: &str, config: Runtime); +} \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..532752f --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,166 @@ + +mod builder; +mod executor; + +use builder::Builder; +use executor::Executor; + +use std::env; +use std::error::Error; +use std::fs::File; +use std::ffi::OsStr; +use std::io::prelude::*; +use std::path::Path; +use std::process; + +#[derive(Debug)] +pub struct Runtime { + pub replicas: Option, + pub shards: Option, + pub url_shard_pattern: Option, + pub executor: String, + pub ports: Option, + pub public_address: Option, +} + + +impl Default for Runtime { + fn default() -> Runtime { + Runtime { + replicas: None, + shards: None, + url_shard_pattern: Some("".to_string()), + executor: "docker".to_string(), + ports: None, + public_address: Some(false) + } + } +} + +#[derive(Debug)] +pub struct Package { + pub name: String, + pub repository: String, + pub verbose: Option, + pub quiet: Option, + pub builder: String, + pub publish: Option, +} + + +impl Default for Package { + fn default() -> Package { + Package { + name: "".to_string(), + repository: "".to_string(), + verbose: Some(false), + quiet: Some(false), + builder: "docker".to_string(), + publish: Some(false) + } + } +} + +pub fn run_docker_process(args: Vec<&str>) { + let name = args[1].clone(); + let cmd = args.join(" "); + + let mut child = process::Command::new("sh") + .arg("-c") + .arg(cmd) + .spawn() + .expect(&format!("failed to execute 'docker {name}'", name=name)); + + let status = child.wait() + .ok().expect(&format!("couldn't wait for 'docker {name}'", name=name)); + + if !status.success() { + match status.code() { + Some(code) => panic!("'docker {}' failed with code {:?}", name, code), + None => panic!("'docker {}' failed", name) + } + } +} + +fn in_docker_container() -> bool { + let env_var_key = OsStr::new("METAPARTICLE_IN_CONTAINER"); + let env_var = env::var(env_var_key); + if let Ok(value) = env_var { + return value == "true" || value == "1"; + } + + let mut buffer = String::with_capacity(256); // kind of a guess on initial capacity + + if let Ok(mut file) = File::open("/proc/1/cgroup") { + if let Ok(_) = file.read_to_string(&mut buffer) { + return buffer.contains("docker"); + } + } + false +} + +fn executor_from_runtime(executor_name: String) -> Box { + let executor : Box = match executor_name.as_ref() { + "docker" => Box::new(executor::docker::DockerExecutor{}), + _ => panic!("Unsupported executor type {}", executor_name), + }; + return executor; +} + +fn build_from_runtime(builder_name: String) -> Box { + let builder : Box = match builder_name.as_ref() { + "docker" => Box::new(builder::docker::DockerBuilder{}), + _ => panic!("Unsupported builder type {}", builder_name), + }; + builder + +} + +fn write_dockerfile(name: &str, dest: &Path) { + let dockerfile = &format!("FROM ubuntu:16.04 + COPY ./{name} /tmp/{name} + CMD /tmp/{name} + ", name=name); + let file = Path::new("Dockerfile"); + let path = dest.join(&file); + let display = path.display(); + + let mut file = match File::create(&path) { + Err(why) => panic!("couldn't create {}: {}", + display, + why.description()), + Ok(file) => file, + }; + + if let Err(why) = file.write_all(dockerfile.as_bytes()) { + panic!("Could not write dockerfile at {} because {}", display, why.description()); + } +} + +pub fn containerize(f: F, runtime: Runtime, package: Package) where F: Fn() { + if in_docker_container() { + f(); + } else { + + if package.repository.len() == 0 { + panic!("A package must be given a 'repository' value"); + } + if package.name.len() == 0 { + panic!("A package must be given a 'name' value"); + } + let image = &format!("{repo}/{name}:latest", repo=package.repository, name=package.name); + + let arg_0 = env::args().nth(0).unwrap(); + let path = Path::new(&arg_0); + let docker_context = Path::new(path.parent().unwrap()); + + write_dockerfile(&package.name, docker_context); + let builder = build_from_runtime(package.builder.clone()); + + builder.build(docker_context.to_str().unwrap(), image); + + let executor = executor_from_runtime(runtime.executor.clone()); + executor.run(image, &package.name, runtime); + executor.logs(&package.name); + } +} \ No newline at end of file diff --git a/tutorials/java/sharding-tutorial.md b/tutorials/java/sharding-tutorial.md index 6e2534f..37e1f05 100644 --- a/tutorials/java/sharding-tutorial.md +++ b/tutorials/java/sharding-tutorial.md @@ -33,7 +33,7 @@ A diagram of the shard architecture is below Typically a shard deployment would consist of two different application containers (the router and the shard), two different deployment configurations and two services to connect pieces together. That's a lot of YAML and a lot of complexity for an enduser to absorb to implement a fairly straightforward concept. -To show how Metaparticle/Sharding can help dramatically simplify this, here is the corresponding code in Java: +To show how Metaparticle/Sharding can help dramatically simplify this, here is the corresponding code in Java (where `your-docker-user-goes-here` needs to be replaced with a Docker Hub user name): ```java package io.metaparticle.examples.web; @@ -58,9 +58,10 @@ public class Main { shards = 4, urlShardPattern = "^\\/users\\/([^\\/]*)\\/.*", executor="metaparticle") - @Package(repository="brendanburns", + @Package(repository="your-docker-user-goes-here", verbose=true, - jarFile="target/metaparticle-package-0.1-SNAPSHOT-jar-with-dependencies.jar") + publish = true, + jarFile="target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") public static void main(String[] args) { Containerize(() -> { try { diff --git a/tutorials/java/tutorial.md b/tutorials/java/tutorial.md index f191c9c..7f6bd78 100644 --- a/tutorials/java/tutorial.md +++ b/tutorials/java/tutorial.md @@ -118,8 +118,8 @@ import com.sun.net.httpserver.HttpServer; public class Main { private static final int port = 8080; - @Package(repository="docker.io/your-docker-user-goes-here", - jarFile="target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") + @Package(repository = "your-docker-user-goes-here", + jarFile = "target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") public static void main(String[] args) { Containerize(() -> { try { @@ -146,7 +146,7 @@ public class Main { You will notice that we added a `@Package` annotation that describes how to package the application. You will need to replace `your-docker-user-goes-here` -with an actual Docker repository path. +with an actual Docker Hub user name. You will also notice that we wrapped the main function in the `Containerize` function which kicks off the Metaparticle code. @@ -203,8 +203,8 @@ public class Main { private static final int port = 8080; @Runtime(ports={port}) - @Package(repository="docker.io/your-docker-user-goes-here", - jarFile="target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") + @Package(repository = "your-docker-user-goes-here", + jarFile = "target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") public static void main(String[] args) { Containerize(() -> { try { @@ -233,10 +233,10 @@ Now if you run this with `mvn compile exec:java -Dexec.mainClass=io.metaparticle ## Replicating and exposing on the web. As a final step, consider the task of exposing a replicated service on the internet. -To do this, we're going to expand our usage of the `@Runtime` tag. First we will -add a `replicas` field, which will specify the number of replicas. Second we will -set our execution environment to `metaparticle` which will launch the service -into the currently configured Kubernetes environment. +To do this, we're going to expand our usage of the `@Runtime` and `@Package` annotations. +First, `@Package` has its `publish` field added with a value of `true`. This is necessary in order to push the built image to the Docker repository. +Next, we add a `executor` field to the `@Runtime` annotation to set our execution environment to `metaparticle` which will launch the service +into the currently configured Kubernetes environment. Finally, we add a `replicas` field to the `@Runtime` annotation. This specifies the number of replicas to schedule. Here's what the snippet looks like: @@ -244,8 +244,11 @@ Here's what the snippet looks like: ... @Runtime(ports={port}, replicas = 4, - publicAddress = true, - executor="metaparticle") + executor = "metaparticle") + @Package(repository = "your-docker-user-goes-here", + jarFile = "target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar", + publish = true, + verbose = true) ... ``` @@ -270,10 +273,11 @@ public class Main { @Runtime(ports={port}, replicas=4, - publicAddress=true, executor="metaparticle") - @Package(repository="docker.io/your-docker-user-goes-here", - jarFile="target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar") + @Package(repository = "your-docker-user-goes-here", + jarFile = "target/metaparticle-package-tutorial-0.1-SNAPSHOT-jar-with-dependencies.jar", + publish = true, + verbose = true) public static void main(String[] args) { Containerize(() -> { try { @@ -298,8 +302,8 @@ public class Main { } ``` -After you compile and run this, you can see that there are four replicas running behind a -Kubernetes Service Load balancer: +After you compile and run this, you can see that there are four pod replicas running behind a +Kubernetes ClusterIP service: ```sh $ kubectl get pods @@ -308,4 +312,97 @@ $ kubectl get services ... ``` +### Spring Boot Apps + +Containerizing Spring Boot applications is just as straightforward. Say you've created a new Boot project using either +the [Spring Initialzr](https://start.spring.io) site or else your IDE's tooling and you selected the web starter. +Add the Metaparticle package dependency as shown below. Note that the Spring Boot Maven plugin will have also been +automatically added to the build file when it was generated. + +```xml +... + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.metaparticle + metaparticle-package + 0.1-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +... +``` + +The complete code for a simple application that's ready to be replicated and exposed +on the web might look something like below: + +```java +package io.metaparticle.tutorial.bootdemo; + +import io.metaparticle.annotations.Package; +import io.metaparticle.annotations.Runtime; +import javax.servlet.http.HttpServletRequest; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import static io.metaparticle.Metaparticle.Containerize; + +@SpringBootApplication +public class BootDemoApplication { + + @Runtime(ports = {8080}, + replicas = 4, + executor = "metaparticle") + @Package(repository = "your-docker-user-goes-here", + jarFile = "target/boot-demo-0.0.1-SNAPSHOT.jar", + publish = true, + verbose = true) + public static void main(String[] args) { + Containerize(() -> SpringApplication.run(BootDemoApplication.class, args)); + } +} + +@RestController +class HelloController { + + @GetMapping("/") + public String hello(HttpServletRequest request) { + System.out.printf("[%s]%n", request.getRequestURL()); + return String.format("Hello containers [%s] from %s", + request.getRequestURL(), System.getenv("HOSTNAME")); + } +} +``` + +To run the application you could either make use of that Spring Boot Maven plugin mentioned earlier +with the command `mvn clean spring-boot:run`. Alternatively, using `mvn clean package` to create an executable +Spring Boot Jar and then running that with `java -jar` will also work. + Still looking for more? Continue on to the more advanced [sharding tutorial](sharding-tutorial.md) diff --git a/tutorials/python/tutorial.md b/tutorials/python/tutorial.md index 2f0f486..7c6d115 100644 --- a/tutorials/python/tutorial.md +++ b/tutorials/python/tutorial.md @@ -117,7 +117,7 @@ docker ps ## Step Two: Exposing the ports If you try to access the web server on [http://localhost:8080](http://localhost:8080) you will see that you can not actually access the server. Despite it running, the service -is not exposed. To do this, you need to add am annotation to supply the +is not exposed. To do this, you need to add an annotation to supply the port(s) to expose. The code snippet to add is: diff --git a/tutorials/python/web.py b/tutorials/python/web.py index e2808c8..e80429b 100755 --- a/tutorials/python/web.py +++ b/tutorials/python/web.py @@ -1,5 +1,7 @@ #!/usr/bin/python +from __future__ import absolute_import +from __future__ import print_function from six.moves import SimpleHTTPServer, socketserver import socket @@ -14,7 +16,7 @@ def do_GET(self): self.send_header("Content-type", "text/plain") self.end_headers() self.wfile.write("Hello Metparticle [{}] @ {}\n".format(self.path, socket.gethostname()).encode('UTF-8')) - print("request for {}".format(self.path)) + print(("request for {}".format(self.path))) def do_HEAD(self): self.send_response(OK) From 5de23fe1e4bda1843ec57aeeab9dd573b50949e0 Mon Sep 17 00:00:00 2001 From: Srini Vasudevan Date: Thu, 20 Sep 2018 07:53:43 +1000 Subject: [PATCH 6/6] refactor and code cleanups --- dotnet/Metaparticle.Package/DockerBuilder.cs | 10 +-- .../Metaparticle.Package/DockerfileWriter.cs | 53 ++++++++++++++++ .../Metaparticle.Tests/Config.cs | 3 + dotnet/Metaparticle.Package/Metaparticle.cs | 61 ++++-------------- dotnet/Metaparticle.Runtime/Config.cs | 31 +++++----- dotnet/dotnet.sln | 62 +++++++++++++++++++ 6 files changed, 149 insertions(+), 71 deletions(-) create mode 100644 dotnet/Metaparticle.Package/DockerfileWriter.cs create mode 100644 dotnet/dotnet.sln diff --git a/dotnet/Metaparticle.Package/DockerBuilder.cs b/dotnet/Metaparticle.Package/DockerBuilder.cs index fcaea3e..5de2c7c 100644 --- a/dotnet/Metaparticle.Package/DockerBuilder.cs +++ b/dotnet/Metaparticle.Package/DockerBuilder.cs @@ -4,24 +4,20 @@ namespace Metaparticle.Package { public class DockerBuilder : ImageBuilder { - public DockerBuilder() - { - } - public bool Build(string configFile, string imageName, TextWriter o, TextWriter e) { System.Console.WriteLine(configFile); var info = Directory.GetParent(configFile); - System.Console.WriteLine(string.Format("build -t {0} {1}", imageName, info.FullName)); + System.Console.WriteLine($"build -t {imageName} {info.FullName}"); var err = new StringWriter(); - var proc = Exec("docker", string.Format("build -t {0} {1}", imageName, info.FullName), stdout: o, stderr: err); + var proc = Exec("docker", $"build -t {imageName} {info.FullName}", o, err); System.Console.WriteLine(err.ToString()); return proc.ExitCode == 0; } public bool Push(string imageName, TextWriter stdout, TextWriter stderr) { - var proc = Exec("docker", string.Format("push {0}", imageName), stdout: stdout, stderr: stderr); + var proc = Exec("docker", $"push {imageName}", stdout, stderr); return proc.ExitCode == 0; } } diff --git a/dotnet/Metaparticle.Package/DockerfileWriter.cs b/dotnet/Metaparticle.Package/DockerfileWriter.cs new file mode 100644 index 0000000..f55dc42 --- /dev/null +++ b/dotnet/Metaparticle.Package/DockerfileWriter.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using dockerfile; + +namespace Metaparticle.Package +{ + public static class DockerfileWriter + { + /// + /// Creates the dockerfle and returns the name of the file + /// + /// Directory to create the file + /// Executable used to run command/s + /// args for the executable + /// + /// + public static string Write(string dir, string exe, string[] args, Config config) + { + var dockerfilename = dir + "/Dockerfile"; + if (!string.IsNullOrEmpty(config.Dockerfile)) { + File.Copy(config.Dockerfile, dockerfilename); + return dockerfilename; + } + var instructions = new List(); + instructions.Add(new Instruction("FROM", "debian:9")); + instructions.Add(new Instruction("RUN", " apt-get update && apt-get -qq -y install libunwind8 libicu57 libssl1.0 liblttng-ust0 libcurl3 libuuid1 libkrb5-3 zlib1g")); + // TODO: lots of things here are constant, figure out how to cache for perf? + instructions.Add(new Instruction("COPY", "* /exe/")); + instructions.Add(new Instruction("CMD", $"/exe/{exe} {GetArgs(args)}")); + + var df = new Dockerfile(instructions.ToArray(), new Comment[0]); + File.WriteAllText(dockerfilename, df.Contents()); + + return dockerfilename; + } + + private static string GetArgs(string[] args) + { + if (args == null || args.Length == 0) + { + return ""; + } + var b = new StringBuilder(); + foreach (var arg in args) + { + b.Append(arg); + b.Append(" "); + } + return b.ToString().Trim(); + } + } +} \ No newline at end of file diff --git a/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs b/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs index 6db13af..07ab72b 100644 --- a/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs +++ b/dotnet/Metaparticle.Package/Metaparticle.Tests/Config.cs @@ -4,6 +4,9 @@ namespace Metaparticle.Tests { public class Config : Attribute { + /// + /// Directories containing the test projects. This may be absolute or relative to the current project. + /// public string[] Names { get;set; } } } \ No newline at end of file diff --git a/dotnet/Metaparticle.Package/Metaparticle.cs b/dotnet/Metaparticle.Package/Metaparticle.cs index 2a26bfd..2353444 100644 --- a/dotnet/Metaparticle.Package/Metaparticle.cs +++ b/dotnet/Metaparticle.Package/Metaparticle.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; -using dockerfile; using Metaparticle.Tests; using static Metaparticle.Package.Util; using RuntimeConfig = Metaparticle.Runtime.Config; @@ -11,20 +8,20 @@ namespace Metaparticle.Package { - public class Driver + public class Driver { - private Config config; - private RuntimeConfig runtimeConfig; - private TestConfig testConfig; + private readonly Config config; + private readonly RuntimeConfig runtimeConfig; + private readonly TestConfig testConfig; - public Driver(Config config, RuntimeConfig runtimeConfig, TestConfig testConfig) + public Driver(Config config, RuntimeConfig runtimeConfig, TestConfig testConfig) { this.config = config; this.runtimeConfig = runtimeConfig; this.testConfig = testConfig; } - private ImageBuilder getBuilder() + private ImageBuilder getBuilder() { switch (config.Builder.ToLowerInvariant()) { @@ -55,27 +52,12 @@ private ContainerExecutor getExecutor() } } - private static string getArgs(string[] args) - { - if (args == null || args.Length == 0) - { - return ""; - } - var b = new StringBuilder(); - foreach (var arg in args) - { - b.Append(arg); - b.Append(" "); - } - return b.ToString().Trim(); - } - public void Build(string[] args) { var proc = Process.GetCurrentProcess(); var procName = proc.ProcessName; string exe = null; - string dir = null; + string dir; TextWriter o = config.Verbose ? Console.Out : null; TextWriter e = config.Quiet ? Console.Error : null; if (procName == "dotnet") @@ -84,7 +66,6 @@ public void Build(string[] args) dir = "bin/release/netcoreapp2.0/debian.8-x64/publish"; Exec("dotnet", "publish -r debian.8-x64 -c release", stdout: o, stderr: e); - //var dirInfo = new UnixDirectoryInfo(dir); var files = Directory.GetFiles(dir); foreach (var filePath in files) { @@ -101,7 +82,7 @@ public void Build(string[] args) var prog = proc.MainModule.FileName; dir = Directory.GetParent(prog).FullName; } - var dockerfilename = writeDockerfile(dir, exe, args, config); + var dockerfilename = DockerfileWriter.Write(dir, exe, args, config); var builder = getBuilder(); string imgName = (string.IsNullOrEmpty(config.Repository) ? exe : config.Repository); @@ -151,31 +132,11 @@ private void RunTests() { throw new Exception("Tests Failed."); } - } - - private string writeDockerfile(string dir, string exe, string[] args, Config config) - { - var dockerfilename = dir + "/Dockerfile"; - if (!string.IsNullOrEmpty(config.Dockerfile)) { - File.Copy(config.Dockerfile, dockerfilename); - return dockerfilename; - } - var instructions = new List(); - instructions.Add(new Instruction("FROM", "debian:9")); - instructions.Add(new Instruction("RUN", " apt-get update && apt-get -qq -y install libunwind8 libicu57 libssl1.0 liblttng-ust0 libcurl3 libuuid1 libkrb5-3 zlib1g")); - // TODO: lots of things here are constant, figure out how to cache for perf? - instructions.Add(new Instruction("COPY", string.Format("* /exe/", dir))); - instructions.Add(new Instruction("CMD", string.Format("/exe/{0} {1}", exe, getArgs(args)))); - - var df = new Dockerfile(instructions.ToArray(), new Comment[0]); - File.WriteAllText(dockerfilename, df.Contents()); - - return dockerfilename; } - public static bool InDockerContainer() + public static bool InDockerContainer() { - switch (System.Environment.GetEnvironmentVariable("METAPARTICLE_IN_CONTAINER")) + switch (Environment.GetEnvironmentVariable("METAPARTICLE_IN_CONTAINER")) { case "true": case "1": @@ -189,7 +150,7 @@ public static bool InDockerContainer() if (File.Exists(cgroupPath)) { var info = File.ReadAllText(cgroupPath); // This is a little approximate... - return info.IndexOf("docker") != -1 || info.IndexOf("kubepods") != -1; + return info.IndexOf("docker", StringComparison.Ordinal) != -1 || info.IndexOf("kubepods", StringComparison.Ordinal) != -1; } return false; } diff --git a/dotnet/Metaparticle.Runtime/Config.cs b/dotnet/Metaparticle.Runtime/Config.cs index 039ac18..ad2280a 100644 --- a/dotnet/Metaparticle.Runtime/Config.cs +++ b/dotnet/Metaparticle.Runtime/Config.cs @@ -1,22 +1,25 @@ -namespace Metaparticle.Runtime { - public class Config : System.Attribute { - public int Replicas { get; set; } +namespace Metaparticle.Runtime +{ + public class Config : System.Attribute + { + public int Replicas { get; set; } - public int Shards { get; set; } + public int Shards { get; set; } - public int JobCount { get; set; } + public int JobCount { get; set; } - public string ShardExpression { get; set; } + public string ShardExpression { get; set; } - public string Executor { get; set; } + public string Executor { get; set; } - public int[] Ports { get; set; } + public int[] Ports { get; set; } - public bool Public { get; set; } + public bool Public { get; set; } - public Config() { - Executor = "docker"; - Replicas = 1; - } - } + public Config() + { + Executor = "docker"; + Replicas = 1; + } + } } \ No newline at end of file diff --git a/dotnet/dotnet.sln b/dotnet/dotnet.sln new file mode 100644 index 0000000..279d3ba --- /dev/null +++ b/dotnet/dotnet.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Metaparticle.Package", "Metaparticle.Package\Metaparticle.Package.csproj", "{37E6F49B-77B6-49D6-86C1-2EBC17080D8C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Metaparticle.Runtime", "Metaparticle.Runtime\Metaparticle.Runtime.csproj", "{4167A6E1-C7C2-4186-8AA1-88DAB52D753B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Metaparticle.Package.Tests", "Metaparticle.Package.Tests\Metaparticle.Package.Tests.csproj", "{108E0A37-6F7E-49A1-AA81-35A540E91588}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|x64.ActiveCfg = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|x64.Build.0 = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|x86.ActiveCfg = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Debug|x86.Build.0 = Debug|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|Any CPU.Build.0 = Release|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|x64.ActiveCfg = Release|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|x64.Build.0 = Release|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|x86.ActiveCfg = Release|Any CPU + {37E6F49B-77B6-49D6-86C1-2EBC17080D8C}.Release|x86.Build.0 = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|x64.ActiveCfg = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|x64.Build.0 = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|x86.ActiveCfg = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Debug|x86.Build.0 = Debug|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|Any CPU.Build.0 = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|x64.ActiveCfg = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|x64.Build.0 = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|x86.ActiveCfg = Release|Any CPU + {4167A6E1-C7C2-4186-8AA1-88DAB52D753B}.Release|x86.Build.0 = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|Any CPU.Build.0 = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|x64.ActiveCfg = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|x64.Build.0 = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|x86.ActiveCfg = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Debug|x86.Build.0 = Debug|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|Any CPU.ActiveCfg = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|Any CPU.Build.0 = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|x64.ActiveCfg = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|x64.Build.0 = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|x86.ActiveCfg = Release|Any CPU + {108E0A37-6F7E-49A1-AA81-35A540E91588}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal