chore: update project structure with contracts and services for CaddyManager, including configuration and Docker integration
All checks were successful
Caddy Manager CI build / docker (push) Successful in 1m16s
All checks were successful
Caddy Manager CI build / docker (push) Successful in 1m16s
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using CaddyManager.Contracts.Caddy;
|
||||
|
||||
namespace CaddyManager.Services.Caddy;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class CaddyConfigurationParsingService: ICaddyConfigurationParsingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex to help parse hostnames from a Caddyfile.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[GeneratedRegex(@"(?m)^[\w.-]+(?:\s*,\s*[\w.-]+)*(?=\s*\{)", RegexOptions.Multiline)]
|
||||
private static partial Regex HostnamesRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Regex to help parse hostnames being used in reverse proxy directives.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[GeneratedRegex(@"(?m)reverse_proxy .*", RegexOptions.Multiline)]
|
||||
private static partial Regex ReverseProxyRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<string> GetHostnamesFromCaddyfileContent(string caddyfileContent)
|
||||
{
|
||||
var hostnamesRegex = HostnamesRegex();
|
||||
var matches = hostnamesRegex.Matches(caddyfileContent);
|
||||
var hostnames = new List<string>();
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
// Split the matched string by commas and trim whitespace
|
||||
var splitHostnames = match.Value.Split(',')
|
||||
.Select(h => h.Trim())
|
||||
.Where(h => !string.IsNullOrWhiteSpace(h))
|
||||
.ToList();
|
||||
|
||||
hostnames.AddRange(splitHostnames);
|
||||
}
|
||||
// Remove duplicates and return the list
|
||||
return hostnames.Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetReverseProxyTargetFromCaddyfileContent(string caddyfileContent)
|
||||
{
|
||||
var reverseProxyRegex = ReverseProxyRegex();
|
||||
var match = reverseProxyRegex.Match(caddyfileContent);
|
||||
if (!match.Success) return string.Empty;
|
||||
|
||||
var parts = match.Value.TrimEnd('}').Trim().Split(' ');
|
||||
var targetPart = parts.LastOrDefault(string.Empty);
|
||||
if (string.IsNullOrEmpty(targetPart)) return string.Empty;
|
||||
|
||||
var targetComponents = targetPart.Split(':');
|
||||
if (targetComponents.Length <= 1) return targetPart;
|
||||
|
||||
// Handle cases like http://backend:9000, 192.168.1.100:3000
|
||||
return targetComponents[0];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<int> GetReverseProxyPortsFromCaddyfileContent(string caddyfileContent)
|
||||
{
|
||||
var reverseProxyRegex = ReverseProxyRegex();
|
||||
var matches = reverseProxyRegex.Matches(caddyfileContent);
|
||||
var results = new List<int>();
|
||||
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var parts = match.Value.TrimEnd('}').Trim().Split(' ');
|
||||
var targetPart = parts.LastOrDefault(string.Empty);
|
||||
if (string.IsNullOrEmpty(targetPart)) continue;
|
||||
|
||||
var targetComponents = targetPart.Split(':');
|
||||
if (targetComponents.Length > 1 && int.TryParse(targetComponents.Last(), out int port))
|
||||
{
|
||||
results.Add(port);
|
||||
}
|
||||
}
|
||||
|
||||
return results.Distinct().ToList();
|
||||
}
|
||||
}
|
||||
162
CaddyManager.Services/Caddy/CaddyService.cs
Normal file
162
CaddyManager.Services/Caddy/CaddyService.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using CaddyManager.Contracts.Configurations.Caddy;
|
||||
using CaddyManager.Contracts.Caddy;
|
||||
using CaddyManager.Contracts.Configurations;
|
||||
using CaddyManager.Contracts.Models.Caddy;
|
||||
|
||||
namespace CaddyManager.Services.Caddy;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class CaddyService(
|
||||
IConfigurationsService configurationsService,
|
||||
ICaddyConfigurationParsingService parsingService) : ICaddyService
|
||||
{
|
||||
/// <summary>
|
||||
/// File name of the global configuration Caddyfile
|
||||
/// </summary>
|
||||
private const string CaddyGlobalConfigName = "Caddyfile";
|
||||
|
||||
private CaddyServiceConfigurations Configurations => configurationsService.Get<CaddyServiceConfigurations>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<CaddyConfigurationInfo> GetExistingCaddyConfigurations()
|
||||
{
|
||||
if (!Directory.Exists(Configurations.ConfigDir))
|
||||
{
|
||||
Directory.CreateDirectory(Configurations.ConfigDir);
|
||||
}
|
||||
|
||||
return [.. Directory.GetFiles(Configurations.ConfigDir)
|
||||
.Where(filePath => Path.GetFileName(filePath) != CaddyGlobalConfigName)
|
||||
.Select(filePath =>
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
var info = GetCaddyConfigurationInfo(fileName);
|
||||
info.FileName = fileName;
|
||||
return info;
|
||||
})
|
||||
.OrderBy(info => info.FileName)];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCaddyConfigurationContent(string configurationName)
|
||||
{
|
||||
var path = configurationName == CaddyGlobalConfigName
|
||||
? Path.Combine(Configurations.ConfigDir, CaddyGlobalConfigName)
|
||||
: Path.Combine(Configurations.ConfigDir, $"{configurationName}.caddy");
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCaddyGlobalConfigurationContent() => GetCaddyConfigurationContent(CaddyGlobalConfigName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaddyOperationResponse SaveCaddyConfiguration(CaddySaveConfigurationRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.FileName))
|
||||
{
|
||||
return new CaddyOperationResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "The configuration file name is required"
|
||||
};
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(Configurations.ConfigDir,
|
||||
request.FileName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{request.FileName}.caddy");
|
||||
// if in the new mode, we would have to check if the file already exists
|
||||
if (request.IsNew && File.Exists(filePath))
|
||||
{
|
||||
return new CaddyOperationResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "The configuration file already exists"
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(filePath, request.Content);
|
||||
return new CaddyOperationResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "Configuration file saved successfully"
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new CaddyOperationResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = e.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaddyOperationResponse SaveCaddyGlobalConfiguration(string content) => SaveCaddyConfiguration(
|
||||
new CaddySaveConfigurationRequest
|
||||
{
|
||||
FileName = CaddyGlobalConfigName,
|
||||
Content = content
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaddyDeleteOperationResponse DeleteCaddyConfigurations(List<string> configurationNames)
|
||||
{
|
||||
var failed = new List<string>();
|
||||
|
||||
foreach (var configurationName in configurationNames)
|
||||
{
|
||||
var filePath = Path.Combine(Configurations.ConfigDir,
|
||||
configurationName == CaddyGlobalConfigName ? CaddyGlobalConfigName : $"{configurationName}.caddy");
|
||||
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
failed.Add(configurationName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
failed.Add(configurationName);
|
||||
}
|
||||
}
|
||||
|
||||
return new CaddyDeleteOperationResponse
|
||||
{
|
||||
Success = failed.Count == 0,
|
||||
Message = failed.Count == 0
|
||||
? "Configuration(s) deleted successfully"
|
||||
: $"Failed to delete the following configuration(s): {string.Join(", ", failed)}",
|
||||
DeletedConfigurations = configurationNames.Except(failed).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaddyConfigurationInfo GetCaddyConfigurationInfo(string configurationName)
|
||||
{
|
||||
var result = new CaddyConfigurationInfo();
|
||||
var content = GetCaddyConfigurationContent(configurationName);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Hostnames = parsingService.GetHostnamesFromCaddyfileContent(content);
|
||||
result.ReverseProxyHostname = parsingService.GetReverseProxyTargetFromCaddyfileContent(content);
|
||||
result.ReverseProxyPorts = parsingService.GetReverseProxyPortsFromCaddyfileContent(content);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
23
CaddyManager.Services/CaddyManager.Services.csproj
Normal file
23
CaddyManager.Services/CaddyManager.Services.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CaddyManager.Contracts\CaddyManager.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetCore.AutoRegisterDi" Version="2.2.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
||||
<PackageReference Include="Docker.DotNet" Version="3.125.15" />
|
||||
<PackageReference Include="Humanizer" Version="3.0.0-beta.96" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
using NetCore.AutoRegisterDi;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using CaddyManager.Contracts.Configurations.Caddy;
|
||||
using CaddyManager.Contracts.Configurations.Docker;
|
||||
using CaddyManager.Contracts.Configurations;
|
||||
|
||||
namespace CaddyManager.Services.Configurations;
|
||||
|
||||
/// <inheritdoc />
|
||||
[RegisterAsSingleton]
|
||||
public class ConfigurationsService(IConfiguration configuration) : IConfigurationsService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public T Get<T>() where T : class
|
||||
{
|
||||
var section = typeof(T).Name;
|
||||
|
||||
// Have the configuration section name be the section name without the "Configurations" suffix
|
||||
if (section.EndsWith("Configurations"))
|
||||
section = section[..^"Configurations".Length];
|
||||
else if (section.EndsWith("Configuration"))
|
||||
section = section[..^"Configuration".Length];
|
||||
|
||||
return configuration.GetSection(section).Get<T>() ?? Activator.CreateInstance<T>();
|
||||
}
|
||||
}
|
||||
48
CaddyManager.Services/Docker/DockerService.cs
Normal file
48
CaddyManager.Services/Docker/DockerService.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using CaddyManager.Contracts.Configurations.Docker;
|
||||
using CaddyManager.Contracts.Configurations;
|
||||
using CaddyManager.Contracts.Docker;
|
||||
|
||||
namespace CaddyManager.Services.Docker;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class DockerService(IConfigurationsService configurationsService) : IDockerService
|
||||
{
|
||||
private DockerServiceConfiguration Configuration => configurationsService.Get<DockerServiceConfiguration>();
|
||||
|
||||
/// <summary>
|
||||
/// Method to get the container id of the Caddy container by the name configured
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<string> GetCaddyContainerId()
|
||||
{
|
||||
var client = new DockerClientConfiguration(new Uri(Configuration.DockerHostWithEnvCheck)).CreateClient();
|
||||
|
||||
if (client == null) return string.Empty;
|
||||
|
||||
var containers = await client.Containers.ListContainersAsync(new ContainersListParameters
|
||||
{
|
||||
All = true
|
||||
});
|
||||
|
||||
return containers.FirstOrDefault(container => container.Names.Contains($"/{Configuration.CaddyContainerName}"))
|
||||
?.ID ?? string.Empty;
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RestartCaddyContainerAsync()
|
||||
{
|
||||
var containerId = await GetCaddyContainerId();
|
||||
|
||||
if (string.IsNullOrEmpty(containerId)) return;
|
||||
|
||||
var client = new DockerClientConfiguration(new Uri(Configuration.DockerHostWithEnvCheck)).CreateClient();
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
await client.Containers.RestartContainerAsync(containerId, new ContainerRestartParameters());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user