Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
- 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.
362 lines
14 KiB
C#
362 lines
14 KiB
C#
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using OTSSignsOrchestrator.Core.Configuration;
|
|
using OTSSignsOrchestrator.Core.Models.DTOs;
|
|
using YamlDotNet.RepresentationModel;
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
namespace OTSSignsOrchestrator.Core.Services;
|
|
|
|
/// <summary>
|
|
/// Generates a Docker Compose v3.9 YAML for a Xibo CMS stack.
|
|
/// Combined format: no separate config.env, no MySQL container (external DB),
|
|
/// CIFS volumes, Newt tunnel service, and inline environment variables.
|
|
/// </summary>
|
|
public class ComposeRenderService
|
|
{
|
|
private readonly ILogger<ComposeRenderService> _logger;
|
|
|
|
public ComposeRenderService(ILogger<ComposeRenderService> logger)
|
|
{
|
|
_logger = logger;
|
|
}
|
|
|
|
public string Render(RenderContext ctx)
|
|
{
|
|
_logger.LogInformation("Rendering Compose for stack: {StackName}", ctx.StackName);
|
|
|
|
var root = new YamlMappingNode();
|
|
|
|
// Version
|
|
root.Children[new YamlScalarNode("version")] = new YamlScalarNode("3.9");
|
|
|
|
// Comment — customer name (added as a YAML comment isn't natively supported,
|
|
// so we prepend it manually after serialization)
|
|
BuildServices(root, ctx);
|
|
BuildNetworks(root, ctx);
|
|
BuildVolumes(root, ctx);
|
|
BuildSecrets(root, ctx);
|
|
|
|
var doc = new YamlDocument(root);
|
|
var stream = new YamlStream(doc);
|
|
|
|
using var writer = new StringWriter();
|
|
stream.Save(writer, assignAnchors: false);
|
|
var output = writer.ToString()
|
|
.Replace("...\n", "").Replace("...", "");
|
|
|
|
// Prepend customer name comment
|
|
output = $"# Customer: {ctx.CustomerName}\n{output}";
|
|
|
|
_logger.LogDebug("Compose rendered for {StackName}: {ServiceCount} services",
|
|
ctx.StackName, 4);
|
|
|
|
return output;
|
|
}
|
|
|
|
// ── Services ────────────────────────────────────────────────────────────
|
|
|
|
private void BuildServices(YamlMappingNode root, RenderContext ctx)
|
|
{
|
|
var services = new YamlMappingNode();
|
|
root.Children[new YamlScalarNode("services")] = services;
|
|
|
|
BuildWebService(services, ctx);
|
|
BuildMemcachedService(services, ctx);
|
|
BuildQuickChartService(services, ctx);
|
|
|
|
if (ctx.IncludeNewt)
|
|
BuildNewtService(services, ctx);
|
|
}
|
|
|
|
private void BuildWebService(YamlMappingNode services, RenderContext ctx)
|
|
{
|
|
var svc = new YamlMappingNode();
|
|
services.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-web")] = svc;
|
|
|
|
svc.Children[new YamlScalarNode("image")] = new YamlScalarNode(ctx.CmsImage);
|
|
|
|
// Environment — all config.env values merged inline
|
|
var env = new YamlMappingNode
|
|
{
|
|
{ "CMS_USE_MEMCACHED", "true" },
|
|
{ "MEMCACHED_HOST", "memcached" },
|
|
{ "MYSQL_HOST", ctx.MySqlHost },
|
|
{ "MYSQL_PORT", ctx.MySqlPort },
|
|
{ "MYSQL_DATABASE", ctx.MySqlDatabase },
|
|
{ "MYSQL_USER", ctx.MySqlUser },
|
|
{ "MYSQL_PASSWORD_FILE", $"/run/secrets/{ctx.CustomerAbbrev}-cms-db-password" },
|
|
{ "CMS_SMTP_SERVER", ctx.SmtpServer },
|
|
{ "CMS_SMTP_USERNAME", ctx.SmtpUsername },
|
|
{ "CMS_SMTP_PASSWORD", ctx.SmtpPassword },
|
|
{ "CMS_SMTP_USE_TLS", ctx.SmtpUseTls },
|
|
{ "CMS_SMTP_USE_STARTTLS", ctx.SmtpUseStartTls },
|
|
{ "CMS_SMTP_REWRITE_DOMAIN", ctx.SmtpRewriteDomain },
|
|
{ "CMS_SMTP_HOSTNAME", ctx.SmtpHostname },
|
|
{ "CMS_SMTP_FROM_LINE_OVERRIDE", ctx.SmtpFromLineOverride },
|
|
{ "CMS_SERVER_NAME", ctx.CmsServerName },
|
|
{ "CMS_PHP_POST_MAX_SIZE", ctx.PhpPostMaxSize },
|
|
{ "CMS_PHP_UPLOAD_MAX_FILESIZE", ctx.PhpUploadMaxFilesize },
|
|
{ "CMS_PHP_MAX_EXECUTION_TIME", ctx.PhpMaxExecutionTime },
|
|
};
|
|
svc.Children[new YamlScalarNode("environment")] = env;
|
|
|
|
// Secrets
|
|
var secrets = new YamlSequenceNode(
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-db-password")
|
|
);
|
|
svc.Children[new YamlScalarNode("secrets")] = secrets;
|
|
|
|
// Volumes
|
|
var volumes = new YamlSequenceNode(
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-custom:/var/www/cms/custom"),
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-backup:/var/www/backup"),
|
|
new YamlScalarNode($"{ctx.ThemeHostPath}:/var/www/cms/web/theme/custom"),
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-library:/var/www/cms/library"),
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-userscripts:/var/www/cms/web/userscripts"),
|
|
new YamlScalarNode($"{ctx.CustomerAbbrev}-cms-ca-certs:/var/www/cms/ca-certs")
|
|
);
|
|
svc.Children[new YamlScalarNode("volumes")] = volumes;
|
|
|
|
// Ports
|
|
var ports = new YamlSequenceNode(
|
|
new YamlScalarNode($"{ctx.HostHttpPort}:80")
|
|
);
|
|
svc.Children[new YamlScalarNode("ports")] = ports;
|
|
|
|
// Networks
|
|
var webNet = new YamlMappingNode
|
|
{
|
|
{ "aliases", new YamlSequenceNode(new YamlScalarNode("web")) }
|
|
};
|
|
var networks = new YamlMappingNode();
|
|
networks.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-net")] = webNet;
|
|
svc.Children[new YamlScalarNode("networks")] = networks;
|
|
|
|
// Deploy
|
|
var deploy = new YamlMappingNode
|
|
{
|
|
{ "restart_policy", new YamlMappingNode { { "condition", "any" } } },
|
|
{ "resources", new YamlMappingNode
|
|
{ { "limits", new YamlMappingNode { { "memory", "1G" } } } }
|
|
}
|
|
};
|
|
svc.Children[new YamlScalarNode("deploy")] = deploy;
|
|
}
|
|
|
|
private void BuildMemcachedService(YamlMappingNode services, RenderContext ctx)
|
|
{
|
|
var svc = new YamlMappingNode();
|
|
services.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-memcached")] = svc;
|
|
|
|
svc.Children[new YamlScalarNode("image")] = new YamlScalarNode(ctx.MemcachedImage);
|
|
|
|
var command = new YamlSequenceNode(
|
|
new YamlScalarNode("memcached"),
|
|
new YamlScalarNode("-m"),
|
|
new YamlScalarNode("15")
|
|
);
|
|
svc.Children[new YamlScalarNode("command")] = command;
|
|
|
|
var mcNet = new YamlMappingNode
|
|
{
|
|
{ "aliases", new YamlSequenceNode(new YamlScalarNode("memcached")) }
|
|
};
|
|
var networks = new YamlMappingNode();
|
|
networks.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-net")] = mcNet;
|
|
svc.Children[new YamlScalarNode("networks")] = networks;
|
|
|
|
svc.Children[new YamlScalarNode("deploy")] = new YamlMappingNode
|
|
{
|
|
{ "restart_policy", new YamlMappingNode { { "condition", "any" } } },
|
|
{ "resources", new YamlMappingNode
|
|
{ { "limits", new YamlMappingNode { { "memory", "100M" } } } }
|
|
}
|
|
};
|
|
}
|
|
|
|
private void BuildQuickChartService(YamlMappingNode services, RenderContext ctx)
|
|
{
|
|
var svc = new YamlMappingNode();
|
|
services.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-quickchart")] = svc;
|
|
|
|
svc.Children[new YamlScalarNode("image")] = new YamlScalarNode(ctx.QuickChartImage);
|
|
|
|
var qcNet = new YamlMappingNode
|
|
{
|
|
{ "aliases", new YamlSequenceNode(new YamlScalarNode("quickchart")) }
|
|
};
|
|
var networks = new YamlMappingNode();
|
|
networks.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-net")] = qcNet;
|
|
svc.Children[new YamlScalarNode("networks")] = networks;
|
|
|
|
svc.Children[new YamlScalarNode("deploy")] = new YamlMappingNode
|
|
{
|
|
{ "restart_policy", new YamlMappingNode { { "condition", "any" } } }
|
|
};
|
|
}
|
|
|
|
private void BuildNewtService(YamlMappingNode services, RenderContext ctx)
|
|
{
|
|
var svc = new YamlMappingNode();
|
|
services.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-newt")] = svc;
|
|
|
|
svc.Children[new YamlScalarNode("image")] = new YamlScalarNode(ctx.NewtImage);
|
|
|
|
var env = new YamlMappingNode
|
|
{
|
|
{ "PANGOLIN_ENDPOINT", ctx.PangolinEndpoint },
|
|
{ "NEWT_ID", ctx.NewtId ?? "CONFIGURE_ME" },
|
|
{ "NEWT_SECRET", ctx.NewtSecret ?? "CONFIGURE_ME" },
|
|
};
|
|
svc.Children[new YamlScalarNode("environment")] = env;
|
|
|
|
var networks = new YamlMappingNode();
|
|
networks.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-net")] = new YamlMappingNode();
|
|
svc.Children[new YamlScalarNode("networks")] = networks;
|
|
|
|
svc.Children[new YamlScalarNode("deploy")] = new YamlMappingNode
|
|
{
|
|
{ "restart_policy", new YamlMappingNode { { "condition", "any" } } }
|
|
};
|
|
}
|
|
|
|
// ── Networks ────────────────────────────────────────────────────────────
|
|
|
|
private void BuildNetworks(YamlMappingNode root, RenderContext ctx)
|
|
{
|
|
var netDef = new YamlMappingNode
|
|
{
|
|
{ "driver", "overlay" },
|
|
{ "attachable", "false" }
|
|
};
|
|
var networks = new YamlMappingNode();
|
|
networks.Children[new YamlScalarNode($"{ctx.CustomerAbbrev}-net")] = netDef;
|
|
root.Children[new YamlScalarNode("networks")] = networks;
|
|
}
|
|
|
|
// ── Volumes (CIFS) ──────────────────────────────────────────────────────
|
|
|
|
private void BuildVolumes(YamlMappingNode root, RenderContext ctx)
|
|
{
|
|
var volumes = new YamlMappingNode();
|
|
root.Children[new YamlScalarNode("volumes")] = volumes;
|
|
|
|
var volumeNames = new[]
|
|
{
|
|
$"{ctx.CustomerAbbrev}-cms-custom",
|
|
$"{ctx.CustomerAbbrev}-cms-backup",
|
|
$"{ctx.CustomerAbbrev}-cms-library",
|
|
$"{ctx.CustomerAbbrev}-cms-userscripts",
|
|
$"{ctx.CustomerAbbrev}-cms-ca-certs",
|
|
};
|
|
|
|
foreach (var volName in volumeNames)
|
|
{
|
|
if (ctx.UseCifsVolumes && !string.IsNullOrWhiteSpace(ctx.CifsServer))
|
|
{
|
|
var device = $"//{ctx.CifsServer}{ctx.CifsShareBasePath}/{volName}";
|
|
var opts = $"addr={ctx.CifsServer},username={ctx.CifsUsername},password={ctx.CifsPassword}";
|
|
if (!string.IsNullOrWhiteSpace(ctx.CifsExtraOptions))
|
|
opts += $",{ctx.CifsExtraOptions}";
|
|
|
|
var volDef = new YamlMappingNode
|
|
{
|
|
{ "driver", "local" },
|
|
{ "driver_opts", new YamlMappingNode
|
|
{
|
|
{ "type", "cifs" },
|
|
{ "device", device },
|
|
{ "o", opts }
|
|
}
|
|
}
|
|
};
|
|
volumes.Children[new YamlScalarNode(volName)] = volDef;
|
|
}
|
|
else
|
|
{
|
|
volumes.Children[new YamlScalarNode(volName)] = new YamlMappingNode();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Secrets ─────────────────────────────────────────────────────────────
|
|
|
|
private void BuildSecrets(YamlMappingNode root, RenderContext ctx)
|
|
{
|
|
var secrets = new YamlMappingNode();
|
|
root.Children[new YamlScalarNode("secrets")] = secrets;
|
|
|
|
foreach (var secretName in ctx.SecretNames)
|
|
{
|
|
secrets.Children[new YamlScalarNode(secretName)] =
|
|
new YamlMappingNode { { "external", "true" } };
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Context object with all inputs needed to render a Compose file.</summary>
|
|
public class RenderContext
|
|
{
|
|
public string CustomerName { get; set; } = string.Empty;
|
|
public string CustomerAbbrev { get; set; } = string.Empty;
|
|
public string StackName { get; set; } = string.Empty;
|
|
public string CmsServerName { get; set; } = string.Empty;
|
|
public int HostHttpPort { get; set; } = 80;
|
|
|
|
// Docker images
|
|
public string CmsImage { get; set; } = "ghcr.io/xibosignage/xibo-cms:release-4.2.3";
|
|
public string MemcachedImage { get; set; } = "memcached:alpine";
|
|
public string QuickChartImage { get; set; } = "ianw/quickchart";
|
|
public string NewtImage { get; set; } = "fosrl/newt";
|
|
|
|
// Theme bind mount path on host
|
|
public string ThemeHostPath { get; set; } = string.Empty;
|
|
|
|
// MySQL (external server)
|
|
public string MySqlHost { get; set; } = string.Empty;
|
|
public string MySqlPort { get; set; } = "3306";
|
|
public string MySqlDatabase { get; set; } = "cms";
|
|
public string MySqlUser { get; set; } = "cms";
|
|
|
|
// SMTP
|
|
public string SmtpServer { get; set; } = string.Empty;
|
|
public string SmtpUsername { get; set; } = string.Empty;
|
|
public string SmtpPassword { get; set; } = string.Empty;
|
|
public string SmtpUseTls { get; set; } = "YES";
|
|
public string SmtpUseStartTls { get; set; } = "YES";
|
|
public string SmtpRewriteDomain { get; set; } = string.Empty;
|
|
public string SmtpHostname { get; set; } = string.Empty;
|
|
public string SmtpFromLineOverride { get; set; } = "NO";
|
|
|
|
// PHP settings
|
|
public string PhpPostMaxSize { get; set; } = "10G";
|
|
public string PhpUploadMaxFilesize { get; set; } = "10G";
|
|
public string PhpMaxExecutionTime { get; set; } = "600";
|
|
|
|
// Pangolin / Newt
|
|
public bool IncludeNewt { get; set; } = true;
|
|
public string PangolinEndpoint { get; set; } = "https://app.pangolin.net";
|
|
public string? NewtId { get; set; }
|
|
public string? NewtSecret { get; set; }
|
|
|
|
// CIFS volume settings
|
|
public bool UseCifsVolumes { get; set; }
|
|
public string? CifsServer { get; set; }
|
|
public string? CifsShareBasePath { get; set; }
|
|
public string? CifsUsername { get; set; }
|
|
public string? CifsPassword { get; set; }
|
|
public string? CifsExtraOptions { get; set; }
|
|
|
|
// Secrets to declare as external
|
|
public List<string> SecretNames { get; set; } = new();
|
|
|
|
// Legacy — kept for backward compat but no longer used
|
|
public string TemplateYaml { get; set; } = string.Empty;
|
|
public Dictionary<string, string> TemplateEnvValues { get; set; } = new();
|
|
public List<string> TemplateEnvLines { get; set; } = new();
|
|
public List<string> Constraints { get; set; } = new();
|
|
public string LibraryHostPath { get; set; } = string.Empty;
|
|
}
|