diff --git a/src/ReadyStackGo.Application/Services/MaintenanceObserverConfigMapper.cs b/src/ReadyStackGo.Application/Services/MaintenanceObserverConfigMapper.cs
new file mode 100644
index 00000000..69dcdbd7
--- /dev/null
+++ b/src/ReadyStackGo.Application/Services/MaintenanceObserverConfigMapper.cs
@@ -0,0 +1,140 @@
+using ReadyStackGo.Domain.Deployment.Observers;
+using ReadyStackGo.Domain.StackManagement.Manifests;
+
+namespace ReadyStackGo.Application.Services;
+
+///
+/// Maps a RsgoMaintenanceObserver (StackManagement domain) to a MaintenanceObserverConfig
+/// (Deployment domain), resolving ${VAR} placeholders against a variable dictionary.
+///
+public static class MaintenanceObserverConfigMapper
+{
+ public static MaintenanceObserverConfig? Map(
+ RsgoMaintenanceObserver? source,
+ IReadOnlyDictionary variables)
+ {
+ if (source == null)
+ return null;
+
+ if (!ObserverType.TryFromValue(source.Type, out var observerType) || observerType == null)
+ return null;
+
+ var pollingInterval = ParseTimeSpan(source.PollingInterval) ?? TimeSpan.FromSeconds(30);
+
+ IObserverSettings settings;
+
+ if (observerType == ObserverType.SqlExtendedProperty)
+ {
+ var connectionString = ResolveConnectionString(source, variables);
+ if (string.IsNullOrEmpty(connectionString))
+ return null;
+
+ settings = SqlObserverSettings.ForExtendedProperty(
+ source.PropertyName ?? throw new InvalidOperationException("PropertyName required"),
+ connectionString);
+ }
+ else if (observerType == ObserverType.SqlQuery)
+ {
+ var connectionString = ResolveConnectionString(source, variables);
+ if (string.IsNullOrEmpty(connectionString))
+ return null;
+
+ settings = SqlObserverSettings.ForQuery(
+ source.Query ?? throw new InvalidOperationException("Query required"),
+ connectionString);
+ }
+ else if (observerType == ObserverType.Http)
+ {
+ var timeout = ParseTimeSpan(source.Timeout) ?? TimeSpan.FromSeconds(10);
+ var url = ResolveVariables(
+ source.Url ?? throw new InvalidOperationException("URL required"), variables);
+ if (string.IsNullOrEmpty(url))
+ return null;
+
+ settings = HttpObserverSettings.Create(
+ url,
+ source.Method ?? "GET",
+ null,
+ timeout,
+ source.JsonPath);
+ }
+ else if (observerType == ObserverType.File)
+ {
+ var mode = source.Mode?.ToLowerInvariant() == "content"
+ ? FileCheckMode.Content
+ : FileCheckMode.Exists;
+
+ var path = ResolveVariables(
+ source.Path ?? throw new InvalidOperationException("Path required"), variables);
+ if (string.IsNullOrEmpty(path))
+ return null;
+
+ settings = mode == FileCheckMode.Content
+ ? FileObserverSettings.ForContent(path, source.ContentPattern)
+ : FileObserverSettings.ForExistence(path);
+ }
+ else
+ {
+ return null;
+ }
+
+ return MaintenanceObserverConfig.Create(
+ observerType,
+ pollingInterval,
+ source.MaintenanceValue,
+ source.NormalValue,
+ settings);
+ }
+
+ private static string? ResolveConnectionString(
+ RsgoMaintenanceObserver source,
+ IReadOnlyDictionary variables)
+ {
+ if (!string.IsNullOrEmpty(source.ConnectionString))
+ return ResolveVariables(source.ConnectionString, variables);
+
+ if (!string.IsNullOrEmpty(source.ConnectionName) &&
+ variables.TryGetValue(source.ConnectionName, out var connectionString))
+ {
+ return connectionString;
+ }
+
+ return null;
+ }
+
+ private static string? ResolveVariables(string template, IReadOnlyDictionary variables)
+ {
+ if (string.IsNullOrEmpty(template))
+ return template;
+
+ var result = template;
+ foreach (var kvp in variables)
+ {
+ result = result.Replace($"${{{kvp.Key}}}", kvp.Value);
+ }
+
+ if (result.Contains("${"))
+ return null;
+
+ return result;
+ }
+
+ private static TimeSpan? ParseTimeSpan(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return null;
+
+ value = value.Trim().ToLowerInvariant();
+
+ if (value.EndsWith('s') && int.TryParse(value[..^1], out var seconds))
+ return TimeSpan.FromSeconds(seconds);
+
+ if (value.EndsWith('m') && int.TryParse(value[..^1], out var minutes))
+ return TimeSpan.FromMinutes(minutes);
+
+ if (value.EndsWith('h') && int.TryParse(value[..^1], out var hours))
+ return TimeSpan.FromHours(hours);
+
+ return null;
+ }
+}
diff --git a/src/ReadyStackGo.Application/UseCases/Deployments/DeployProduct/DeployProductHandler.cs b/src/ReadyStackGo.Application/UseCases/Deployments/DeployProduct/DeployProductHandler.cs
index cc1a5015..70eb619b 100644
--- a/src/ReadyStackGo.Application/UseCases/Deployments/DeployProduct/DeployProductHandler.cs
+++ b/src/ReadyStackGo.Application/UseCases/Deployments/DeployProduct/DeployProductHandler.cs
@@ -117,6 +117,24 @@ public async Task Handle(DeployProductCommand request, Ca
request.SharedVariables,
request.ContinueOnError);
+ // Resolve and attach the product-level maintenance observer config.
+ // Without this the MaintenanceObserverService has no config to poll.
+ var observerConfig = MaintenanceObserverConfigMapper.Map(
+ product.MaintenanceObserver, request.SharedVariables);
+ if (observerConfig != null)
+ {
+ productDeployment.SetMaintenanceObserverConfig(observerConfig);
+ _logger.LogInformation(
+ "Product deployment {ProductDeploymentId} wired maintenance observer type={ObserverType}",
+ productDeploymentId, observerConfig.Type.Value);
+ }
+ else if (product.MaintenanceObserver != null)
+ {
+ _logger.LogWarning(
+ "Product {ProductName} defines maintenanceObserver but it could not be mapped (unresolved variables?) — observer disabled",
+ product.Name);
+ }
+
_repository.Add(productDeployment);
_repository.SaveChanges();
diff --git a/src/ReadyStackGo.Application/UseCases/Deployments/DeployStack/DeployStackHandler.cs b/src/ReadyStackGo.Application/UseCases/Deployments/DeployStack/DeployStackHandler.cs
index d6cab6e2..ea92179f 100644
--- a/src/ReadyStackGo.Application/UseCases/Deployments/DeployStack/DeployStackHandler.cs
+++ b/src/ReadyStackGo.Application/UseCases/Deployments/DeployStack/DeployStackHandler.cs
@@ -2,8 +2,6 @@
using Microsoft.Extensions.Logging;
using ReadyStackGo.Application.Notifications;
using ReadyStackGo.Application.Services;
-using ReadyStackGo.Domain.Deployment.Observers;
-using ReadyStackGo.Domain.StackManagement.Manifests;
using ReadyStackGo.Domain.StackManagement.Stacks;
using RuntimeConfig = ReadyStackGo.Domain.Deployment.RuntimeConfig;
@@ -52,9 +50,8 @@ public async Task Handle(DeployStackCommand request, Cancel
: null;
// Map MaintenanceObserver from StackManagement to Deployment domain model
- var observerConfig = product?.MaintenanceObserver != null
- ? MapToDeploymentObserverConfig(product.MaintenanceObserver, request.Variables)
- : null;
+ var observerConfig = MaintenanceObserverConfigMapper.Map(
+ product?.MaintenanceObserver, request.Variables);
// Extract health check configurations from services
var healthCheckConfigs = ExtractHealthCheckConfigs(stackDefinition.Services);
@@ -196,148 +193,6 @@ private async Task CreateDeploymentNotificationAsync(
return null;
}
- ///
- /// Maps RsgoMaintenanceObserver (StackManagement domain) to MaintenanceObserverConfig (Deployment domain).
- /// This is the boundary mapping between bounded contexts.
- ///
- private static MaintenanceObserverConfig? MapToDeploymentObserverConfig(
- RsgoMaintenanceObserver source,
- Dictionary deploymentVariables)
- {
- // Parse observer type
- if (!ObserverType.TryFromValue(source.Type, out var observerType) || observerType == null)
- {
- return null;
- }
-
- // Parse polling interval
- var pollingInterval = ParseTimeSpan(source.PollingInterval) ?? TimeSpan.FromSeconds(30);
-
- // Create type-specific settings
- IObserverSettings settings;
-
- if (observerType == ObserverType.SqlExtendedProperty)
- {
- var connectionString = ResolveConnectionString(source, deploymentVariables);
- if (string.IsNullOrEmpty(connectionString))
- return null;
-
- settings = SqlObserverSettings.ForExtendedProperty(
- source.PropertyName ?? throw new InvalidOperationException("PropertyName required"),
- connectionString);
- }
- else if (observerType == ObserverType.SqlQuery)
- {
- var connectionString = ResolveConnectionString(source, deploymentVariables);
- if (string.IsNullOrEmpty(connectionString))
- return null;
-
- settings = SqlObserverSettings.ForQuery(
- source.Query ?? throw new InvalidOperationException("Query required"),
- connectionString);
- }
- else if (observerType == ObserverType.Http)
- {
- var timeout = ParseTimeSpan(source.Timeout) ?? TimeSpan.FromSeconds(10);
- settings = HttpObserverSettings.Create(
- source.Url ?? throw new InvalidOperationException("URL required"),
- source.Method ?? "GET",
- null, // Headers not in RsgoMaintenanceObserver
- timeout,
- source.JsonPath);
- }
- else if (observerType == ObserverType.File)
- {
- var mode = source.Mode?.ToLowerInvariant() == "content"
- ? FileCheckMode.Content
- : FileCheckMode.Exists;
-
- settings = mode == FileCheckMode.Content
- ? FileObserverSettings.ForContent(
- source.Path ?? throw new InvalidOperationException("Path required"),
- source.ContentPattern)
- : FileObserverSettings.ForExistence(
- source.Path ?? throw new InvalidOperationException("Path required"));
- }
- else
- {
- return null;
- }
-
- return MaintenanceObserverConfig.Create(
- observerType,
- pollingInterval,
- source.MaintenanceValue,
- source.NormalValue,
- settings);
- }
-
- ///
- /// Resolves connection string from direct value or variable reference.
- ///
- private static string? ResolveConnectionString(
- RsgoMaintenanceObserver source,
- Dictionary variables)
- {
- // Direct connection string - resolve variables if present
- if (!string.IsNullOrEmpty(source.ConnectionString))
- {
- return ResolveVariables(source.ConnectionString, variables);
- }
-
- // Connection name - look up in deployment variables
- if (!string.IsNullOrEmpty(source.ConnectionName) &&
- variables.TryGetValue(source.ConnectionName, out var connectionString))
- {
- return connectionString;
- }
-
- return null;
- }
-
- ///
- /// Resolves ${VAR_NAME} placeholders in a template string.
- ///
- private static string? ResolveVariables(string template, Dictionary variables)
- {
- if (string.IsNullOrEmpty(template))
- return template;
-
- var result = template;
- foreach (var kvp in variables)
- {
- result = result.Replace($"${{{kvp.Key}}}", kvp.Value);
- }
-
- // Check if any unresolved placeholders remain
- if (result.Contains("${"))
- return null;
-
- return result;
- }
-
- ///
- /// Parses time span strings like "30s", "1m", "5m", "1h".
- ///
- private static TimeSpan? ParseTimeSpan(string? value)
- {
- if (string.IsNullOrEmpty(value))
- return null;
-
- value = value.Trim().ToLowerInvariant();
-
- if (value.EndsWith('s') && int.TryParse(value[..^1], out var seconds))
- return TimeSpan.FromSeconds(seconds);
-
- if (value.EndsWith('m') && int.TryParse(value[..^1], out var minutes))
- return TimeSpan.FromMinutes(minutes);
-
- if (value.EndsWith('h') && int.TryParse(value[..^1], out var hours))
- return TimeSpan.FromHours(hours);
-
- return null;
- }
-
///
/// Extracts health check configurations from service templates.
/// Maps ServiceHealthCheck (StackManagement) to ServiceHealthCheckConfig (Deployment).
diff --git a/src/ReadyStackGo.Application/UseCases/Deployments/RedeployProduct/RedeployProductHandler.cs b/src/ReadyStackGo.Application/UseCases/Deployments/RedeployProduct/RedeployProductHandler.cs
index 6b6ae054..fb067274 100644
--- a/src/ReadyStackGo.Application/UseCases/Deployments/RedeployProduct/RedeployProductHandler.cs
+++ b/src/ReadyStackGo.Application/UseCases/Deployments/RedeployProduct/RedeployProductHandler.cs
@@ -16,6 +16,7 @@ namespace ReadyStackGo.Application.UseCases.Deployments.RedeployProduct;
public class RedeployProductHandler : IRequestHandler
{
private readonly IProductDeploymentRepository _repository;
+ private readonly IProductSourceService _productSourceService;
private readonly IMediator _mediator;
private readonly IDeploymentService _deploymentService;
private readonly IDeploymentNotificationService? _notificationService;
@@ -25,6 +26,7 @@ public class RedeployProductHandler : IRequestHandler logger,
@@ -33,6 +35,7 @@ public RedeployProductHandler(
TimeProvider? timeProvider = null)
{
_repository = repository;
+ _productSourceService = productSourceService;
_mediator = mediator;
_deploymentService = deploymentService;
_logger = logger;
@@ -72,6 +75,34 @@ public async Task Handle(RedeployProductCommand request,
return DeployProductResponse.Failed(ex.Message);
}
+ // Re-resolve the product-level maintenance observer from the current catalog.
+ // This is how stack.yaml edits to maintenance.observer take effect on redeploy.
+ if (!string.IsNullOrEmpty(productDeployment.ProductId))
+ {
+ var product = await _productSourceService.GetProductAsync(
+ productDeployment.ProductId, cancellationToken);
+ if (product != null)
+ {
+ var observerVariables = BuildObserverVariables(productDeployment, request.VariableOverrides);
+ var observerConfig = MaintenanceObserverConfigMapper.Map(
+ product.MaintenanceObserver, observerVariables);
+ productDeployment.SetMaintenanceObserverConfig(observerConfig);
+
+ if (observerConfig != null)
+ {
+ _logger.LogInformation(
+ "Redeploy {ProductDeploymentId} refreshed maintenance observer type={ObserverType}",
+ productDeployment.Id, observerConfig.Type.Value);
+ }
+ else if (product.MaintenanceObserver != null)
+ {
+ _logger.LogWarning(
+ "Product {ProductName} defines maintenanceObserver but it could not be mapped on redeploy (unresolved variables?) — observer disabled",
+ productDeployment.ProductName);
+ }
+ }
+ }
+
_repository.Update(productDeployment);
_repository.SaveChanges();
@@ -261,6 +292,25 @@ await NotifyStackCompletedAsync(
};
}
+ // Merge shared variables with redeploy overrides for observer placeholder resolution.
+ // Observer is product-level, so we only use shared values — not per-stack overrides.
+ private static Dictionary BuildObserverVariables(
+ ProductDeployment productDeployment,
+ IReadOnlyDictionary? overrides)
+ {
+ var merged = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in productDeployment.SharedVariables)
+ merged[kvp.Key] = kvp.Value;
+
+ if (overrides != null)
+ {
+ foreach (var kvp in overrides)
+ merged[kvp.Key] = kvp.Value;
+ }
+
+ return merged;
+ }
+
private static void FinalizeProductStatus(ProductDeployment productDeployment)
{
if (productDeployment.Status is ProductDeploymentStatus.Running
diff --git a/tests/ReadyStackGo.UnitTests/Application/Deployments/DeployProductHandlerTests.cs b/tests/ReadyStackGo.UnitTests/Application/Deployments/DeployProductHandlerTests.cs
index 1040e4b8..c502d7aa 100644
--- a/tests/ReadyStackGo.UnitTests/Application/Deployments/DeployProductHandlerTests.cs
+++ b/tests/ReadyStackGo.UnitTests/Application/Deployments/DeployProductHandlerTests.cs
@@ -864,4 +864,111 @@ public async Task Handle_Success_CreatesInAppNotification()
}
#endregion
+
+ #region Maintenance Observer Wiring
+
+ [Fact]
+ public async Task Handle_ProductHasResolvableObserver_SetsConfigOnProductDeployment()
+ {
+ var product = CreateProductWithObserver(
+ new global::ReadyStackGo.Domain.StackManagement.Manifests.RsgoMaintenanceObserver
+ {
+ Type = "sqlExtendedProperty",
+ PropertyName = "ams-MaintenanceMode",
+ ConnectionString = "Server=${DB_SERVER};Database=${DB_NAME};",
+ MaintenanceValue = "1",
+ NormalValue = "0",
+ PollingInterval = "30s"
+ });
+
+ SetupProductFound(product);
+ SetupNoExistingDeployment();
+ SetupAllStacksSucceed();
+
+ ProductDeployment? captured = null;
+ _repositoryMock
+ .Setup(r => r.Add(It.IsAny()))
+ .Callback(pd => captured = pd);
+
+ var cmd = CreateCommand(product, new Dictionary
+ {
+ ["DB_SERVER"] = "sqldev2017",
+ ["DB_NAME"] = "dev-amsproject"
+ });
+
+ await _handler.Handle(cmd, CancellationToken.None);
+
+ captured.Should().NotBeNull();
+ captured!.MaintenanceObserverConfig.Should().NotBeNull(
+ "the product-level observer must be wired onto the ProductDeployment or MaintenanceObserverService has nothing to poll");
+ captured.MaintenanceObserverConfig!.Type.Value.Should().Be("sqlExtendedProperty");
+ captured.MaintenanceObserverConfig.MaintenanceValue.Should().Be("1");
+ }
+
+ [Fact]
+ public async Task Handle_ObserverConnectionStringUnresolvable_LeavesConfigNull()
+ {
+ var product = CreateProductWithObserver(
+ new global::ReadyStackGo.Domain.StackManagement.Manifests.RsgoMaintenanceObserver
+ {
+ Type = "sqlExtendedProperty",
+ PropertyName = "ams-MaintenanceMode",
+ ConnectionString = "Server=${DB_SERVER};Database=${DB_NAME};",
+ MaintenanceValue = "1"
+ });
+
+ SetupProductFound(product);
+ SetupNoExistingDeployment();
+ SetupAllStacksSucceed();
+
+ ProductDeployment? captured = null;
+ _repositoryMock
+ .Setup(r => r.Add(It.IsAny()))
+ .Callback(pd => captured = pd);
+
+ // DB_NAME intentionally missing — mapper must refuse to produce a config.
+ var cmd = CreateCommand(product, new Dictionary
+ {
+ ["DB_SERVER"] = "sqldev2017"
+ });
+
+ await _handler.Handle(cmd, CancellationToken.None);
+
+ captured.Should().NotBeNull();
+ captured!.MaintenanceObserverConfig.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task Handle_ProductHasNoObserver_LeavesConfigNull()
+ {
+ var product = CreateTestProduct(1);
+ SetupProductFound(product);
+ SetupNoExistingDeployment();
+ SetupAllStacksSucceed();
+
+ ProductDeployment? captured = null;
+ _repositoryMock
+ .Setup(r => r.Add(It.IsAny()))
+ .Callback(pd => captured = pd);
+
+ await _handler.Handle(CreateCommand(product), CancellationToken.None);
+
+ captured.Should().NotBeNull();
+ captured!.MaintenanceObserverConfig.Should().BeNull();
+ }
+
+ private static ProductDefinition CreateProductWithObserver(
+ global::ReadyStackGo.Domain.StackManagement.Manifests.RsgoMaintenanceObserver observer)
+ {
+ var baseProduct = CreateTestProduct(1);
+ return new ProductDefinition(
+ sourceId: "stacks",
+ name: baseProduct.Name,
+ displayName: baseProduct.DisplayName,
+ stacks: baseProduct.Stacks,
+ productVersion: baseProduct.ProductVersion,
+ maintenanceObserver: observer);
+ }
+
+ #endregion
}
diff --git a/tests/ReadyStackGo.UnitTests/Application/Deployments/RedeployProductHandlerTests.cs b/tests/ReadyStackGo.UnitTests/Application/Deployments/RedeployProductHandlerTests.cs
index c1c18790..90c38def 100644
--- a/tests/ReadyStackGo.UnitTests/Application/Deployments/RedeployProductHandlerTests.cs
+++ b/tests/ReadyStackGo.UnitTests/Application/Deployments/RedeployProductHandlerTests.cs
@@ -17,6 +17,7 @@ namespace ReadyStackGo.UnitTests.Application.Deployments;
public class RedeployProductHandlerTests
{
private readonly Mock _repositoryMock;
+ private readonly Mock _productSourceServiceMock;
private readonly Mock _mediatorMock;
private readonly Mock _deploymentServiceMock;
private readonly Mock> _loggerMock;
@@ -28,6 +29,7 @@ public class RedeployProductHandlerTests
public RedeployProductHandlerTests()
{
_repositoryMock = new Mock();
+ _productSourceServiceMock = new Mock();
_mediatorMock = new Mock();
_deploymentServiceMock = new Mock();
_loggerMock = new Mock>();
@@ -45,6 +47,7 @@ public RedeployProductHandlerTests()
_handler = new RedeployProductHandler(
_repositoryMock.Object,
+ _productSourceServiceMock.Object,
_mediatorMock.Object,
_deploymentServiceMock.Object,
_loggerMock.Object,
@@ -343,4 +346,133 @@ public async Task Handle_GeneratesSessionId_WhenNotProvided()
}
#endregion
+
+ #region Maintenance Observer Refresh
+
+ [Fact]
+ public async Task Handle_ReResolvesMaintenanceObserverFromCatalog()
+ {
+ var pd = CreateRunningDeploymentWithSharedVars(new Dictionary
+ {
+ ["DB_SERVER"] = "sqldev2017",
+ ["DB_NAME"] = "dev-amsproject"
+ });
+ SetupDeploymentFound(pd);
+
+ _productSourceServiceMock
+ .Setup(s => s.GetProductAsync(pd.ProductId, It.IsAny()))
+ .ReturnsAsync(CreateCatalogProductWithObserver(pd.ProductId));
+
+ var command = CreateCommand(pd.Id.Value.ToString());
+ await _handler.Handle(command, CancellationToken.None);
+
+ pd.MaintenanceObserverConfig.Should().NotBeNull(
+ "redeploy must re-read maintenance observer from the catalog so stack.yaml edits take effect");
+ pd.MaintenanceObserverConfig!.Type.Value.Should().Be("sqlExtendedProperty");
+ }
+
+ [Fact]
+ public async Task Handle_ObserverRemovedFromCatalog_ClearsConfig()
+ {
+ var pd = CreateRunningDeploymentWithSharedVars(new Dictionary());
+ // Pre-seed an existing observer — simulating the old catalog state.
+ pd.SetMaintenanceObserverConfig(global::ReadyStackGo.Domain.Deployment.Observers.MaintenanceObserverConfig.Create(
+ global::ReadyStackGo.Domain.Deployment.Observers.ObserverType.File,
+ TimeSpan.FromSeconds(30),
+ "1",
+ "0",
+ global::ReadyStackGo.Domain.Deployment.Observers.FileObserverSettings.ForExistence("/tmp/x")));
+ SetupDeploymentFound(pd);
+
+ // Catalog returns product without observer.
+ _productSourceServiceMock
+ .Setup(s => s.GetProductAsync(pd.ProductId, It.IsAny()))
+ .ReturnsAsync(CreateCatalogProductWithoutObserver(pd.ProductId));
+
+ var command = CreateCommand(pd.Id.Value.ToString());
+ await _handler.Handle(command, CancellationToken.None);
+
+ pd.MaintenanceObserverConfig.Should().BeNull(
+ "observer removed from the catalog must also be removed from the ProductDeployment");
+ }
+
+ private static ProductDeployment CreateRunningDeploymentWithSharedVars(Dictionary sharedVars)
+ {
+ var pd = ProductDeployment.InitiateDeployment(
+ ProductDeploymentId.NewId(), EnvironmentId.NewId(),
+ "stacks:testproduct", "stacks:testproduct:1.0.0",
+ "testproduct", "Test Product", "1.0.0",
+ UserId.NewId(), "test-deploy",
+ CreateStackConfigs(1),
+ sharedVars);
+
+ pd.StartStack("stack-0", DeploymentId.NewId());
+ pd.CompleteStack("stack-0");
+ return pd;
+ }
+
+ private static global::ReadyStackGo.Domain.StackManagement.Stacks.ProductDefinition CreateCatalogProductWithObserver(string productId)
+ {
+ var stack = new global::ReadyStackGo.Domain.StackManagement.Stacks.StackDefinition(
+ "stacks",
+ "stack-0",
+ new global::ReadyStackGo.Domain.StackManagement.Stacks.ProductId(productId),
+ services: new[]
+ {
+ new global::ReadyStackGo.Domain.StackManagement.Stacks.ServiceTemplate
+ {
+ Name = "svc", Image = "test:latest"
+ }
+ },
+ variables: Array.Empty(),
+ productName: "testproduct",
+ productDisplayName: "Test Product",
+ productVersion: "1.0.0");
+
+ return new global::ReadyStackGo.Domain.StackManagement.Stacks.ProductDefinition(
+ sourceId: "stacks",
+ name: "testproduct",
+ displayName: "Test Product",
+ stacks: new[] { stack },
+ productVersion: "1.0.0",
+ maintenanceObserver: new global::ReadyStackGo.Domain.StackManagement.Manifests.RsgoMaintenanceObserver
+ {
+ Type = "sqlExtendedProperty",
+ PropertyName = "ams-MaintenanceMode",
+ ConnectionString = "Server=${DB_SERVER};Database=${DB_NAME};",
+ MaintenanceValue = "1",
+ NormalValue = "0",
+ PollingInterval = "30s"
+ },
+ productId: productId);
+ }
+
+ private static global::ReadyStackGo.Domain.StackManagement.Stacks.ProductDefinition CreateCatalogProductWithoutObserver(string productId)
+ {
+ var stack = new global::ReadyStackGo.Domain.StackManagement.Stacks.StackDefinition(
+ "stacks",
+ "stack-0",
+ new global::ReadyStackGo.Domain.StackManagement.Stacks.ProductId(productId),
+ services: new[]
+ {
+ new global::ReadyStackGo.Domain.StackManagement.Stacks.ServiceTemplate
+ {
+ Name = "svc", Image = "test:latest"
+ }
+ },
+ variables: Array.Empty(),
+ productName: "testproduct",
+ productDisplayName: "Test Product",
+ productVersion: "1.0.0");
+
+ return new global::ReadyStackGo.Domain.StackManagement.Stacks.ProductDefinition(
+ sourceId: "stacks",
+ name: "testproduct",
+ displayName: "Test Product",
+ stacks: new[] { stack },
+ productVersion: "1.0.0",
+ productId: productId);
+ }
+
+ #endregion
}