Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -32,6 +32,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Calamari.CloudAccounts\Calamari.CloudAccounts.csproj" />
<ProjectReference Include="..\Calamari.Contracts\Calamari.Contracts.csproj" />
<ProjectReference Include="..\Calamari.Shared\Calamari.Shared.csproj" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions source/Calamari.Aws/Commands/DeployEcsServiceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ EcsCommandInputs ReadAndValidateInputs()
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.WaitOption.Type);
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.WaitOption.Timeout);
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.");
Expand Down
46 changes: 13 additions & 33 deletions source/Calamari.Aws/Commands/UpdateEcsServiceCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Deployment;
using Calamari.Serialization;
using Newtonsoft.Json;
using Octopus.Calamari.Contracts.Aws.Ecs;

namespace Calamari.Aws.Commands;

Expand Down Expand Up @@ -47,8 +46,7 @@ public override int Execute(string[] commandLineArguments)
inputs.TargetTaskDefinitionName,
inputs.Containers,
inputs.Tags,
inputs.WaitOption,
inputs.WaitTimeout)
inputs.WaitOption)
],
log).RunConventions();

Expand All @@ -60,20 +58,19 @@ EcsUpdateServiceInputs ReadAndValidateInputs()
var clusterName = variables.Get(AwsSpecialVariables.Ecs.ClusterName);
Guard.NotNullOrWhiteSpace(clusterName, "Cluster name is required");

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

var targetFamily = variables.Get(AwsSpecialVariables.Ecs.TargetTaskDefinitionName);
var targetFamily = variables.Get(AwsSpecialVariables.Ecs.Update.TargetTaskDefinitionName);
Guard.NotNullOrWhiteSpace(targetFamily, "Target task definition name is required");

var templateFamily = variables.Get(AwsSpecialVariables.Ecs.TemplateTaskDefinitionName);
var templateFamily = variables.Get(AwsSpecialVariables.Ecs.Update.TemplateTaskDefinitionName);
if (string.IsNullOrWhiteSpace(templateFamily))
{
templateFamily = targetFamily;
}

var containersJson = variables.Get(AwsSpecialVariables.Ecs.Containers) ?? "[]";
var containers = JsonConvert.DeserializeObject<List<EcsContainerUpdate>>(containersJson, JsonSerialization.GetDefaultSerializerSettings()) ?? [];
var containers = variables.GetValueDeserialisedAs<List<ContainerUpdate>>(AwsSpecialVariables.Ecs.Update.ContainerUpdates);
if (containers.Count == 0)
{
throw new CommandException("At least one container is required.");
Expand All @@ -84,8 +81,7 @@ EcsUpdateServiceInputs ReadAndValidateInputs()
Guard.NotNullOrWhiteSpace(c.ContainerName, "Container name is required");
}

var tagsJson = variables.Get(AwsSpecialVariables.CloudFormation.Tags) ?? "[]";
var userTags = JsonConvert.DeserializeObject<List<KeyValuePair<string, string>>>(tagsJson) ?? [];
var userTags = variables.GetValueDeserialisedAs<List<KeyValuePair<string, string>>>(AwsSpecialVariables.ResourceTags);
Comment thread
sathvikkumar-octo marked this conversation as resolved.
var seenTagKeys = new HashSet<string>(StringComparer.Ordinal);
foreach (var tag in userTags)
{
Expand All @@ -95,24 +91,10 @@ EcsUpdateServiceInputs ReadAndValidateInputs()
}
}

var waitOptionRaw = variables.Get(AwsSpecialVariables.Ecs.WaitOption.Type);
Guard.NotNullOrWhiteSpace(waitOptionRaw, "The wait option is required");
if (!Enum.TryParse<WaitOptionType>(waitOptionRaw, ignoreCase: true, out var waitOption))
var waitOption = variables.GetValueDeserialisedAs<WaitOption>(AwsSpecialVariables.Ecs.WaitOption);
if (waitOption.Type == WaitType.WaitWithTimeout && waitOption.GetTimeoutSpan() is null)
{
throw new CommandException(
$"The wait option has an invalid value '{waitOptionRaw}'. Expected one of: 'waitUntilCompleted', 'waitWithTimeout', 'dontWait'.");
}

