diff --git a/environments/dotnet20/Dockerfile b/environments/dotnet20/Dockerfile new file mode 100644 index 00000000..b1205fad --- /dev/null +++ b/environments/dotnet20/Dockerfile @@ -0,0 +1,9 @@ +FROM microsoft/dotnet:2.0-runtime + +WORKDIR /fission-workdir +COPY out . +EXPOSE 8888 + +ENTRYPOINT ["dotnet"] + +CMD ["fission-dotnet20.dll"] \ No newline at end of file diff --git a/environments/dotnet20/ExecutorModule.cs b/environments/dotnet20/ExecutorModule.cs new file mode 100644 index 00000000..311140fb --- /dev/null +++ b/environments/dotnet20/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; + } + + try + { + return _userFunc.Invoke(FissionContext.Build(Request, new Logger())); + } + catch (Exception e) + { + _logger.WriteError(e.ToString()); + var response = (Response)e.Message; + response.StatusCode = HttpStatusCode.BadRequest; + return response; + } + } + } +} + diff --git a/environments/dotnet20/FissionCompiler.cs b/environments/dotnet20/FissionCompiler.cs new file mode 100644 index 00000000..f4dc4ee6 --- /dev/null +++ b/environments/dotnet20/FissionCompiler.cs @@ -0,0 +1,75 @@ +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), + MetadataReference.CreateFromFile(typeof(System.Runtime.Serialization.Json.DataContractJsonSerializer).GetTypeInfo().Assembly.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/dotnet20/FissionContext.cs b/environments/dotnet20/FissionContext.cs new file mode 100644 index 00000000..b4117531 --- /dev/null +++ b/environments/dotnet20/FissionContext.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Nancy; + +namespace Fission.DotNetCore.Api +{ + public class FissionContext + { + public FissionContext(Dictionary args, Logger logger, FissionHttpRequest request) + { + if (args == null) throw new ArgumentNullException(nameof(args)); + if (logger == null) throw new ArgumentNullException(nameof(logger)); + if (request == null) throw new ArgumentNullException(nameof(request)); + Arguments = args; + Logger = logger; + Request = request; + } + + public Dictionary Arguments { get; private set; } + + public FissionHttpRequest Request { get; private set; } + + public Logger Logger { get; private set; } + + public static FissionContext Build(Request request, Logger logger) + { + return new FissionContext(((DynamicDictionary)request.Query).ToDictionary(), + logger, + new FissionHttpRequest(request)); + } + } + + 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 + } + + public class FissionHttpRequest + { + private readonly Request _request; + internal FissionHttpRequest(Request request) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + _request = request; + } + + public Stream Body { get { return _request.Body; } } + + public string BodyAsString() + { + int length = (int)_request.Body.Length; + byte[] data = new byte[length]; + _request.Body.Read(data, 0, length); + return Encoding.UTF8.GetString(data); + } + + public Dictionary> Headers + { + get + { + var headers = new Dictionary>(); + foreach (var kv in _request.Headers) + { + headers.Add(kv.Key, kv.Value); + } + return headers; + } + } + + public X509Certificate Certificate { get { return _request.ClientCertificate; } } + public string Url { get { return _request.Url.ToString(); } } + public string Method { get { return _request.Method; } } + } +} \ No newline at end of file diff --git a/environments/dotnet20/Function.cs b/environments/dotnet20/Function.cs new file mode 100644 index 00000000..f8b5f724 --- /dev/null +++ b/environments/dotnet20/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/dotnet20/Program.cs b/environments/dotnet20/Program.cs new file mode 100644 index 00000000..85dc1628 --- /dev/null +++ b/environments/dotnet20/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/dotnet20/README.md b/environments/dotnet20/README.md new file mode 100644 index 00000000..907c8906 --- /dev/null +++ b/environments/dotnet20/README.md @@ -0,0 +1,306 @@ +# Fission: dotnet 2.0 C# Environment + +This is a simple dotnet core 2.0 C# environment for Fission. + +It's a Docker image containing the dotnet 2.0.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 core 2.0.0 +installed or else you will have to map the source directory into a +container containing the dotnet core 2.0.0 environment. + +### Locally installed Dotnet core 2.0.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/dotnet20-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/dotnet20-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/dotnet20-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 +``` + +## Accessing http request information 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 buffer = new System.Text.StringBuilder(); + foreach(var header in context.Request.Headers){ + buffer.AppendLine(header.Key); + foreach(var item in header.Value){ + buffer.AppendLine($"\t{item}"); + } + } + buffer.AppendLine($"Url: {context.Request.Url}, method: {context.Request.Method}"); + return buffer.ToString(); + } +} + +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet20-env + +$ fission function create --name httpinfo --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /http_info --function httpinfo + +$ curl "http://$FISSION_ROUTER/http_info" +Accept + */*;q=1 +Host + fissionserver:8888 +User-Agent + curl/7.47.0 +Url: http://fissionserver:8888, method: GET + +``` + +## Accessing http request body 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.IO; +using System.Runtime.Serialization.Json; +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context) + { + var person = Person.Deserialize(context.Request.Body); + return $"Hello, my name is {person.Name} and I am {person.Age} years old."; + } +} + +public class Person +{ + public string Name { get; set; } + public int Age { get; set; } + + public static Person Deserialize(Stream json) + { + var serializer = new DataContractJsonSerializer(typeof(Person)); + return (Person)serializer.ReadObject(json); + } +} + +``` +### Run the example + +Lastly to run the example: + +``` +$ fission env create --name dotnet --image fission/dotnet20-env + +$ fission function create --name httpbody --env dotnet --code /tmp/func.cs + +$ fission route create --method GET --url /http_body --function httpbody + +$ curl -XPOST "http://$FISSION_ROUTER/http_body" -d '{ "Name":"Arthur", "Age":42}' +Hello, my name is Arthur and I am 42 years old. + +``` + + +## 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/dotnet20/build.sh b/environments/dotnet20/build.sh new file mode 100644 index 00000000..fb3b9549 --- /dev/null +++ b/environments/dotnet20/build.sh @@ -0,0 +1,3 @@ +dotnet restore +dotnet publish -c Release -o out +docker build -t fission/dotnet20-env . diff --git a/environments/dotnet20/dotnet20.sln b/environments/dotnet20/dotnet20.sln new file mode 100644 index 00000000..df86017f --- /dev/null +++ b/environments/dotnet20/dotnet20.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fission-dotnet20", "dotnet20\fission-dotnet20.csproj", "{3F044DE1-74E5-48F7-8D23-233C24AAA45C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3F044DE1-74E5-48F7-8D23-233C24AAA45C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F044DE1-74E5-48F7-8D23-233C24AAA45C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F044DE1-74E5-48F7-8D23-233C24AAA45C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F044DE1-74E5-48F7-8D23-233C24AAA45C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {272000F7-BBEC-4B61-8865-783EF7E5CEE9} + EndGlobalSection +EndGlobal diff --git a/environments/dotnet20/fission-dotnet20.csproj b/environments/dotnet20/fission-dotnet20.csproj new file mode 100644 index 00000000..065f845d --- /dev/null +++ b/environments/dotnet20/fission-dotnet20.csproj @@ -0,0 +1,17 @@ + + + + Exe + netcoreapp2.0 + + + + + + + + + + + + diff --git a/examples/dotnet20/arguments.cs b/examples/dotnet20/arguments.cs new file mode 100644 index 00000000..a39e0421 --- /dev/null +++ b/examples/dotnet20/arguments.cs @@ -0,0 +1,11 @@ +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(); + } + diff --git a/examples/dotnet20/echo.cs b/examples/dotnet20/echo.cs new file mode 100644 index 00000000..9e399583 --- /dev/null +++ b/examples/dotnet20/echo.cs @@ -0,0 +1,10 @@ +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"]; + } +} \ No newline at end of file diff --git a/examples/dotnet20/helloworld.cs b/examples/dotnet20/helloworld.cs new file mode 100644 index 00000000..cae5a7e9 --- /dev/null +++ b/examples/dotnet20/helloworld.cs @@ -0,0 +1,9 @@ +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context) + { + return "Hello World!"; + } +} diff --git a/examples/dotnet20/requestbody.cs b/examples/dotnet20/requestbody.cs new file mode 100644 index 00000000..c4be5f44 --- /dev/null +++ b/examples/dotnet20/requestbody.cs @@ -0,0 +1,24 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context) + { + var person = Person.Deserialize(context.Request.Body); + return $"Hello, my name is {person.Name} and I am {person.Age} years old."; + } +} + +public class Person +{ + public string Name { get; set; } + public int Age { get; set; } + + public static Person Deserialize(Stream json) + { + var serializer = new DataContractJsonSerializer(typeof(Person)); + return (Person)serializer.ReadObject(json); + } +} diff --git a/examples/dotnet20/requestheaders.cs b/examples/dotnet20/requestheaders.cs new file mode 100644 index 00000000..8e44f2f7 --- /dev/null +++ b/examples/dotnet20/requestheaders.cs @@ -0,0 +1,17 @@ +using System; +using Fission.DotNetCore.Api; + +public class FissionFunction +{ + public string Execute(FissionContext context){ + var buffer = new System.Text.StringBuilder(); + foreach(var header in context.Request.Headers){ + buffer.AppendLine(header.Key); + foreach(var item in header.Value){ + buffer.AppendLine($"\t{item}"); + } + } + buffer.AppendLine($"Url: {context.Request.Url}, method: {context.Request.Method}"); + return buffer.ToString(); + } +}