Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
85ff567
Add abstraction class for handling command inputs
Jtango18 May 20, 2026
5d2e3db
Add AWS CDK package
Jtango18 May 20, 2026
98201fd
Add Deploy ECS Variables that will be in Server
Jtango18 May 20, 2026
0af50c0
Skeleton of ECS template generation
Jtango18 May 20, 2026
5e34b5d
Update stack name generator
Jtango18 May 20, 2026
2870a65
Restore missing using
Jtango18 May 20, 2026
ef57c9b
comment out tests for now
Jtango18 May 20, 2026
ab4a408
input testing
Jtango18 May 20, 2026
4fc0bb3
Initial skeleton of reworked DeployECS command
Jtango18 May 20, 2026
a514638
Populate some more variables and add to template
Jtango18 May 20, 2026
e5f0d1f
TODOs
Jtango18 May 20, 2026
23a05bc
Start integrating contracts
Jtango18 May 20, 2026
8dc9783
Wire up some more variables
Jtango18 May 21, 2026
4492772
Add EcsManagedFlag
Jtango18 May 21, 2026
a4cb062
Get template gen into a runnable state for testing
Jtango18 May 22, 2026
b40fe19
Switch to Cfn style constructs to make matching SPF easier
Jtango18 May 22, 2026
10e43fa
Cleanup using
Jtango18 May 22, 2026
b7919a9
Tweaks to align with SPF output
Jtango18 May 22, 2026
20f75f0
Remove deserialize customisation
Jtango18 May 22, 2026
22bcc43
revert extensions to main
Jtango18 May 22, 2026
dc1f951
Update variables and types
Jtango18 May 26, 2026
71ac677
Fix some merge issues
Jtango18 May 26, 2026
841b719
Clean up command construction
Jtango18 May 26, 2026
d30a4f6
Clean up variables
Jtango18 May 27, 2026
475a640
Add helpers for transforming inputs to the Cf types
Jtango18 May 27, 2026
0a05885
Update inputs structure
Jtango18 May 27, 2026
287c413
next pass at deploy template
Jtango18 May 27, 2026
17244dd
Clean up some code structure to make SPF comparisons easier
Jtango18 May 27, 2026
151d210
Add formatting for whole doubles to make SPF comparisons easier
Jtango18 May 27, 2026
bc08b7d
Fix doubles
Jtango18 May 27, 2026
31319c0
Parse Environment Variables
Jtango18 May 27, 2026
0c1a637
Map Secrets and Environment Variables
Jtango18 May 27, 2026
3fc251d
move files
Jtango18 May 27, 2026
e2cc34a
fix namespaces
Jtango18 May 27, 2026
c03ec4b
Move files
Jtango18 May 27, 2026
86a2df9
fix namespaces
Jtango18 May 27, 2026
d2750a3
Handles CfnTags
Jtango18 May 27, 2026
a5b60ff
test: fix
Jtango18 May 27, 2026
c610467
rework cfn mapping approacj
Jtango18 May 27, 2026
fcecb5f
fix namesspace
Jtango18 May 27, 2026
0192871
Updated Deploy command implementation.
Jtango18 May 27, 2026
9c119f5
Tidy up output variables
Jtango18 May 28, 2026
13116f6
Minor tweaks to match SPF functionality
Jtango18 May 28, 2026
88b8eab
more consistency tweaks
Jtango18 May 28, 2026
4ba3fe6
Add volumes to template
Jtango18 May 28, 2026
372a13b
Fix entry point
Jtango18 May 28, 2026
190d832
Comments about Access point
Jtango18 May 28, 2026
e77f8cb
style: cleanup
Jtango18 May 28, 2026
5dc5252
Enable logging
Jtango18 May 28, 2026
39ecb95
style: grammar
Jtango18 May 29, 2026
34a897e
Add ShouldWaitForDeploymentCompletion property
Jtango18 May 29, 2026
2efff4b
rename and wireup
Jtango18 May 29, 2026
fe16a22
Refactor convention
Jtango18 May 29, 2026
0fe1c99
Make template generator non static
Jtango18 May 29, 2026
3377a1c
Map log group path
Jtango18 May 29, 2026
63f9880
Handle parameter passing
Jtango18 May 29, 2026
7c2ad3a
Better error handling message
Jtango18 Jun 1, 2026
4f60968
More tweaks + tests
Jtango18 Jun 1, 2026
f40baff
Add original template scenarios
Jtango18 Jun 1, 2026
f932e48
Get rid of AWS library
Jtango18 Jun 1, 2026
fdc23cd
Fix up tests
Jtango18 Jun 1, 2026
31f24c5
fix up more tests
Jtango18 Jun 1, 2026
37be4c2
Remove Test Fixture
Jtango18 Jun 2, 2026
9941821
Remove unnecessary cast
Jtango18 Jun 2, 2026
eb20815
Switch desired count to int
Jtango18 Jun 2, 2026
00ee514
style: formatting
Jtango18 Jun 2, 2026
0b3ce0b
Update variable deserialization to evaluate variable first
Jtango18 Jun 2, 2026
9135d3a
Fix number types
Jtango18 Jun 2, 2026
1490ec0
More type fixes
Jtango18 Jun 2, 2026
5d10057
Add abstraction for resolving image names
Jtango18 Jun 2, 2026
cf52f22
Fix number formatting
Jtango18 Jun 2, 2026
d5915d1
fix numbers
Jtango18 Jun 2, 2026
80a036f
Remove verbose evaluation as it is not needed
Jtango18 Jun 2, 2026
81ebb25
Remove remaining doubles + fix boolean parsing
Jtango18 Jun 3, 2026
3e6f0a2
style: add explainer
Jtango18 Jun 3, 2026
deb8469
Add parsing safety
Jtango18 Jun 3, 2026
eb12c55
tests: fix
Jtango18 Jun 3, 2026
680d906
Fix more int parsing
Jtango18 Jun 3, 2026
4d18bdc
fix int parse
Jtango18 Jun 3, 2026
aaf3cd7
replace remaining unsafe parses
Jtango18 Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions source/Calamari.Aws/AwsModule.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Autofac;
using Calamari.Aws.Inputs.Ecs;
using Calamari.Aws.Integration.Ecs;