TimeSpan? timeout = null;
var timeoutMs = variables.GetInt32(AwsSpecialVariables.Ecs.WaitOption.Timeout);
if (waitOption == WaitOptionType.WaitWithTimeout)
{
if (!timeoutMs.HasValue)
{
throw new CommandException("Wait option is 'waitWithTimeout' but timeout value is not set.");
}

timeout = TimeSpan.FromMilliseconds(timeoutMs.Value);
throw new CommandException($"Wait option is '{nameof(WaitType.WaitWithTimeout)}' but got invalid timeout '{waitOption.TimeoutMinutes}'.");
}

return new EcsUpdateServiceInputs(
Expand All @@ -122,8 +104,7 @@ EcsUpdateServiceInputs ReadAndValidateInputs()
templateFamily,
containers,
userTags,
waitOption,
timeout);
waitOption);
}
}

Expand All @@ -132,7 +113,6 @@ public record EcsUpdateServiceInputs(
string ServiceName,
string TargetTaskDefinitionName,
string TemplateTaskDefinitionName,
List<EcsContainerUpdate> Containers,
List<ContainerUpdate> Containers,
List<KeyValuePair<string, string>> Tags,
WaitOptionType WaitOption,
TimeSpan? WaitTimeout);
WaitOption WaitOption);
17 changes: 13 additions & 4 deletions source/Calamari.Aws/Deployment/AwsSpecialVariables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public static class AwsSpecialVariables
{
public const string IamCapabilities = "Octopus.Action.Aws.IamCapabilities";
public const string ResourceTags = "Octopus.Action.Aws.Tags";

public static class Authentication
{
Expand All @@ -23,11 +24,18 @@ public static class Ecs
{
public const string ClusterName = "Octopus.Action.Aws.Ecs.ClusterName";
public const string ServiceName = "Octopus.Action.Aws.Ecs.ServiceName";
public const string TargetTaskDefinitionName = "Octopus.Action.Aws.Ecs.TargetTaskDefinitionName";
public const string TemplateTaskDefinitionName = "Octopus.Action.Aws.Ecs.TemplateTaskDefinitionName";
public const string Containers = "Octopus.Action.Aws.Ecs.Containers";
public const string WaitOption = "Octopus.Action.Aws.Ecs.WaitOption";

public static class WaitOption
public static class Update
{
public const string ServiceName = "Octopus.Action.Aws.Ecs.Update.ServiceName";
public const string TargetTaskDefinitionName = "Octopus.Action.Aws.Ecs.Update.TargetTaskDefinitionName";
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";
Expand All @@ -42,6 +50,7 @@ public static class CloudFormation
public const string TemplateParameters = "Octopus.Action.Aws.CloudFormationTemplateParameters";
public const string TemplateParametersRaw = "Octopus.Action.Aws.CloudFormationTemplateParametersRaw";
public const string RoleArn = "Octopus.Action.Aws.CloudFormation.RoleArn";
// TODO: Tags aren't CFN specific so migrate to use ResourceTags = "Octopus.Action.Aws.Tags" above
public const string Tags = "Octopus.Action.Aws.CloudFormation.Tags";

public static class Changesets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Calamari.Common.Plumbing.Logging;
using Calamari.Common.Plumbing.Variables;
using Calamari.Deployment.Conventions;
using Octopus.Calamari.Contracts.Aws.Ecs;
using Task = System.Threading.Tasks.Task;

namespace Calamari.Aws.Deployment.Conventions;
Expand All @@ -22,15 +23,14 @@ public class UpdateEcsServiceConvention(
string serviceName,
string templateTaskDefinitionName,
string targetTaskDefinitionName,
List<EcsContainerUpdate> containers,
List<ContainerUpdate> containers,
List<KeyValuePair<string, string>> tags,
WaitOptionType waitOption,
TimeSpan? waitTimeout,
WaitOption waitOption,
Func<TimeSpan> deploymentPollInterval = null,
Func<TimeSpan> taskPollInterval = null)
: IInstallConvention
{
readonly EcsPostDeployWatcher watcher = new(ecs, log, clusterName, serviceName, waitOption, waitTimeout, deploymentPollInterval, taskPollInterval);
readonly EcsPostDeployWatcher watcher = new(ecs, log, clusterName, serviceName, waitOption, deploymentPollInterval, taskPollInterval);

public void Install(RunningDeployment deployment) => InstallAsync(deployment).GetAwaiter().GetResult();

Expand Down Expand Up @@ -67,7 +67,7 @@ await ecs.DescribeTaskDefinitionAsync(
}

var taskDefTags = EcsDefaultTags.MergeAndDeduplicateTags(deployment.Variables, tags, templateResp.Tags);
var registerRequest = RegisterTaskDefinitionRequestFactory.FromTaskDefinition(template, targetTaskDefinitionName, containers, taskDefTags);
var registerRequest = RegisterTaskDefinitionRequestFactory.FromTaskDefinition(template, targetTaskDefinitionName, containers, taskDefTags, deployment.Variables);
var registerResp = await ecs.RegisterTaskDefinitionAsync(registerRequest, ct);
var registeredTaskDef = registerResp.TaskDefinition;

Expand Down
7 changes: 0 additions & 7 deletions source/Calamari.Aws/Integration/Ecs/EcsContainerUpdate.cs

This file was deleted.

25 changes: 16 additions & 9 deletions source/Calamari.Aws/Integration/Ecs/EcsPostDeployWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Amazon.ECS.Model;
using Calamari.Common.Commands;
using Calamari.Common.Plumbing.Logging;
using Octopus.Calamari.Contracts.Aws.Ecs;
using Task = System.Threading.Tasks.Task;

namespace Calamari.Aws.Integration.Ecs;
Expand All @@ -15,8 +16,7 @@ public class EcsPostDeployWatcher
readonly ILog log;
readonly string clusterName;
readonly string serviceName;
readonly WaitOptionType waitOption;
readonly TimeSpan? waitTimeout;
readonly WaitOption waitOption;
readonly Func<TimeSpan> deploymentPollInterval;
readonly Func<TimeSpan> taskPollInterval;

Expand All @@ -25,8 +25,7 @@ public EcsPostDeployWatcher(
ILog log,
string clusterName,
string serviceName,
WaitOptionType waitOption,
TimeSpan? waitTimeout,
WaitOption waitOption,
Func<TimeSpan> deploymentPollInterval = null,
Func<TimeSpan> taskPollInterval = null)
{
Expand All @@ -35,14 +34,13 @@ public EcsPostDeployWatcher(
this.clusterName = clusterName;
this.serviceName = serviceName;
this.waitOption = waitOption;
this.waitTimeout = waitTimeout;
this.deploymentPollInterval = deploymentPollInterval ?? (() => TimeSpan.FromSeconds(3));
this.taskPollInterval = taskPollInterval ?? (() => TimeSpan.FromSeconds(10));
}

public async Task WaitAsync(Service service, CancellationToken ct = default)
{
if (waitOption == WaitOptionType.DontWait)
if (waitOption.Type == WaitType.DontWait)
{
return;
}
Expand All @@ -60,7 +58,7 @@ public async Task WaitAsync(Service service, CancellationToken ct = default)

async Task WaitForDeploymentAsync(CancellationToken ct)
{
var timeout = GetTimeout(waitTimeout);
var timeout = GetTimeout();

log.Info($"Waiting for ECS service '{serviceName}' deployment to reach COMPLETED.");
string lastReportedState = null;
Expand Down Expand Up @@ -120,7 +118,7 @@ async Task WaitForDeploymentAsync(CancellationToken ct)

async Task WaitForTaskStatesAsync(CancellationToken ct)
{
var timeout = GetTimeout(waitTimeout);
var timeout = GetTimeout();

while (true)
{
Expand Down Expand Up @@ -171,5 +169,14 @@ async Task WaitForTaskStatesAsync(CancellationToken ct)
}
}

static DateTime? GetTimeout(TimeSpan? waitTimeout) => waitTimeout.HasValue ? DateTime.UtcNow + waitTimeout.Value : null;
DateTime? GetTimeout()
{
Comment thread
sathvikkumar-octo marked this conversation as resolved.
if (waitOption.Type != WaitType.WaitWithTimeout)
{
return null;
}

var span = waitOption.GetTimeoutSpan();
return span.HasValue ? DateTime.UtcNow + span.Value : null;
}
}
7 changes: 0 additions & 7 deletions source/Calamari.Aws/Integration/Ecs/EnvAction.cs

This file was deleted.

5 changes: 0 additions & 5 deletions source/Calamari.Aws/Integration/Ecs/EnvVarItem.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.Linq;
using Amazon.ECS.Model;
using Calamari.Common.Commands;
using Calamari.Common.Plumbing.Variables;
using Newtonsoft.Json;
using Octopus.Calamari.Contracts.Aws.Ecs;

namespace Calamari.Aws.Integration.Ecs;

Expand All @@ -13,8 +15,9 @@ public static class RegisterTaskDefinitionRequestFactory
public static RegisterTaskDefinitionRequest FromTaskDefinition(
TaskDefinition source,
string targetFamily,
IReadOnlyList<EcsContainerUpdate> containerUpdates,
IReadOnlyList<KeyValuePair<string, string>> tags)
IReadOnlyList<ContainerUpdate> containerUpdates,
IReadOnlyList<KeyValuePair<string, string>> tags,
IVariables variables)
{
// Serialize task definition to JSON and then deserialize back to request
// This avoids us dropping any fields from task definition when performing our update
Expand All @@ -24,36 +27,40 @@ public static RegisterTaskDefinitionRequest FromTaskDefinition(
request.Tags = tags.Select(t => new Tag { Key = t.Key, Value = t.Value }).ToList();

var containerLookup = (request.ContainerDefinitions ?? []).ToDictionary(c => c.Name);
foreach (var containerUpdate in containerUpdates)
foreach (var update in containerUpdates)
{
if (!containerLookup.TryGetValue(containerUpdate.ContainerName, out var containerToUpdate))
if (!containerLookup.TryGetValue(update.ContainerName, out var containerToUpdate))
{
throw new CommandException($"No matching container found for '{containerUpdate.ContainerName}' in template task definition '{source.Family}'.");
throw new CommandException($"No matching container found for '{update.ContainerName}' in template task definition '{source.Family}'.");
}

if (!string.IsNullOrWhiteSpace(containerUpdate.Image))
if (!string.IsNullOrWhiteSpace(update.PackageReference))
{
containerToUpdate.Image = containerUpdate.Image;
var image = variables.Get(PackageVariables.IndexedImage(update.PackageReference));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new way of handling container images works well and resolves this bug

OctopusDeploy/Issues#10033

This bug was caused by having nested Octostache as the previous container image handling wrapped any SPF value in an Octostache expression of its own

if (!string.IsNullOrWhiteSpace(image))
{
containerToUpdate.Image = image;
}
}

ApplyEnvVars(containerToUpdate, containerUpdate.EnvironmentVariables);
ApplyEnvFiles(containerToUpdate, containerUpdate.EnvironmentFiles);
ApplyEnvVars(containerToUpdate, update.EnvironmentVariables);
ApplyEnvFiles(containerToUpdate, update.EnvironmentFiles);
}

return request;
}

static void ApplyEnvVars(ContainerDefinition container, EnvAction<EnvVarItem> action)
static void ApplyEnvVars(ContainerDefinition container, EnvAction<TypedKeyValuePair> action)
{
if (action is null || action.Items.Count == 0)
{
Comment thread
sathvikkumar-octo marked this conversation as resolved.
return;
}

var plain = action.Items.Where(i => i.Type == EnvVarType.Text)
var plain = action.Items.Where(i => i.Type == KeyValueType.Plain)
.Select(i => new Amazon.ECS.Model.KeyValuePair { Name = i.Key, Value = i.Value })
.ToList();
var secrets = action.Items.Where(i => i.Type == EnvVarType.Secret)
var secrets = action.Items.Where(i => i.Type == KeyValueType.Secret)
.Select(i => new Secret { Name = i.Key, ValueFrom = i.Value })
.ToList();

Expand All @@ -62,7 +69,7 @@ static void ApplyEnvVars(ContainerDefinition container, EnvAction<EnvVarItem> ac
container.Environment = plain;
container.Secrets = secrets;
}
else // Merge
else // Append
{
var existingEnv = container.Environment ?? [];
container.Environment = existingEnv
Expand Down Expand Up @@ -91,7 +98,7 @@ static void ApplyEnvFiles(ContainerDefinition container, EnvAction<string> actio
{
container.EnvironmentFiles = files;
}
else // Merge
else // Append
{
var existing = container.EnvironmentFiles ?? [];
container.EnvironmentFiles = existing.Concat(files)
Expand Down
Loading