Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
155 lines
5.2 KiB
C#
155 lines
5.2 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using LibGit2Sharp;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using OTSSignsOrchestrator.Core.Configuration;
|
|
using OTSSignsOrchestrator.Core.Models.DTOs;
|
|
|
|
namespace OTSSignsOrchestrator.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Fetches template.yml and template.env from a Git repository using LibGit2Sharp.
|
|
/// Caches clones on disk, keyed by SHA-256 hash of the repo URL.
|
|
/// </summary>
|
|
public class GitTemplateService
|
|
{
|
|
private readonly GitOptions _options;
|
|
private readonly ILogger<GitTemplateService> _logger;
|
|
|
|
public GitTemplateService(IOptions<GitOptions> options, ILogger<GitTemplateService> logger)
|
|
{
|
|
_options = options.Value;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<TemplateConfig> FetchAsync(string repoUrl, string? pat = null, bool forceRefresh = false)
|
|
{
|
|
var cacheKey = ComputeCacheKey(repoUrl);
|
|
var cacheDir = Path.Combine(_options.CacheDir, cacheKey);
|
|
|
|
_logger.LogInformation("Fetching templates from repo (cacheKey={CacheKey}, force={Force})", cacheKey, forceRefresh);
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
if (!Directory.Exists(cacheDir) || !Repository.IsValid(cacheDir))
|
|
{
|
|
CloneRepo(repoUrl, pat, cacheDir);
|
|
}
|
|
else if (forceRefresh || IsCacheStale(cacheDir))
|
|
{
|
|
FetchLatest(repoUrl, pat, cacheDir);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Using cached clone at {CacheDir}", cacheDir);
|
|
}
|
|
});
|
|
|
|
var yamlPath = FindFile(cacheDir, "template.yml");
|
|
|
|
if (yamlPath == null)
|
|
throw new FileNotFoundException("template.yml not found in repository root. Commit the template file produced by ComposeRenderService.GetTemplateYaml() to the repo root.");
|
|
|
|
var yaml = await File.ReadAllTextAsync(yamlPath);
|
|
|
|
return new TemplateConfig
|
|
{
|
|
Yaml = yaml,
|
|
FetchedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
private void CloneRepo(string repoUrl, string? pat, string cacheDir)
|
|
{
|
|
_logger.LogInformation("Shallow cloning repo to {CacheDir}", cacheDir);
|
|
|
|
if (Directory.Exists(cacheDir))
|
|
Directory.Delete(cacheDir, recursive: true);
|
|
|
|
Directory.CreateDirectory(cacheDir);
|
|
|
|
var cloneOpts = new CloneOptions { IsBare = false, RecurseSubmodules = false };
|
|
|
|
if (!string.IsNullOrEmpty(pat))
|
|
{
|
|
cloneOpts.FetchOptions.CredentialsProvider = (_, _, _) =>
|
|
new UsernamePasswordCredentials { Username = pat, Password = string.Empty };
|
|
}
|
|
|
|
try
|
|
{
|
|
Repository.Clone(repoUrl, cacheDir, cloneOpts);
|
|
}
|
|
catch (LibGit2SharpException ex)
|
|
{
|
|
_logger.LogError(ex, "Git clone failed for repo (cacheKey={CacheKey})", ComputeCacheKey(repoUrl));
|
|
throw new InvalidOperationException($"Failed to clone repository. Check URL and credentials. Error: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
private void FetchLatest(string repoUrl, string? pat, string cacheDir)
|
|
{
|
|
_logger.LogInformation("Fetching latest from origin in {CacheDir}", cacheDir);
|
|
|
|
try
|
|
{
|
|
using var repo = new Repository(cacheDir);
|
|
|
|
var fetchOpts = new FetchOptions();
|
|
if (!string.IsNullOrEmpty(pat))
|
|
{
|
|
fetchOpts.CredentialsProvider = (_, _, _) =>
|
|
new UsernamePasswordCredentials { Username = pat, Password = string.Empty };
|
|
}
|
|
|
|
var remote = repo.Network.Remotes["origin"];
|
|
var refSpecs = remote.FetchRefSpecs.Select(x => x.Specification).ToArray();
|
|
Commands.Fetch(repo, "origin", refSpecs, fetchOpts, "Auto-fetch for template update");
|
|
|
|
var trackingBranch = repo.Head.TrackedBranch;
|
|
if (trackingBranch != null)
|
|
{
|
|
repo.Reset(ResetMode.Hard, trackingBranch.Tip);
|
|
}
|
|
|
|
WriteCacheTimestamp(cacheDir);
|
|
}
|
|
catch (LibGit2SharpException ex)
|
|
{
|
|
_logger.LogError(ex, "Git fetch failed for repo at {CacheDir}", cacheDir);
|
|
throw new InvalidOperationException($"Failed to fetch latest templates. Error: {ex.Message}", ex);
|
|
}
|
|
}
|
|
|
|
private bool IsCacheStale(string cacheDir)
|
|
{
|
|
var stampFile = Path.Combine(cacheDir, ".fetch-timestamp");
|
|
if (!File.Exists(stampFile)) return true;
|
|
|
|
if (DateTime.TryParse(File.ReadAllText(stampFile), out var lastFetch))
|
|
{
|
|
return (DateTime.UtcNow - lastFetch).TotalMinutes > _options.CacheTtlMinutes;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static void WriteCacheTimestamp(string cacheDir)
|
|
{
|
|
var stampFile = Path.Combine(cacheDir, ".fetch-timestamp");
|
|
File.WriteAllText(stampFile, DateTime.UtcNow.ToString("O"));
|
|
}
|
|
|
|
private static string? FindFile(string dir, string fileName)
|
|
{
|
|
var path = Path.Combine(dir, fileName);
|
|
return File.Exists(path) ? path : null;
|
|
}
|
|
|
|
private static string ComputeCacheKey(string repoUrl)
|
|
{
|
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(repoUrl));
|
|
return Convert.ToHexString(hash)[..16].ToLowerInvariant();
|
|
}
|
|
}
|