namespace Calamari.Aws;
Expand All @@ -8,5 +9,6 @@ public class AwsModule: Module
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<EcsStackNameGenerator>().As<IEcsStackNameGenerator>().SingleInstance();
builder.RegisterType<EcsImageNameResolver>().As<IEcsImageNameResolver>().SingleInstance();
}
}
126 changes: 14 additions & 112 deletions source/Calamari.Aws/Commands/DeployEcsServiceCommand.cs
Original file line number Diff line number Diff line change
@@ -1,142 +1,44 @@
using System;
using System.Collections.Generic;
using Amazon.CloudFormation;
using Calamari.Aws.Deployment;
using Calamari.Aws.Deployment.Conventions;
using Calamari.Aws.Integration.CloudFormation;
using Calamari.Aws.Integration.CloudFormation.Templates;
using Calamari.Aws.Inputs.Ecs;
using Calamari.Aws.Integration.Ecs;
using Calamari.Aws.Util;
using Calamari.CloudAccounts;
using Calamari.Commands.Support;
using Calamari.Common.Commands;
using Calamari.Common.Plumbing;
using Calamari.Common.Plumbing.FileSystem;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Common.Util;
using Calamari.Deployment;
using Newtonsoft.Json;

namespace Calamari.Aws.Commands;

[Command("deploy-aws-ecs-service", Description = "Deploys a service to an Amazon ECS cluster")]
public class DeployEcsServiceCommand : Command
[Command(CommandName, Description = "Deploys a service to an Amazon ECS cluster")]
public class DeployEcsServiceCommand(ILog log, IVariables variables, IEcsStackNameGenerator stackNameGenerator, IEcsImageNameResolver ecsImageNameResolver) : Command
{
readonly ILog log;
readonly IVariables variables;
readonly ICalamariFileSystem fileSystem;
readonly IEcsStackNameGenerator stackNameGenerator;
string templateFile;
string templateParameterFile;

public DeployEcsServiceCommand(ILog log, IVariables variables, ICalamariFileSystem fileSystem, IEcsStackNameGenerator stackNameGenerator)
{
this.log = log;
this.variables = variables;
this.fileSystem = fileSystem;
this.stackNameGenerator = stackNameGenerator;
Options.Add("template=", "Path to the CloudFormation template file.", v => templateFile = v);
Options.Add("templateParameters=", "Path to the CloudFormation template parameters JSON file.", v => templateParameterFile = v);
}
const string CommandName = "deploy-aws-ecs-service";

public override int Execute(string[] commandLineArguments)
{
Options.Parse(commandLineArguments);

Guard.NotNullOrWhiteSpace(templateFile, "The --template argument is required.");

var environment = AwsEnvironmentGeneration.Create(log, variables).GetAwaiter().GetResult();
var inputs = ReadAndValidateInputs();
var inputs = new DeployEcsCommandInputs(variables, stackNameGenerator, ecsImageNameResolver, log);
var inputValidity = inputs.Validate();
if (!inputValidity.IsValid)
{
throw new CommandException(inputValidity.MissingKeyList);
}

var stackArn = new StackArn(inputs.StackName);
var templateResolver = new TemplateResolver(fileSystem);

new ConventionProcessor(new RunningDeployment(variables),
[
new LogAwsUserInfoConvention(environment),
new DeployAwsCloudFormationConvention(ClientFactory,
TemplateFactory,
new StackEventLogger(log),
_ => stackArn,
inputs.WaitForComplete,
inputs.StackName,
environment,
log,
inputs.WaitTimeout),
new DeployEcsServiceConvention(inputs, environment, log, variables),
new SetEcsOutputVariablesConvention(environment,
inputs.StackName,
inputs.CfStackName,
inputs.ClusterName,
inputs.ServiceName,
inputs.ServiceTaskName,
log)
],
log).RunConventions();

return 0;

IAmazonCloudFormation ClientFactory() => ClientHelpers.CreateCloudFormationClient(environment);

ICloudFormationRequestBuilder TemplateFactory() =>
CloudFormationTemplate.Create(templateResolver,
templateFile,
templateParameterFile,
filesInPackage: false,
fileSystem,
variables,
inputs.StackName,
capabilities: ["CAPABILITY_NAMED_IAM"],
disableRollback: false,
roleArn: null,
tags: inputs.Tags,
stackArn,
ClientFactory);
}

EcsCommandInputs ReadAndValidateInputs()
{
var clusterName = variables.Get(AwsSpecialVariables.Ecs.ClusterName);
Guard.NotNullOrWhiteSpace(clusterName, "Cluster name is required");

var serviceName = variables.Get(AwsSpecialVariables.Ecs.ServiceName);
Guard.NotNullOrWhiteSpace(serviceName, "Service name is required");

var stackName = variables.Get(AwsSpecialVariables.CloudFormation.StackName);
if (string.IsNullOrWhiteSpace(stackName))
{
stackName = stackNameGenerator.Generate(variables, clusterName, serviceName);
log.Verbose($"No stack name supplied; generated \"{stackName}\".");
}

var userTags = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(variables.Get(AwsSpecialVariables.CloudFormation.Tags) ?? "[]") ?? [];
var tags = EcsDefaultTags.Merge(variables, userTags);

var waitOptionType = variables.Get(AwsSpecialVariables.Ecs.WaitOptionLegacy.Type);
Guard.NotNullOrWhiteSpace(waitOptionType, "The wait option is required");
if (waitOptionType != "waitUntilCompleted" && waitOptionType != "waitWithTimeout" && waitOptionType != "dontWait")
{
throw new CommandException($"The wait option has an invalid value '{waitOptionType}'. Expected one of: 'waitUntilCompleted', 'waitWithTimeout', 'dontWait'.");
}

var waitOptionTimeoutMs = variables.GetInt32(AwsSpecialVariables.Ecs.WaitOptionLegacy.Timeout);
if (waitOptionType == "waitWithTimeout" && !waitOptionTimeoutMs.HasValue)
{
throw new CommandException("Wait option is 'waitWithTimeout' but timeout value is not set.");
}

return new EcsCommandInputs(
StackName: stackName,
ClusterName: clusterName,
ServiceName: serviceName,
Tags: tags,
WaitForComplete: waitOptionType != "dontWait",
WaitTimeout: waitOptionType == "waitWithTimeout" ? TimeSpan.FromMilliseconds(waitOptionTimeoutMs!.Value) : null);
}

record EcsCommandInputs(
string StackName,
string ClusterName,
string ServiceName,
List<KeyValuePair<string, string>> Tags,
bool WaitForComplete,
TimeSpan? WaitTimeout);
}
}
35 changes: 27 additions & 8 deletions source/Calamari.Aws/Deployment/AwsSpecialVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,35 @@ public static class S3

