diff --git a/dotnet60/Common/BuilderSettings.cs b/dotnet60/Common/BuilderSettings.cs new file mode 100644 index 00000000..69589c42 --- /dev/null +++ b/dotnet60/Common/BuilderSettings.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Fission.Common +{ + public readonly record struct BuilderSettings( + string NugetSpecsFile, + string DllExcludeFile, + string BuildLogDirectory, + string NugetPackageRegEx, + string ExcludeDllRegEx, + bool RunningOnWindows, + string functionBodyFileName, + string functionSpecFileName, + string DllDirectory + ); +} diff --git a/dotnet60/Common/Common.csproj b/dotnet60/Common/Common.csproj new file mode 100644 index 00000000..30313518 --- /dev/null +++ b/dotnet60/Common/Common.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + true + + + + + + + diff --git a/dotnet60/Common/CompilerHelper.cs b/dotnet60/Common/CompilerHelper.cs new file mode 100644 index 00000000..1856c06c --- /dev/null +++ b/dotnet60/Common/CompilerHelper.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Text.Json; +using System.Linq; + +namespace Fission.Common +{ + + public sealed class CompilerHelper + { + + private static readonly Lazy lazy = + new Lazy(() => new CompilerHelper()); + + public string _logFileName = string.Empty; + public static CompilerHelper Instance { get { return lazy.Value; } } + + private BuilderSettings? _builderSettings = null; + + public BuilderSettings builderSettings + { + get + { + if (_builderSettings == null) + { + string builderSettingsjson = GetBuilderSettingsJson(); + _builderSettings = JsonSerializer.Deserialize(builderSettingsjson); + } + + return _builderSettings.Value; + } + set + { + builderSettings = value; + } + } + + static CompilerHelper() + { + } + + private CompilerHelper() + { + } + + public static string GetRelevantPathAsPerOS(string currentPath) + { + if (CompilerHelper.Instance.builderSettings.RunningOnWindows) + { + return currentPath; + } + else + { + return currentPath.Replace("\\","/"); + } + } + + private string GetBuilderSettingsJson() + { + var path = AppDomain.CurrentDomain.BaseDirectory + "builderSettings.json"; + return System.IO.File.ReadAllText(path); + } + + public FunctionSpecification GetFunctionSpecs(string directoryPath) + { + string functionSpecsFilePath = Path.Combine(directoryPath, this.builderSettings.functionSpecFileName); + if (!File.Exists(functionSpecsFilePath)) + { + string specsJson = File.ReadAllText(functionSpecsFilePath); + return JsonSerializer.Deserialize(specsJson); + } + + throw new Exception($"Function Specification file not found at {functionSpecsFilePath}"); + } + + public static IEnumerable GetDirectoryFiles(string rootPath, string patternMatch, SearchOption searchOption) + { + var foundFiles = Enumerable.Empty(); + + if (searchOption == SearchOption.AllDirectories) + { + try + { + IEnumerable subDirs = Directory.EnumerateDirectories(rootPath); + foreach (string dir in subDirs) + { + foundFiles = foundFiles.Concat(GetDirectoryFiles(dir, patternMatch, searchOption)); // Add files in subdirectories recursively to the list + } + } + catch (UnauthorizedAccessException) {} + catch (PathTooLongException) {} + } + + try + { + foundFiles = foundFiles.Concat(Directory.EnumerateFiles(rootPath, patternMatch)); // Add files from the current directory + } + catch (UnauthorizedAccessException) {} + + return foundFiles; + } + + static public IEnumerable GetCSharpSources(string path) + { + return GetDirectoryFiles(path, "*.cs", SearchOption.AllDirectories); + } + } + +} diff --git a/dotnet60/Common/DllInfo.cs b/dotnet60/Common/DllInfo.cs new file mode 100644 index 00000000..4756c6c8 --- /dev/null +++ b/dotnet60/Common/DllInfo.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Fission.Common +{ + public readonly record struct DllInfo(string name, string rootPackage, string framework, string processor, string path); +} diff --git a/dotnet60/Common/FunctionSpecification.cs b/dotnet60/Common/FunctionSpecification.cs new file mode 100644 index 00000000..d48848ca --- /dev/null +++ b/dotnet60/Common/FunctionSpecification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Fission.Common +{ + public readonly record struct FunctionSpecification( + string functionName, + List libraries, + string hash, + string certificatePath + ); + + public readonly record struct Library( + string name, + string path, + string nugetPackage + ); +} diff --git a/dotnet60/Dockerfile b/dotnet60/Dockerfile new file mode 100644 index 00000000..b27d5efb --- /dev/null +++ b/dotnet60/Dockerfile @@ -0,0 +1,18 @@ +# Prep work for all images. +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 8888 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS restore +WORKDIR /src +COPY . /src +RUN dotnet restore "./fission-dotnet6.sln" + +# Specific work for environment image. +FROM restore as build-env +RUN dotnet publish "./fission-dotnet6/fission-dotnet6.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=build-env /app/publish . +ENTRYPOINT ["dotnet", "fission-dotnet6.dll"] diff --git a/dotnet60/Dockerfile.builder b/dotnet60/Dockerfile.builder new file mode 100644 index 00000000..a91e2c73 --- /dev/null +++ b/dotnet60/Dockerfile.builder @@ -0,0 +1,39 @@ +ARG BUILDER_IMAGE=fission/builder +FROM ${BUILDER_IMAGE} AS fission-builder + + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS builderimage + + +WORKDIR /app + +# Copy csproj and restore as distinct layers +COPY Common ./../Common +COPY Fission.Functions ./../Fission.Functions +COPY builder/*.csproj ./ +RUN dotnet restore + +# Copy everything else and build +COPY builder ./ +RUN dotnet publish -c Release -o out + + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app +COPY --from=builderimage /app/out . + +# this builder is actually compilation from : https://github.com/fission/fission/tree/master/builder/cmd and renamed cmd.exe to builder +# make sure to compile it in linux only else you will get exec execute error as binary was compiled in windows and running on linux + +COPY --from=fission-builder /builder /builder + +#ADD builder /builder + +ADD builder/build.sh /usr/local/bin/build +RUN chmod +x /usr/local/bin/build + +ADD builder/build.sh /bin/build +RUN chmod +x /bin/build + +EXPOSE 8001 \ No newline at end of file diff --git a/dotnet60/Fission.Functions/Fission.Functions.csproj b/dotnet60/Fission.Functions/Fission.Functions.csproj new file mode 100644 index 00000000..30313518 --- /dev/null +++ b/dotnet60/Fission.Functions/Fission.Functions.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + true + + + + + + + diff --git a/dotnet60/Fission.Functions/FissionContext.cs b/dotnet60/Fission.Functions/FissionContext.cs new file mode 100644 index 00000000..72a8887a --- /dev/null +++ b/dotnet60/Fission.Functions/FissionContext.cs @@ -0,0 +1,66 @@ +#region header + +// Fission.Functions - FissionContext.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/28 11:30 PM. + +#endregion + +#region using + +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +using JetBrains.Annotations; + +#endregion + +namespace Fission.Functions +{ + /// + /// The context supplied to Fission functions by the .NET 5 environment, including arguments, HTTP request information, + /// logging facilities, and (for built functions) settings. + /// + [PublicAPI] + public record FissionContext + { + /// + /// The arguments specified to the Fission function, derived from the HTTP request's query string. + /// + public IReadOnlyDictionary Arguments; + + /// + /// Functions permitting the Fission function to write to the container log. + /// + public FissionLogger Logger; + + /// + /// The path to the location in which the Fission function package is loaded into the container. + /// + /// + /// This is primarily used internally to fetch the location for JSON settings files. + /// + public string PackagePath; + + /// + /// Details of the HTTP request which generated the function call. + /// + public FissionRequest Request; + + /// + /// Read a JSON settings file supplied in the function package, and deserialize it into a corresponding .NET object. + /// + /// Type of the .NET object corresponding to the settings file. + /// Path, beneath , of the JSON settings file. + /// A .NET object corresponding to the JSON settings file. + [CanBeNull] + public T GetSettings ([NotNull] string relativePath) + { + string filePath = Path.Combine (path1: this.PackagePath, path2: relativePath); + string json = File.ReadAllText (path: filePath); + + return JsonSerializer.Deserialize (json: json); + } + } +} diff --git a/dotnet60/Fission.Functions/FissionLogger.cs b/dotnet60/Fission.Functions/FissionLogger.cs new file mode 100644 index 00000000..452ae10e --- /dev/null +++ b/dotnet60/Fission.Functions/FissionLogger.cs @@ -0,0 +1,51 @@ +#region header + +// Fission.Functions - FissionLogger.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/30 8:08 AM. + +#endregion + +#region using + +using JetBrains.Annotations; + +#endregion + +namespace Fission.Functions +{ + /// + /// A delegate defining a function capable of performing Fission function logging; i.e., a possible FissionLogger + /// function. + /// + /// The message to log, in the form of a format string for the following arguments, if any. + /// Any arguments to log (parameters for the format string). + public delegate void FissionWriteLog (string format, params object[] args); + + /// + /// Logging functions for the receiving Fission function. + /// + [PublicAPI] + public record FissionLogger + { + /// + /// Log a message reporting a critical error. + /// + public FissionWriteLog WriteCritical; + + /// + /// Log a message reporting an error. + /// + public FissionWriteLog WriteError; + + /// + /// Log an informational message. + /// + public FissionWriteLog WriteInfo; + + /// + /// Log a warning message. + /// + public FissionWriteLog WriteWarning; + } +} diff --git a/dotnet60/Fission.Functions/FissionRequest.cs b/dotnet60/Fission.Functions/FissionRequest.cs new file mode 100644 index 00000000..5bde9ac7 --- /dev/null +++ b/dotnet60/Fission.Functions/FissionRequest.cs @@ -0,0 +1,73 @@ +#region header + +// Fission.Functions - FissionRequest.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/30 8:08 AM. + +#endregion + +#region using + +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +using JetBrains.Annotations; + +#endregion + +namespace Fission.Functions +{ + /// + /// Underlying request details supplied to the handling function. + /// + [PublicAPI] + public record FissionRequest + { + /// + /// The body of the request, supplied as a readable . See also + /// to receive the entire body of the request as a single + /// . + /// + public Stream Body; + + /// + /// The client certificate supplied with the request. + /// + [CanBeNull] + public X509Certificate2 ClientCertificate; + + /// + /// The HTTP headers supplied with the request. + /// + public IReadOnlyDictionary> Headers; + + /// + /// The HTTP method used to make the request (i.e., "GET", "POST", etc.). + /// + public string Method; + + /// + /// The URL used to make the request, formatted as a string. + /// + /// + /// The URL is urlencoded, and includes all elements, including (for example) the query string. + /// + public string Url; + + /// + /// Get the body of the request as a single . + /// + /// The body of the request as a single . + [NotNull] + public string GetBodyAsString () + { + var length = (int) this.Body.Length; + var data = new byte[length]; + this.Body.Read (buffer: data, offset: 0, count: length); + + return Encoding.UTF8.GetString (bytes: data); + } + } +} diff --git a/dotnet60/Fission.Functions/IFissionFunction.cs b/dotnet60/Fission.Functions/IFissionFunction.cs new file mode 100644 index 00000000..3318d768 --- /dev/null +++ b/dotnet60/Fission.Functions/IFissionFunction.cs @@ -0,0 +1,35 @@ +#region header + +// Fission.Functions - IFissionFunction.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/28 11:29 PM. + +#endregion + +#region using + +using JetBrains.Annotations; + +#endregion + +namespace Fission.Functions +{ + /// + /// The interface which is implemented by Fission functions, permitting the environment container to identify possible + /// entry + /// points. + /// + [PublicAPI] + public interface IFissionFunction + { + /// + /// The entry point of a Fission function. + /// + /// The function call context supplied by the environment container. + /// + /// An object which will be appropriately formatted by the environment container and returned to the + /// caller. + /// + public object Execute (FissionContext context); + } +} diff --git a/dotnet60/LICENSE b/dotnet60/LICENSE new file mode 100644 index 00000000..66a27ec5 --- /dev/null +++ b/dotnet60/LICENSE @@ -0,0 +1,177 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/dotnet60/README.md b/dotnet60/README.md new file mode 100644 index 00000000..e4e3f54a --- /dev/null +++ b/dotnet60/README.md @@ -0,0 +1,53 @@ +# fission-dotnet6 +A .NET 6 function environment for [Fission](https://fission.io/). + +The environment Docker image (_fission-dotnet6_) contains the .NET 6.0 runtime and uses an ASP.NET Core web api application to make the relevant endpoints available to Fission. This image supports compiling and running single-file functions using the types available in the core .NET 6 assemblies. To use multi-file projects and download packages with NuGet, use builder image in it's directory. + +The environment works via the IFissionFunction interface (provided by the _Fission.Functions_ assembly.) A function for _fission-dotnet6_ is presented as a class which implements the _IFissionFunction_ interface, thus: + +``` +using System; +using Fission.Functions; + +public class HelloWorld : IFissionFunction +{ + public object Execute(FissionContext context) + { + return "hello, world!"; + } +} + +``` + +Logging and access to function call parameters are accessible through the _context_ parameter. Please see the inline documentation for _FissionContext.cs_ and the examples for further details. + +## Rebuilding the image + +To rebuild the containers, in the top-level directory, execute the following: + +``` +docker build . --platform=linux/amd64,linux/arm64,linux/arm -t repository/fission-dotnet6:dev --push +``` + +## Setting up the Fission environment + +To set up the .NET 6 Fission environment, execute the command: + +``` +fission env create --name dotnet6 --image repository/fission-dotnet6:dev --version 1 +``` + +## Configuring and testing a function + +If the above function is contained within the file `HelloWorld.cs` (see the examples directory), and the environment has been set up as above, then it can be installed as follows: + +``` +fission fn create --name hello-dotnet --env dotnet6 --code HelloWorld.cs +``` + +And tested thus: + +``` +fission fn test --name hello-dotnet +``` + diff --git a/dotnet60/builder/Builder.cs b/dotnet60/builder/Builder.cs new file mode 100644 index 00000000..e60b8f35 --- /dev/null +++ b/dotnet60/builder/Builder.cs @@ -0,0 +1,51 @@ +using Builder.Utility; +using NugetWorker; +using System; +using System.IO; +using Builder.Engine; +using Fission.Common; + +namespace Builder +{ + class Builder + { + static void Main(string[] args) + { + Console.WriteLine("Starting builder task"); + var logFileName = $"{DateTime.Now.ToString("yyyy_MM_dd")}_{Guid.NewGuid().ToString()}.log"; + + try + { + string _logdirectory = CompilerHelper.Instance.builderSettings.BuildLogDirectory; + BuilderHelper.Instance._logFileName = Path.Combine(_logdirectory, logFileName); + BuilderHelper.Instance.logger = new Utility.Logger( + BuilderHelper.Instance._logFileName); + + NugetHelper.Instance.logger = new NugetWorker.Logger(BuilderHelper.Instance._logFileName); + + Console.WriteLine($"detailed logs for this build will be at: {Path.Combine(_logdirectory, logFileName)}"); + + + BuilderEngine builderEngine = new BuilderEngine(); + builderEngine.BuildPackage().Wait(); + } + catch (Exception ex) + { + string detailedException = string.Empty; + try + { + detailedException= BuilderHelper.Instance.DeepException(ex); + Console.WriteLine($"Exception during build: {Environment.NewLine} {ex.Message} | {ex.StackTrace} | {Environment.NewLine} {detailedException}"); + } + catch(Exception childEx) + { + Console.WriteLine($"{Environment.NewLine} Exception during build:{ex.Message} |{Environment.NewLine} {ex.StackTrace} {Environment.NewLine} "); + } + + throw; + } + + Console.WriteLine("Builder task done"); + } + } +} diff --git a/dotnet60/builder/Builder.csproj b/dotnet60/builder/Builder.csproj new file mode 100644 index 00000000..0f02d80d --- /dev/null +++ b/dotnet60/builder/Builder.csproj @@ -0,0 +1,47 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + System + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + + diff --git a/dotnet60/builder/Builder.sln b/dotnet60/builder/Builder.sln new file mode 100644 index 00000000..14f4184d --- /dev/null +++ b/dotnet60/builder/Builder.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27906.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Builder", "Builder.csproj", "{D479FF57-44A0-483D-B5B0-B6C475D12292}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D479FF57-44A0-483D-B5B0-B6C475D12292}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D479FF57-44A0-483D-B5B0-B6C475D12292}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D479FF57-44A0-483D-B5B0-B6C475D12292}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D479FF57-44A0-483D-B5B0-B6C475D12292}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AA20D0B8-E5D5-4BA1-9E29-FC5132A4DD00} + EndGlobalSection +EndGlobal diff --git a/dotnet60/builder/BuilderEngine/BuilderEngine.cs b/dotnet60/builder/BuilderEngine/BuilderEngine.cs new file mode 100644 index 00000000..7bfcd576 --- /dev/null +++ b/dotnet60/builder/BuilderEngine/BuilderEngine.cs @@ -0,0 +1,247 @@ +using Builder.Model; +using Builder.Utility; +using NugetWorker; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Linq; +using NuGet.Packaging; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Reflection; +using Microsoft.CodeAnalysis.Emit; +using System.Runtime.Loader; +using System.Text.Json; +using Fission.Functions; +using Fission.Common; + +namespace Builder.Engine +{ + public class BuilderEngine + { + public string SRC_PKG = string.Empty; + + List dllInfos = new List(); + List excludeDlls = new List(); + List includeNugets = new List(); + List compileErrors = new List(); + List compileInfo = new List(); + + public BuilderEngine() + { + SRC_PKG = Environment.GetEnvironmentVariable("SRC_PKG"); + } + + + public async Task BuildPackage() + { + await BuildDllInfo(); + + Console.WriteLine("Compiling"); + bool compiled = await TryCompile(); + if (compiled) + { + CopyToSourceDir(); + await BuildSpecs(); + Console.WriteLine("Compilation and building function specification done!"); + } + else + { + Console.WriteLine("Compilation failed:"); + foreach(var error in compileErrors) + { + Console.WriteLine($"COMPILATION ERROR: {error}"); + } + throw new Exception($"COMPILATION FAILED! See builder logs for details, total errors: {compileErrors.Count}"); + + } + + } + + public void CopyToSourceDir() + { + // create folder if it doesn't already exist + string destinationFile = Path.Combine(SRC_PKG, CompilerHelper.Instance.builderSettings.DllDirectory, "dummy.txt"); + new FileInfo(destinationFile).Directory.Create(); + + foreach (var dllinfo in dllInfos) + { + string filename = Path.GetFileName(dllinfo.path); + destinationFile = Path.Combine(SRC_PKG, CompilerHelper.Instance.builderSettings.DllDirectory, filename); + File.Copy(dllinfo.path, destinationFile,true); + } + } + + public async Task TryCompile() + { + string codeFile = Path.Combine(SRC_PKG, CompilerHelper.Instance.builderSettings.functionBodyFileName); + if (!File.Exists(codeFile)) + { + Console.WriteLine($"Source Code not found at : {codeFile} !" + + $" to use TryCompile() in Builder, make sure, your main function file name is " + + $"{CompilerHelper.Instance.builderSettings.functionBodyFileName} and " + + $"it is located at root of zip!" ); + return false; + } + return await Compile(); + } + + public async Task Compile() + { + var syntaxTrees = new List(); + var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); + + foreach (var codeInDirectory in CompilerHelper.GetCSharpSources(SRC_PKG)) + { + syntaxTrees.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(codeInDirectory), options)); + } + 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(coreDir.FullName + Path.DirectorySeparatorChar + "netstandard.dll"), + MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(Assembly.GetEntryAssembly().Location), + MetadataReference.CreateFromFile(typeof(System.Runtime.Serialization.Json.DataContractJsonSerializer).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(path: typeof(IFissionFunction).GetTypeInfo().Assembly.Location), + }; + + foreach (var referencedAssembly in Assembly.GetEntryAssembly().GetReferencedAssemblies()) + { + var assembly = Assembly.Load(referencedAssembly); + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + BuilderHelper.Instance.logger.Log($"Referring assembly-based dlls: {assembly.Location}"); + } + + + AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + foreach (var dll in dllInfos) + { + BuilderHelper.Instance.logger.Log($"Referring nuget-based dll: {dll.path}"); + references.Add(MetadataReference.CreateFromFile(dll.path)); + } + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: syntaxTrees.ToArray(), + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + + 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) + { + compileErrors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}"); + BuilderHelper.Instance.logger.Log($"COMPILE ERROR :{diagnostic.Id}: {diagnostic.GetMessage()}"); + } + return false; + } + + BuilderHelper.Instance.logger.Log("Compile success!",true); + + return true; + } + + public async Task BuildSpecs() + { + var functionName = CompilerHelper.Instance.builderSettings.functionBodyFileName; + var libraries = new List(); + + foreach (var dllinfo in dllInfos) + { + string combinedPath = Path.Combine(CompilerHelper.Instance.builderSettings.DllDirectory, Path.GetFileName(dllinfo.path)); + string destinationFile = CompilerHelper.GetRelevantPathAsPerOS(combinedPath); + + var library = new Library() + { + name = dllinfo.name, + nugetPackage = dllinfo.rootPackage, + path = destinationFile + }; + libraries.Add(library); + } + var functionSpecification = new FunctionSpecification(functionName, libraries, "", ""); + string funcMetaJson = JsonSerializer.Serialize(functionSpecification); + string funcMetaFile = Path.Combine(this.SRC_PKG, CompilerHelper.Instance.builderSettings.functionSpecFileName); + BuilderHelper.Instance.WriteToFile(funcMetaFile, funcMetaJson); + } + + public async Task BuildDllInfo() + { + includeNugets = BuilderHelper.Instance.GetNugetToInclude(SRC_PKG); + + foreach (var nuget in includeNugets) + { + NugetEngine nugetEngine = new NugetEngine(); + await nugetEngine.GetPackage(nuget.packageName, nuget.version); + dllInfos.AddRange(nugetEngine.dllInfos); + } + + dllInfos = System.Linq.Enumerable.DistinctBy(dllInfos, x => x.path).ToList(); + +#if DEBUG + dllInfos.LogDllPathstoCSV("preFilter.CSV"); +#endif + + excludeDlls = BuilderHelper.Instance.GetDllsToExclude(SRC_PKG); + foreach (var excludedll in excludeDlls) + { + BuilderHelper.Instance.logger.Log($"Trying to remove excluded dll, if available: {excludedll.dllName} from package {excludedll.packageName}"); + dllInfos.RemoveAll(x => x.rootPackage.ToLower() == excludedll.packageName.ToLower() && x.name.ToLower() == excludedll.dllName.ToLower()); + } + +#if DEBUG + dllInfos.LogDllPathstoCSV("PostFilter.CSV"); +#endif + + } + + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + // This handler is called only when the common language runtime tries to bind to the assembly and fails. + + BuilderHelper.Instance.logger.Log($"Dynamically trying to load dll {(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()} in parent assembly"); + + // Retrieve the list of referenced assemblies in an array of AssemblyName. + Assembly MyAssembly = null, objExecutingAssemblies; + string assemblyPath = ""; + + objExecutingAssemblies = Assembly.GetExecutingAssembly(); + AssemblyName[] referencedAssemblyNames = objExecutingAssemblies.GetReferencedAssemblies(); + + // Loop through the array of referenced assembly names. + if (dllInfos.Any(x => x.name.ToLower() == (args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower())) + { + assemblyPath = dllInfos.Where(x => x.name.ToLower() == (args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()).FirstOrDefault().path; + + BuilderHelper.Instance.logger.Log($"loading dll in parent assembly: {assemblyPath}"); + + MyAssembly = Assembly.LoadFile(assemblyPath); + } + + if (MyAssembly == null) + { + BuilderHelper.Instance.logger.Log($"WARNING! Unable to locate dll: {(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()} ", true); + } + + return MyAssembly; + } + + } +} diff --git a/dotnet60/builder/Makefile b/dotnet60/builder/Makefile new file mode 100644 index 00000000..19e33f18 --- /dev/null +++ b/dotnet60/builder/Makefile @@ -0,0 +1,8 @@ +PLATFORMS ?= linux/amd64 + +-include ../../rules.mk + +.PHONY: all +all: dotnet6-builder-img + +dotnet20-builder-img: Dockerfile \ No newline at end of file diff --git a/dotnet60/builder/Model/ExcludeDll.cs b/dotnet60/builder/Model/ExcludeDll.cs new file mode 100644 index 00000000..75765e85 --- /dev/null +++ b/dotnet60/builder/Model/ExcludeDll.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Builder.Model +{ + public readonly record struct ExcludeDll( + string dllName, string packageName + ); +} diff --git a/dotnet60/builder/Model/FissionContext.cs b/dotnet60/builder/Model/FissionContext.cs new file mode 100644 index 00000000..256fb0aa --- /dev/null +++ b/dotnet60/builder/Model/FissionContext.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using Nancy; +using Newtonsoft.Json; + + +namespace Fission.DotNet.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)); + } + + //this are currently dummy , not being implemented, just to pass compilation + //actual execution is written in environment to use the app settings as there we need it + public T GetSettings(string relativePath) + { + throw new NotImplementedException(); + } + + //this are currently dummy , not being implemented, just to pass compilation + //actual execution is written in environment to use the app settings as there we need it + private string GetSettingsJson(string relativePath) + { + throw new NotImplementedException(); + } + + } + + 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/dotnet60/builder/Model/IncludeNuget.cs b/dotnet60/builder/Model/IncludeNuget.cs new file mode 100644 index 00000000..8e890b0b --- /dev/null +++ b/dotnet60/builder/Model/IncludeNuget.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Builder.Model +{ + public readonly record struct IncludeNuget( + string packageName, string version + ); +} diff --git a/dotnet60/builder/README.md b/dotnet60/builder/README.md new file mode 100644 index 00000000..bc4aa715 --- /dev/null +++ b/dotnet60/builder/README.md @@ -0,0 +1,85 @@ +# Fission: .NET 6.0 C# Environment Builder + +This is a .NET 6.0 C# environment builder for Fission. It supports building multi-file projects with NuGet dependencies. + +During build, it also does a pre-compilation to prevent any compilation issues during function environment pod specialization. +Thus we get the function compilation issues during builder phase in package info's build logs itself. + +Once the build is finished, the output package (deploy archive) will be uploaded to storagesvc to store. +Then, during the specialization, the fetcher inside function pod will fetch the package from storagesvc for function loading and will call on the **/v2/specialize** endpoint of fission environment with required parameters. + +There further environment will compile it and execute the function. + +## Examples + +See `/examples/builder-example` for a complete example. + +Example of simplest possible class to be executed: + +The source package structure in zip file : + +``` + Source Package zip : + --soruce.zip + |--func.cs + |--nuget.txt + |--exclude.txt + |--....MiscFiles(optional) + |--....MiscFiles(optional) +``` + +**func.cs** --> This contains original function body with Executing method name as : Execute + + +**nuget.txt**--> this file contains list of nuget packages required by your function , in this file put one line per nuget with nugetpackage name:version(optional) format, for example : + +``` +RestSharp +CsvHelper +Newtonsoft.json:10.2.1.0 +``` + + **exclude.txt**--> list of dlls that will be excluded from compilation + +``` +Newtonsoft.json:Newtonsoft.json.dll +``` + +## Usage + +``` +fission env list +fission fn list + ``` + Create Environment with builder, supposing that the builder image name is `fission/dotnet6-builder` and hosted on dockerhub as `fission/dotnet6-builder` + ``` +fission environment create --name dotnetwithnuget --image fission/dotnet6 --builder fission/dotnet6-builder + ``` + Verify fission-builder and fission-function namespace for new pods (pods name beginning with env name which we have given like *dotnetwithnuget-xxx-xxx*) + ``` +kubectl get pods -n fission-builder +kubectl get pods -n fission-function + ``` +Create a package from source zip using this environment name + ``` +fission package create --src funccsv.zip --env dotnetwithnuget + ``` +Check package status + ``` +fission package info --name funccsv-zip-xyz +``` + +#Status of package should be *failed / running / succeeded* . +Wait if the status is running, until it fails or succeeded. For detailed build logs, you can shell into builder pod in fission-builder namespace and verify log location mentioned in above command's result output. + +Now If the result is succeeded , then go ahead and create function using this package. + +*--entrypoint* flag is optional if your function body file name is func.cs (which it should be as builder need that), else put the filename (without extension) + ``` + fission fn create --name dotnetcsvtest --pkg funccsv-zip-xyz --env dotnetcorewithnuget --entrypoint "func" + ``` +Test the function execution: +``` + fission fn test --name dotnetcsvtest +``` +Above would execute the function and will output the enum value as written in dll. \ No newline at end of file diff --git a/dotnet60/builder/Utility/BuilderHelper.cs b/dotnet60/builder/Utility/BuilderHelper.cs new file mode 100644 index 00000000..3883bf39 --- /dev/null +++ b/dotnet60/builder/Utility/BuilderHelper.cs @@ -0,0 +1,154 @@ +using Builder.Model; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Linq; +using System.Text.Json; + +using Fission.Common; + +namespace Builder.Utility +{ + public sealed class BuilderHelper + { + private static readonly Lazy lazy = + new Lazy(() => new BuilderHelper()); + + public string _logFileName = string.Empty; + public static BuilderHelper Instance { get { return lazy.Value; } } + + private static Logger _logger = new Logger("Initial.log"); + + public Logger logger + { + get + { + return _logger; + } + set + { + _logger = value; + } + } + + static BuilderHelper() + { + } + + private BuilderHelper() + { + } + + public List GetNugetToInclude(string directoryPath) + { + List includeNugets = new List(); + string includeNugetsFilePath = Path.Combine(directoryPath, CompilerHelper.Instance.builderSettings.NugetSpecsFile); + if (File.Exists(includeNugetsFilePath)) + { + Regex _pkgName = new Regex(CompilerHelper.Instance.builderSettings.NugetPackageRegEx, + RegexOptions.Compiled); + + string filetext = File.ReadAllText(includeNugetsFilePath); + var _pkgMatchCollection = _pkgName.Matches(filetext); + + foreach (Match match in _pkgMatchCollection) + { + if(!string.IsNullOrWhiteSpace(match.Value)) + { + string package = match.Groups["package"]?.Value?.Trim(); + string version = match.Groups["version"]?.Value?.Trim(); + this.logger.Log($"adding {package} | {version} to includeNugets collection"); + + includeNugets.Add(new IncludeNuget() + { + packageName = package, + version = version + } + ); + } + + } + } + + return includeNugets; + } + + public List GetDllsToExclude(string directoryPath) + { + List excludeDlls = new List(); + string excludeDllsFilePath = Path.Combine(directoryPath, CompilerHelper.Instance.builderSettings.DllExcludeFile); + if (File.Exists(excludeDllsFilePath)) + { + // xyzPackage:abc.dll + Regex _exclude = new Regex(CompilerHelper.Instance.builderSettings.ExcludeDllRegEx, + RegexOptions.Compiled); + + string filetext = File.ReadAllText(excludeDllsFilePath); + var _excludeMatchCollection = _exclude.Matches(filetext); + + foreach (Match match in _excludeMatchCollection) + { + if (!string.IsNullOrWhiteSpace(match.Value)) + { + string _package = match.Groups["package"]?.Value?.Trim(); + string _dllName = match.Groups["dll"]?.Value?.Trim(); + this.logger.Log($"adding {_package} | {_dllName} to excludeDlls collection"); + + excludeDlls.Add( + new ExcludeDll() + { + packageName = _package, + dllName = _dllName + } + ); + } + } + } + + return excludeDlls; + } + + public string DeepException(Exception ex) + { + string response = string.Empty; + + response = " Exception : LEVEL 1: " + Environment.NewLine + ex.Message; + if (ex.InnerException != null) + { + response = response + Environment.NewLine + "LEVEL 2:" + Environment.NewLine + ex.InnerException.Message; + if (ex.InnerException.InnerException != null) + { + response = response + Environment.NewLine + "LEVEL 3:" + Environment.NewLine + ex.InnerException.InnerException.Message; + + if (ex.InnerException.InnerException.InnerException != null) + { + response = response + Environment.NewLine + "LEVEL 4:" + Environment.NewLine + ex.InnerException.InnerException.InnerException.Message; + if (ex.InnerException.InnerException.InnerException.InnerException != null) + { + response = response + Environment.NewLine + "LEVEL 5:" + Environment.NewLine + ex.InnerException.InnerException.InnerException.InnerException.Message; + } + } + } + } + + if(ex.StackTrace != null) + { + response = response + "|| STACK :"+ ex.StackTrace; + } + + return response; + } + + public void WriteToFile(string filenameWithPath , string content) + { + using (StreamWriter sw = new StreamWriter(filenameWithPath, false)) + { + sw.AutoFlush = true; + sw.Write(content); + } + + } + } +} diff --git a/dotnet60/builder/Utility/Logger.cs b/dotnet60/builder/Utility/Logger.cs new file mode 100644 index 00000000..32c24cbf --- /dev/null +++ b/dotnet60/builder/Utility/Logger.cs @@ -0,0 +1,109 @@ +using log4net; +using NuGet.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace Builder.Utility +{ + public class Logger : NuGet.Common.ILogger + { + private ILog _ILog { get; set; } + public Logger(string logpath) + { + XmlDocument ConfigLoader = new XmlDocument(); + ConfigLoader.Load(File.OpenRead("log4net.config")); + var repo = LogManager.CreateRepository(Assembly.GetEntryAssembly(), + typeof(log4net.Repository.Hierarchy.Hierarchy)); + log4net.Config.XmlConfigurator.Configure(repo, ConfigLoader["log4net"]); + + var appender = ((log4net.Appender.FileAppender)repo.GetAppenders().Where(x => x.Name == "RollingLogFileAppender").FirstOrDefault()); + appender.File = logpath; + + appender.ActivateOptions(); + _ILog = LogManager.GetLogger(typeof(Logger)); + } + + public void Log(string message,bool logToConsoleAsWell=false) + { + if(logToConsoleAsWell) + Console.WriteLine(message); + + _ILog.Info(message); + } + + public void Log(LogLevel level, string data) + { + //Console.WriteLine(data); + _ILog.Info(data); + } + + public void Log(ILogMessage message) + { + //Console.WriteLine(message); + _ILog.Info(message.Message); + } + + public Task LogAsync(LogLevel level, string data) + { + //Console.WriteLine(data); + _ILog.Info(data); + return null; + } + + public Task LogAsync(ILogMessage message) + { + //Console.WriteLine(message); + _ILog.Info(message.Message); + return null; + } + + public void LogDebug(string data) + { + //Console.WriteLine(data); + _ILog.Debug(data); + + } + + public void LogError(string data) + { + //Console.WriteLine(data); + _ILog.Error(data); + } + + public void LogInformation(string data) + { + //Console.WriteLine(data); + _ILog.Info(data); + } + + public void LogInformationSummary(string data) + { + //Console.WriteLine(data); + _ILog.Info(data); + } + + public void LogMinimal(string data) + { + //Console.WriteLine(data); + _ILog.Info(data); + } + + public void LogVerbose(string data) + { + //Console.WriteLine(data); + _ILog.Debug(data); + } + + public void LogWarning(string data) + { + //Console.WriteLine(data); + _ILog.Warn(data); + } + } +} diff --git a/dotnet60/builder/build.sh b/dotnet60/builder/build.sh new file mode 100644 index 00000000..a9f1dda3 --- /dev/null +++ b/dotnet60/builder/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euxo pipefail +cd ${SRC_PKG} +#now start execution of custom logic dll in such a way that it should copy everything in ${SRC_PKG} +#first lets try putting sample file which will prove that this worked +echo src : ${SRC_PKG} ,dest: ${DEPLOY_PKG} > builderpaths.txt + +#now run actual dll for custom builder logic +# please note as this need to be executed from app folder so that all dependent files are avilable +# else you will end up getting File not found error +cd /app + +#now execute dll +dotnet Builder.dll + +#copy entire content to deployment package +cp -r ${SRC_PKG} ${DEPLOY_PKG} \ No newline at end of file diff --git a/dotnet60/builder/builderSettings.json b/dotnet60/builder/builderSettings.json new file mode 100644 index 00000000..a05a46ea --- /dev/null +++ b/dotnet60/builder/builderSettings.json @@ -0,0 +1,11 @@ +{ + "NugetSpecsFile": "nuget.txt", + "DllExcludeFile": "exclude.txt", + "BuildLogDirectory": "logs", + "DllDirectory": "Dlls", + "NugetPackageRegEx": "\\s*(?[^:\\n]*)(?:\\:)?(?.*)?", + "ExcludeDllRegEx": "\\:?\\s*(?[^:\\n]*)(?:\\:)?(?.*)?", + "RunningOnWindows": false, + "functionBodyFileName": "func.cs", + "functionSpecFileName": "func.meta.json" +} \ No newline at end of file diff --git a/dotnet60/builder/log4net.config b/dotnet60/builder/log4net.config new file mode 100644 index 00000000..7ae56e49 --- /dev/null +++ b/dotnet60/builder/log4net.config @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet60/builder/nugetSettings.json b/dotnet60/builder/nugetSettings.json new file mode 100644 index 00000000..1303b350 --- /dev/null +++ b/dotnet60/builder/nugetSettings.json @@ -0,0 +1,37 @@ +{ + "NugetFolder": "Nugetdownload", + "DisableCache": false, + "CSVDirectory": "logs", + "RunningOnWindows": false, + "NugetRepositories": [ + { + "Order": 1, + "IsPrivate": false, + "Name": "local", + "Source": "Nugetdownload", //in case of local , relative path to folder + "IsPasswordClearText": false, + "Username": "", + "Password": "" + }, + { + "Order": 2, + "IsPrivate": false, + "Name": "NugetV3", + "Source": "https://api.nuget.org/v3/index.json", + "IsPasswordClearText": false, + "Username": "", + "Password": "" + } + //, + //{ + // "Order": 3, + // "IsPrivate": true, + // "Name": "myPrivateRepository", //in case of private repo , if not using, then just remove this section No 3 + // "Source": "https://URL-FOR-myPrivateRepository/api/nuget/nugetcore/COMPLETE-END-POINT", + // "IsPasswordClearText": true, + // "Username": "", + // "Password": "" + //} + + ] +} \ No newline at end of file diff --git a/dotnet60/examples/builder-example/MyClass.cs b/dotnet60/examples/builder-example/MyClass.cs new file mode 100644 index 00000000..69b039da --- /dev/null +++ b/dotnet60/examples/builder-example/MyClass.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +public readonly record struct MyStruct(string myField); + +public sealed class MyClass +{ + private static readonly Lazy lazy = + new Lazy(() => new MyClass()); + + public static MyClass Instance { get { return lazy.Value; } } + + private double _myValue = 0; + + public double myValue + { + get + { + if (_myValue == 0) + { + _myValue = MathNet.Numerics.SpecialFunctions.Erf(0.5); + } + return _myValue; + } + set + { + myValue = value; + } + } + + static MyClass() + { + } + + private MyClass() + { + } +} diff --git a/dotnet60/examples/builder-example/exclude.txt b/dotnet60/examples/builder-example/exclude.txt new file mode 100644 index 00000000..e69de29b diff --git a/dotnet60/examples/builder-example/func.cs b/dotnet60/examples/builder-example/func.cs new file mode 100644 index 00000000..743a67e5 --- /dev/null +++ b/dotnet60/examples/builder-example/func.cs @@ -0,0 +1,22 @@ +using System; +using Fission.Functions; + +public class HelloWorld : IFissionFunction +{ + public object Execute(FissionContext context) + { + string response = "initial value"; + try + { + context.Logger.WriteInfo("Starting..."); + response = MyClass.Instance.myValue.ToString(); + } + catch(Exception ex) + { + context.Logger.WriteError(ex.Message); + response = ex.Message; + } + context.Logger.WriteInfo("Done"); + return response; + } +} \ No newline at end of file diff --git a/dotnet60/examples/builder-example/nuget.txt b/dotnet60/examples/builder-example/nuget.txt new file mode 100644 index 00000000..3d2871e4 --- /dev/null +++ b/dotnet60/examples/builder-example/nuget.txt @@ -0,0 +1 @@ +MathNet.Numerics diff --git a/dotnet60/examples/hello-world/HelloWorld.cs b/dotnet60/examples/hello-world/HelloWorld.cs new file mode 100644 index 00000000..625da081 --- /dev/null +++ b/dotnet60/examples/hello-world/HelloWorld.cs @@ -0,0 +1,10 @@ +using System; +using Fission.Functions; + +public class HelloWorld : IFissionFunction +{ + public object Execute(FissionContext context) + { + return "hello, world!"; + } +} diff --git a/dotnet60/fission-dotnet6.sln b/dotnet60/fission-dotnet6.sln new file mode 100644 index 00000000..46f8b815 --- /dev/null +++ b/dotnet60/fission-dotnet6.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fission-dotnet6", "fission-dotnet6\fission-dotnet6.csproj", "{7FDAA994-D65D-4525-8375-EE77CF150AB8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fission.Functions", "Fission.Functions\Fission.Functions.csproj", "{A04A5119-DEF9-467E-9ABC-5030F2EF38CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A91D1E8C-4FB8-4A18-90D4-BC94440CE5DC}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + Dockerfile = Dockerfile + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7FDAA994-D65D-4525-8375-EE77CF150AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FDAA994-D65D-4525-8375-EE77CF150AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FDAA994-D65D-4525-8375-EE77CF150AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FDAA994-D65D-4525-8375-EE77CF150AB8}.Release|Any CPU.Build.0 = Release|Any CPU + {A04A5119-DEF9-467E-9ABC-5030F2EF38CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A04A5119-DEF9-467E-9ABC-5030F2EF38CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A04A5119-DEF9-467E-9ABC-5030F2EF38CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A04A5119-DEF9-467E-9ABC-5030F2EF38CF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EB21B789-DB52-41EC-87D2-B6618931797C} + EndGlobalSection +EndGlobal diff --git a/dotnet60/fission-dotnet6/BuilderRequest.cs b/dotnet60/fission-dotnet6/BuilderRequest.cs new file mode 100644 index 00000000..1b575c87 --- /dev/null +++ b/dotnet60/fission-dotnet6/BuilderRequest.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Fission.DotNet +{ + public readonly record struct FunctionMetadata( + string name, + string @namespace, + string selfLink, + string uid, + string resourceVersion, + int generation, + DateTime creationTimestamp + ); + + public readonly record struct BuilderRequest( + string filepath, + string functionName, + string url, + FunctionMetadata FunctionMetadata + ); +} diff --git a/dotnet60/fission-dotnet6/Controllers/FunctionController.cs b/dotnet60/fission-dotnet6/Controllers/FunctionController.cs new file mode 100644 index 00000000..d5a946a1 --- /dev/null +++ b/dotnet60/fission-dotnet6/Controllers/FunctionController.cs @@ -0,0 +1,187 @@ +#region header + +// fission-dotnet6 - FunctionController.cs +// +// Created by: Alistair J R Young(avatar) at 2020/12/28 11:19 PM. + +#endregion + +#region using + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; + +using Fission.DotNet.Properties; +using Fission.Functions; + +using JetBrains.Annotations; + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +#endregion + +namespace Fission.DotNet.Controllers +{ + /// + /// Controller handling calls to the Fission function in this environment container. + /// + [ApiController] + [Route(template: "/")] + public class FunctionController : ControllerBase + { + private readonly ILogger funcLogger; + private readonly ILogger logger; + private readonly IFunctionStore store; + + /// + /// Creates an instance of this . + /// + /// A logger for the . + /// A logger for the function invoked by the . + /// The function storage service(see ). + /// + /// The second logger exists to permit ready differentiation of function-internal errors and other messages from those + /// originating with the host environment. + /// + public FunctionController(ILogger logger, ILogger funcLogger, IFunctionStore store) + { + this.logger = logger; + this.funcLogger = funcLogger; + this.store = store; + } + + /// + /// Handle HTTP GET requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpGet] + [NotNull] + public object Get() => this.Run(); + + /// + /// Handle HTTP POST requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpPost] + [NotNull] + public object Post() => this.Run(); + + /// + /// Handle HTTP PUT requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpPut] + [NotNull] + public object Put() => this.Run(); + + /// + /// Handle HTTP HEAD requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpHead] + [NotNull] + public object Head() => this.Run(); + + /// + /// Handle HTTP OPTIONS requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpOptions] + [NotNull] + public object Options() => this.Run(); + + /// + /// Handle HTTP DELETE requests by forwarding to the Fission function. + /// + /// The function return value. + [HttpDelete] + [NotNull] + public object Delete() => this.Run(); + + /// + /// Invokes the Fission function on behalf of the caller. + /// + /// + /// 200 OK with the Fission function return value; or 400 Bad Request with the exception message if an exception + /// occurred in the Fission function; or 500 Internal Server Error if the environment container has not yet been + /// specialized. + /// + [NotNull] + private object Run() + { + this.logger.LogInformation(message: "Invoking function"); + + if(this.store.Func == null) + { + this.logger.LogError(message: Resources.FunctionController_Run_GenericContainer); + + return this.StatusCode(statusCode:(int) HttpStatusCode.InternalServerError, + value: Resources.FunctionController_Run_GenericContainer); + } + + try + { + FissionContext context = this.BuildContext(); + this.logger.LogInformation(message: "Context built"); + return this.Ok(value: this.store.Func.Invoke(context: context)); + } + catch (Exception e) + { + this.logger.LogError(message: e.Message); + return this.StatusCode(statusCode:(int) HttpStatusCode.BadRequest, value: e.Message); + } + } + + /// + /// Build the context for the Fission function, assembling data from the call request and the function logger. + /// + /// The path to the function package. + /// A to be passed to the Fission function. + private FissionContext BuildContext(string packagePath = "") + { + var fl = new FissionLogger + { + WriteInfo =(format, objects) => this.funcLogger.LogInformation(message: format, args: objects), + WriteWarning =(format, objects) => this.funcLogger.LogWarning(message: format, args: objects), + WriteError =(format, objects) => this.funcLogger.LogError(message: format, args: objects), + WriteCritical =(format, objects) => this.funcLogger.LogCritical(message: format, args: objects), + }; + + IQueryCollection arguments = this.Request.Query; + + var args = new Dictionary(); + + foreach(var k in arguments.Keys) args[key: k] = arguments[key: k]; + + var headers = new Dictionary>(); + + foreach(KeyValuePair kv in this.Request.Headers) + headers.Add(key: kv.Key, value: kv.Value); + + var fr = new FissionRequest + { + Body = this.Request.Body, + ClientCertificate = this.Request.HttpContext.Connection.ClientCertificate, + Headers = new ReadOnlyDictionary>(dictionary: headers), + Method = this.Request.Method, + Url = this.Request.GetEncodedUrl(), + }; + + var fc = new FissionContext + { + Logger = fl, + Arguments = new ReadOnlyDictionary(dictionary: args), + Request = fr, + PackagePath = packagePath, + }; + + return fc; + } + } +} diff --git a/dotnet60/fission-dotnet6/Controllers/HealthController.cs b/dotnet60/fission-dotnet6/Controllers/HealthController.cs new file mode 100644 index 00000000..282db7e7 --- /dev/null +++ b/dotnet60/fission-dotnet6/Controllers/HealthController.cs @@ -0,0 +1,34 @@ +#region header + +// fission-dotnet6 - HealthController.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/29 12:10 AM. + +#endregion + +#region using + +using JetBrains.Annotations; + +using Microsoft.AspNetCore.Mvc; + +#endregion + +namespace Fission.DotNet.Controllers +{ + /// + /// Controller to handle the Docker/Kubernetes container health-check. + /// + [Route (template: "/healthz")] + [ApiController] + public class HealthController : ControllerBase + { + /// + /// When this endpoint receives a GET request, simply return 200 OK to demonstrate that the container is alive. + /// + /// 200 OK + [HttpGet] + [NotNull] + public object Get () => this.Ok (); + } +} diff --git a/dotnet60/fission-dotnet6/Controllers/SpecializeController.cs b/dotnet60/fission-dotnet6/Controllers/SpecializeController.cs new file mode 100644 index 00000000..7ec08c39 --- /dev/null +++ b/dotnet60/fission-dotnet6/Controllers/SpecializeController.cs @@ -0,0 +1,100 @@ +#region header + +// fission-dotnet6 - SpecializeController.cs +// +// Created by: Alistair J R Young(avatar) at 2020/12/29 12:10 AM. +// Modified by: Vsevolod Kvachev (Rasie1) at 2022. + +#endregion + +#region using + +using System; +using System.Collections.Generic; +using System.Net; + +using Fission.DotNet.Properties; + +using JetBrains.Annotations; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +#endregion + +namespace Fission.DotNet.Controllers +{ + /// + /// Controller to handle specializing the container to handle a particular Fission function. + /// + /// + /// Essentially, this handles fetching, compiling, and caching the function for later use by the + /// . + /// + [Route(template: "/specialize")] + [ApiController] + public class SpecializeController : ControllerBase + { + private readonly ILogger logger; + private readonly IFunctionStore store; + + /// + /// Creates an instance of the . + /// + /// A logger instance for the . + /// The function store service. See . + public SpecializeController(ILogger logger, IFunctionStore store) + { + this.logger = logger; + this.store = store; + } + + /// + /// The path to the function code to compile. In Debug builds, this invokes a built-in test function to simplify + /// debugging without a container. In Release builds, this uses the mount path of the function package. + /// + [NotNull] + private static string CodePath +#if DEBUG + => "tmp/TestFunc.cs"; +#else + => "/userfunc/user"; +#endif + + /// + /// Handle version 1 requests to specialize the container; i.e., to compile and cache a single-file function. + /// + /// 200 OK on success; 500 Internal Server Error on failure. + [HttpPost] + [NotNull] + public object Post() + { + this.logger.LogInformation(message: "/specialize called."); + + if (System.IO.File.Exists(path: SpecializeController.CodePath)) + { + string source = System.IO.File.ReadAllText(path: SpecializeController.CodePath); + + var compiler = new FissionCompiler(); + FunctionRef? binary = compiler.Compile(source: source, errors: out List errors); + + if (binary == null) + { + string? error = string.Join(separator: Environment.NewLine, values: errors); + this.logger.LogError(message: error); + return this.StatusCode(statusCode:(int) HttpStatusCode.InternalServerError, value: error); + } + + this.store.SetFunctionRef(func: binary); + } + else + { + var error = $"Unable to locate function source code at '{SpecializeController.CodePath}'."; + this.logger.LogError(message: error); + return this.StatusCode(statusCode:(int) HttpStatusCode.InternalServerError, value: error); + } + + return this.Ok(); + } + } +} diff --git a/dotnet60/fission-dotnet6/Controllers/V2SpecializeController.cs b/dotnet60/fission-dotnet6/Controllers/V2SpecializeController.cs new file mode 100644 index 00000000..48775e06 --- /dev/null +++ b/dotnet60/fission-dotnet6/Controllers/V2SpecializeController.cs @@ -0,0 +1,120 @@ +#region header + +// fission-dotnet6 - V2SpecializeController.cs + +#endregion + +#region using + +using System; +using System.Collections.Generic; +using System.Net; +using System.IO; + +using Fission.DotNet.Properties; +using Fission.Common; + +using JetBrains.Annotations; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +#endregion + +namespace Fission.DotNet.Controllers +{ + /// + /// Controller to handle specializing the container to handle a particular Fission function. + /// + /// + /// Essentially, this handles fetching, compiling, and caching the function for later use by the + /// . + /// + [Route (template: "/v2/specialize")] + [ApiController] + public class V2SpecializeController : ControllerBase + { + private readonly ILogger logger; + private readonly IFunctionStore store; + + public string GetBodyAsString () + { + var length = (int) Request.ContentLength; + var data = new byte[length]; + Request.Body.Read(buffer: data, offset: 0, count: length); + + return System.Text.Encoding.UTF8.GetString(bytes: data); + } + /// + /// Creates an instance of the . + /// + /// A logger instance for the . + /// The function store service. See . + public V2SpecializeController (ILogger logger, IFunctionStore store) + { + this.logger = logger; + this.store = store; + } + + /// + /// Handle version 2 requests to specialize the container; i.e., to compile and cache a multi-file function. + /// + /// 200 OK on success; 500 Internal Server Error on failure. + [HttpPost] + [NotNull] + public object Post () + { + this.logger.LogInformation (message: "/v2/specialize called."); + var errors = new List(); + var oinfo = new List(); + var body = GetBodyAsString(); + Console.WriteLine($"Request received by endpoint from builder: {body}"); + var builderRequest = System.Text.Json.JsonSerializer.Deserialize(body); + + string functionPath = string.Empty; + + store.SetPackagePath(builderRequest.filepath); + + // following will enable us to skip --entrypoint flag during function creation + if (!string.IsNullOrWhiteSpace(builderRequest.functionName)) + { + functionPath = System.IO.Path.Combine(builderRequest.filepath, $"{builderRequest.functionName}.cs"); + } + else + { + functionPath = System.IO.Path.Combine(builderRequest.filepath, CompilerHelper.Instance.builderSettings.functionBodyFileName); + } + + Console.WriteLine($"Going to read function body from path: {functionPath}"); + + if (!System.IO.File.Exists(functionPath)) + { + var error = $"Unable to locate function source code at '{functionPath}'."; + this.logger.LogError(message: error); + return this.StatusCode(statusCode:(int) HttpStatusCode.InternalServerError, value: error); + } + var code = System.IO.File.ReadAllText(functionPath); + FunctionRef? binary = null; + try + { + FissionCompiler fissionCompiler = new FissionCompiler(); + binary = fissionCompiler.CompileV2(builderRequest.filepath, out errors, out oinfo); + } + catch (Exception ex) + { + Console.WriteLine($"Error getting function: {ex.Message}, Trace: {ex.StackTrace}"); + } + if (binary == null) + { + string? error = string.Join(separator: Environment.NewLine, values: errors); + this.logger.LogError(message: error); + return this.StatusCode(statusCode:(int) HttpStatusCode.InternalServerError, value: error); + } + else + { + store.SetFunctionRef(binary); + } + return this.Ok(); + } + } +} diff --git a/dotnet60/fission-dotnet6/FissionCompiler.cs b/dotnet60/fission-dotnet6/FissionCompiler.cs new file mode 100644 index 00000000..e5ee3671 --- /dev/null +++ b/dotnet60/fission-dotnet6/FissionCompiler.cs @@ -0,0 +1,233 @@ + +// fission-dotnet6 - FissionCompiler.cs +// +// Created by: Alistair J R Young(avatar) at 2020/12/29 9:08 AM. +// Modified by: Vsevolod Kvachev (Rasie1) at 2022. + + + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Runtime.Serialization.Json; + +using Fission.DotNet.Properties; +using Fission.Functions; +using Fission.Common; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; + + +namespace Fission.DotNet +{ + /// + /// The compiler which builds Fission functions from source text, when a builder container is not used. + /// + internal class FissionCompiler + { + /// + /// Compile C# source text(implementing ) to an assembly stored in memory. + /// + /// The source code to compile. + /// On exit, a list of compilation errors. + /// A referencing the compiled Fission function. + + // ReSharper disable once MemberCanBeMadeStatic.Global + [SuppressMessage(category: "Performance", + checkId: "CA1822:Mark members as static", + Justification = "Instance members are expected in later iterations. -- AJRY 2020/12/30")] + internal FunctionRef? Compile(string source, out List errors) + { + errors = new List(); + + var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, options); + + var coreDir = Directory.GetParent(path: typeof(Enumerable).GetTypeInfo().Assembly.Location); + + var references = new List + { + MetadataReference.CreateFromFile(path: $"{coreDir!.FullName}{Path.DirectorySeparatorChar}mscorlib.dll"), + MetadataReference.CreateFromFile(path: typeof(object).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(path: typeof(IFissionFunction).GetTypeInfo().Assembly.Location), + MetadataReference.CreateFromFile(path: Assembly.GetEntryAssembly()!.Location), + MetadataReference.CreateFromFile(path: typeof(DataContractJsonSerializer).GetTypeInfo().Assembly.Location), + }; + + foreach (var referencedAssembly in Assembly.GetEntryAssembly()!.GetReferencedAssemblies()) + { + var loaded = Assembly.Load(assemblyRef: referencedAssembly); + references.Add(item: MetadataReference.CreateFromFile(path: loaded.Location)); + } + + string assemblyName = Path.GetRandomFileName(); + CSharpCompilation compilation = CSharpCompilation.Create(assemblyName: assemblyName, + syntaxTrees: new[] {syntaxTree,}, + references: references, + options: new CSharpCompilationOptions( + outputKind: OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + + using var ms = new MemoryStream(); + + EmitResult result = compilation.Emit(peStream: ms); + + if (!result.Success) + { + Console.WriteLine($"Compile failed, see pod logs for more details"); + IEnumerable failures = result.Diagnostics + .Where(predicate: diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == + DiagnosticSeverity.Error) + .ToList(); + + foreach (Diagnostic diagnostic in failures) { + errors.Add(item: $"{diagnostic.Id}: {diagnostic.GetMessage()}"); + Console.WriteLine($"COMPILE ERROR :{diagnostic.Id}: {diagnostic.GetMessage()}", "ERROR"); + } + return null; + } + + ms.Seek(offset: 0, loc: SeekOrigin.Begin); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(assembly: ms); + + Type? type = assembly.GetTypes() + .FirstOrDefault(predicate: t => typeof(IFissionFunction).IsAssignableFrom(c: t)); + + if (type == null) + { + errors.Add(item: Resources.FissionCompiler_Compile_NoEntrypoint); + return null; + } + + return new FunctionRef(assembly: assembly, type: type); + } + + public FunctionRef? CompileV2(string packagePath, out List errors, out List outputInfo) + { + errors = new List(); + outputInfo = new List(); + var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest); + + var syntaxTrees = new List(); + foreach (var codeInDirectory in CompilerHelper.GetCSharpSources(packagePath)) + { + syntaxTrees.Add(CSharpSyntaxTree.ParseText(File.ReadAllText(codeInDirectory), options)); + } + + 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(coreDir.FullName + Path.DirectorySeparatorChar + "netstandard.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 loaded = Assembly.Load(referencedAssembly); + references.Add(MetadataReference.CreateFromFile(loaded.Location)); + } + + functionSpec = CompilerHelper.Instance.GetFunctionSpecs(packagePath); + + foreach (var library in functionSpec.libraries) + { + string dllCompletePath = CompilerHelper.GetRelevantPathAsPerOS(Path.Combine(packagePath, library.path)); + references.Add(MetadataReference.CreateFromFile(dllCompletePath)); + } + + AppDomain currentDomain = AppDomain.CurrentDomain; + this.packagePath = packagePath; + currentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName, + syntaxTrees: syntaxTrees, + references: references, + options: new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release)); + + var ms = new MemoryStream(); + EmitResult result = compilation.Emit(ms); + + if (!result.Success) + { + Console.WriteLine($"Compile failed, see pod logs for more details"); + IEnumerable failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error).ToList(); + + foreach (Diagnostic diagnostic in failures) + { + errors.Add($"{diagnostic.Id}: {diagnostic.GetMessage()}"); + Console.WriteLine($"COMPILE ERROR :{diagnostic.Id}: {diagnostic.GetMessage()}", "ERROR"); + } + + return null; + } + + Console.WriteLine($"COMPILE SUCCESS!"); + + ms.Seek(0, SeekOrigin.Begin); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms); + + Type? type = assembly.GetTypes() + .FirstOrDefault(predicate: t => typeof(IFissionFunction).IsAssignableFrom(c: t)); + if (type == null) + { + errors.Add(item: Resources.FissionCompiler_Compile_NoEntrypoint); + return null; + } + + return new FunctionRef(assembly, type); + } + + private string? packagePath; + FunctionSpecification functionSpec; + + private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + // This handler is called only when the common language runtime tries to bind to the assembly and fails. + + Console.WriteLine($"Dynamically trying to load dll {(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()} in parent assembly"); + + Assembly myAssembly = null, objExecutingAssemblies; + string assemblyPathRelative = "", assemblyPathAbsolute = ""; + + objExecutingAssemblies = Assembly.GetExecutingAssembly(); + AssemblyName[] referencedAssemblyNames = objExecutingAssemblies.GetReferencedAssemblies(); + + // load all available dlls from deployment folder in dllinfo object + if (functionSpec.libraries.Any(x => x.name.ToLower() ==(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower())) + { + assemblyPathRelative = functionSpec.libraries.Where(x => x.name.ToLower() ==(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()).FirstOrDefault().path; + assemblyPathAbsolute = Path.Combine(packagePath, assemblyPathRelative); + Console.WriteLine($"loading dll in parent assembly: {CompilerHelper.GetRelevantPathAsPerOS(assemblyPathAbsolute)}"); + myAssembly = Assembly.LoadFile(CompilerHelper.GetRelevantPathAsPerOS(assemblyPathAbsolute)); + Console.WriteLine($"Load success for: {CompilerHelper.GetRelevantPathAsPerOS(assemblyPathAbsolute)}"); + } + + if (myAssembly == null) + { + Console.WriteLine($"WARNING! Unable to locate dll: {(args.Name.Substring(0, args.Name.IndexOf(",")).ToString() + ".dll").ToLower()} ", "WARNING"); + } + return myAssembly; + } + } +} diff --git a/dotnet60/fission-dotnet6/FunctionRef.cs b/dotnet60/fission-dotnet6/FunctionRef.cs new file mode 100644 index 00000000..558d79b6 --- /dev/null +++ b/dotnet60/fission-dotnet6/FunctionRef.cs @@ -0,0 +1,48 @@ +#region header + +// fission-dotnet6 - FunctionRef.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/29 9:45 AM. +// Modified by: Vsevolod Kvachev (Rasie1) at 2022. + +#endregion + +#region using + +using System; +using System.Reflection; + +using Fission.Functions; + +#endregion + +namespace Fission.DotNet +{ + /// + /// A reference to a Fission function, used to invoke it. + /// + public class FunctionRef + { + private readonly Assembly assembly; + private readonly Type type; + + /// + /// Create an instance of a Fission function. + /// + /// The assembly containing the Fission function. + /// The type, implementing , containing the Fission function. + public FunctionRef (Assembly assembly, Type type) + { + this.assembly = assembly; + this.type = type; + } + + /// + /// Invoke the Fission function referenced by this FunctionRef instance. + /// + /// The function invocation context. + /// The Fission function return value. + public object Invoke (FissionContext context) + => ((IFissionFunction) this.assembly.CreateInstance (typeName: this.type.FullName!)!)!.Execute (context: context); + } +} diff --git a/dotnet60/fission-dotnet6/FunctionStore.cs b/dotnet60/fission-dotnet6/FunctionStore.cs new file mode 100644 index 00000000..39e44208 --- /dev/null +++ b/dotnet60/fission-dotnet6/FunctionStore.cs @@ -0,0 +1,46 @@ +#region header + +// fission-dotnet6 - FunctionStore.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/29 10:41 AM. +// Modified by: Vsevolod Kvachev (Rasie1) at 2022. + +#endregion + +#region using + +using System; + +using Fission.DotNet.Properties; + +#endregion + +namespace Fission.DotNet +{ + /// + /// Implementation of the function store service, as defined by . + /// + internal class FunctionStore : IFunctionStore + { + private FunctionRef? func; + private string? packagePath; + + /// + FunctionRef? IFunctionStore.Func => this.func; + + /// + void IFunctionStore.SetFunctionRef(FunctionRef func) + { + this.func = func; + } + + /// + string? IFunctionStore.PackagePath => this.packagePath; + + /// + void IFunctionStore.SetPackagePath(string packagePath) + { + this.packagePath = packagePath; + } + } +} diff --git a/dotnet60/fission-dotnet6/IFunctionStore.cs b/dotnet60/fission-dotnet6/IFunctionStore.cs new file mode 100644 index 00000000..ac00b124 --- /dev/null +++ b/dotnet60/fission-dotnet6/IFunctionStore.cs @@ -0,0 +1,25 @@ +#region header + +// fission-dotnet6 - IFunctionStore.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/29 10:39 AM. + +#endregion + +namespace Fission.DotNet +{ + /// + /// Interface for the function store service, which caches the post-specialization function for repeated use by the + /// . + /// + public interface IFunctionStore + { + public FunctionRef? Func { get; } + + public void SetFunctionRef(FunctionRef func); + + public string? PackagePath { get; } + + public void SetPackagePath(string func); + } +} diff --git a/dotnet60/fission-dotnet6/Program.cs b/dotnet60/fission-dotnet6/Program.cs new file mode 100644 index 00000000..752a6548 --- /dev/null +++ b/dotnet60/fission-dotnet6/Program.cs @@ -0,0 +1,27 @@ +#region header + +// fission-dotnet6 - Program.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/28 11:19 PM. + +#endregion + +#region using + +using Fission.DotNet; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; + +#endregion + +IWebHost host = new WebHostBuilder() + .ConfigureLogging (configureLogging: log => log.AddConsole()) + .UseKestrel() + .UseUrls("http://*:8888") + .UseStartup() + .Build(); + +host.Run(); + +return 0; diff --git a/dotnet60/fission-dotnet6/Properties/Resources.Designer.cs b/dotnet60/fission-dotnet6/Properties/Resources.Designer.cs new file mode 100644 index 00000000..5f491f86 --- /dev/null +++ b/dotnet60/fission-dotnet6/Properties/Resources.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Fission.DotNet.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Fission.DotNet.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to FIS0001: No compatible type found during compilation.. + /// + internal static string FissionCompiler_Compile_NoEntrypoint { + get { + return ResourceManager.GetString("FissionCompiler_Compile_NoEntrypoint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to generic container: no requests supported. + /// + internal static string FunctionController_Run_GenericContainer { + get { + return ResourceManager.GetString("FunctionController_Run_GenericContainer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cannot overwrite an existing function.. + /// + internal static string FunctionStore_SetFunctionRef_CannotOverwrite { + get { + return ResourceManager.GetString("FunctionStore_SetFunctionRef_CannotOverwrite", resourceCulture); + } + } + } +} diff --git a/dotnet60/fission-dotnet6/Properties/Resources.resx b/dotnet60/fission-dotnet6/Properties/Resources.resx new file mode 100644 index 00000000..fe92541f --- /dev/null +++ b/dotnet60/fission-dotnet6/Properties/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot overwrite an existing function. + + + generic container: no requests supported + + + FIS0001: No compatible type found during compilation. + + \ No newline at end of file diff --git a/dotnet60/fission-dotnet6/Properties/launchSettings.json b/dotnet60/fission-dotnet6/Properties/launchSettings.json new file mode 100644 index 00000000..56616c80 --- /dev/null +++ b/dotnet60/fission-dotnet6/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "fission_dotnet6": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": "true", + "applicationUrl": "http://localhost:8888" + } + } +} \ No newline at end of file diff --git a/dotnet60/fission-dotnet6/Startup.cs b/dotnet60/fission-dotnet6/Startup.cs new file mode 100644 index 00000000..f366fcb0 --- /dev/null +++ b/dotnet60/fission-dotnet6/Startup.cs @@ -0,0 +1,51 @@ +#region header + +// fission-dotnet6 - Startup.cs +// +// Created by: Alistair J R Young (avatar) at 2020/12/28 11:19 PM. + +#endregion + +#region using + +using JetBrains.Annotations; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +#endregion + +namespace Fission.DotNet +{ + /// + /// Configure the environment container's web interface. + /// + public class Startup + { + [UsedImplicitly] + public Startup (IConfiguration configuration) => this.Configuration = configuration; + + public IConfiguration Configuration { get; } + + public void ConfigureServices (IServiceCollection services) + { + services.AddSingleton(); + services.AddControllers(); + services.Configure(options => + { + options.AllowSynchronousIO = true; + }); + } + + public void Configure ([NotNull] IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints (configure: endpoints => { endpoints.MapControllers(); }); + } + } +} diff --git a/dotnet60/fission-dotnet6/appsettings.Development.json b/dotnet60/fission-dotnet6/appsettings.Development.json new file mode 100644 index 00000000..8983e0fc --- /dev/null +++ b/dotnet60/fission-dotnet6/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet60/fission-dotnet6/appsettings.json b/dotnet60/fission-dotnet6/appsettings.json new file mode 100644 index 00000000..d9d9a9bf --- /dev/null +++ b/dotnet60/fission-dotnet6/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet60/fission-dotnet6/builderSettings.json b/dotnet60/fission-dotnet6/builderSettings.json new file mode 100644 index 00000000..a05a46ea --- /dev/null +++ b/dotnet60/fission-dotnet6/builderSettings.json @@ -0,0 +1,11 @@ +{ + "NugetSpecsFile": "nuget.txt", + "DllExcludeFile": "exclude.txt", + "BuildLogDirectory": "logs", + "DllDirectory": "Dlls", + "NugetPackageRegEx": "\\s*(?[^:\\n]*)(?:\\:)?(?.*)?", + "ExcludeDllRegEx": "\\:?\\s*(?[^:\\n]*)(?:\\:)?(?.*)?", + "RunningOnWindows": false, + "functionBodyFileName": "func.cs", + "functionSpecFileName": "func.meta.json" +} \ No newline at end of file diff --git a/dotnet60/fission-dotnet6/fission-dotnet6.csproj b/dotnet60/fission-dotnet6/fission-dotnet6.csproj new file mode 100644 index 00000000..72c45d63 --- /dev/null +++ b/dotnet60/fission-dotnet6/fission-dotnet6.csproj @@ -0,0 +1,52 @@ + + + + net6.0 + Fission.DotNet + enable + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + Always + + + + diff --git a/dotnet60/fission-dotnet6/tmp/TestFunc.cs b/dotnet60/fission-dotnet6/tmp/TestFunc.cs new file mode 100644 index 00000000..8db74657 --- /dev/null +++ b/dotnet60/fission-dotnet6/tmp/TestFunc.cs @@ -0,0 +1,22 @@ +using System; +using Fission.Functions; + +/// +/// Test function for use when debugging. +/// +public class TestFunc : IFissionFunction +{ + /// + /// Test function for use when debugging. + /// + /// Function call context. + /// Test parameters added together. + public object Execute(FissionContext context) + { + context.Logger.WriteInfo ("Test message."); + //var x = Convert.ToInt32(context.Arguments["x"]); + //var y = Convert.ToInt32(context.Arguments["y"]); + //return (x + y); + return "123"; + } +}