diff --git a/environments/dotnet/.gitignore b/environments/dotnet/.gitignore new file mode 100644 index 00000000..8a82e2f6 --- /dev/null +++ b/environments/dotnet/.gitignore @@ -0,0 +1,6 @@ +bin/* +obj/* +out/* +.vscode/* +project.lock* +TODO_dotnet \ No newline at end of file diff --git a/environments/dotnet/Dockerfile b/environments/dotnet/Dockerfile new file mode 100644 index 00000000..97582361 --- /dev/null +++ b/environments/dotnet/Dockerfile @@ -0,0 +1,9 @@ +FROM microsoft/dotnet:1.1.0-runtime + +WORKDIR /fission-workdir +COPY out . +EXPOSE 8888 + +ENTRYPOINT ["dotnet"] + +CMD ["fission-dotnet.dll"] \ No newline at end of file diff --git a/environments/dotnet/ExecutorModule.cs b/environments/dotnet/ExecutorModule.cs new file mode 100644 index 00000000..ef1d7fd2 --- /dev/null +++ b/environments/dotnet/ExecutorModule.cs @@ -0,0 +1,82 @@ +using Fission.DotNetCore.Compiler; +using Fission.DotNetCore.Api; +using System.Collections.Generic; +using Nancy; +using System.IO; +using System; + +namespace Fission.DotNetCore +{ + public class ExecutorModule : NancyModule + { +#if DEBUG + private const string CODE_PATH = "/tmp/func.cs"; +#else + private const string CODE_PATH = "/userfunc/user"; +#endif + private static Function _userFunc; + private static Logger _logger = new Logger(); + + public ExecutorModule() + { + Post("/specialize", args => Specialize()); + Get("/", _ => Run()); + Post("/", _ => Run()); + Put("/", _ => Run()); + Head("/", _ => Run()); + Options("/", _ => Run()); + Delete("/", _ => Run()); + } + + private object Specialize() + { + var errors = new List(); + if (File.Exists(CODE_PATH)) + { + var code = File.ReadAllText(CODE_PATH); + _userFunc = FissionCompiler.Compile(code, out errors); + if (_userFunc == null) + { + var errstr = string.Join(Environment.NewLine, errors); + _logger.WriteError(errstr); + var response = (Response)errstr; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + + } + return null; + } + else + { + var errstr = $"Unable to locate code at '{CODE_PATH}'"; + _logger.WriteError(errstr); + var response = (Response)errstr; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + } + + private object Run() + { + if (_userFunc == null) + { + var response = (Response)"Generic container: no requests supported"; + response.StatusCode = HttpStatusCode.InternalServerError; + return response; + } + var args = ((DynamicDictionary)Request.Query).ToDictionary(); + try + { + return _userFunc.Invoke(new FissionContext(args, new Logger())); + } + catch (Exception e) + { + _logger.WriteError(e.ToString()); + var response = (Response)e.Message; + response.StatusCode = HttpStatusCode.BadRequest; + return response; + } + } + } +} + diff --git a/environments/dotnet/FissionCompiler.cs b/environments/dotnet/FissionCompiler.cs new file mode 100644 index 00000000..99f236cd --- /dev/null +++ b/environments/dotnet/FissionCompiler.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + +namespace Fission.DotNetCore.Compiler +{ + //adapted from this article http://www.tugberkugurlu.com/archive/compiling-c-sharp-code-into-memory-and-executing-it-with-roslyn + class FissionCompiler + { + public static Function Compile(string code, out List errors) + { + errors = new List(); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(code); + + string assemblyName = Path.GetRandomFileName(); + + var coreDir = Directory.GetParent(typeof(Enumerable).GetTypeInfo().Assembly.Location); + + List references = new List + { + MetadataReference.CreateFromFile(coreDir.FullName + Path.DirectorySeparatorChar + "mscorlib.dll"), + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(Assembly.GetEntryAssembly().Location) + }; + + foreach (var referencedAssembly in Assembly.GetEntryAssembly().GetReferencedAssemblies()) + { + var assembly = Assembly.Load(referencedAssembly); + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: new[] { syntaxTree }, + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + + using (var ms = new MemoryStream()) + { + EmitResult result = compilation.Emit(ms); + + if (!result.Success) + { + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error).ToList(); + + foreach (Diagnostic diagnostic in failures) + { + errors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}"); + } + } + else + { + ms.Seek(0, SeekOrigin.Begin); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + var type = assembly.GetType("FissionFunction"); + var info = type.GetMember("Execute").First() as MethodInfo; + return new Function(assembly, type, info); + } + } + return null; + } + } +} diff --git a/environments/dotnet/FissionContext.cs b/environments/dotnet/FissionContext.cs new file mode 100644 index 00000000..977444c7 --- /dev/null +++ b/environments/dotnet/FissionContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +namespace Fission.DotNetCore.Api +{ + public class FissionContext + { + public FissionContext(Dictionary args, Logger logger) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + Arguments = args; + Logger = logger; + } + + public Dictionary Arguments { get; private set; } + public Logger Logger { get; private set; } + } + + public class Logger + { + public void Write(Severity severity, string format, params object[] args) + { + Console.WriteLine($"{DateTime.Now.ToString("MM/dd/yy H:mm:ss zzz")} {severity}: " + format, args); + } + + public void WriteInfo(string format, params object[] args){ + Write(Severity.Info, format, args); + } + + public void WriteWarning(string format, params object[] args){ + Write(Severity.Warning, format, args); + } + + public void WriteError(string format, params object[] args){ + Write(Severity.Error, format, args); + } + + public void WriteCritical(string format, params object[] args){ + Write(Severity.Critical, format, args); + } + + public void WriteVerbose(string format, params object[] args){ + Write(Severity.Verbose, format, args); + } + } + + public enum Severity + { + Info, + Warning, + Error, + Critical, + Verbose + } +} \ No newline at end of file diff --git a/environments/dotnet/Function.cs b/environments/dotnet/Function.cs new file mode 100644 index 00000000..f8b5f724 --- /dev/null +++ b/environments/dotnet/Function.cs @@ -0,0 +1,28 @@ +using System; +using System.Reflection; +using Fission.DotNetCore.Api; + +namespace Fission.DotNetCore.Compiler +{ + class Function + { + private readonly Assembly _assembly; + private readonly Type _type; + private readonly MethodInfo _info; + + public Function(Assembly assembly, Type type, MethodInfo info) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + if (assembly == null) throw new ArgumentNullException(nameof(assembly)); + if (type == null) throw new ArgumentNullException(nameof(type)); + _assembly = assembly; + _type = type; + _info = info; + } + + public object Invoke(FissionContext context) + { + return _info.Invoke(_assembly.CreateInstance(_type.FullName), new[] { context }); + } + } +} diff --git a/environments/dotnet/Program.cs b/environments/dotnet/Program.cs new file mode 100755 index 00000000..85dc1628 --- /dev/null +++ b/environments/dotnet/Program.cs @@ -0,0 +1,21 @@ +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Nancy.Owin; + +namespace Fission.DotNetCore +{ + public class Program + { + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseKestrel() + .UseUrls("http://*:8888") + .Configure(app => app.UseOwin(x => x.UseNancy())) + .Build(); + host.Run(); + } + } +} diff --git a/environments/dotnet/README.md b/environments/dotnet/README.md new file mode 100644 index 00000000..9f012558 --- /dev/null +++ b/environments/dotnet/README.md @@ -0,0 +1,198 @@ +# Fission: dotnet C# Environment + +This is a simple dotnet C# environment for Fission. + +It's a Docker image containing the dotnet 1.1.0 runtime. The image +uses Kestrel with Nancy to host the internal web server and uses +Roslyn to compile the uploaded code. + +The image supports compiling and running code with types defined in +mscorlib and does not at present support other library references. +One workaround for this would be to add the references to this project's +project.json file and rebuild the container. + +The environment works via convention where you create a C# class +called FissionFunction which has a method named Execute taking a single +parameter, a FissionContext object. + +The FissionContext object gives access to the arguments and other items +like logging. Please see FissionContext.cs for public API. + +Example of simplest possible class to be executed: + +``` +using System; +using Fission.DotNetCore.Api; + +public class FissionFunction { + public string Execute(FissionContext context) { + return null; + } +} +``` + +Please see examples below. + +## Rebuilding and pushing the image + +To rebuild the image you need either a computer with dotnet 1.1.0 +installed or else you will have to map the source directory into a +container containing the dotnet 1.1.0 environment. + +### Locally installed Dotnet 1.1.0 + +Simply move to the source directory in a terminal and run the ./build.sh script. + +The script will restore dependencies, compile a release build and +and build the container. If you need to change the name of the container +simply change it in the script. + +After the build finishes push the new image to a Docker registry using the +standard procedure. + +### Build in a container + +Move to the directory containing the source and start the Docker container +with dotnet and mount the current directory to a build location: + +``` +docker run -it --rm -v $PWD:/build microsoft/dotnet +``` + +Move to the build directory inside the container and restore the packages: + +``` +cd /build +dotnet restore +log : Restoring packages for /source/project.json... +log : Installing System.Net.WebSockets 4.0.0. +log : Installing runtime.native.System.IO.Compression 4.1.0. +... +``` + +Compile and publish a release build of the source to the 'out' folder: + +``` +dotnet publish -c Release -o out +Publishing source for .NETCoreApp,Version +... +``` +Exit the build container and build the Docker container on the local host: + +``` +exit +docker build -t USER/dotnet-env . +``` +After the build finishes push the new image to a Docker registry using the +standard procedure. + +## Echo example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context){ + context.Logger.WriteInfo("executing.. {0}", context.Arguments["text"]); + return (string)context.Arguments["text"]; + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name echo --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /echo --function echo + +$ curl http://$FISSION_ROUTER/echo?text=hello%20world! + hello world +``` + +## Addition service example + +### Setup fission environment +First you need to setup the fission according to your cluster setup as +specified here: https://github.com/fission/fission + + +### Create the class to run + +Secondly you need to create a file /tmp/func.cs containing the following code: + +``` +using System; +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context){ + var x = Convert.ToInt32(context.Arguments["x"]); + var y = Convert.ToInt32(context.Arguments["y"]); + return (x+y).ToString(); + } +} +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet-env + +$ fission function create --name addition --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /add --function addition + +$ curl "http://$FISSION_ROUTER/add?x=30&y=12" + 42 +``` + +## Developing/debugging the enviroment locally + +The easiest way to debug the environment is to open the directory in +Visual Studio Code (VSCode) as that will setup debugger for you the +first time. + +Remember to install the excellent extension +"C# for Visual Studio Code(powered by OmniSharp)" to get statement completion + +The class ExecutorModule contain preprocessor directive overriding where +the input code file should be found: + +``` +#if DEBUG + private const string CODE_PATH = "/tmp/func.cs"; +#else + private const string CODE_PATH = "/userfunc/user"; +#endif +``` + +So what you need to do is: +1. Open the directory in VSCode. +This will prompt restore of packages and query is debugger setup is needed. Accept both prompts. +2. Press F5 to start the web server. Set breakpoints etc.. +3. Add a code file containing valid C# at /tmp/func.cs +4. Specialize the service with curl via post +``` +$ curl -XPOST http://localhost:8888/specialize +``` +5. Call your function with curl +``` +$ curl -XGET http://localhost:8888 +``` diff --git a/environments/dotnet/build.sh b/environments/dotnet/build.sh new file mode 100755 index 00000000..b6622ac3 --- /dev/null +++ b/environments/dotnet/build.sh @@ -0,0 +1,3 @@ +dotnet restore +dotnet publish -c Release -o out +docker build -t fission/dotnet-env . diff --git a/environments/dotnet/project.json b/environments/dotnet/project.json new file mode 100755 index 00000000..2c1ea729 --- /dev/null +++ b/environments/dotnet/project.json @@ -0,0 +1,24 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "emitEntryPoint": true, + "outputName": "fission-dotnet" + }, + + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.1.0", + "type": "platform" + }, + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Owin": "1.0.0", + "Nancy": "2.0.0-barneyrubble", + "Microsoft.CodeAnalysis.CSharp": "1.3.0-*", + "System.Runtime.Loader": "4.0.0-*", + }, + "frameworks": { + "netcoreapp1.0": { + "imports": "dnxcore50" + } + } +} \ No newline at end of file