public static class Ecs
{
public static class Deploy
{

// Not reusing CloudFormation variable here to make it easier to remove all traces of this when we migrate to native ECS API
public const string StackName = "Octopus.Action.Aws.Ecs.Deploy.CFStackName";

public const string DesiredCount = "Octopus.Action.Aws.Ecs.Deploy.DesiredCount";
public const string MinimumHealthPercent = "Octopus.Action.Aws.Ecs.Deploy.MinimumHealthPercent";
public const string MaximumHealthPercent = "Octopus.Action.Aws.Ecs.Deploy.MaximumHealthPercent";
public const string Cpu = "Octopus.Action.Aws.Ecs.Deploy.Cpu";
public const string Memory = "Octopus.Action.Aws.Ecs.Deploy.Memory";
public const string RuntimeArchitecturePlatform = "Octopus.Action.Aws.Ecs.Deploy.RuntimeArchitecturePlatform";
public const string AutoAssignPublicIp = "Octopus.Action.Aws.Ecs.Deploy.AutoAssignPublicIp";
public const string EnableEcsManagedTags = "Octopus.Action.Aws.Ecs.Deploy.EnableEcsManagedTags";
public const string ServiceTaskName = "Octopus.Action.Aws.Ecs.Deploy.ServiceTaskName";
public const string TaskRole = "Octopus.Action.Aws.Ecs.Deploy.TaskRole";
public const string TaskExecutionRole = "Octopus.Action.Aws.Ecs.Deploy.TaskExecutionRole";
public const string SecurityGroupIds = "Octopus.Action.Aws.Ecs.Deploy.SecurityGroupIds";
public const string SubnetIds = "Octopus.Action.Aws.Ecs.Deploy.SubnetIds";
public const string LoadBalancerMappings = "Octopus.Action.Aws.Ecs.Deploy.LoadBalancerMappings";
public const string Volumes = "Octopus.Action.Aws.Ecs.Deploy.Volumes";
public const string Containers = "Octopus.Action.Aws.Ecs.Deploy.Containers";
}

// Not reusing CloudFormation variable here to make it easier to remove all traces of this when we migrate to native ECS API
public const string Tags = "Octopus.Action.Aws.Ecs.Tags";
public const string ClusterName = "Octopus.Action.Aws.Ecs.ClusterName";
public const string ServiceName = "Octopus.Action.Aws.Ecs.ServiceName";
public const string WaitOption = "Octopus.Action.Aws.Ecs.WaitOption";


public static class Update
{
Expand All @@ -33,13 +59,6 @@ public static class Update
public const string TemplateTaskDefinitionName = "Octopus.Action.Aws.Ecs.Update.TemplateTaskDefinitionName";
public const string ContainerUpdates = "Octopus.Action.Aws.Ecs.Update.ContainerUpdates";
}

// Deploy ECS step: legacy flat key/value pair. Will consolidate when Deploy migrates.
public static class WaitOptionLegacy
{
public const string Type = "Octopus.Action.Aws.Ecs.WaitOption.Type";
public const string Timeout = "Octopus.Action.Aws.Ecs.WaitOption.Timeout";
}
}

