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

This commit is contained in:
2025-07-23 10:37:51 +07:00
parent 18c710d341
commit ec454d0346
56 changed files with 8511 additions and 34 deletions

View File

@@ -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();
}
}

View 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;
}
}

View 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>

View File

@@ -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>();
}
}

View 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());
}
}
}