-
Notifications
You must be signed in to change notification settings - Fork 116
feat: ECS Cluster Target Discovery #2013
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
cf6a46c
86ea89a
17b286e
18d0c2e
216e5e6
e23f34a
7948d56
dc29823
5b0aeed
93c29f2
70b10e6
283cbea
fa795e9
7e91034
7bc2841
0bdd0a1
da819b5
ba6d5ea
08a1604
2036202
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,121 @@ | ||
| using System.Threading.Tasks; | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Linq; | ||
| using Amazon.Runtime; | ||
| using Calamari.Aws.Discovery; | ||
| using Calamari.Aws.Integration.Ecs; | ||
| using Calamari.Common.Commands; | ||
| using Calamari.Common.Features.Discovery; | ||
| using Calamari.Common.Plumbing.Logging; | ||
| using Calamari.Common.Plumbing.Pipeline; | ||
| using Octopus.Calamari.Contracts.TargetDiscovery; | ||
| using Octopus.CoreUtilities.Extensions; | ||
| using Task = System.Threading.Tasks.Task; | ||
|
|
||
| namespace Calamari.Aws.Behaviours; | ||
|
|
||
| public class EcsClusterDiscoveryBehaviour : IDeployBehaviour | ||
| public class EcsClusterDiscoveryBehaviour(IEcsDiscoverer ecsDiscoverer, IAwsTargetDiscoveryContextResolver contextResolver, IEcsClusterDiscoveryWriter clusterDiscoveryWriter, ILog log) : IDeployBehaviour | ||
| { | ||
| public bool IsEnabled(RunningDeployment context) => true; | ||
|
|
||
| public async Task Execute(RunningDeployment deployment) | ||
| { | ||
| const string contextVariableName = TargetDiscoverySpecialVariables.TargetDiscoveryContext; | ||
|
|
||
| var contextJson = deployment.Variables.Get(contextVariableName); | ||
| if (string.IsNullOrEmpty(contextJson)) | ||
| { | ||
| log.Warn($"Could not find target discovery context in variable {contextVariableName}."); | ||
| log.Warn("Aborting target discovery."); | ||
| return; | ||
| } | ||
|
|
||
| if (!contextResolver.TryResolve(contextJson, log, out var discoveryContext)) | ||
| { | ||
| log.Warn("Aborting target discovery. Could not resolve context."); | ||
| return; | ||
| } | ||
|
|
||
| var authentication = discoveryContext.Authentication; | ||
| var scope = discoveryContext.Scope; | ||
|
|
||
| LogAuthenticationDetails(authentication); | ||
|
|
||
| if (!authentication.TryGetCredentials(log, out var credentials)) | ||
| { | ||
| log.Warn("Aborting target discovery. Invalid credentials."); | ||
| return; | ||
| } | ||
|
|
||
| var discoveredTargetCount = 0; | ||
| try | ||
| { | ||
| foreach (var region in authentication.Regions) | ||
| { | ||
| foreach (var cluster in await ecsDiscoverer.DiscoverClustersInRegion(credentials, region)) | ||
| { | ||
| var tags = (cluster.Tags ?? []) | ||
| .Select(tag => new KeyValuePair<string, string>(tag.Key, tag.Value)) | ||
| .ToTargetTags(); | ||
|
|
||
| var matchResult = scope.Match(tags); | ||
| if (matchResult.IsSuccess) | ||
| { | ||
| discoveredTargetCount++; | ||
| log.Info($"Discovered matching ECS cluster '{cluster.ClusterName}' in {region}."); | ||
| clusterDiscoveryWriter.WriteTargetCreationServiceMessage(region, cluster, authentication, scope, matchResult); | ||
| } | ||
| else | ||
| { | ||
| log.Verbose($"ECS cluster '{cluster.ClusterName}' in {region} does not match target requirements:"); | ||
| foreach (var reason in matchResult.FailureReasons) | ||
| { | ||
| log.Verbose($"- {reason}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| catch (AmazonServiceException ex) | ||
| { | ||
| log.Warn("Error connecting to AWS to look for ECS clusters:"); | ||
| log.Warn(ex.Message); | ||
| log.Warn("Aborting target discovery."); | ||
| return; | ||
| } | ||
|
|
||
| log.Info(discoveredTargetCount > 0 | ||
| ? $"{discoveredTargetCount} ECS cluster target{(discoveredTargetCount > 1 ? "s" : "")} found." | ||
| : "Could not find any ECS cluster targets."); | ||
| } | ||
|
|
||
|
|
||
| public Task Execute(RunningDeployment context) | ||
| void LogAuthenticationDetails(IAwsAuthenticationDetails authentication) | ||
| { | ||
| return Task.CompletedTask; | ||
| log.Verbose("Looking for ECS clusters in AWS using:"); | ||
| log.Verbose($"\tAccount: {authentication.AccountId}"); | ||
| log.Verbose($"\tRegions: [{string.Join(",", authentication.Regions)}]"); | ||
|
|
||
| if (authentication.Role.Type == "assumeRole") | ||
| { | ||
| log.Verbose("\tRole:"); | ||
| log.Verbose($"\t\tARN: {authentication.Role.Arn}"); | ||
| if (!authentication.Role.SessionName.IsNullOrEmpty()) | ||
| { | ||
| log.Verbose($"\t\tSession Name: {authentication.Role.SessionName}"); | ||
| } | ||
| if (authentication.Role.SessionDuration != null) | ||
| { | ||
| log.Verbose($"\t\tSession Duration: {authentication.Role.SessionDuration}"); | ||
| } | ||
| if (!authentication.Role.ExternalId.IsNullOrEmpty()) | ||
| { | ||
| log.Verbose($"\t\tExternal Id: {authentication.Role.ExternalId}"); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| log.Verbose("\tRole: No IAM Role provided."); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ public override int Execute(string[] commandLineArguments) | |
| var inputs = ReadAndValidateInputs(); | ||
| var environment = AwsEnvironmentGeneration.Create(log, variables).GetAwaiter().GetResult(); | ||
|
|
||
| using var ecsClient = EcsClientFactory.Create(environment); | ||
| using var ecsClient = EcsClientFactoryHelper.Create(environment); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can probably inject this as an autofac delegate factory (EcsClientFactory.Factory)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I got chipped last time I did that :-( |
||
|
|
||
| new ConventionProcessor(new RunningDeployment(variables), | ||
| [ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| using Calamari.Common.Plumbing.Logging; | ||
| using Newtonsoft.Json; | ||
| using Octopus.Calamari.Contracts.TargetDiscovery; | ||
|
|
||
| namespace Calamari.Aws.Discovery; | ||
|
|
||
| public interface IAwsTargetDiscoveryContextResolver | ||
| { | ||
| bool TryResolve(string contextJson, ILog log, out TargetDiscoveryContext<IAwsAuthenticationDetails> context); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Deserializes the Discovery Context into AWS Credentials | ||
| /// </summary> | ||
| public class AwsTargetDiscoveryContextResolver: IAwsTargetDiscoveryContextResolver | ||
| { | ||
| public bool TryResolve(string contextJson, ILog log, out TargetDiscoveryContext<IAwsAuthenticationDetails> context) | ||
| { | ||
| context = null; | ||
| try | ||
| { | ||
| context = JsonConvert.DeserializeObject<TargetDiscoveryContext<IAwsAuthenticationDetails>>(contextJson); | ||
| } | ||
| catch (JsonException ex) | ||
| { | ||
| log.Warn($"AWS target discovery context is in the wrong format: {ex.Message}"); | ||
| return false; | ||
| } | ||
|
|
||
| if (context?.Authentication == null || context?.Scope == null) | ||
| { | ||
| log.Warn("AWS target discovery context is in the wrong format."); | ||
| context = null; | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| namespace Calamari.Aws.Integration.Ecs; | ||
|
|
||
| /// <summary> | ||
| /// The service message name and attribute keys used by server. | ||
| /// Note: Should these migrate to CalamariContracts? | ||
| /// </summary> | ||
| public static class AwsEcsServiceMessageNames | ||
| { | ||
| public const string CreateTargetName = "create-aws-ecs-target"; | ||
| public const string AccountIdOrNameAttribute = "octopusAccountIdOrName"; | ||
| public const string ClusterNameAttribute = "clusterName"; | ||
| public const string WorkerPoolIdOrNameAttribute = "octopusDefaultWorkerPoolIdOrName"; | ||
| public const string ClusterRegionAttribute = "clusterRegion"; | ||
| public const string UseInstanceRole = "useInstanceRole"; | ||
| public const string AssumeRole = "assumeRole"; | ||
| public const string AssumeRoleArn = "assumeRoleArn"; | ||
| public const string AssumeRoleSession = "assumeRoleSession"; | ||
| public const string AssumeRoleSessionDurationSeconds = "assumeRoleSessionDurationSeconds"; | ||
| public const string AssumeRoleExternalId = "assumeRoleExternalId"; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,18 @@ | ||
| using Amazon; | ||
| using Amazon.ECS; | ||
| using Calamari.Aws.Util; | ||
| using Calamari.CloudAccounts; | ||
| using Amazon.Runtime; | ||
|
|
||
| namespace Calamari.Aws.Integration.Ecs; | ||
|
|
||
| public static class EcsClientFactory | ||
| public interface IEcsClientFactory | ||
| { | ||
| public static IAmazonECS Create(AwsEnvironmentGeneration environment) => | ||
| new AmazonECSClient(environment.AwsCredentials, environment.AsClientConfig<AmazonECSConfig>()); | ||
| IAmazonECS Create(AWSCredentials credentials, string region); | ||
| } | ||
|
|
||
| public class EcsClientFactory : IEcsClientFactory | ||
| { | ||
| public IAmazonECS Create(AWSCredentials credentials, string region) | ||
| { | ||
| return new AmazonECSClient(credentials, RegionEndpoint.GetBySystemName(region)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| using Amazon.ECS; | ||
| using Calamari.Aws.Util; | ||
| using Calamari.CloudAccounts; | ||
|
|
||
| namespace Calamari.Aws.Integration.Ecs; | ||
|
|
||
| // TODO: Replace Static Helper with concrete class/interface to enable better testing | ||
| public static class EcsClientFactoryHelper | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this class adding value - could people just go direct to the factory?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They could, but I'm focused addition with this PR, avoiding modifying other functionality until a future PR. |
||
| { | ||
| public static IAmazonECS Create(AwsEnvironmentGeneration environment) => | ||
| new AmazonECSClient(environment.AwsCredentials, environment.AsClientConfig<AmazonECSConfig>()); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
standard question - IF we failed finding the 3rd cluster - should we have reported the first 2?
Eg: should this logic create a list of discovered clusters - then turn them into service msgs at the very end (Rather than after each discovery)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@zentron This is an intersting talking point. I've matched the existing behaviour, but it's worthy of further exploration moving forward (outside the confines of the SPF Deprecation Project)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair question. I would naievely say that it should still report the first 2, even if they one of them failed, since they are presumably valid.
So long as it doesnt fail and then no longer detect cluster #4 if #3 fails, it seems reasonable to still detect the ones that did show up.