Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Core/Services/GitTemplateService.cs
Matt Batchelder 45c94b6536
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
feat: Add main application views and structure
- Implemented CreateInstanceView for creating new instances.
- Added HostsView for managing SSH hosts with CRUD operations.
- Created InstancesView for displaying and managing instances.
- Developed LogsView for viewing operation logs.
- Introduced SecretsView for managing secrets associated with hosts.
- Established SettingsView for configuring application settings.
- Created MainWindow as the main application window with navigation.
- Added app manifest and configuration files for logging and settings.
2026-02-18 10:43:27 -05:00

162 lines
5.5 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");
var envPath = FindFile(cacheDir, "template.env");
if (yamlPath == null)
throw new FileNotFoundException("template.yml not found in repository root.");
if (envPath == null)
throw new FileNotFoundException("template.env not found in repository root.");
var yaml = await File.ReadAllTextAsync(yamlPath);
var envLines = (await File.ReadAllLinesAsync(envPath))
.Where(l => !string.IsNullOrWhiteSpace(l) && !l.TrimStart().StartsWith('#'))
.ToList();
return new TemplateConfig
{
Yaml = yaml,
EnvLines = envLines,
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();
}
}