public static class CloudFormation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ async Task<string> UpdateCloudFormation(

/// <summary>
/// Not all exceptions are bad. Some just mean there is nothing to do, which is fine.
/// This method will ignore expected exceptions, and rethrow any that are really issues.
/// This method will ignore expected exceptions and rethrow any that are really issues.
/// </summary>
/// <param name="ex">The exception we need to deal with</param>
/// <exception cref="AmazonCloudFormationException">The supplied exception if it really is an error</exception>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using Amazon.CloudFormation;
using Amazon.CloudFormation.Model;
using Calamari.Aws.Inputs.Ecs;
using Calamari.Aws.Integration.CloudFormation;
using Calamari.Aws.Integration.CloudFormation.Templates;
using Calamari.Aws.Integration.Ecs;
using Calamari.Aws.Integration.Ecs.Deploy;
using Calamari.Aws.Util;
using Calamari.CloudAccounts;
using Calamari.Common.Commands;
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Deployment.Conventions;

namespace Calamari.Aws.Deployment.Conventions;

// Currently a thin wrapper over existing template deployment process with the goal to swapping it out for native ECS API solution in the future.
public class DeployEcsServiceConvention(DeployEcsCommandInputs commandInputs, AwsEnvironmentGeneration awsEnvironment, ILog log, IVariables variables)
: IInstallConvention
{
readonly EcsDeployTemplateGenerator templateGenerator = new(commandInputs);


public void Install(RunningDeployment deployment)
{
var generated = templateGenerator.Generate();
var stackEventLogger = new StackEventLogger(log);

var deployCloudFormationConvention = new DeployAwsCloudFormationConvention(ClientFactory,
TemplateFactory,
stackEventLogger,
StackProvider,
commandInputs.ShouldWaitForDeploymentCompletion,
commandInputs.CfStackName,
awsEnvironment,
log,
commandInputs.WaitOption.GetTimeoutSpan()
);
deployCloudFormationConvention.Install(deployment);
return;

StackArn StackProvider(RunningDeployment _) => commandInputs.CfStackArn;
IAmazonCloudFormation ClientFactory() => ClientHelpers.CreateCloudFormationClient(awsEnvironment);

ICloudFormationRequestBuilder TemplateFactory()
{
return new CloudFormationTemplate(() => generated.Body,
new ListTemplateInputs<Parameter>(generated.Parameters),
commandInputs.CfStackName,
["CAPABILITY_NAMED_IAM"],
false,
null,
commandInputs.Tags,
commandInputs.CfStackArn,
ClientFactory,
variables);
}
}
}
Loading