Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
Loading items

Target

Select target project
  • coscine/backend/scripts/graphdeployer
1 result
Select Git revision
Loading items
Show changes
Commits on Source (6)
......@@ -4,17 +4,46 @@
## 📝 Overview
This repository handles the deployment of every static graph which gets included into the Coscine environment. It is a C# program that performs tasks related to deploying RDF graphs to a Virtuoso triple store. The program connects to a Virtuoso server and obtains configuration settings from a Consul configuration store. It then clones one or more Git repositories containing RDF graphs, merges the graphs into a single graph, and inserts the resulting graph into the Virtuoso triple store. The program logs its progress and any errors it encounters using the NLog logging framework.
The Graph Deployer, intended to be used as a CRON job, is a .NET application designed to manage and deploy RDF graphs. It interacts with configured GitLab repositories to pull RDF graph definitions and updates a centralized repository if changes are detected or redeployment is triggered. The job uses a series of configurations to determine the operational parameters including the execution environment, repository access, and deployment specifications.
## ⚙️ Configuration
Before you can run and use the script, you need to ensure that the following dependencies and prerequisites are in place:
### Features
- **Configuration-driven**: Behavior driven by settings defined in configuration files and environment variables (see `appsettings.json`).
- **Supports Dummy Mode**: Can run in a non-operative mode for testing configurations and process flow without affecting live data (CLI arg `--dummy`).
- **Selective Redeployment**: Allows for forced redeployment of graphs regardless of changes (CLI arg `--redeploy`).
- **Logging**: Extensive logging capabilities to track the process and troubleshoot issues.
1. The project's referenced .NET SDK(s) must be installed. Please refer to the project's source code for information on which .NET SDK(s) are required.
2. For every deployable graph repository, an entry is expected in the configuration path: `coscine/local/graphs/`.
3. Every repository must include the key `coscine/local/graphs/{graphname}/repositoryurl` for specifying the URL (e.g. `git.rwth-aachen.de/coscine/applicationprofiles.git`) and can include the key `coscine/local/graphs/{graphname}/branch` which specifies the state of the repository being imported (e.g. `master`).
## ⚙️ Configuration
Once you have all the necessary dependencies and prerequisites in place, you should be able to run and use this script.
The deployment script uses a configuration class `GraphDeployerConfiguration` to manage settings such as:
- `IsEnabled`: Toggles the deployer on or off.
- `WorkingFolder`: Specifies the directory where repositories will be cloned.
- `Logger` Configuration: Specifies the logging level and output directory.
- `GitLab` Configuration: Contains information required to access GitLab, including host URL, token, and repository details.
### Example `appsettings.json`
```json
{
"GraphDeployerConfiguration": {
"IsEnabled": true,
"WorkingFolder": "./output/",
"Logger": {
"LogLevel": "Information",
"LogHome": "C:/Logs"
},
"GitLab": {
"HostUrl": "https://gitlab.example.com",
"Token": "YourTokenHere",
"Repositories": [
{
"Name": "RepositoryName",
"Url": "https://gitlab.example.com/group/project.git",
"Branch": "main"
}
]
}
}
}
```
## 📖 Usage
......@@ -22,6 +51,31 @@ To get started with this project, you will need to ensure you have configured an
1. Execute the built executable (`.exe`)
To use the **GraphDeployer**, execute the main program with appropriate command-line arguments to control its operation.
### Command-Line Arguments
- `--redeploy`: Forces redeployment of all graphs.
- `--dummy`: Runs the deployer in dummy mode, making no actual changes.
### Running the Deployer
```sh
dotnet GraphDeployer.dll --dummy true --redeploy false
```
### Deployment Process
1. **Initialization**: Configurations are loaded and services are initialized.
2. **Repository Management**: For each configured repository, the latest content is cloned (`git clone`) into the specified working folder.
3. **Processing**: Turtle files (`*.ttl`) are processed to accumulate RDF graph changes. Accumulation is nedessary, as they may be split across multiple files.
4. **Deployment Decision**: Compares the current graph file hashes against stored versions to decide if deployment is necessary. Redeployment is enforced on graphs that have no changes, whenever the `--redeploy` CLI argument is set (value: `true`).
5. **Graph Deployment**: Updates are pushed to the central graph repository if changes are detected or redeployment is forced.
6. **Clean-up**: Cleans the working directory after processing each repository. The working directory will not be deleted, only its contents.
### Considerations
- **Security**: Ensure the security of the GitLab token and other sensitive data in configuration files. Furthermore, ensure the validity of the used GitLab token, as it may expire.
- **Performance**: Large graph datasets or numerous repositories may impact performance; consider scaling solutions or optimizing configurations.
- **Error Handling**: Robust error handling and logging are crucial for diagnosing issues during deployments.
## 👥 Contributing
As an open source plattform and project, we welcome contributions from our community in any form. You can do so by submitting bug reports or feature requests, or by directly contributing to Coscine's source code. To submit your contribution please follow our [Contributing Guideline](https://git.rwth-aachen.de/coscine/docs/public/wiki/-/blob/master/Contributing%20To%20Coscine.md).
......

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32526.322
VisualStudioVersion = 17.9.34714.143
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphDeployer", "GraphDeployer\GraphDeployer.csproj", "{B4FBEED2-334C-4EE8-94A5-71ADF6A94856}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphDeployer", "GraphDeployer\GraphDeployer.csproj", "{BE815477-F908-4517-8A09-34083EB44573}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
......@@ -11,15 +11,15 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B4FBEED2-334C-4EE8-94A5-71ADF6A94856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4FBEED2-334C-4EE8-94A5-71ADF6A94856}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4FBEED2-334C-4EE8-94A5-71ADF6A94856}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4FBEED2-334C-4EE8-94A5-71ADF6A94856}.Release|Any CPU.Build.0 = Release|Any CPU
{BE815477-F908-4517-8A09-34083EB44573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE815477-F908-4517-8A09-34083EB44573}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE815477-F908-4517-8A09-34083EB44573}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE815477-F908-4517-8A09-34083EB44573}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A90A7F8C-DB85-4E7C-B796-CC8B5A487267}
SolutionGuid = {DFB0DBD5-2A8E-41B9-9532-0AA50FDD1793}
EndGlobalSection
EndGlobal
using Coscine.ApiClient;
using Coscine.ApiClient.Core.Api;
using Coscine.ApiClient.Core.Model;
using Coscine.GraphDeployer.Models.ConfigurationModels;
using Coscine.GraphDeployer.Utils;
using LibGit2Sharp;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Polly;
using VDS.RDF;
using static Coscine.GraphDeployer.Utils.CommandLineOptions;
using Configuration = Coscine.ApiClient.Core.Client.Configuration;
namespace Coscine.GraphDeployer;
public class Deployer(ILogger<Deployer> logger, IOptionsMonitor<GraphDeployerConfiguration> graphDeployerConfiguration)
{
private readonly ILogger<Deployer> _logger = logger;
private readonly GraphDeployerConfiguration _graphDeployerConfiguration = graphDeployerConfiguration.CurrentValue;
private readonly AdminApi _adminApi = new(_configuration);
private static readonly Configuration _configuration = new()
{
BasePath = "http://localhost:7206/coscine",
ApiKeyPrefix = { { "Authorization", "Bearer" } },
ApiKey = { { "Authorization", ApiConfigurationUtil.GenerateAdminToken(ApiConfigurationUtil.RetrieveJwtConfiguration()) } },
Timeout = 300000 // 5 minutes
};
public static string WorkingFolder { get; set; } = "./output/";
public static List<string> DeployedGraphs { get; set; } = [];
public static List<string> SkippedGraphs { get; set; } = [];
// RDF URIs
public static Uri CoscineEntitiesGraphDeployed { get; } = new("https://purl.org/coscine/entities/graph#deployed");
public static Uri CoscineTermsDeployedVersion { get; } = new("https://purl.org/coscine/terms/deployed#version");
public async Task<bool> RunAsync(GraphDeployerOptions opts)
{
// Check if the graph deployer is enabled
if (!_graphDeployerConfiguration.IsEnabled)
{
_logger.LogInformation("Graph Deployer is disabled in the configuration. Exiting...");
return false;
}
// Log the current application execution mode
if (opts.DummyMode)
{
_logger.LogInformation("Running in Dummy Mode. No changes will be made.");
}
// Override the working folder if specified in the configuration
if (!string.IsNullOrWhiteSpace(_graphDeployerConfiguration.WorkingFolder))
{
WorkingFolder = _graphDeployerConfiguration.WorkingFolder;
}
var graphs = PaginationHelper.GetAllAsync<DeployedGraphDtoPagedResponse, DeployedGraphDto>(
(currentPage) => _adminApi.GetDeployedGraphsAsync(pageNumber: currentPage, pageSize: 50));
var deployedGraphsList = await graphs.ToListAsync();
// Iterate over the repositories and deploy the graphs
foreach (var graphRepo in _graphDeployerConfiguration.GitLab.Repositories)
{
_logger.LogInformation("Working with {repoName}", graphRepo.Name);
// Clone the repository inside the Working Folder
var success = CloneRepo(graphRepo.Url, WorkingFolder, _graphDeployerConfiguration.GitLab.Token, graphRepo.Branch);
if (success)
{
// Graph deployment logic
var queries = new List<string>();
var turtleFiles = Directory.GetFiles(WorkingFolder, "*.ttl", SearchOption.AllDirectories);
// Accumulate the graphs to be deployed, as they may be split across multiple files
_logger.LogDebug("Computing accumulated graphs for deployment from {count} Turtle files...", turtleFiles.Length);
var graphAccumulation = new Dictionary<Uri, (Graph, List<string>)>();
Array.ForEach(turtleFiles, (file) =>
{
try
{
var graph = new Graph();
graph.LoadFromFile(file);
if (graphAccumulation.TryGetValue(graph.BaseUri, out (Graph, List<string>) value))
{
value.Item1.Merge(graph);
value.Item2.Add(file);
}
else
{
graphAccumulation.Add(graph.BaseUri, (graph, new List<string>() { file }));
}
}
catch (Exception e)
{
_logger.LogError("Failed to load and process Turtle file: \"{file}\". Error: {errorMessage}", file, e.Message);
}
});
_logger.LogDebug("Accumulated {count} graphs for possible deployment.", graphAccumulation.Count);
// Iterate over the accumulated graphs and deploy them
foreach (var kv in graphAccumulation)
{
var graph = kv.Value.Item1;
var files = kv.Value.Item2;
var graphId = kv.Key.ToString();
var currentRun = new Dictionary<string, string>();
// Get the hash of the currently deployed graph and compare it with the hash of the graph to be deployed
files.ForEach((path) => currentRun.TryAdd(graphId, HashUtil.GetFileHash(path)));
var deployedGraph = deployedGraphsList.FirstOrDefault((g) => g.Uri == graphId);
var hasChanged = deployedGraph is null || !deployedGraph.FileHashes.Contains(currentRun[graphId]) || opts.Redeploy;
// Deploy the graph if it has changed or if the redeploy flag is set
if (hasChanged)
{
// Insert the information about the deployed version
var deployedGraphSubject = graph.CreateUriNode(CoscineEntitiesGraphDeployed);
var deployedVersionPredicate = graph.CreateUriNode(CoscineTermsDeployedVersion);
foreach (var fileHash in currentRun.Values)
{
graph.Assert(deployedGraphSubject, deployedVersionPredicate, graph.CreateLiteralNode(fileHash));
}
// Update the graph
var formatEnum = RdfFormat.TextTurtle;
var format = "text/turtle";
var rdfWriter = MimeTypesHelper.GetWriter(format);
var content = VDS.RDF.Writing.StringWriter.Write(graph, rdfWriter);
if (!opts.DummyMode)
{
// Retry the operation in case of failure up to 3 times using exponential backoff (2^retryAttempt seconds as the delay between retries)
await Policy.Handle<Exception>().WaitAndRetry(3, (retryAttempt) => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))).Execute(async () =>
{
await _adminApi.UpdateMetadataGraphAsync(
graph.BaseUri.AbsoluteUri,
new MetadataUpdateAdminParameters(new RdfDefinitionForManipulationDto(content, formatEnum))
);
});
}
_logger.LogInformation("Deployed {graphName} successfully.", graphId);
DeployedGraphs.Add(graphId);
}
else
{
_logger.LogInformation("Skipped {graphName} as it has not changed.", graphId);
SkippedGraphs.Add(graphId);
continue;
}
}
}
// Clean up the working folder
EmptyWorkingFolder();
}
return true;
}
/// <summary>
/// Clones a repository from a given URL into a specified local directory.
/// </summary>
/// <param name="projectUrl">The URL of the project repository to clone.</param>
/// <param name="projectPath">The local path where the repository should be cloned to.</param>
/// <param name="token">The token used for authentication with the repository host.</param>
/// <param name="branchName">Optional. The specific branch name to clone. If <c>null</c>, the default branch is cloned.</param>
/// <returns>Returns <c>true</c> if the repository was successfully cloned, otherwise <c>false</c>.</returns>
private bool CloneRepo(Uri projectUrl, string projectPath, string token, string? branchName)
{
// Ensure the target directory is empty, if it exists
EmptyWorkingFolder();
// Ensure the target directory exists
Directory.CreateDirectory(projectPath);
// Prepare the repository clone URL with authentication token
var gitLink = projectUrl.AbsoluteUri.Replace("https://", "").Replace("http://", "");
string url = $"https://gitlab-ci-token:{token}@{gitLink}";
try
{
var cloneOptions = new CloneOptions();
if (!string.IsNullOrWhiteSpace(branchName))
{
cloneOptions.BranchName = branchName;
}
// Perform the clone operation
_logger.LogDebug("Starting clone of repository: \"{projectUrl}\" into \"{projectPath}\"", projectUrl, projectPath);
var repo = Repository.Clone(url, projectPath, cloneOptions);
var localRepo = new Repository(repo);
_logger.LogDebug("Repository successfully cloned on branch \"{branch}\".", localRepo.Head.FriendlyName);
return true;
}
catch (LibGit2SharpException e)
{
_logger.LogError(e, "LibGit2Sharp-specific error while cloning the repository.");
return false;
}
catch (Exception e)
{
_logger.LogError(e, "General error while cloning the repository.");
return false;
}
}
/// <summary>
/// Empties all files and directories from the working folder.
/// </summary>
public void EmptyWorkingFolder()
{
try
{
var directory = new DirectoryInfo(WorkingFolder);
if (!directory.Exists)
{
_logger.LogDebug("The specified directory does not exist: \"{workingFolder}\"", WorkingFolder);
return;
}
// Retrieve all files and directories
var files = directory.EnumerateFiles("*", SearchOption.AllDirectories);
var directories = directory.EnumerateDirectories("*", SearchOption.AllDirectories).OrderByDescending(d => d.FullName.Length);
_logger.LogDebug("Deleting files and directories from \"{workingFolder}\".", WorkingFolder);
// First delete all files
foreach (var file in files)
{
file.Attributes = FileAttributes.Normal;
file.Delete();
}
// Then delete all directories from deepest to shallowest
foreach (var dir in directories)
{
dir.Delete(recursive: true); // Remove directories and their contents
}
_logger.LogDebug("Successfully cleared the working folder: \"{workingFolder}\"", WorkingFolder);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to delete contents of the working folder: \"{workingFolder}\"", WorkingFolder);
}
}
}
\ No newline at end of file
......@@ -2,18 +2,18 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Coscine.GraphDeployer</RootNamespace>
<AssemblyName>Coscine.GraphDeployer</AssemblyName>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>1.2.9</Version>
<Version>1.2.10</Version>
</PropertyGroup>
<PropertyGroup>
<Authors>RWTH Aachen University</Authors>
<Company>IT Center, RWTH Aachen University</Company>
<Copyright>2022 IT Center, RWTH Aachen University</Copyright>
<Copyright>©2024 IT Center, RWTH Aachen University</Copyright>
<Description>GraphDeployer is a part of the Coscine group.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://git.rwth-aachen.de/coscine/backend/scripts/GraphDeployer</PackageProjectUrl>
......@@ -21,16 +21,27 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Coscine.Metadata" Version="*-*" />
<PackageReference Include="dotNetRDF.Data.Virtuoso" Version="2.7.5" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Coscine.ApiClient" Version="1.7.0-issue-2668-graph0001" />
<PackageReference Include="dotNetRdf" Version="3.1.1" />
<PackageReference Include="GitLabApiClient" Version="1.8.1-beta.5" />
<PackageReference Include="LibGit2Sharp" Version="0.26.2" />
<PackageReference Include="NLog" Version="5.1.0" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="LibGit2Sharp" Version="0.30.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="NLog" Version="5.2.8" />
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<ItemGroup>
<None Update="nlog.config" CopyToOutputDirectory="Always" />
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="nlog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
namespace Coscine.GraphDeployer;
public class GraphRepo
{
public string Name { get; set; } = null!;
public string Branch { get; set; } = null!;
public string Url { get; set; } = null!;
}
using System.Security.Cryptography;
namespace Coscine.GraphDeployer
{
public static class HashUtil
{
public static string GetFileHash(string path)
{
using (SHA256 sha256 = SHA256.Create())
{
using (FileStream fileStream = File.OpenRead(path))
{
return BitConverter.ToString(sha256.ComputeHash(fileStream));
}
}
}
}
}
using Polly;
namespace Coscine.GraphDeployer;
public static class Helpers
{
/// <summary>
/// Retry Virtuoso Requests since they sometimes just fail
/// </summary>
/// <typeparam name="W"></typeparam>
/// <param name="function"></param>
/// <returns></returns>
public static void WrapRequest(Action action)
{
Policy
.Handle<Exception>()
.WaitAndRetry(5, retryNumber => TimeSpan.FromMilliseconds(200))
.Execute(() => action.Invoke());
}
/// <summary>
/// Retry Virtuoso Requests since they sometimes just fail
/// </summary>
/// <typeparam name="W"></typeparam>
/// <param name="function"></param>
/// <returns></returns>
public static W WrapRequest<W>(Func<W> function)
{
return Policy
.Handle<Exception>()
.WaitAndRetry(5, retryNumber => TimeSpan.FromMilliseconds(200))
.ExecuteAndCapture(() => function.Invoke()).Result;
}
}
namespace Coscine.GraphDeployer.Models.ConfigurationModels;
public class GraphDeployerConfiguration
{
/// <summary>
/// The section name in the configuration file.
/// </summary>
public static readonly string Section = "GraphDeployerConfiguration";
/// <summary>
/// Value indicating whether the graph deployer is enabled.
/// </summary>
public bool IsEnabled { get; init; }
/// <summary>
/// The working folder where the graph deployer will store the cloned repositories.
/// </summary>
public string? WorkingFolder { get; init; }
/// <summary>
/// Logger configuration settings.
/// </summary>
/// <value>
/// The logger storage configuration settings, or <c>null</c> if not configured.
/// </value>
public LoggerConfiguration? Logger { get; init; }
/// <summary>
/// GitLab configuration settings.
/// </summary>
/// <value>
/// The GitLab configuration settings, or <c>null</c> if not configured.
/// </value>
public GitLabConfiguration GitLab { get; init; } = null!;
/// <summary>
/// Represents the configuration settings for the logger.
/// </summary>
public record LoggerConfiguration(string? LogLevel, string? LogHome);
/// <summary>
/// Represents the configuration settings for GitLab.
/// </summary>
public record GitLabConfiguration
{
public required string HostUrl { get; init; }
public required string Token { get; init; }
public required List<RepositoryConfiguration> Repositories { get; init; }
};
public record RepositoryConfiguration
{
public required string Name { get; init; }
public required Uri Url { get; init; }
public string? Branch { get; init; }
};
}
using Coscine.Configuration;
using Coscine.GraphDeployer.Logging;
using Coscine.Metadata;
using LibGit2Sharp;
using CommandLine;
using Coscine.GraphDeployer.Models.ConfigurationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using NLog.Config;
using NLog;
using NLog.Extensions.Logging;
using System.Diagnostics;
using VDS.RDF;
using VDS.RDF.Storage;
using Winton.Extensions.Configuration.Consul;
using static Coscine.GraphDeployer.Utils.CommandLineOptions;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Coscine.GraphDeployer;
public class Program
{
public static ConsulConfiguration Configuration { get; } = new ConsulConfiguration();
public static string WorkingFolder { get; } = Path.GetFullPath("C:/voc");
public static string GraphsConsulLocation { get; } = "coscine/local/graphs";
private static string Token { get; set; } = string.Empty;
private static IServiceProvider _serviceProvider = null!;
private static ILogger _logger = null!;
public static void Main(string[] args)
{
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("assembly-name", typeof(AssemblyNameLayoutRenderer));
ConfigurationItemFactory.Default.LayoutRenderers.RegisterDefinition("assembly-version", typeof(AssemblyVersionLayoutRenderer));
_logger = LoggerFactory.Create(builder => builder.AddNLog()).CreateLogger<Program>();
try
public static int Main(string[] args)
{
Token = Configuration.GetStringAndWait("coscine/global/gitlabtoken");
Run();
}
catch (Exception ex)
{
// Handle exceptions by logging them in
_logger.LogError(ex, ex.Message);
LogInnerException(ex);
throw;
}
}
InitializeServices();
private static void Run()
{
var currentRun = new Dictionary<string, string>();
var lastRun = JsonConvert.DeserializeObject<Dictionary<string, string>>(Configuration.GetString("coscine/local/graph_deployer/last_run", "{}"));
lastRun ??= new Dictionary<string, string>();
var logger = _serviceProvider.GetRequiredService<ILogger<Program>>();
var virtuosoServer = Configuration.GetString("coscine/local/virtuoso/additional/url");
var virtuosoHost = new Uri(virtuosoServer).Host;
var virtuosoUser = Configuration.GetString("coscine/global/virtuoso_db_user");
var virtuosoPassword = Configuration.GetString("coscine/global/virtuoso_db_password");
var virtuosoManager = new VirtuosoManager($"Server={virtuosoHost};Uid={virtuosoUser};pwd={virtuosoPassword}");
var _rdfStoreConnector = new RdfStoreConnector(virtuosoServer);
_logger.LogInformation("Connecting to Virtuoso Server - {serverUrl}", virtuosoServer);
var virtuosoISQLLocation = Configuration.GetString(
"coscine/local/virtuoso/isql",
"C:/Programs/Virtuoso/bin/isql.exe"
);
// Collect Graphs Types to deploy from Consul
var graphRepos = new List<GraphRepo>();
var graphRepoKeys = Configuration.Keys(GraphsConsulLocation).Select(x =>
{
x = x.Replace($"{GraphsConsulLocation}/", "");
return x[..x.IndexOf('/')];
}).Distinct().ToList();
graphRepoKeys.ForEach(e =>
{
var graphRepo = new GraphRepo()
{
Name = e,
Branch = Configuration.GetStringAndWait(Path.Combine(GraphsConsulLocation, e, "branch")),
Url = Configuration.GetStringAndWait(Path.Combine(GraphsConsulLocation, e, "repositoryurl"))
};
_logger.LogInformation("Found Graph Repository Url - {@graphRepo}", graphRepo);
graphRepos.Add(graphRepo);
});
foreach (var graphRepo in graphRepos)
try
{
_logger.LogInformation("Working with {repoName}", graphRepo.Name.ToUpper());
// Clear contents inside Working Folder
EmptyWorkingFolder();
var parserResult = Parser.Default.ParseArguments<GraphDeployerOptions>(args);
// Clone the repository inside the Working Folder
CloneGraphRepo(graphRepo, out var success);
if (success)
{
// Graph Insertion
var queries = new List<string>();
var turtleFiles = Directory.GetFiles(WorkingFolder, "*.ttl", SearchOption.AllDirectories);
var result = parserResult.MapResult(
(opts) => _serviceProvider.GetRequiredService<Deployer>().RunAsync(opts).Result,
HandleParseError
);
var graphAccumulation = new Dictionary<Uri, (Graph, List<string>)>();
Array.ForEach(turtleFiles, (file) =>
{
var graph = new Graph();
graph.LoadFromFile(file);
if (graphAccumulation.ContainsKey(graph.BaseUri))
if (result)
{
graphAccumulation[graph.BaseUri].Item1.Merge(graph);
graphAccumulation[graph.BaseUri].Item2.Add(file);
logger.LogInformation("Finished.");
return 0; // Exit Code 0 for Success
}
else
{
graphAccumulation.Add(graph.BaseUri, (graph, new List<string>() { file }));
logger.LogInformation("Program execution was interrupted.");
return -1; // Exit Code -1 for Failure
}
});
foreach (var kv in graphAccumulation)
{
var graph = kv.Value.Item1;
var graphName = kv.Key.ToString();
kv.Value.Item2.ForEach((path) => currentRun.TryAdd(graphName + path, HashUtil.GetFileHash(path)));
var changed = kv.Value.Item2.Any((path) =>
!lastRun.ContainsKey(graphName + path) || lastRun[graphName + path] != currentRun[graphName + path]);
if (!changed)
{
_logger.LogInformation("Skipping {graphName}", graphName);
continue;
}
var currentGraph = Helpers.WrapRequest(() => _rdfStoreConnector.GetGraph(graphName));
// Project the triples for easier comparison
var projectedGraph = graph.Triples.Select(ProjectTriple).ToList();
var projectedCurrentGraph = currentGraph.Triples.Select(ProjectTriple).ToList();
var graphWasChanged = graph.Triples.Count != currentGraph.Triples.Count
|| projectedGraph.Except(projectedCurrentGraph).Any();
if (!graphWasChanged)
catch (Exception ex)
{
_logger.LogInformation("Skipping {graphName}", graphName);
continue;
logger.LogError(ex, "Exception: {message}", ex.Message);
return -1; // Exit Code -1 for Failure
}
if (!currentGraph.IsEmpty)
finally
{
Helpers.WrapRequest(() => _rdfStoreConnector.ClearGraph(graphName));
_logger.LogInformation("Cleared Graph {graphName}", graphName);
DisposeServices();
}
else
{
_logger.LogInformation("No Graph {graphName}", graphName);
}
foreach (var file in kv.Value.Item2)
private static void InitializeServices()
{
var fileInfo = new FileInfo(file);
queries.Add($"ld_dir('{fileInfo.DirectoryName[2..].Replace("\\", "/")}', '{fileInfo.Name}', '{graphName}');");
}
}
// Create a new instance of ConfigurationBuilder
var configBuilder = new ConfigurationBuilder();
// Define the Consul URL
var consulUrl = Environment.GetEnvironmentVariable("CONSUL_URL") ?? "http://localhost:8500";
queries.Add($"rdf_loader_run ();");
queries.Add($"DELETE from DB.DBA.load_list where 1=1;");
// Remove the default sources
configBuilder.Sources.Clear();
foreach (var query in queries)
// Add Consul as a configuration source
var configuration = configBuilder
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddConsul(
"coscine/Coscine.Infrastructure/GraphDeployer/appsettings",
options =>
{
ExecuteCommand(
"powershell.exe",
$"\"\\\"{query}\\\" | {virtuosoISQLLocation}\""
);
_logger.LogInformation("Query executed {@query}", query);
}
}
options.ConsulConfigurationOptions =
cco => cco.Address = new Uri(consulUrl);
options.Optional = true;
options.ReloadOnChange = true;
options.PollWaitTime = TimeSpan.FromSeconds(5);
options.OnLoadException = exceptionContext => exceptionContext.Ignore = true;
}
)
.AddEnvironmentVariables()
.Build();
Configuration.Put("coscine/local/graph_deployer/last_run", JsonConvert.SerializeObject(currentRun));
_logger.LogInformation("Done");
}
var services = new ServiceCollection()
.AddSingleton<IConfiguration>(configuration);
private static void ExecuteCommand(string fileName, string arguments)
// Add the configuration to the service collection
services.Configure<GraphDeployerConfiguration>(config =>
{
var startInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var process = new Process
configuration.GetSection(GraphDeployerConfiguration.Section).Bind(config);
});
// Add logging
services.AddLogging(builder =>
{
StartInfo = startInfo
};
process.Start();
builder.ClearProviders();
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddNLog();
});
string output = process.StandardOutput.ReadToEnd();
if (!string.IsNullOrWhiteSpace(output))
_logger.LogDebug("Executed queries {filename} {arguments} >>> {output}", fileName, arguments, output);
var graphDeployerConfiguration = new GraphDeployerConfiguration();
configuration.Bind(GraphDeployerConfiguration.Section, graphDeployerConfiguration);
// Set the default LogLevel
LogManager.Configuration.Variables["logLevel"] = graphDeployerConfiguration.Logger?.LogLevel ?? "Trace";
// Set the log location
LogManager.Configuration.Variables["logHome"] = graphDeployerConfiguration.Logger?.LogHome ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
string errors = process.StandardError.ReadToEnd();
if (!string.IsNullOrWhiteSpace(errors))
_logger.LogError("Errors during execution of command {filename} {arguments} >>> {errors}", fileName, arguments, errors);
// Register the HTTP client
services.AddHttpClient();
// Add services for reporting
services.AddTransient<Deployer>();
_serviceProvider = services.BuildServiceProvider();
}
public static void CloneGraphRepo(GraphRepo graphRepo, out bool success)
private static void DisposeServices()
{
string url = $"https://gitlab-ci-token:{Token}@{graphRepo.Url}";
var cloneOptions = new CloneOptions()
{
BranchName = graphRepo.Branch
};
try
if (_serviceProvider == null)
{
var cloneResult = Repository.Clone(url, WorkingFolder, cloneOptions);
using var repo = new Repository(WorkingFolder);
_logger.LogInformation("Repository successfully cloned and switched to branch {branchName}", repo.Head.FriendlyName);
success = true;
return;
}
catch (Exception ex)
if (_serviceProvider is IDisposable disposable)
{
_logger.LogError(ex, "Cloning Repository {repoUrl} on branch {branchName}", graphRepo.Url, graphRepo.Branch);
success = false;
disposable.Dispose();
}
}
public static void EmptyWorkingFolder()
private static bool HandleParseError(IEnumerable<Error> errs)
{
var logger = _serviceProvider.GetService<ILogger<Program>>();
foreach (var err in errs)
{
var directory = new DirectoryInfo(WorkingFolder);
var filesToDelete = directory.EnumerateFiles("*.*", SearchOption.AllDirectories);
var dirsToDelete = directory.EnumerateDirectories();
_logger.LogInformation("Deleting {fileCount} files and {directoryCount} directories from {workingFolder}.", filesToDelete.Count(), dirsToDelete.Count(), WorkingFolder);
foreach (var file in filesToDelete)
if (err is HelpRequestedError || err is VersionRequestedError)
{
file.Attributes = FileAttributes.Normal;
file.Delete();
// Handle the display of help or version information
// Usually, the library will automatically display the help/version info,
// but you can customize it if needed.
}
foreach (var dir in dirsToDelete)
else
{
dir.Delete(true);
// For other types of errors, you can log them or write them to the console
logger?.LogError("Error encountered parsing command-line options: {Error}", err.ToString());
Console.Error.WriteLine($"Error: {err}");
}
}
private static void LogInnerException(Exception ex)
// Since there were errors, we typically return false to indicate that the program should not proceed
return false;
}
public static TResult SanitizeOptions<TResult>(TResult unsanitizedOptions)
{
// Sanitize all input that accepts an array or is a list of inputs.
if (unsanitizedOptions is not null)
{
if (ex.InnerException is not null)
var type = unsanitizedOptions.GetType();
if (type == typeof(GraphDeployerOptions))
{
_logger.LogError(ex.InnerException, "InnerException: {innerException}", ex.InnerException.Message);
LogInnerException(ex.InnerException);
var options = unsanitizedOptions as GraphDeployerOptions;
if (options is not null)
{
// Sanitize options here
return (TResult)(object)options;
}
}
private static string ProjectTriple(Triple triple)
{
return $"{(triple.Subject.NodeType == NodeType.Blank ? "BLANK" : triple.Subject.ToString())}," +
$"{triple.Predicate.ToString()}," +
$"{(triple.Object.NodeType == NodeType.Blank ? "BLANK" : triple.Object.ToString())}";
}
return unsanitizedOptions;
}
}
\ No newline at end of file
using CommandLine;
namespace Coscine.GraphDeployer.Utils;
public static partial class CommandLineOptions
{
public class GraphDeployerOptions
{
[Option("redeploy", Required = false, Default = false, HelpText = "An argument that tells the program to redeploy all graphs.")]
public bool Redeploy { get; set; }
[Option("dummy", Required = false, Default = false, HelpText = "An argument that tells the program to execute in dummy mode.")]
public bool DummyMode { get; set; }
}
}
\ No newline at end of file
using System.Security.Cryptography;
namespace Coscine.GraphDeployer.Utils;
public static class HashUtil
{
public static string GetFileHash(string path)
{
using SHA256 sha256 = SHA256.Create();
using FileStream fileStream = File.OpenRead(path);
return BitConverter.ToString(sha256.ComputeHash(fileStream));
}
}
\ No newline at end of file
{
"GraphDeployerConfiguration": {
"IsEnabled": true,
"WorkingFolder": "C:/coscine/GraphDeployer/output/",
"Logger": {
"LogLevel": "Information",
"LogHome": "C:/coscine/GraphDeployer/"
},
"GitLab": {
"HostUrl": "https://git.rwth-aachen.de/",
"Token": "glpat-iXFFtZSjmbhn8U4-YFzv",
"Repositories": [
{
"Name": "Application Profiles",
"Url": "https://git.rwth-aachen.de/coscine/graphs/applicationprofiles.git",
"Branch": "master"
},
{
"Name": "Organisations",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organizations.git",
"Branch": "master"
},
{
"Name": "FH Bielefeld",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/00edvg943.git"
},
{
"Name": "HS Düsseldorf",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/00ftx0026.git"
},
{
"Name": "FH Münster",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/00pv45a02.git"
},
{
"Name": "TH Köln",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/014nnvj65.git"
},
{
"Name": "TU Dortmund",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/01k97gp34.git"
},
{
"Name": "HS Ruhr West",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/02nkxrq89.git"
},
{
"Name": "FH Dortmund",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/03dv91853.git"
},
{
"Name": "HS für Gesundheit Bochum",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/03hj8rz96.git"
},
{
"Name": "TH Ostwestfalen-Lippe",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04eka8j06.git"
},
{
"Name": "HS Bonn-Rhein-Sieg",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04m2anh63.git"
},
{
"Name": "University of Duisburg-Essen",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04mz5ra38.git"
},
{
"Name": "Westfälische Hochschule",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04p7ekn23.git"
},
{
"Name": "FH Aachen",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04tqgg260.git"
},
{
"Name": "Ruhr-Universität Bochum",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04tsk2644.git"
},
{
"Name": "Hochschule Rhein-Waal",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04wdt0z89.git"
},
{
"Name": "HS Bochum",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04x02q560.git"
},
{
"Name": "RWTH Aachen University",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/04xfq0f34.git"
},
{
"Name": "TU Darmstadt",
"Url": "https://git.rwth-aachen.de/coscine/graphs/organisations/05n911h24.git"
}
]
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
throwConfigExceptions="true"
throwExceptions="false"
internalLogFile="Logs/internal-nlog.txt"
internalLogLevel="Trace">
autoReload="true"
internalLogLevel="Warn"
internalLogFile=".\internal_logs\internallog.txt">
<variable name="logHome" value="${basedir}/Logs" />
<variable name="logLevel" value="Warn" />
<!--Possible aspnet- variables: https://nlog-project.org/config/?tab=layout-renderers&search=package:nlog.web-->
<variable name="layout" value="${longdate} | [${level:uppercase=true}] ${message} ${exception:format=tostring}" />
<extensions>
<!--Enable NLog.Web for ASP.NET Core.-->
<add assembly="NLog.Web.AspNetCore" />
</extensions>
<targets>
<!-- Write logs to File -->
<target xsi:type="FallbackGroup"
name="fileGroup">
<target
name="fullfile"
xsi:type="File"
name="fileLogD"
fileName="D:/coscine/logs/${assembly-name}/${assembly-version}/log-${shortdate}.log"
maxArchiveFiles="7"
>
<layout xsi:type="CompoundLayout">
<layout xsi:type="JsonLayout" EscapeForwardSlash="true">
<attribute layout="${longdate}" name="Timestamp"/>
<attribute layout="${level:upperCase=true}" name="Level"/>
<attribute layout="${message}" name="Message"/>
<attribute layout="${exception:format=tostring,StackTrace}" name="Exception"/>
<attribute layout="${ndlc}" name="Context"/>
<attribute layout="${event-properties:item=Metric}" name="Alarm" encode="false"/>
<attribute name="EventProperties" encode="false" >
<layout xsi:type='JsonLayout' includeAllProperties="true" maxRecursionLimit="2"/>
</attribute>
</layout>
<layout xsi:type='SimpleLayout' text="," />
</layout>
</target>
<target
xsi:type="File"
name="fileLogC"
fileName="C:/coscine/logs/${assembly-name}/${assembly-version}/log-${shortdate}.log"
layout="${var:layout}"
fileName="${var:logHome}/full_${shortdate}.log"
keepFileOpen="false"
archiveFileName="${var:logHome}/Archive/full_${shortdate}.log"
archiveNumbering="Sequence"
archiveEvery="Day"
maxArchiveFiles="7"
>
<layout xsi:type="CompoundLayout">
<layout xsi:type="JsonLayout" EscapeForwardSlash="true">
<attribute layout="${longdate}" name="Timestamp"/>
<attribute layout="${level:upperCase=true}" name="Level"/>
<attribute layout="${message}" name="Message"/>
<attribute layout="${exception:format=tostring,StackTrace}" name="Exception"/>
<attribute layout="${ndlc}" name="Context"/>
<attribute layout="${event-properties:item=Metric}" name="Alarm" encode="false"/>
<attribute name="EventProperties" encode="false" >
<layout xsi:type='JsonLayout' includeAllProperties="true" maxRecursionLimit="2"/>
</attribute>
</layout>
<layout xsi:type='SimpleLayout' text="," />
</layout>
</target>
/>
</target>
<!-- Write colored logs to Console -->
<target name="consoleLog" xsi:type="ColoredConsole" layout="[${uppercase:${level}}]: ${message}">
<highlight-row condition="level == LogLevel.Debug" foregroundColor="DarkGray" />
<highlight-row condition="level == LogLevel.Info" foregroundColor="White" />
<highlight-row condition="level == LogLevel.Warn" foregroundColor="Yellow" />
<highlight-row condition="level == LogLevel.Error" foregroundColor="DarkRed" />
<highlight-row condition="level == LogLevel.Fatal" foregroundColor="Red" backgroundColor="White" />
<highlight-word text="[TRACE]" foregroundColor="DarkGray" />
<highlight-word text="[Debug]" foregroundColor="Gray" />
<highlight-word text="[INFO]" foregroundColor="Green" />
<highlight-word text="[WARN]" foregroundColor="Yellow" />
<highlight-word text="[ERROR]" foregroundColor="Red" />
<highlight-word text="[FATAL]" foregroundColor="DarkRed" backgroundColor="White" />
</target>
</targets>
<rules>
<!--All logs, including from Microsoft, Level Trace-->
<logger name="*" minlevel="Trace" writeTo="fileGroup">
</logger>
<!--All logs, including Microsoft-->
<logger name="*" minlevel="${var:logLevel}" writeTo="fullfile" />
<!--All logs, including from Microsoft, Level Info-->
<logger name="*" minlevel="Info" writeTo="consoleLog">
<filters defaultAction="Log">
<when condition="contains('${ndlc}','/api/heartbeat')" action="Ignore"/>
</filters>
</logger>
<!--Skip non-critical Microsoft logs and so log only own logs (BlackHole).-->
<logger name="Microsoft.*" maxlevel="Info" final="true" />
<logger name="System.Net.Http.*" maxlevel="Info" final="true" />
<!--All logs, including from Microsoft, Level Info-->
<logger name="*" minlevel="${var:logLevel}" writeTo="consoleLog" />
</rules>
</nlog>
\ No newline at end of file