wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park. Preserved from fragile git stash to named branch 2026-08-23. NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree. Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using DashCaddy.Desktop.Models;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public class ApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public ApiClient(string baseUrl)
|
||||
{
|
||||
_baseUrl = baseUrl.TrimEnd('/');
|
||||
_httpClient = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri(_baseUrl),
|
||||
Timeout = TimeSpan.FromSeconds(30)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<ServiceModel>> GetServicesAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/v1/services");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<ServiceModel>>>();
|
||||
return result?.Data ?? new List<ServiceModel>();
|
||||
}
|
||||
|
||||
public async Task<ServiceModel> GetServiceAsync(string id)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/services/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
|
||||
return result?.Data;
|
||||
}
|
||||
|
||||
public async Task<ServiceModel> CreateServiceAsync(ServiceModel service)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/v1/services", service);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
|
||||
return result?.Data;
|
||||
}
|
||||
|
||||
public async Task<ServiceModel> UpdateServiceAsync(string id, ServiceModel service)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"/api/v1/services/{id}", service);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<ServiceModel>>();
|
||||
return result?.Data;
|
||||
}
|
||||
|
||||
public async Task RemoveServiceAsync(string id)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/v1/services/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
public async Task<HealthResponse> GetHealthAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/v1/health");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<HealthResponse>>();
|
||||
return result?.Data ?? new HealthResponse();
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, string>> GetConfigAsync()
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/v1/config");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<Dictionary<string, string>>>();
|
||||
return result?.Data ?? new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public async Task UpdateConfigAsync(Dictionary<string, string> config)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync("/api/v1/config", config);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T Data { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text;
|
||||
using DashCaddy.Desktop.Models;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public class CaddyConfigGenerator
|
||||
{
|
||||
private const string CaddyfilePath = @"E:\dockerdata\dashcaddy\caddy\Caddyfile";
|
||||
|
||||
public void RegenerateConfig(List<ServiceModel> services)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Global options
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" admin :2019");
|
||||
sb.AppendLine(" email admin@example.com");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
|
||||
// Each service gets a site block
|
||||
foreach (var svc in services.Where(s => s.Health != "unhealthy"))
|
||||
{
|
||||
var domain = ExtractDomain(svc.Url);
|
||||
if (string.IsNullOrEmpty(domain)) continue;
|
||||
|
||||
sb.AppendLine($"{domain} {{");
|
||||
sb.AppendLine($" reverse_proxy {svc.Host}:{svc.Port}");
|
||||
sb.AppendLine(" tls internal");
|
||||
sb.AppendLine("}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
File.WriteAllText(CaddyfilePath, sb.ToString());
|
||||
}
|
||||
|
||||
public async Task ReloadCaddyAsync()
|
||||
{
|
||||
// Call Caddy admin API to reload config
|
||||
using var http = new HttpClient();
|
||||
await http.PostAsync("http://localhost:2019/load",
|
||||
new StringContent(File.ReadAllText(CaddyfilePath), Encoding.UTF8, "text/caddyfile"));
|
||||
}
|
||||
|
||||
private string ExtractDomain(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
return uri.Host;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using System.Net.Http.Json;
|
||||
using DashCaddy.Desktop.Models;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public enum DnsProviderType
|
||||
{
|
||||
None, // No DNS management - use existing DNS
|
||||
Technitium,
|
||||
CoreDNS,
|
||||
Cloudflare,
|
||||
Route53,
|
||||
Custom
|
||||
}
|
||||
|
||||
public class DnsProviderConfig
|
||||
{
|
||||
public DnsProviderType Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
public string ApiKey { get; set; }
|
||||
public string ZoneId { get; set; }
|
||||
public Dictionary<string, string> Extra { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DnsClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly DnsProviderConfig _config;
|
||||
|
||||
public DnsProviderType ProviderType => _config?.Type ?? DnsProviderType.None;
|
||||
|
||||
public DnsClient(DnsProviderConfig config)
|
||||
{
|
||||
_config = config;
|
||||
if (config?.Type != DnsProviderType.None)
|
||||
{
|
||||
_httpClient = new HttpClient { BaseAddress = new Uri(config.BaseUrl) };
|
||||
if (!string.IsNullOrEmpty(config.ApiKey))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static DnsClient Create(DnsProviderType type, Dictionary<string, string> settings)
|
||||
{
|
||||
if (type == DnsProviderType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var config = type switch
|
||||
{
|
||||
DnsProviderType.Technitium => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Technitium,
|
||||
Name = "Technitium DNS",
|
||||
BaseUrl = settings.GetValueOrDefault("technitium_url", "http://localhost:5380/api"),
|
||||
ApiKey = settings.GetValueOrDefault("technitium_api_key", ""),
|
||||
ZoneId = settings.GetValueOrDefault("technitium_zone", "")
|
||||
},
|
||||
DnsProviderType.CoreDNS => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.CoreDNS,
|
||||
Name = "CoreDNS",
|
||||
BaseUrl = settings.GetValueOrDefault("coredns_api_url", "http://localhost:8080"),
|
||||
ApiKey = "",
|
||||
ZoneId = settings.GetValueOrDefault("coredns_zone", "local")
|
||||
},
|
||||
DnsProviderType.Cloudflare => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Cloudflare,
|
||||
Name = "Cloudflare",
|
||||
BaseUrl = "https://api.cloudflare.com/client/v4",
|
||||
ApiKey = settings.GetValueOrDefault("cloudflare_api_token", ""),
|
||||
ZoneId = settings.GetValueOrDefault("cloudflare_zone_id", "")
|
||||
},
|
||||
DnsProviderType.Route53 => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Route53,
|
||||
Name = "AWS Route 53",
|
||||
BaseUrl = "https://route53.amazonaws.com",
|
||||
ApiKey = settings.GetValueOrDefault("aws_access_key", ""),
|
||||
Extra = new Dictionary<string, string>
|
||||
{
|
||||
["secret_key"] = settings.GetValueOrDefault("aws_secret_key", ""),
|
||||
["region"] = settings.GetValueOrDefault("aws_region", "us-east-1")
|
||||
}
|
||||
},
|
||||
_ => new DnsProviderConfig
|
||||
{
|
||||
Type = DnsProviderType.Custom,
|
||||
Name = "Custom DNS",
|
||||
BaseUrl = settings.GetValueOrDefault("custom_dns_url", ""),
|
||||
ApiKey = settings.GetValueOrDefault("custom_dns_key", "")
|
||||
}
|
||||
};
|
||||
|
||||
return new DnsClient(config);
|
||||
}
|
||||
|
||||
public async Task<List<DnsRecord>> GetRecordsAsync(string zone = null)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return new List<DnsRecord>();
|
||||
|
||||
zone ??= _config?.ZoneId;
|
||||
if (string.IsNullOrEmpty(zone)) return new List<DnsRecord>();
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await GetTechnitiumRecordsAsync(zone),
|
||||
DnsProviderType.CoreDNS => await GetCoreDnsRecordsAsync(zone),
|
||||
DnsProviderType.Cloudflare => await GetCloudflareRecordsAsync(zone),
|
||||
DnsProviderType.Route53 => await GetRoute53RecordsAsync(zone),
|
||||
_ => new List<DnsRecord>()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DnsRecord> CreateRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return record;
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await CreateTechnitiumRecordAsync(zone, record),
|
||||
DnsProviderType.CoreDNS => await CreateCoreDnsRecordAsync(zone, record),
|
||||
DnsProviderType.Cloudflare => await CreateCloudflareRecordAsync(zone, record),
|
||||
DnsProviderType.Route53 => await CreateRoute53RecordAsync(zone, record),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<DnsRecord> UpdateRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return record;
|
||||
|
||||
return _config.Type switch
|
||||
{
|
||||
DnsProviderType.Technitium => await UpdateTechnitiumRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.CoreDNS => await UpdateCoreDnsRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.Cloudflare => await UpdateCloudflareRecordAsync(zone, recordId, record),
|
||||
DnsProviderType.Route53 => await UpdateRoute53RecordAsync(zone, recordId, record),
|
||||
_ => record
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteRecordAsync(string zone, string recordId)
|
||||
{
|
||||
if (_config?.Type == DnsProviderType.None) return;
|
||||
|
||||
switch (_config.Type)
|
||||
{
|
||||
case DnsProviderType.Technitium:
|
||||
await DeleteTechnitiumRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.CoreDNS:
|
||||
await DeleteCoreDnsRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.Cloudflare:
|
||||
await DeleteCloudflareRecordAsync(zone, recordId);
|
||||
break;
|
||||
case DnsProviderType.Route53:
|
||||
await DeleteRoute53RecordAsync(zone, recordId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Technitium ───
|
||||
private async Task<List<DnsRecord>> GetTechnitiumRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/zones/{zone}/records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<TechnitiumZoneResponse>();
|
||||
return result?.Records?.Select(r => new DnsRecord
|
||||
{
|
||||
Id = r.Id,
|
||||
Name = r.Name,
|
||||
Type = r.Type,
|
||||
Content = r.Value,
|
||||
Ttl = r.Ttl
|
||||
}).ToList() ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateTechnitiumRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var req = new TechnitiumRecordRequest
|
||||
{
|
||||
Name = record.Name,
|
||||
Type = record.Type,
|
||||
Value = record.Content,
|
||||
Ttl = record.Ttl
|
||||
};
|
||||
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/records", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<TechnitiumRecordResponse>();
|
||||
record.Id = result?.Record?.Id;
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateTechnitiumRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var req = new TechnitiumRecordRequest
|
||||
{
|
||||
Name = record.Name,
|
||||
Type = record.Type,
|
||||
Value = record.Content,
|
||||
Ttl = record.Ttl
|
||||
};
|
||||
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/records/{recordId}", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task DeleteTechnitiumRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/zones/{zone}/records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── CoreDNS (via API wrapper) ───
|
||||
private async Task<List<DnsRecord>> GetCoreDnsRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/dns/zones/{zone}/records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<List<DnsRecord>>>();
|
||||
return result?.Data ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateCoreDnsRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync($"/api/v1/dns/zones/{zone}/records", record);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
|
||||
return result?.Data ?? record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateCoreDnsRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"/api/v1/dns/zones/{zone}/records/{recordId}", record);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<DnsRecord>>();
|
||||
return result?.Data ?? record;
|
||||
}
|
||||
|
||||
private async Task DeleteCoreDnsRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/v1/dns/zones/{zone}/records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── Cloudflare ───
|
||||
private async Task<List<DnsRecord>> GetCloudflareRecordsAsync(string zone)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/zones/{zone}/dns_records");
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<CloudflareResponse>();
|
||||
return result?.Result?.Select(r => new DnsRecord
|
||||
{
|
||||
Id = r.Id,
|
||||
Name = r.Name,
|
||||
Type = r.Type,
|
||||
Content = r.Content,
|
||||
Ttl = r.Ttl ?? 300,
|
||||
Proxied = r.Proxied ?? false
|
||||
}).ToList() ?? new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateCloudflareRecordAsync(string zone, DnsRecord record)
|
||||
{
|
||||
var req = new CloudflareRecordRequest
|
||||
{
|
||||
Type = record.Type,
|
||||
Name = record.Name,
|
||||
Content = record.Content,
|
||||
Ttl = record.Ttl,
|
||||
Proxied = record.Proxied
|
||||
};
|
||||
var response = await _httpClient.PostAsJsonAsync($"/zones/{zone}/dns_records", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var result = await response.Content.ReadFromJsonAsync<CloudflareSingleResponse>();
|
||||
record.Id = result?.Result?.Id;
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> UpdateCloudflareRecordAsync(string zone, string recordId, DnsRecord record)
|
||||
{
|
||||
var req = new CloudflareRecordRequest
|
||||
{
|
||||
Type = record.Type,
|
||||
Name = record.Name,
|
||||
Content = record.Content,
|
||||
Ttl = record.Ttl,
|
||||
Proxied = record.Proxied
|
||||
};
|
||||
var response = await _httpClient.PutAsJsonAsync($"/zones/{zone}/dns_records/{recordId}", req);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return record;
|
||||
}
|
||||
|
||||
private async Task DeleteCloudflareRecordAsync(string zone, string recordId)
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/zones/{zone}/dns_records/{recordId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
// ─── Route 53 ───
|
||||
private async Task<List<DnsRecord>> GetRoute53RecordsAsync(string zone)
|
||||
{
|
||||
return new List<DnsRecord>();
|
||||
}
|
||||
|
||||
private async Task<DnsRecord> CreateRoute53RecordAsync(string zone, DnsRecord record) => record;
|
||||
private async Task<DnsRecord> UpdateRoute53RecordAsync(string zone, string recordId, DnsRecord record) => record;
|
||||
private async Task DeleteRoute53RecordAsync(string zone, string recordId) { }
|
||||
|
||||
// ─── Response DTOs ───
|
||||
private class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T Data { get; set; }
|
||||
public string Error { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumZoneResponse
|
||||
{
|
||||
public List<TechnitiumRecord> Records { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecord
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecordRequest
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
}
|
||||
|
||||
private class TechnitiumRecordResponse
|
||||
{
|
||||
public TechnitiumRecord Record { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public List<CloudflareRecord> Result { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareRecord
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int? Ttl { get; set; }
|
||||
public bool? Proxied { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareRecordRequest
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Content { get; set; }
|
||||
public int Ttl { get; set; }
|
||||
public bool Proxied { get; set; }
|
||||
}
|
||||
|
||||
private class CloudflareSingleResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public CloudflareRecord Result { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using Docker.DotNet;
|
||||
using Docker.DotNet.Models;
|
||||
using DashCaddy.Desktop.Models;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public class DockerService
|
||||
{
|
||||
private readonly DockerClient _client;
|
||||
|
||||
public DockerService(string endpoint)
|
||||
{
|
||||
_client = new DockerClientConfiguration(new Uri(endpoint)).CreateClient();
|
||||
}
|
||||
|
||||
public async Task<List<ContainerListResponse>> GetContainersAsync(string labelFilter = null)
|
||||
{
|
||||
var parameters = new ContainersListParameters
|
||||
{
|
||||
All = true,
|
||||
Filters = new Dictionary<string, IDictionary<string, bool>>
|
||||
{
|
||||
["label"] = new Dictionary<string, bool> { ["dashcaddy.managed"] = true }
|
||||
}
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(labelFilter))
|
||||
{
|
||||
parameters.Filters["name"] = new Dictionary<string, bool> { [labelFilter] = true };
|
||||
}
|
||||
|
||||
return await _client.Containers.ListContainersAsync(parameters);
|
||||
}
|
||||
|
||||
public async Task<ContainerInspectResponse> GetContainerAsync(string id)
|
||||
{
|
||||
return await _client.Containers.InspectContainerAsync(id);
|
||||
}
|
||||
|
||||
public async Task StartContainerAsync(string id)
|
||||
{
|
||||
await _client.Containers.StartContainerAsync(id, null);
|
||||
}
|
||||
|
||||
public async Task StopContainerAsync(string id)
|
||||
{
|
||||
await _client.Containers.StopContainerAsync(id, new ContainerStopParameters { WaitBeforeKillSeconds = 10 });
|
||||
}
|
||||
|
||||
public async Task RestartContainerAsync(string id)
|
||||
{
|
||||
await _client.Containers.RestartContainerAsync(id, new ContainerRestartParameters { WaitBeforeKillSeconds = 10 });
|
||||
}
|
||||
|
||||
public async Task RemoveContainerAsync(string id)
|
||||
{
|
||||
await _client.Containers.RemoveContainerAsync(id, new ContainerRemoveParameters { Force = true, RemoveVolumes = false });
|
||||
}
|
||||
|
||||
public async Task CreateContainerAsync(ServiceModel service)
|
||||
{
|
||||
var createParams = new CreateContainerParameters
|
||||
{
|
||||
Image = service.Image,
|
||||
Name = $"dashcaddy-{service.Id}",
|
||||
Labels = new Dictionary<string, string>
|
||||
{
|
||||
["dashcaddy.managed"] = "true",
|
||||
["dashcaddy.service-id"] = service.Id,
|
||||
["dashcaddy.service-name"] = service.Name
|
||||
},
|
||||
Env = service.Environment.Select(kvp => $"{kvp.Key}={kvp.Value}").ToList(),
|
||||
HostConfig = new HostConfig
|
||||
{
|
||||
PortBindings = service.Environment.TryGetValue("PORT", out var port) && int.TryParse(port, out var p)
|
||||
? new Dictionary<string, IList<PortBinding>>
|
||||
{
|
||||
[$"{p}/tcp"] = new List<PortBinding> { new PortBinding { HostPort = p.ToString() } }
|
||||
}
|
||||
: new Dictionary<string, IList<PortBinding>>(),
|
||||
RestartPolicy = new RestartPolicy { Name = "unless-stopped" }
|
||||
}
|
||||
};
|
||||
|
||||
if (service.Volumes != null)
|
||||
{
|
||||
createParams.HostConfig.Binds = service.Volumes.ToList();
|
||||
}
|
||||
|
||||
await _client.Containers.CreateContainerAsync(createParams);
|
||||
}
|
||||
|
||||
public async Task<string> GetContainerLogsAsync(string id, int tailLines = 100)
|
||||
{
|
||||
var parameters = new ContainerLogsParameters
|
||||
{
|
||||
ShowStdout = true,
|
||||
ShowStderr = true,
|
||||
Tail = tailLines.ToString()
|
||||
};
|
||||
|
||||
using var stream = await _client.Containers.GetContainerLogsAsync(id, false, parameters);
|
||||
using var reader = new StreamReader(stream);
|
||||
return await reader.ReadToEndAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsDockerRunningAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.System.PingAsync();
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<VersionResponse> GetVersionAsync()
|
||||
{
|
||||
return await _client.System.GetVersionAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using DashCaddy.Desktop.Models;
|
||||
|
||||
namespace DashCaddy.Desktop.Services;
|
||||
|
||||
public static class ComposeParser
|
||||
{
|
||||
public static List<ServiceModel> Parse(string yamlContent)
|
||||
{
|
||||
var services = new List<ServiceModel>();
|
||||
|
||||
// Simple YAML parsing - in production use YamlDotNet
|
||||
// For now, we'll do a basic JSON-based approach assuming compose is converted
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Deserialize<JsonNode>(yamlContent); // This won't work for YAML directly
|
||||
// Real implementation would use YamlDotNet
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static List<ServiceModel> ParseJson(string jsonContent)
|
||||
{
|
||||
var compose = JsonSerializer.Deserialize<ComposeFile>(jsonContent, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
var services = new List<ServiceModel>();
|
||||
|
||||
if (compose?.Services != null)
|
||||
{
|
||||
foreach (var (name, svc) in compose.Services)
|
||||
{
|
||||
var port = ExtractPort(svc);
|
||||
var env = svc.Environment ?? new Dictionary<string, string>();
|
||||
|
||||
services.Add(new ServiceModel
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N")[..8],
|
||||
Name = name,
|
||||
Type = "docker-compose",
|
||||
Image = svc.Image ?? "",
|
||||
Host = "localhost",
|
||||
Port = port,
|
||||
Environment = env,
|
||||
Volumes = svc.Volumes ?? new List<string>(),
|
||||
Labels = svc.Labels ?? new Dictionary<string, string>(),
|
||||
ComposeFile = jsonContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static int ExtractPort(ComposeService svc)
|
||||
{
|
||||
if (svc.Ports != null)
|
||||
{
|
||||
foreach (var (key, value) in svc.Ports)
|
||||
{
|
||||
if (int.TryParse(key.Split(':')[0], out var hostPort))
|
||||
return hostPort;
|
||||
if (int.TryParse(key, out var containerPort))
|
||||
return containerPort;
|
||||
}
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TemplateRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, ServiceTemplate> Templates = new()
|
||||
{
|
||||
["plex"] = new ServiceTemplate
|
||||
{
|
||||
Id = "plex",
|
||||
Name = "Plex Media Server",
|
||||
Description = "Media server for movies, TV shows, and music",
|
||||
Icon = "📺",
|
||||
DefaultPort = 32400,
|
||||
Image = "plexinc/pms-docker:latest",
|
||||
Environment = new Dictionary<string, string>
|
||||
{
|
||||
["PLEX_CLAIM"] = "",
|
||||
["TZ"] = "America/Los_Angeles"
|
||||
},
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/plex/config:/config",
|
||||
"E:/dockerdata/plex/transcode:/transcode",
|
||||
"E:/media:/data"
|
||||
},
|
||||
RequiredVariables = new[] { "PLEX_CLAIM" }
|
||||
},
|
||||
["homeassistant"] = new ServiceTemplate
|
||||
{
|
||||
Id = "homeassistant",
|
||||
Name = "Home Assistant",
|
||||
Description = "Open source home automation platform",
|
||||
Icon = "🏠",
|
||||
DefaultPort = 8123,
|
||||
Image = "ghcr.io/home-assistant/home-assistant:stable",
|
||||
Environment = new Dictionary<string, string>
|
||||
{
|
||||
["TZ"] = "America/Los_Angeles"
|
||||
},
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/homeassistant:/config"
|
||||
},
|
||||
NetworkMode = "host"
|
||||
},
|
||||
["jellyfin"] = new ServiceTemplate
|
||||
{
|
||||
Id = "jellyfin",
|
||||
Name = "Jellyfin",
|
||||
Description = "Free software media system",
|
||||
Icon = "🎬",
|
||||
DefaultPort = 8096,
|
||||
Image = "jellyfin/jellyfin:latest",
|
||||
Environment = new Dictionary<string, string>
|
||||
{
|
||||
["TZ"] = "America/Los_Angeles"
|
||||
},
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/jellyfin/config:/config",
|
||||
"E:/dockerdata/jellyfin/cache:/cache",
|
||||
"E:/media:/media"
|
||||
}
|
||||
},
|
||||
["portainer"] = new ServiceTemplate
|
||||
{
|
||||
Id = "portainer",
|
||||
Name = "Portainer",
|
||||
Description = "Docker container management UI",
|
||||
Icon = "🐳",
|
||||
DefaultPort = 9443,
|
||||
Image = "portainer/portainer-ce:latest",
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/portainer:/data",
|
||||
"//./pipe/docker_engine:/var/run/docker.sock"
|
||||
}
|
||||
},
|
||||
["adguard"] = new ServiceTemplate
|
||||
{
|
||||
Id = "adguard",
|
||||
Name = "AdGuard Home",
|
||||
Description = "Network-wide ad blocking & DNS",
|
||||
Icon = "🛡️",
|
||||
DefaultPort = 3000,
|
||||
Image = "adguard/adguardhome:latest",
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/adguard/work:/opt/adguardhome/work",
|
||||
"E:/dockerdata/adguard/conf:/opt/adguardhome/conf"
|
||||
},
|
||||
Ports = new Dictionary<int, int> { [53] = 53, [3000] = 3000 }
|
||||
},
|
||||
["uptime-kuma"] = new ServiceTemplate
|
||||
{
|
||||
Id = "uptime-kuma",
|
||||
Name = "Uptime Kuma",
|
||||
Description = "Self-hosted monitoring tool",
|
||||
Icon = "📊",
|
||||
DefaultPort = 3001,
|
||||
Image = "louislam/uptime-kuma:1",
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/uptime-kuma:/app/data"
|
||||
}
|
||||
},
|
||||
["vaultwarden"] = new ServiceTemplate
|
||||
{
|
||||
Id = "vaultwarden",
|
||||
Name = "Vaultwarden (Bitwarden)",
|
||||
Description = "Self-hosted password manager",
|
||||
Icon = "🔐",
|
||||
DefaultPort = 8080,
|
||||
Image = "vaultwarden/server:latest",
|
||||
Environment = new Dictionary<string, string>
|
||||
{
|
||||
["SIGNUPS_ALLOWED"] = "true",
|
||||
["INVITATIONS_ALLOWED"] = "true"
|
||||
},
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/vaultwarden:/data"
|
||||
}
|
||||
},
|
||||
["paperless"] = new ServiceTemplate
|
||||
{
|
||||
Id = "paperless",
|
||||
Name = "Paperless-ngx",
|
||||
Description = "Document management system",
|
||||
Icon = "📄",
|
||||
DefaultPort = 8000,
|
||||
Image = "ghcr.io/paperless-ngx/paperless-ngx:latest",
|
||||
Environment = new Dictionary<string, string>
|
||||
{
|
||||
["PAPERLESS_REDIS"] = "redis://localhost:6379",
|
||||
["PAPERLESS_DBHOST"] = "localhost",
|
||||
["PAPERLESS_DBNAME"] = "paperless",
|
||||
["PAPERLESS_DBUSER"] = "paperless",
|
||||
["PAPERLESS_DBPASS"] = "paperless"
|
||||
},
|
||||
Volumes = new List<string>
|
||||
{
|
||||
"E:/dockerdata/paperless/data:/usr/src/paperless/data",
|
||||
"E:/dockerdata/paperless/media:/usr/src/paperless/media",
|
||||
"E:/dockerdata/paperless/export:/usr/src/paperless/export",
|
||||
"E:/dockerdata/paperless/consume:/usr/src/paperless/consume"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static ServiceTemplate Get(string id)
|
||||
{
|
||||
return Templates.TryGetValue(id, out var template) ? template : null;
|
||||
}
|
||||
|
||||
public static IEnumerable<ServiceTemplate> GetAll()
|
||||
{
|
||||
return Templates.Values;
|
||||
}
|
||||
}
|
||||
|
||||
public class ServiceTemplate
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
public int DefaultPort { get; set; }
|
||||
public string Image { get; set; }
|
||||
public Dictionary<string, string> Environment { get; set; } = new();
|
||||
public List<string> Volumes { get; set; } = new();
|
||||
public Dictionary<int, int> Ports { get; set; } = new();
|
||||
public string NetworkMode { get; set; }
|
||||
public string[] RequiredVariables { get; set; } = Array.Empty<string>();
|
||||
|
||||
public ServiceModel Instantiate(Dictionary<string, string> variables)
|
||||
{
|
||||
var env = new Dictionary<string, string>(Environment);
|
||||
foreach (var (key, value) in variables)
|
||||
{
|
||||
env[key] = value;
|
||||
}
|
||||
|
||||
var port = DefaultPort;
|
||||
if (Ports.Count > 0)
|
||||
{
|
||||
port = Ports.Keys.First();
|
||||
}
|
||||
|
||||
return new ServiceModel
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N")[..8],
|
||||
Name = Name,
|
||||
Type = "template",
|
||||
Image = Image,
|
||||
Host = "localhost",
|
||||
Port = port,
|
||||
Environment = env,
|
||||
Volumes = Volumes,
|
||||
Labels = new Dictionary<string, string>
|
||||
{
|
||||
["dashcaddy.template"] = Id,
|
||||
["dashcaddy.managed"] = "true"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user