Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
484ff60
Remove unused parameter from DeployAwsCloudFormationConvention
Jtango18 May 29, 2026
86ea805
Add abstraction class for handling command inputs
Jtango18 May 20, 2026
1fff922
Add AWS CDK package
Jtango18 May 20, 2026
3ad279a
Add Deploy ECS Variables that will be in Server
Jtango18 May 20, 2026
c66eacf
Skeleton of ECS template generation
Jtango18 May 20, 2026
d30044f
Update stack name generator
Jtango18 May 20, 2026
409df4b
Restore missing using
Jtango18 May 20, 2026
03c2e4f
comment out tests for now
Jtango18 May 20, 2026
4235704
input testing
Jtango18 May 20, 2026
2f17ac3
Initial skeleton of reworked DeployECS command
Jtango18 May 20, 2026
9cb563c
Populate some more variables and add to template
Jtango18 May 20, 2026
e238ae3
TODOs
Jtango18 May 20, 2026
248ee87
Start integrating contracts
Jtango18 May 20, 2026
b9c5d63
Wire up some more variables
Jtango18 May 21, 2026
9b68372
Add EcsManagedFlag
Jtango18 May 21, 2026
7b39fdd
Get template gen into a runnable state for testing
Jtango18 May 22, 2026
d8ff9ed
Switch to Cfn style constructs to make matching SPF easier
Jtango18 May 22, 2026
d62133f
Cleanup using
Jtango18 May 22, 2026
b4724c5
Tweaks to align with SPF output
Jtango18 May 22, 2026
ec037b5
Remove deserialize customisation
Jtango18 May 22, 2026
cb84c3d
revert extensions to main
Jtango18 May 22, 2026
5310723
Update variables and types
Jtango18 May 26, 2026
53bb100
Fix some merge issues
Jtango18 May 26, 2026
dc70a8a
Clean up command construction
Jtango18 May 26, 2026
d9bfbe4
Clean up variables
Jtango18 May 27, 2026
e2ecac1
Add helpers for transforming inputs to the Cf types
Jtango18 May 27, 2026
5dd4eb3
Update inputs structure
Jtango18 May 27, 2026
2aec86c
next pass at deploy template
Jtango18 May 27, 2026
f01f0c2
Clean up some code structure to make SPF comparisons easier
Jtango18 May 27, 2026
5dd2005
Add formatting for whole doubles to make SPF comparisons easier
Jtango18 May 27, 2026
2985042
Fix doubles
Jtango18 May 27, 2026
ac3bda9
Parse Environment Variables
Jtango18 May 27, 2026
da7e47b
Map Secrets and Environment Variables
Jtango18 May 27, 2026
33f5c55
move files
Jtango18 May 27, 2026
8c192fb
fix namespaces
Jtango18 May 27, 2026
359b3d4
Move files
Jtango18 May 27, 2026
6841235
fix namespaces
Jtango18 May 27, 2026
3812b73
Handles CfnTags
Jtango18 May 27, 2026
64709f3
test: fix
Jtango18 May 27, 2026
3021f95
rework cfn mapping approacj
Jtango18 May 27, 2026
95b87c8
fix namesspace
Jtango18 May 27, 2026
baf9a2a
Updated Deploy command implementation.
Jtango18 May 27, 2026
063b462
Tidy up output variables
Jtango18 May 28, 2026
1af7daa
Minor tweaks to match SPF functionality
Jtango18 May 28, 2026
44f69ac
more consistency tweaks
Jtango18 May 28, 2026
71956ef
Add volumes to template
Jtango18 May 28, 2026
74ccf7b
Fix entry point
Jtango18 May 28, 2026
12568d1
Comments about Access point
Jtango18 May 28, 2026
e118e49
style: cleanup
Jtango18 May 28, 2026
d639db3
Enable logging
Jtango18 May 28, 2026
77c69bb
style: grammar
Jtango18 May 29, 2026
f6946c8
Add ShouldWaitForDeploymentCompletion property
Jtango18 May 29, 2026
5148c3e
rename and wireup
Jtango18 May 29, 2026
82451c6
Refactor convention
Jtango18 May 29, 2026
83e4eea
Make template generator non static
Jtango18 May 29, 2026
88f9d2d
Map log group path
Jtango18 May 29, 2026
88c5aa8
Handle parameter passing
Jtango18 May 29, 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
1 change: 1 addition & 0 deletions source/Calamari.Aws/Calamari.Aws.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Amazon.CDK.Lib" Version="2.256.0" />
<PackageReference Include="AWSSDK.ECS" Version="4.0.19" />
<PackageReference Include="AWSSDK.EKS" Version="4.0.15.1" />
<PackageReference Include="AWSSDK.CloudFormation" Version="4.0.8.19" />
Expand Down
1 change: 0 additions & 1 deletion source/Calamari.Aws/Commands/CreateAwsS3Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ ICloudFormationRequestBuilder TemplateFactory()
TemplateFactory,
stackEventLogger,
StackProvider,
_ => null,
true,
stackName,
environment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ ICloudFormationRequestBuilder TemplateFactory() => string.IsNullOrWhiteSpace(tem
TemplateFactory,
stackEventLogger,
StackProvider,
RoleArnProvider,
waitForComplete,
stackName,
environment,
Expand Down
128 changes: 15 additions & 113 deletions source/Calamari.Aws/Commands/DeployEcsServiceCommand.cs
Original file line number Diff line number Diff line change
@@ -1,143 +1,45 @@
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) : 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, log);
var inputValidity = inputs.Validate();
if (!inputValidity.IsValid)
{
// TODO: Better implementation
throw new CommandException($"Invalid inputs provided to {CommandName}");
}

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,
_ => null,
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 @@ -31,7 +31,6 @@ public class DeployAwsCloudFormationConvention : CloudFormationInstallationConve
readonly Func<IAmazonCloudFormation> clientFactory;
readonly Func<ICloudFormationRequestBuilder> templateFactory;
readonly Func<RunningDeployment, StackArn> stackProvider;
readonly Func<RunningDeployment, string> roleArnProvider;
readonly bool waitForComplete;
readonly string stackName;
readonly TimeSpan? waitTimeout;
Expand All @@ -44,7 +43,6 @@ public DeployAwsCloudFormationConvention(
Func<ICloudFormationRequestBuilder> templateFactory,
StackEventLogger stackEventLogger,
Func<RunningDeployment, StackArn> stackProvider,
Func<RunningDeployment, string> roleArnProvider,
bool waitForComplete,
string stackName,
AwsEnvironmentGeneration awsEnvironmentGeneration,
Expand All @@ -54,7 +52,6 @@ public DeployAwsCloudFormationConvention(
this.clientFactory = clientFactory;
this.templateFactory = templateFactory;
this.stackProvider = stackProvider;
this.roleArnProvider = roleArnProvider;
this.waitForComplete = waitForComplete;
this.stackName = stackName;
this.awsEnvironmentGeneration = awsEnvironmentGeneration;
Expand Down Expand Up @@ -222,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,59 @@
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.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