Skip to content
Snippets Groups Projects
Select Git revision
  • master
  • ci
2 results

tclient.cpp

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    Deployer.cs 12.90 KiB
    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
    {
        private readonly ILogger<Deployer> _logger;
        private readonly GraphDeployerConfiguration _graphDeployerConfiguration;
        private readonly AdminApi _adminApi;
    
        public Deployer(ILogger<Deployer> logger, IOptionsMonitor<GraphDeployerConfiguration> graphDeployerConfiguration)
        {
            _logger = logger;
            _graphDeployerConfiguration = graphDeployerConfiguration.CurrentValue;
            // Build the configuration for the API client based on the configuration settings
            var apiClientConfig = new Configuration
            {
                BasePath = $"{_graphDeployerConfiguration.Endpoint.TrimEnd('/')}/coscine",
                ApiKeyPrefix = { { "Authorization", "Bearer" } },
                ApiKey = { { "Authorization", _graphDeployerConfiguration.ApiKey } },
                Timeout = 300000 // 5 minutes
            };
    
            // Check if the graph deployer has to skip SSL checks when connecting to the API
            if (_graphDeployerConfiguration.SkipSslCheck)
            {
                _logger.LogInformation("Skipping SSL certificate validation...");
                // Skip SSL certificate validation
                apiClientConfig.RemoteCertificateValidationCallback = (_, _, _, _) => true;
            }
            _adminApi = new(apiClientConfig);
        }
    
        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.");
            }
            _logger.LogDebug("Redeploy: {redeploy}", opts.Redeploy);
    
            // 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.Ref);
    
                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>();
    
                        _logger.LogDebug("Deploying graph: {graphName}", graphId);
    
                        // 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;
    
                        if (hasChanged) 
                        {
                            _logger.LogDebug("The graph has changed");
                        } else 
                        {
                            _logger.LogDebug("The graph has not changed");
                        }
    
                        if(deployedGraph is null)
                        {
                            _logger.LogDebug("Deployed graph is null");
                        } else {
                            _logger.LogDebug("Deployed hash: {hash}", string.Join(',', deployedGraph.FileHashes));
                        }
    
                        _logger.LogDebug("Incoming hash: {hash}", currentRun[graphId]);
    
                        // 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.LogDebug("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();
    
                // Perform the clone operation
                _logger.LogDebug("Starting clone of repository: \"{projectUrl}\" into \"{projectPath}\"", projectUrl, projectPath);
                var repo = Repository.Clone(url, projectPath);
                var localRepo = new Repository(repo);
    
                // First clone on, then checkout, as direct cloning of commit SHA is not supported
                if (!string.IsNullOrWhiteSpace(branchName))
                {
                    Commands.Checkout(localRepo, branchName);
                }
    
                // Retrieve the reference of the repository, either the branch name or the commit hash
                var repoRef = localRepo.Head.IsTracking ? localRepo.Head.FriendlyName : localRepo.Head.Tip.Sha;
                _logger.LogInformation("Repository successfully cloned and switched on ref \"{ref}\".", repoRef);
    
                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);
            }
        }
    }