From 715dc5d3d9d2dc5598e3cc356dfa6cea60d824be Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Thu, 3 Dec 2020 14:13:42 +0100 Subject: [PATCH] Added SecretStorage as mentioned in #3, reworked on Hook->Job workflow to prepare for "Release" support (like #1) --- ln.build.server/Program.cs | 5 +- ln.build/CIJob.cs | 29 +-- ln.build/CIService.cs | 36 +--- ln.build/JsonApiClient.cs | 58 ++++++ ln.build/commands/CommandEnvironment.cs | 4 + ln.build/ln.build.csproj | 1 - ln.build/pipeline/DefaultPipeLine.cs | 3 +- ln.build/pipeline/DotNetPipeLine.cs | 5 - ...epositoryInterface.cs => GitRepository.cs} | 12 +- .../repositories/GiteaRepositoryInterface.cs | 127 ------------ ln.build/repositories/Release.cs | 40 ++++ ln.build/repositories/Repository.cs | 18 ++ ln.build/repositories/RepositoryInterface.cs | 18 -- ln.build/repositories/gitea/GiteaRelease.cs | 34 ++++ .../repositories/gitea/GiteaRepository.cs | 188 ++++++++++++++++++ ln.build/secrets/SecretStorage.cs | 44 ++++ 16 files changed, 422 insertions(+), 200 deletions(-) create mode 100644 ln.build/JsonApiClient.cs rename ln.build/repositories/{GitRepositoryInterface.cs => GitRepository.cs} (65%) delete mode 100644 ln.build/repositories/GiteaRepositoryInterface.cs create mode 100644 ln.build/repositories/Release.cs create mode 100644 ln.build/repositories/Repository.cs delete mode 100644 ln.build/repositories/RepositoryInterface.cs create mode 100644 ln.build/repositories/gitea/GiteaRelease.cs create mode 100644 ln.build/repositories/gitea/GiteaRepository.cs create mode 100644 ln.build/secrets/SecretStorage.cs diff --git a/ln.build.server/Program.cs b/ln.build.server/Program.cs index 104aba4..7074d43 100644 --- a/ln.build.server/Program.cs +++ b/ln.build.server/Program.cs @@ -11,6 +11,7 @@ using ln.build.repositories; using Microsoft.VisualBasic; using ln.application; using System.IO; +using ln.build.repositories.gitea; namespace ln.build.server { @@ -38,7 +39,9 @@ namespace ln.build.server job.RunJob(); } else { CIService.Initialize(); - CIService.AddRepositoryInterface(new GiteaRepositoryInterface("https://git.l--n.de"){ AuthorizationToken = "1d03e9577c404b5b4f46b340147b1d500ff95b2e", }); + CIService.AddWebHookHandler("gitea", GiteaRepository.WebHookHandler); + +// (new GiteaRepository("https://git.l--n.de"){ AuthorizationToken = "1d03e9577c404b5b4f46b340147b1d500ff95b2e", }); CIService.Start(); } diff --git a/ln.build/CIJob.cs b/ln.build/CIJob.cs index 60440df..b6d6deb 100644 --- a/ln.build/CIJob.cs +++ b/ln.build/CIJob.cs @@ -6,6 +6,7 @@ using System.Text; using ln.build.commands; using ln.build.pipeline; using ln.build.repositories; +using ln.build.secrets; using ln.json; using ln.logging; using ln.threading; @@ -15,15 +16,18 @@ namespace ln.build public class CIJob : PoolJob { public string JobID { get; } = Guid.NewGuid().ToString("N"); - public string RepositoryURL { get; } - public string WorkingDirectory { get; set; } public CIService CIService { get; } + public Repository Repository { get; set; } + public SecretStorage SecretStorage { get; set; } + + public string WorkingDirectory { get; set; } + public Logger Logger { get; private set; } public CommandEnvironment Environment { get; } - public RepositoryInterface RepositoryInterface { get; set; } + public DefaultPipeLine PipeLine { get; set; } @@ -32,12 +36,15 @@ namespace ln.build Dictionary logStreams = new Dictionary(); Dictionary logStreamLoggers = new Dictionary(); - public CIJob(CIService ciService, string repositoryURL) - :this(ciService, null, repositoryURL){ } - public CIJob(CIService ciService, RepositoryInterface repositoryInterface ,string repositoryURL) + public CIJob(CIService ciService, Repository repository, SecretStorage secretStorage) + :this(ciService, repository) + { + SecretStorage = secretStorage; + } + public CIJob(CIService ciService, Repository repository) { CIService = ciService; - RepositoryInterface = repositoryInterface; + Repository = repository; WorkingDirectory = Path.Combine(Path.GetTempPath(), JobID); Directory.CreateDirectory(Path.Combine(ciService.ReportsDirectory, JobID)); @@ -45,8 +52,6 @@ namespace ln.build Logger = new Logger(new FileLogger(Path.Combine(ciService.ReportsDirectory, JobID, "build.log"))); Logger.Backends.Add(Logger.ConsoleLogger); - RepositoryURL = repositoryURL; - Environment = new CommandEnvironment(); } @@ -86,7 +91,7 @@ namespace ln.build public void UpdateBuildState(BuildState buildState) { BuildState = buildState; - RepositoryInterface?.UpdateBuildState(this); + Repository?.UpdateBuildState(this); } public override void RunJob() @@ -115,7 +120,7 @@ namespace ln.build public bool CloneRepository() { - RepositoryInterface?.CloneSources(this); + Repository?.CloneSources(this); return true; } @@ -148,7 +153,7 @@ namespace ln.build { CIService.CreateReport(this); - if (RepositoryInterface != null) + if (Repository != null) Directory.Delete(WorkingDirectory, true); } diff --git a/ln.build/CIService.cs b/ln.build/CIService.cs index 1e9ecc5..1593a7c 100644 --- a/ln.build/CIService.cs +++ b/ln.build/CIService.cs @@ -20,6 +20,9 @@ namespace ln.build { public class CIService { + [StaticArgument( LongOption = "context-directory")] + public string ContextDirectory { get; set; } = AppContext.BaseDirectory; + [StaticArgument( Option = 'b', LongOption = "base-directory")] public string BaseDirectory { get; set; } @@ -38,32 +41,14 @@ namespace ln.build StaticRouter staticsRouter; TemplateRouter templateRouter; HTTPServer httpServer; - - List repositoryInterfaces = new List(); HashSet pipelines = new HashSet(); public IEnumerable PipeLines => pipelines; - Dictionary secrets = new Dictionary(); - public CIService() { buildPool = new Pool(2); - - String secretsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),".ln.build.secrets.json"); - Logging.Log(LogLevel.INFO, "Trying to load secrets from {0}", secretsPath); - if (File.Exists(secretsPath)) - { - JSONObject secretsObject = JSONParser.ParseFile(secretsPath) as JSONObject; - foreach (string key in secretsObject.Keys) - { - secrets.Add(key, secretsObject[key].ToNative().ToString()); - } - Logging.Log(LogLevel.INFO, "Secrets loaded from {0}", secretsPath); - } - - } public CIService(string baseDirectory) : this() { @@ -73,10 +58,9 @@ namespace ln.build public void Initialize() { - BaseDirectory ??= AppContext.BaseDirectory; + BaseDirectory ??= ContextDirectory; ReportsDirectory ??= Path.Combine(BaseDirectory, "builds" ); - ReportsDirectory = Path.GetFullPath(ReportsDirectory); Logging.Log(LogLevel.INFO, "Commandline: {0}", Environment.CommandLine); @@ -85,6 +69,7 @@ namespace ln.build Logging.Log(LogLevel.INFO, "Reports Directory: {0}", ReportsDirectory); Directory.CreateDirectory(ReportsDirectory); + Directory.CreateDirectory(Path.Combine(BaseDirectory, "secrets")); InitializeHttpServer(); } @@ -104,8 +89,8 @@ namespace ln.build httpRouter.AddSimpleRoute("/builds/*", new StaticRouter(ReportsDirectory)); hookRouter = new SimpleRouter(); - staticsRouter = new StaticRouter(Path.Combine(BaseDirectory, "html", "static")); - templateRouter = new TemplateRouter(new FileSystemTemplateSource(Path.Combine(BaseDirectory, "html", "documents"), false)); + staticsRouter = new StaticRouter(Path.Combine(ContextDirectory, "html", "static")); + templateRouter = new TemplateRouter(new FileSystemTemplateSource(Path.Combine(ContextDirectory, "html", "documents"), false)); templateRouter.DefaultTemplatePath = "index.html"; templateRouter.OnPrepareRenderContext += (templateRouter, templateDocument, renderContext) => { renderContext.SetScriptObject("CIService", this); @@ -130,8 +115,6 @@ namespace ln.build public string GetJobURL(CIJob job) => string.Format("{0}/builds/{1}", BaseURL, job.JobID); - public string GetSecret(string key) => secrets[key]; - public void Start() { buildPool.Start(); @@ -143,10 +126,9 @@ namespace ln.build pipelines.Add(pipeLine); } - public void AddRepositoryInterface(RepositoryInterface repositoryInterface) + public void AddWebHookHandler(string name, Func webHookHandler) { - hookRouter.AddSimpleRoute(String.Format("/{0}", repositoryInterface.WebHookName), (HttpRoutingContext context,HttpRequest request) => repositoryInterface.WebHookHandler(this, request)); - repositoryInterfaces.Add(repositoryInterface); + hookRouter.AddSimpleRoute(String.Format("/{0}", name), (HttpRoutingContext context,HttpRequest request) => webHookHandler(this, request)); } diff --git a/ln.build/JsonApiClient.cs b/ln.build/JsonApiClient.cs new file mode 100644 index 0000000..c2f589b --- /dev/null +++ b/ln.build/JsonApiClient.cs @@ -0,0 +1,58 @@ + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using ln.json; +using ln.logging; + +namespace ln.build +{ + + public class JsonApiClient : IDisposable + { + public string BaseURL { get; } + + public HttpClient HttpClient { get; } + + public JsonApiClient(string baseUrl) + { + HttpClient = new HttpClient(); + BaseURL = baseUrl; + } + + public HttpStatusCode GetJson(out JSONValue response,params string[] path) + { + HttpResponseMessage httpResponse = HttpClient.GetAsync(string.Format("{0}/{1}",BaseURL, string.Join('/', path))).Result; + if (httpResponse.StatusCode == System.Net.HttpStatusCode.OK) + { + response = JSONParser.Parse(httpResponse.Content.ReadAsStringAsync().Result); + } else { + response = null; + } + return httpResponse.StatusCode; + } + public HttpStatusCode PostJson(JSONValue content, out JSONValue response,params string[] path) + { + StringContent jsonContent = new StringContent(content.ToString(),Encoding.UTF8, "application/json"); + HttpResponseMessage httpResponse = HttpClient.PostAsync(string.Format("{0}/{1}",BaseURL, string.Join('/', path)),jsonContent).Result; + if (httpResponse.StatusCode == System.Net.HttpStatusCode.Created) + { + response = JSONParser.Parse(httpResponse.Content.ReadAsStringAsync().Result); + } else { + response = null; + Logging.Log(LogLevel.DEBUG, "failed URL: {0}", httpResponse.RequestMessage.RequestUri); + Logging.Log(LogLevel.DEBUG, "httpResponse; {0}", httpResponse); + } + return httpResponse.StatusCode; + } + + + public void Dispose() + { + HttpClient?.Dispose(); + } + } + +} \ No newline at end of file diff --git a/ln.build/commands/CommandEnvironment.cs b/ln.build/commands/CommandEnvironment.cs index 8f644c0..b1aac79 100644 --- a/ln.build/commands/CommandEnvironment.cs +++ b/ln.build/commands/CommandEnvironment.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using ln.build.secrets; using ln.json; using ln.logging; @@ -26,6 +27,9 @@ namespace ln.build.commands public bool IsReadOnly => ((ICollection>)variables).IsReadOnly; public string this[string key] { get => Get(key); set => Set(key,value); } + SecretStorage secretStorage; + public SecretStorage SecretStorage { get => secretStorage ?? Parent?.SecretStorage; set => secretStorage = value; } + public CommandEnvironment(CommandEnvironment parent) : this(parent.Logger) { Parent = parent; WorkingDirectory = parent.WorkingDirectory; } public CommandEnvironment(CommandEnvironment parent, Logger logger) : this(logger) { Parent = parent; WorkingDirectory = parent.WorkingDirectory; } public CommandEnvironment() : this(Logger.Default) diff --git a/ln.build/ln.build.csproj b/ln.build/ln.build.csproj index 3053a1a..1abc4a3 100644 --- a/ln.build/ln.build.csproj +++ b/ln.build/ln.build.csproj @@ -19,7 +19,6 @@ - diff --git a/ln.build/pipeline/DefaultPipeLine.cs b/ln.build/pipeline/DefaultPipeLine.cs index 0df063f..9ff04e9 100644 --- a/ln.build/pipeline/DefaultPipeLine.cs +++ b/ln.build/pipeline/DefaultPipeLine.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using ln.build.commands; using ln.json; @@ -100,7 +99,7 @@ namespace ln.build.pipeline JSONObject jsonSecrets = jsonStage["secrets"] as JSONObject; foreach (string key in jsonSecrets.Keys) { - CommandEnvironment.Set(key, PipeLine.CIService.GetSecret(jsonSecrets[key].ToNative().ToString())); + CommandEnvironment.Set(key, CommandEnvironment.SecretStorage.GetSecret(jsonSecrets[key].ToNative().ToString())); } } diff --git a/ln.build/pipeline/DotNetPipeLine.cs b/ln.build/pipeline/DotNetPipeLine.cs index e020211..1de2b24 100644 --- a/ln.build/pipeline/DotNetPipeLine.cs +++ b/ln.build/pipeline/DotNetPipeLine.cs @@ -1,13 +1,8 @@ using System; using System.IO; -using System.Reflection.Metadata.Ecma335; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.WindowsRuntime; -using System.Runtime.Serialization.Formatters; using System.Text; using System.Text.RegularExpressions; -using LibGit2Sharp.Handlers; using ln.build.commands; using ln.logging; using ln.type; diff --git a/ln.build/repositories/GitRepositoryInterface.cs b/ln.build/repositories/GitRepository.cs similarity index 65% rename from ln.build/repositories/GitRepositoryInterface.cs rename to ln.build/repositories/GitRepository.cs index 77f7b11..7b59e80 100644 --- a/ln.build/repositories/GitRepositoryInterface.cs +++ b/ln.build/repositories/GitRepository.cs @@ -6,14 +6,12 @@ using ln.http; namespace ln.build.repositories { - public abstract class GitRepositoryInterface : RepositoryInterface + public abstract class GitRepository : Repository { - string webHookName; - public override string WebHookName => webHookName; - - public GitRepositoryInterface(string webHookName) + public string CloneURL { get; } + public GitRepository(string cloneUrl) { - this.webHookName = webHookName; + CloneURL = cloneUrl; } public override void CloneSources(CIJob job) @@ -21,7 +19,7 @@ namespace ln.build.repositories job.Logger.Log("{0}: cloning repository to {1}", GetType().Name, job.WorkingDirectory); job.Environment.WorkingDirectory = Path.GetTempPath(); - bool success = new CommandRunner("git", "clone", job.RepositoryURL, job.WorkingDirectory).Run(job.Environment) == 0; + bool success = new CommandRunner("git", "clone", CloneURL, job.WorkingDirectory).Run(job.Environment) == 0; job.Environment.WorkingDirectory = job.WorkingDirectory; if (success && job.ContainsVariable("COMMIT_ID")) diff --git a/ln.build/repositories/GiteaRepositoryInterface.cs b/ln.build/repositories/GiteaRepositoryInterface.cs deleted file mode 100644 index a169a66..0000000 --- a/ln.build/repositories/GiteaRepositoryInterface.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Net.Http; -using System.Text; -using ln.http; -using ln.json; -using ln.logging; - -namespace ln.build.repositories -{ - - public class GiteaRepositoryInterface : GitRepositoryInterface - { - public string BaseURL { get; } - - public string AuthorizationToken { get; set; } - - public GiteaRepositoryInterface(string baseURL) :base("gitea") - { - BaseURL = baseURL; - } - - HttpClient CreateHttpClient() - { - HttpClient httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("Authorization",String.Format("token {0}", AuthorizationToken)); - return httpClient; - } - - public override bool DetectValidity(string cloneUrl) => cloneUrl.StartsWith(BaseURL); - public override void UpdateBuildState(CIJob job) - { - string buildStateURL = string.Format("{3}/api/v1/repos/{0}/{1}/statuses/{2}", - job.GetVariable("REPO_OWNER"), - job.GetVariable("REPO_NAME"), - job.GetVariable("COMMIT_ID"), - BaseURL - ); - - if (buildStateURL != null) - { - JSONObject stateObject = new JSONObject(); - stateObject.Add("context", string.Format("ln.build - {0}", job.CIService.HostName)); - stateObject.Add("description", "build job pending"); - stateObject.Add("state", job.BuildState.ToString().ToLower()); - stateObject.Add("target_url", job.CIService.GetJobURL(job)); - - using (HttpClient httpClient = CreateHttpClient()) - { - HttpResponseMessage response = httpClient.PostAsync(buildStateURL, new StringContent(stateObject.ToString(),Encoding.UTF8,"application/json")).Result; - job.Logger.Log(LogLevel.DEBUG, "UpdateBuildState({0}): {1}", job.BuildState, buildStateURL); - job.Logger.Log(LogLevel.DEBUG, "Request: {0}", stateObject.ToString()); - job.Logger.Log(LogLevel.DEBUG, "Response: {0}", response ); - } - } - } - - public override HttpResponse WebHookHandler(CIService ciService, HttpRequest request) - { - HttpResponse response = new HttpResponse(request); - - if (!request.Method.Equals("POST")) - { - response.StatusCode = 405; - } else if (!request.GetRequestHeader("content-type").Equals("application/json")) - { - response.StatusCode = 415; - response.ContentWriter.WriteLine("Unsupported Media Type, should be application/json"); - } else { - JSONValue jsonRequest = JSONParser.Parse(request.ContentReader.ReadToEnd()); - if (jsonRequest is JSONObject message) - { - try - { - string cloneUrl = message["repository"]["clone_url"].ToNative().ToString(); - - foreach (JSONValue commit in (message["commits"] as JSONArray).Children) - { - string commitID = commit["id"].ToNative().ToString(); - Logging.Log("Received CI request of repository {0} for commit {1}", cloneUrl, commitID); - - CIJob job = new CIJob(ciService, this, cloneUrl) - .SetVariable("REPO_OWNER", message["repository"]["owner"]["username"].ToNative().ToString()) - .SetVariable("REPO_NAME", message["repository"]["name"].ToNative().ToString()) - .SetVariable("COMMIT_ID", commitID) - .SetVariable("NOTIFY", message["pusher"]["email"].ToNative().ToString()) - .SetVariable("NUGET_APIKEY", "3yAJPMxcaEhb_HP62dxK") - .SetVariable("NUGET_SOURCE", "http://nuget.l--n.de/nuget/l--n/v3/index.json") - ; - - using (HttpClient httpClient = CreateHttpClient()) - { - string triggerFile = string.Format("{3}/api/v1/repos/{0}/{1}/contents/build.ln?ref={2}", - job.GetVariable("REPO_OWNER"), - job.GetVariable("REPO_NAME"), - job.GetVariable("COMMIT_ID"), - BaseURL - ); - - HttpResponseMessage triggerResponse = httpClient.GetAsync(triggerFile).Result; - if (triggerResponse.StatusCode == System.Net.HttpStatusCode.OK) - { - job.UpdateBuildState(BuildState.PENDING); - ciService.Enqueue(job); - } - } - } - } catch (Exception e) - { - response.StatusCode = 500; - response.StatusMessage = "An exception occured"; - response.ContentWriter.WriteLine("{0}", e.ToString()); - Logging.Log(e); - } - - } else { - response.StatusCode = 400; - } - - } - - - return response; - - } - } - -} \ No newline at end of file diff --git a/ln.build/repositories/Release.cs b/ln.build/repositories/Release.cs new file mode 100644 index 0000000..6df1e85 --- /dev/null +++ b/ln.build/repositories/Release.cs @@ -0,0 +1,40 @@ + +using System; + +namespace ln.build.repositories +{ + public abstract class Release + { + public string Name { get; set; } + public int Id { get; set; } = -1; + public string TagName { get; set; } + public Boolean IsDraft {get; set; } + public Boolean IsPreRelease {get; set; } + + public string Body { get; set; } + + public abstract Repository Repository { get; } + + public abstract Attachement[] GetAttachements(); + public abstract void AddAttachement(Attachement attachement,string localPath); + + + + public class Attachement + { + public Release Release { get; set; } + public int Id { get; set; } = -1; + public string Name { get; set; } + public Guid UUID { get; set; } + + public string DownloadURL { get; set; } + + public Attachement(Release release) + { + Release = release; + } + + } + } + +} \ No newline at end of file diff --git a/ln.build/repositories/Repository.cs b/ln.build/repositories/Repository.cs new file mode 100644 index 0000000..0622ea9 --- /dev/null +++ b/ln.build/repositories/Repository.cs @@ -0,0 +1,18 @@ + +using System; +using ln.http; + +namespace ln.build.repositories +{ + public abstract class Repository + { + public abstract void CloneSources(CIJob job); + public abstract void UpdateBuildState(CIJob job); + + + public abstract Release[] GetReleases(); + public abstract Release GetRelease(string tagName); + + + } +} \ No newline at end of file diff --git a/ln.build/repositories/RepositoryInterface.cs b/ln.build/repositories/RepositoryInterface.cs deleted file mode 100644 index 8291f27..0000000 --- a/ln.build/repositories/RepositoryInterface.cs +++ /dev/null @@ -1,18 +0,0 @@ - -using System; -using ln.http; - -namespace ln.build.repositories -{ - public abstract class RepositoryInterface - { - - public abstract String WebHookName { get; } - public abstract HttpResponse WebHookHandler(CIService service, HttpRequest request); - - public abstract bool DetectValidity(string cloneUrl); - public abstract void CloneSources(CIJob job); - public abstract void UpdateBuildState(CIJob job); - - } -} \ No newline at end of file diff --git a/ln.build/repositories/gitea/GiteaRelease.cs b/ln.build/repositories/gitea/GiteaRelease.cs new file mode 100644 index 0000000..776d52e --- /dev/null +++ b/ln.build/repositories/gitea/GiteaRelease.cs @@ -0,0 +1,34 @@ + +using ln.json; + +namespace ln.build.repositories.gitea +{ + public class GiteaRelease : Release + { + public override Repository Repository => GiteaRepository; + public GiteaRepository GiteaRepository { get; } + public GiteaRelease(GiteaRepository repository) + { + GiteaRepository = repository; + } + public GiteaRelease(GiteaRepository repository,JSONObject jsonRelease) : this(repository) + { + Id = (int)jsonRelease["id"].ToNative(); + Name = jsonRelease["name"].ToNative().ToString(); + TagName = jsonRelease["tag_name"].ToNative().ToString(); + Body = jsonRelease["body"].ToNative().ToString(); + IsDraft = (bool)jsonRelease["draft"].ToNative(); + IsPreRelease = (bool)jsonRelease["prerelease"].ToNative(); + } + + public override void AddAttachement(Attachement attachement, string localPath) + { + throw new System.NotImplementedException(); + } + + public override Attachement[] GetAttachements() + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/ln.build/repositories/gitea/GiteaRepository.cs b/ln.build/repositories/gitea/GiteaRepository.cs new file mode 100644 index 0000000..130dccf --- /dev/null +++ b/ln.build/repositories/gitea/GiteaRepository.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using ln.build.secrets; +using ln.http; +using ln.json; +using ln.logging; + +namespace ln.build.repositories.gitea +{ + + public class GiteaRepository : GitRepository + { + public string BaseURL { get; } + public string Owner { get; } + public string Name { get; } + + public SecretStorage SecretStorage { get; set; } + + public JsonApiClient Client { get; } + + public GiteaRepository(string baseURL, string owner, string name) + :base(string.Format("{0}/{1}/{2}.git",baseURL, owner, name)) + { + Logging.Log(LogLevel.DEBUG, "new GiteaRepository({0},{1},{2})", baseURL, owner, name); + + BaseURL = baseURL; + Owner = owner; + Name = name; + + Client = new JsonApiClient(baseURL); + } + public GiteaRepository(string baseURL, string owner, string name, string accessToken) + :this(baseURL, owner, name) + { + if (accessToken != null) + Client.HttpClient.DefaultRequestHeaders.Add("Authorization",String.Format("token {0}", accessToken)); + } + + public override void UpdateBuildState(CIJob job) + { + JSONObject stateObject = new JSONObject(); + stateObject.Add("context", string.Format("ln.build - {0}", job.CIService.HostName)); + stateObject.Add("description", "build job pending"); + stateObject.Add("state", job.BuildState.ToString().ToLower()); + stateObject.Add("target_url", job.CIService.GetJobURL(job)); + + if (HttpStatusCode.Created != Client.PostJson( + stateObject, + out JSONValue response, + "api/v1/repos", job.GetVariable("REPO_OWNER"), job.GetVariable("REPO_NAME"), "statuses", job.GetVariable("REPO_REF") + )) + { + job.Logger.Log(LogLevel.ERROR, "{0}: UpdateBuildState(): could not post state", GetType().Name); + } + } + + public override Release[] GetReleases() + { + string releasesUrl = string.Format("{0}/api/v1/repos/{1}/{2}/releases", + BaseURL, + Owner, + Name + ); + if (HttpStatusCode.OK == Client.GetJson( + out JSONValue jsonReleases, + "api/v1/repos", Owner, "releases" + )) + { + List releases = new List(); + + foreach (JSONObject jsonRelease in jsonReleases.Children) + { + GiteaRelease giteaRelease = new GiteaRelease(this, jsonRelease); + releases.Add(giteaRelease); + } + + + return releases.ToArray(); + } + return new Release[0]; + } + + public override Release GetRelease(string tagName) + { + throw new NotImplementedException(); + } + + + public bool ContainsFile(string filename,string _ref) => (Client.GetJson(out JSONValue response, "/api/v1/repos", Owner, Name, "contents", string.Format("{0}?ref={1}",filename, _ref)) == HttpStatusCode.OK); + + + + public static HttpResponse WebHookHandler(CIService ciService, HttpRequest request) + { + HttpResponse response = new HttpResponse(request); + + if (!request.Method.Equals("POST")) + { + response.StatusCode = 405; + } else if (!request.GetRequestHeader("content-type").Equals("application/json")) + { + response.StatusCode = 415; + response.ContentWriter.WriteLine("Unsupported Media Type, should be application/json"); + } else { + JSONValue jsonRequest = JSONParser.Parse(request.ContentReader.ReadToEnd()); + if (jsonRequest is JSONObject message) + { + try + { + string hookSecret = message["secret"].ToNative().ToString(); + string owner = message["repository"]["owner"]["username"].ToNative().ToString(); + string repoName = message["repository"]["name"].ToNative().ToString(); + + string htmlUrl = message["repository"]["html_url"].ToNative().ToString(); + string repoPath = String.Format("/{0}/{1}", owner, repoName); + if (!htmlUrl.EndsWith(repoPath)) + { + Logging.Log(LogLevel.WARNING, "unable to detect gitea base url from [{0}] for ({1}/{2})", htmlUrl, owner, repoName); + response.StatusCode = 500; + return response; + } + + string giteaBaseUrl = htmlUrl.Remove(htmlUrl.Length - repoPath.Length); + + + SecretStorage secretStorage = new SecretStorage(Path.Combine(ciService.BaseDirectory,"secrets", String.Format("{0}.json", hookSecret))); + GiteaRepository giteaRepository = new GiteaRepository(giteaBaseUrl, owner, repoName, secretStorage.GetSecret(giteaBaseUrl)){ + SecretStorage = secretStorage, + }; + + string eventType = request.GetRequestHeader("X-GITEA-EVENT"); + List buildRefs = new List(); + + switch (eventType) + { + case "release": + buildRefs.Add(message["release"]["tag_name"].ToNative().ToString()); + break; + case "push": + foreach (JSONObject jsonCommit in (message["commits"] as JSONArray).Children) + buildRefs.Add(jsonCommit["id"].ToNative().ToString()); + break; + default: + Logging.Log(LogLevel.WARNING, "received webhook with unsupported event type [{0}]", eventType); + return response; + } + + foreach (string _ref in buildRefs) + { + if (giteaRepository.ContainsFile("build.ln", _ref)) + { + CIJob ciJob = new CIJob(ciService, giteaRepository) + .SetVariable("REPO_EVENT", eventType) + .SetVariable("REPO_OWNER", owner) + .SetVariable("REPO_NAME", repoName) + .SetVariable("REPO_REF", _ref) + .SetVariable("NOTIFY", message["pusher"]["email"].ToNative().ToString()) + ; + ciJob.Environment.SecretStorage = secretStorage; + + ciJob.UpdateBuildState(BuildState.PENDING); + ciService.Enqueue(ciJob); + } + } + } catch (Exception e) + { + response.StatusCode = 500; + response.StatusMessage = "An exception occured"; + response.ContentWriter.WriteLine("{0}", e.ToString()); + Logging.Log(e); + } + + } else { + response.StatusCode = 400; + } + + } + + + return response; + + } + + } + +} \ No newline at end of file diff --git a/ln.build/secrets/SecretStorage.cs b/ln.build/secrets/SecretStorage.cs new file mode 100644 index 0000000..0e726a1 --- /dev/null +++ b/ln.build/secrets/SecretStorage.cs @@ -0,0 +1,44 @@ + +using System.Collections.Generic; +using System.IO; +using System.IO.Enumeration; +using ln.json; +using ln.logging; + +namespace ln.build.secrets +{ + public class SecretStorage + { + public string FileName { get; } + Dictionary secrets = new Dictionary(); + + public SecretStorage(string filename) + { + FileName = filename; + LoadSecrets(); + } + + void LoadSecrets() + { + lock (this) + { + Logging.Log(LogLevel.INFO, "loading secrets from {0}", FileName); + if (File.Exists(FileName)) + { + JSONObject secretsObject = JSONParser.ParseFile(FileName) as JSONObject; + foreach (string key in secretsObject.Keys) + { + secrets.Add(key, secretsObject[key].ToNative().ToString()); + } + } + } + } + + public string GetSecret(string key) + { + TryGetSecret(key, out string secret); + return secret; + } + public bool TryGetSecret(string key, out string secret) => secrets.TryGetValue(key, out secret); + } +} \ No newline at end of file