feat: Add main application views and structure
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
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.
This commit is contained in:
361
OTSSignsOrchestrator.Core/Services/ComposeRenderService.cs
Normal file
361
OTSSignsOrchestrator.Core/Services/ComposeRenderService.cs
Normal file
@@ -0,0 +1,361 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user