diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/data/Keys.groovy b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/data/Keys.groovy index 44f294b9185..5189d256de9 100644 --- a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/data/Keys.groovy +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/data/Keys.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.clouddriver.aws.data +import com.amazonaws.services.elasticloadbalancingv2.model.TargetTypeEnum import com.google.common.collect.ImmutableMap import com.google.common.collect.ImmutableSet import com.netflix.frigga.Names @@ -175,7 +176,12 @@ class Keys implements KeyParser { } static String getTargetGroupKey(String targetGroupName, String account, String region, String targetGroupType, String vpcId) { - "${ID}:${Namespace.TARGET_GROUPS}:${account}:${region}:${targetGroupName}:${targetGroupType}:${vpcId}" + //Lambda targetGroup don't have the vpcId + if (TargetTypeEnum.Lambda.toString().equalsIgnoreCase(targetGroupType)) { + "${ID}:${Namespace.TARGET_GROUPS}:${account}:${region}:${targetGroupName}:${targetGroupType}" + } else { + "${ID}:${Namespace.TARGET_GROUPS}:${account}:${region}:${targetGroupName}:${targetGroupType}:${vpcId}" + } } static String getClusterKey(String clusterName, String application, String account) { diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/UpsertAmazonLoadBalancerV2Description.java b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/UpsertAmazonLoadBalancerV2Description.java index de8c3a34425..badb881ad29 100644 --- a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/UpsertAmazonLoadBalancerV2Description.java +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/description/UpsertAmazonLoadBalancerV2Description.java @@ -334,6 +334,8 @@ public static class Attributes { private String stickinessType = "lb_cookie"; private Integer stickinessDuration = 86400; private Boolean proxyProtocolV2 = false; + /** The following attribute is supported only if the target is a Lambda function. */ + private Boolean multiValueHeadersEnabled = false; public Integer getDeregistrationDelay() { return deregistrationDelay; @@ -374,6 +376,14 @@ public Boolean getProxyProtocolV2() { public void setProxyProtocolV2(Boolean proxyProtocolV2) { this.proxyProtocolV2 = proxyProtocolV2; } + + public Boolean getMultiValueHeadersEnabled() { + return multiValueHeadersEnabled; + } + + public void setMultiValueHeadersEnabled(Boolean multiValueHeadersEnabled) { + this.multiValueHeadersEnabled = multiValueHeadersEnabled; + } } public static class RuleCondition { diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/handlers/LoadBalancerV2UpsertHandler.groovy b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/handlers/LoadBalancerV2UpsertHandler.groovy index 775fc4615e7..4bad3ce32c7 100644 --- a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/handlers/LoadBalancerV2UpsertHandler.groovy +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/handlers/LoadBalancerV2UpsertHandler.groovy @@ -42,24 +42,31 @@ class LoadBalancerV2UpsertHandler { private static String modifyTargetGroupAttributes(AmazonElasticLoadBalancing loadBalancing, LoadBalancer loadBalancer, TargetGroup targetGroup, UpsertAmazonLoadBalancerV2Description.Attributes attributes, DeployDefaults deployDefaults) { def targetGroupAttributes = [] if (attributes) { - Integer deregistrationDelay = [attributes.deregistrationDelay, deployDefaults?.loadBalancing?.deregistrationDelayDefault].findResult(Closure.IDENTITY) - if (deregistrationDelay != null) { - targetGroupAttributes.add(new TargetGroupAttribute(key: "deregistration_delay.timeout_seconds", value: deregistrationDelay.toString())) - } - if (loadBalancer.type == 'application') { - if (attributes.stickinessEnabled != null) { - targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.enabled", value: attributes.stickinessEnabled.toString())) + if (TargetTypeEnum.Lambda.toString().equalsIgnoreCase(targetGroup.getTargetType())) + { + if (attributes.multiValueHeadersEnabled != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "lambda.multi_value_headers.enabled", value: attributes.multiValueHeadersEnabled)) } - if (attributes.stickinessType != null) { - targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.type", value: attributes.stickinessType)) + } else { + Integer deregistrationDelay = [attributes.deregistrationDelay, deployDefaults?.loadBalancing?.deregistrationDelayDefault].findResult(Closure.IDENTITY) + if (deregistrationDelay != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "deregistration_delay.timeout_seconds", value: deregistrationDelay.toString())) } - if (attributes.stickinessDuration != null) { - targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.lb_cookie.duration_seconds", value: attributes.stickinessDuration.toString())) + if (loadBalancer.type == 'application') { + if (attributes.stickinessEnabled != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.enabled", value: attributes.stickinessEnabled.toString())) + } + if (attributes.stickinessType != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.type", value: attributes.stickinessType)) + } + if (attributes.stickinessDuration != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "stickiness.lb_cookie.duration_seconds", value: attributes.stickinessDuration.toString())) + } } - } - if(loadBalancer.type == 'network' ){ - if(attributes.proxyProtocolV2 != null){ - targetGroupAttributes.add(new TargetGroupAttribute(key: "proxy_protocol_v2.enabled", value: attributes.proxyProtocolV2)) + if (loadBalancer.type == 'network' ) { + if(attributes.proxyProtocolV2 != null) { + targetGroupAttributes.add(new TargetGroupAttribute(key: "proxy_protocol_v2.enabled", value: attributes.proxyProtocolV2)) + } } } } @@ -84,9 +91,23 @@ class LoadBalancerV2UpsertHandler { targetGroupsToCreate.each { targetGroup -> TargetGroup createdTargetGroup try { + String status = "Target group created in ${loadBalancerName} (${targetGroup.name}:${targetGroup.port}:${targetGroup.protocol})." + CreateTargetGroupRequest createTargetGroupRequest = new CreateTargetGroupRequest(); + if (TargetTypeEnum.Lambda.toString().equalsIgnoreCase(targetGroup.targetType)) { - CreateTargetGroupRequest createTargetGroupRequest = new CreateTargetGroupRequest() - .withProtocol(targetGroup.protocol) + createTargetGroupRequest.withName(targetGroup.name) + .withHealthCheckIntervalSeconds(targetGroup.healthCheckInterval) + .withHealthCheckTimeoutSeconds(targetGroup.healthCheckTimeout) + .withHealthyThresholdCount(targetGroup.healthyThreshold) + .withUnhealthyThresholdCount(targetGroup.unhealthyThreshold) + .withTargetType(targetGroup.targetType) + .withMatcher(new Matcher().withHttpCode(targetGroup.healthCheckMatcher)) + .withHealthCheckPath(targetGroup.healthCheckPath) + + status = "Lambda Target group created in ${loadBalancerName} (${targetGroup.name})." + + } else { + createTargetGroupRequest.withProtocol(targetGroup.protocol) .withPort(targetGroup.port) .withName(targetGroup.name) .withVpcId(loadBalancer.vpcId) @@ -97,22 +118,23 @@ class LoadBalancerV2UpsertHandler { .withUnhealthyThresholdCount(targetGroup.unhealthyThreshold) .withTargetType(targetGroup.targetType) - if (targetGroup.healthCheckProtocol in [ProtocolEnum.HTTP, ProtocolEnum.HTTPS]) { - createTargetGroupRequest - .withHealthCheckPath(targetGroup.healthCheckPath) - - // HTTP(s) health checks for TCP does not support custom matchers and timeouts. Also, health thresholds must be equal. - if (targetGroup.protocol == ProtocolEnum.TCP) { - createTargetGroupRequest.withUnhealthyThresholdCount(createTargetGroupRequest.getHealthyThresholdCount()) - } else { - createTargetGroupRequest.withMatcher(new Matcher().withHttpCode(targetGroup.healthCheckMatcher)) - .withHealthCheckTimeoutSeconds(targetGroup.healthCheckTimeout) + if (targetGroup.healthCheckProtocol in [ProtocolEnum.HTTP, ProtocolEnum.HTTPS]) { + createTargetGroupRequest + .withHealthCheckPath(targetGroup.healthCheckPath) + + // HTTP(s) health checks for TCP does not support custom matchers and timeouts. Also, health thresholds must be equal. + if (targetGroup.protocol == ProtocolEnum.TCP) { + createTargetGroupRequest.withUnhealthyThresholdCount(createTargetGroupRequest.getHealthyThresholdCount()) + } else { + createTargetGroupRequest.withMatcher(new Matcher().withHttpCode(targetGroup.healthCheckMatcher)) + .withHealthCheckTimeoutSeconds(targetGroup.healthCheckTimeout) + } } } - CreateTargetGroupResult createTargetGroupResult = loadBalancing.createTargetGroup( createTargetGroupRequest ) - task.updateStatus BASE_PHASE, "Target group created in ${loadBalancerName} (${targetGroup.name}:${targetGroup.port}:${targetGroup.protocol})." + task.updateStatus BASE_PHASE, status createdTargetGroup = createTargetGroupResult.getTargetGroups().get(0) + } catch (AmazonServiceException e) { String exceptionMessage = "Failed to create target group ${targetGroup.name} for ${loadBalancerName} - reason: ${e.errorMessage}." task.updateStatus BASE_PHASE, exceptionMessage diff --git a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/validators/CreateAmazonLoadBalancerDescriptionValidator.java b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/validators/CreateAmazonLoadBalancerDescriptionValidator.java index c0243f5401b..e87f70f18c3 100644 --- a/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/validators/CreateAmazonLoadBalancerDescriptionValidator.java +++ b/clouddriver-aws/src/main/groovy/com/netflix/spinnaker/clouddriver/aws/deploy/validators/CreateAmazonLoadBalancerDescriptionValidator.java @@ -17,10 +17,12 @@ package com.netflix.spinnaker.clouddriver.aws.deploy.validators; import com.amazonaws.services.elasticloadbalancingv2.model.AuthenticateOidcActionConfig; +import com.amazonaws.services.elasticloadbalancingv2.model.TargetTypeEnum; import com.netflix.spinnaker.clouddriver.aws.AmazonOperation; import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerClassicDescription; import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerDescription; import com.netflix.spinnaker.clouddriver.aws.deploy.description.UpsertAmazonLoadBalancerV2Description; +import com.netflix.spinnaker.clouddriver.aws.model.AmazonLoadBalancerType; import com.netflix.spinnaker.clouddriver.aws.security.AmazonCredentials; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; import java.util.HashSet; @@ -137,14 +139,18 @@ public void validate( errors.rejectValue( "targetGroups", "createAmazonLoadBalancerDescription.targetGroups.name.missing"); } - if (targetGroup.getProtocol() == null) { - errors.rejectValue( - "targetGroups", - "createAmazonLoadBalancerDescription.targetGroups.protocol.missing"); - } - if (targetGroup.getPort() == null) { - errors.rejectValue( - "targetGroups", "createAmazonLoadBalancerDescription.targetGroups.port.missing"); + if (TargetTypeEnum.Lambda.toString().equalsIgnoreCase(targetGroup.getTargetType())) { + validateLambdaTargetGroup(albDescription, targetGroup, errors); + } else { + if (targetGroup.getProtocol() == null) { + errors.rejectValue( + "targetGroups", + "createAmazonLoadBalancerDescription.targetGroups.protocol.missing"); + } + if (targetGroup.getPort() == null) { + errors.rejectValue( + "targetGroups", "createAmazonLoadBalancerDescription.targetGroups.port.missing"); + } } } Set unusedTargetGroupNames = new HashSet<>(); @@ -173,4 +179,17 @@ public void validate( break; } } + + private void validateLambdaTargetGroup( + UpsertAmazonLoadBalancerV2Description albDescription, + UpsertAmazonLoadBalancerV2Description.TargetGroup targetGroup, + Errors errors) { + // Add lambda specific validation, if required. + if (!AmazonLoadBalancerType.APPLICATION + .toString() + .equalsIgnoreCase(albDescription.getLoadBalancerType().toString())) { + errors.rejectValue( + "loadBalancerType", "createAmazonLoadBalancerDescription.loadBalancerType.invalid"); + } + } } diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/OnDemandAgent.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/OnDemandAgent.java index 5d2b0578428..5132baa6630 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/OnDemandAgent.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/OnDemandAgent.java @@ -46,7 +46,8 @@ enum OnDemandType { Job, TargetGroup, CloudFormation, - Manifest; + Manifest, + Function; static OnDemandType fromString(String s) { return Arrays.stream(values()) diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/FunctionProvider.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/FunctionProvider.java index d04c04f2e68..a933230a46c 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/FunctionProvider.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/FunctionProvider.java @@ -16,10 +16,26 @@ package com.netflix.spinnaker.clouddriver.model; +import com.netflix.spinnaker.clouddriver.documentation.Empty; import java.util.Collection; +import java.util.Collections; +import java.util.Set; public interface FunctionProvider { Collection getAllFunctions(); Function getFunction(String account, String region, String functionName); + + /** + * Returns all functions related to an application based on one of the following criteria: - the + * load balancer name follows the Frigga naming conventions for load balancers (i.e., the load + * balancer name starts with the application name, followed by a hyphen) + * + * @param applicationName the name of the application + * @return a collection of functions. + */ + @Empty + default Set getApplicationFunctions(String applicationName) { + return Collections.emptySet(); + } } diff --git a/clouddriver-lambda/clouddriver-lambda.gradle b/clouddriver-lambda/clouddriver-lambda.gradle index eea680f8a70..2f885942087 100644 --- a/clouddriver-lambda/clouddriver-lambda.gradle +++ b/clouddriver-lambda/clouddriver-lambda.gradle @@ -22,4 +22,6 @@ dependencies { implementation "com.squareup.okhttp:okhttp-urlconnection" implementation "com.squareup.retrofit:converter-jackson" implementation "com.squareup.retrofit:retrofit" + implementation "com.netflix.spectator:spectator-api" + implementation "com.netflix.frigga:frigga" } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/cache/model/LambdaFunction.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/cache/model/LambdaFunction.java index fa9ae0a1d54..f76f5265e8f 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/cache/model/LambdaFunction.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/cache/model/LambdaFunction.java @@ -18,6 +18,7 @@ import com.amazonaws.services.lambda.model.AliasConfiguration; import com.amazonaws.services.lambda.model.EventSourceMappingConfiguration; +import com.amazonaws.services.lambda.model.FunctionCodeLocation; import com.amazonaws.services.lambda.model.FunctionConfiguration; import com.netflix.spinnaker.clouddriver.model.Function; import java.util.List; @@ -33,4 +34,7 @@ public class LambdaFunction extends FunctionConfiguration implements Function { private Map revisions; private List aliasConfigurations; private List eventSourceMappings; + private FunctionCodeLocation code; + private Map tags; + private List targetGroups; } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionConfigurationDescription.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionConfigurationDescription.java index 66e952d0759..e75fdc256d0 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionConfigurationDescription.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionConfigurationDescription.java @@ -16,6 +16,10 @@ package com.netflix.spinnaker.clouddriver.lambda.deploy.description; +import com.amazonaws.services.lambda.model.DeadLetterConfig; +import com.amazonaws.services.lambda.model.TracingConfig; +import java.util.List; +import java.util.Map; import lombok.Data; import lombok.EqualsAndHashCode; @@ -30,4 +34,13 @@ public class CreateLambdaFunctionConfigurationDescription String role; String runtime; Integer timeout; + List subnetIds; + List securityGroupIds; + Map envVariables; + Map tags; + DeadLetterConfig deadLetterConfig; + String encryptionKMSKeyArn; + TracingConfig tracingConfig; + String targetGroups; + String runTime; } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionDescription.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionDescription.java index 802e6a8516c..52f8e06ce76 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionDescription.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/CreateLambdaFunctionDescription.java @@ -16,6 +16,8 @@ package com.netflix.spinnaker.clouddriver.lambda.deploy.description; +import com.amazonaws.services.lambda.model.DeadLetterConfig; +import com.amazonaws.services.lambda.model.TracingConfig; import java.util.List; import java.util.Map; import lombok.Data; @@ -31,11 +33,22 @@ public class CreateLambdaFunctionDescription extends AbstractLambdaFunctionDescr String handler; String role; String runtime; + String appName; Integer memory; Integer timeout; - List> tags; + Map tags; Boolean publish; + + Map envVariables; + List subnetIds; + List securityGroupIds; + + String targetGroups; + + DeadLetterConfig deadLetterConfig; + TracingConfig tracingConfig; + String encryptionKMSKeyArn; } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/UpdateLambdaFunctionCodeDescription.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/UpdateLambdaFunctionCodeDescription.java index e0caced089d..cca4e4757e7 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/UpdateLambdaFunctionCodeDescription.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/description/UpdateLambdaFunctionCodeDescription.java @@ -24,7 +24,7 @@ public class UpdateLambdaFunctionCodeDescription extends AbstractLambdaFunctionDescription { String functionName; - String s3Bucket; - String s3Key; + String s3bucket; + String s3key; Boolean publish; } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/CreateLambdaAtomicOperation.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/CreateLambdaAtomicOperation.java index d5b66b83dd8..f70f3bc52a9 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/CreateLambdaAtomicOperation.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/CreateLambdaAtomicOperation.java @@ -16,16 +16,30 @@ package com.netflix.spinnaker.clouddriver.lambda.deploy.ops; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.elasticloadbalancingv2.AmazonElasticLoadBalancing; +import com.amazonaws.services.elasticloadbalancingv2.model.DescribeTargetGroupsRequest; +import com.amazonaws.services.elasticloadbalancingv2.model.DescribeTargetGroupsResult; +import com.amazonaws.services.elasticloadbalancingv2.model.RegisterTargetsRequest; +import com.amazonaws.services.elasticloadbalancingv2.model.RegisterTargetsResult; +import com.amazonaws.services.elasticloadbalancingv2.model.TargetDescription; +import com.amazonaws.services.elasticloadbalancingv2.model.TargetGroup; import com.amazonaws.services.lambda.AWSLambda; +import com.amazonaws.services.lambda.model.AddPermissionRequest; import com.amazonaws.services.lambda.model.CreateFunctionRequest; import com.amazonaws.services.lambda.model.CreateFunctionResult; +import com.amazonaws.services.lambda.model.Environment; import com.amazonaws.services.lambda.model.FunctionCode; +import com.amazonaws.services.lambda.model.VpcConfig; +import com.netflix.frigga.Names; +import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials; import com.netflix.spinnaker.clouddriver.lambda.deploy.description.CreateLambdaFunctionDescription; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.UUID; public class CreateLambdaAtomicOperation extends AbstractLambdaAtomicOperation @@ -48,8 +62,9 @@ private CreateFunctionResult createFunction() { .withS3Key(description.getProperty("s3key").toString()); Map objTag = new HashMap<>(); - for (Map tags : description.getTags()) { - for (Entry entry : tags.entrySet()) { + if (null != description.getTags()) { + + for (Entry entry : description.getTags().entrySet()) { objTag.put(entry.getKey(), entry.getValue()); } } @@ -57,7 +72,8 @@ private CreateFunctionResult createFunction() { AWSLambda client = getLambdaClient(); CreateFunctionRequest request = new CreateFunctionRequest(); - request.setFunctionName(description.getFunctionName()); + request.setFunctionName( + combineAppDetail(description.getAppName(), description.getFunctionName())); request.setDescription(description.getDescription()); request.setHandler(description.getHandler()); request.setMemorySize(description.getMemory()); @@ -69,9 +85,103 @@ private CreateFunctionResult createFunction() { request.setCode(code); request.setTags(objTag); + Map envVariables = description.getEnvVariables(); + if (null != envVariables) { + request.setEnvironment(new Environment().withVariables(envVariables)); + } + + if (null != description.getSecurityGroupIds() || null != description.getSubnetIds()) { + request.setVpcConfig( + new VpcConfig() + .withSecurityGroupIds(description.getSecurityGroupIds()) + .withSubnetIds(description.getSubnetIds())); + } + request.setDeadLetterConfig(description.getDeadLetterConfig()); + request.setKMSKeyArn(description.getEncryptionKMSKeyArn()); + request.setTracingConfig(description.getTracingConfig()); + CreateFunctionResult result = client.createFunction(request); updateTaskStatus("Finished Creation of AWS Lambda Function Operation..."); + if (description.getTargetGroups() != null && !description.getTargetGroups().isEmpty()) { + + updateTaskStatus( + String.format( + "Started registering lambda to targetGroup (%s)", description.getTargetGroups())); + String functionArn = result.getFunctionArn(); + registerTargetGroup(functionArn, client); + } return result; } + + protected String combineAppDetail(String appName, String functionName) { + Names functionAppName = Names.parseName(functionName); + if (null != functionAppName) { + return functionAppName.getApp().equals(appName) + ? functionName + : (appName + "-" + functionName); + } else { + throw new IllegalArgumentException( + String.format("Function name {%s} contains invlaid charachetrs ", functionName)); + } + } + + private RegisterTargetsResult registerTargetGroup(String functionArn, AWSLambda lambdaClient) { + + AmazonElasticLoadBalancing loadBalancingV2 = getAmazonElasticLoadBalancingClient(); + TargetGroup targetGroup = retrieveTargetGroup(loadBalancingV2); + + AddPermissionRequest addPermissionRequest = + new AddPermissionRequest() + .withFunctionName(functionArn) + .withAction("lambda:InvokeFunction") + .withSourceArn(targetGroup.getTargetGroupArn()) + .withPrincipal("elasticloadbalancing.amazonaws.com") + .withStatementId(UUID.randomUUID().toString()); + + lambdaClient.addPermission(addPermissionRequest); + + updateTaskStatus( + String.format( + "Lambda (%s) invoke permissions added to Target group (%s).", + functionArn, targetGroup.getTargetGroupArn())); + + RegisterTargetsResult result = + loadBalancingV2.registerTargets( + new RegisterTargetsRequest() + .withTargets(new TargetDescription().withId(functionArn)) + .withTargetGroupArn(targetGroup.getTargetGroupArn())); + + updateTaskStatus( + String.format( + "Registered the Lambda (%s) with Target group (%s).", + functionArn, targetGroup.getTargetGroupArn())); + return result; + } + + private TargetGroup retrieveTargetGroup(AmazonElasticLoadBalancing loadBalancingV2) { + + DescribeTargetGroupsRequest request = + new DescribeTargetGroupsRequest().withNames(description.getTargetGroups()); + DescribeTargetGroupsResult describeTargetGroupsResult = + loadBalancingV2.describeTargetGroups(request); + + if (describeTargetGroupsResult.getTargetGroups().size() == 1) { + return describeTargetGroupsResult.getTargetGroups().get(0); + } else if (describeTargetGroupsResult.getTargetGroups().size() > 1) { + throw new IllegalArgumentException( + "There are multiple target groups with the name " + description.getTargetGroups() + "."); + } else { + throw new IllegalArgumentException( + "There is no target group with the name " + description.getTargetGroups() + "."); + } + } + + private AmazonElasticLoadBalancing getAmazonElasticLoadBalancingClient() { + AWSCredentialsProvider credentialsProvider = getCredentials().getCredentialsProvider(); + NetflixAmazonCredentials credentialAccount = description.getCredentials(); + + return amazonClientProvider.getAmazonElasticLoadBalancingV2( + credentialAccount, getRegion(), false); + } } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaCodeAtomicOperation.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaCodeAtomicOperation.java index 668cfa2ae12..1bf46c30699 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaCodeAtomicOperation.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaCodeAtomicOperation.java @@ -51,8 +51,8 @@ private UpdateFunctionCodeResult updateFunctionConfigurationResult() { new UpdateFunctionCodeRequest() .withFunctionName(lambdaFunction.getFunctionArn()) .withPublish(description.getPublish()) - .withS3Bucket(description.getS3Bucket()) - .withS3Key(description.getS3Key()); + .withS3Bucket(description.getS3bucket()) + .withS3Key(description.getS3key()); UpdateFunctionCodeResult result = client.updateFunctionCode(request); updateTaskStatus("Finished Updating of AWS Lambda Function Code Operation..."); diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaConfigurationAtomicOperation.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaConfigurationAtomicOperation.java index 4e4ba2c392d..86dfc942289 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaConfigurationAtomicOperation.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/deploy/ops/UpdateLambdaConfigurationAtomicOperation.java @@ -16,13 +16,19 @@ package com.netflix.spinnaker.clouddriver.lambda.deploy.ops; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.elasticloadbalancingv2.AmazonElasticLoadBalancing; +import com.amazonaws.services.elasticloadbalancingv2.model.*; import com.amazonaws.services.lambda.AWSLambda; -import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationRequest; -import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationResult; +import com.amazonaws.services.lambda.model.*; +import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials; import com.netflix.spinnaker.clouddriver.lambda.cache.model.LambdaFunction; import com.netflix.spinnaker.clouddriver.lambda.deploy.description.CreateLambdaFunctionConfigurationDescription; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import org.springframework.util.StringUtils; public class UpdateLambdaConfigurationAtomicOperation extends AbstractLambdaAtomicOperation< @@ -47,6 +53,7 @@ private UpdateFunctionConfigurationResult updateFunctionConfigurationResult() { description.getAccount(), description.getRegion(), description.getFunctionName()); AWSLambda client = getLambdaClient(); + UpdateFunctionConfigurationRequest request = new UpdateFunctionConfigurationRequest() .withFunctionName(cache.getFunctionArn()) @@ -54,10 +61,127 @@ private UpdateFunctionConfigurationResult updateFunctionConfigurationResult() { .withHandler(description.getHandler()) .withMemorySize(description.getMemory()) .withRole(description.getRole()) - .withTimeout(description.getTimeout()); + .withTimeout(description.getTimeout()) + .withDeadLetterConfig(description.getDeadLetterConfig()) + .withVpcConfig( + new VpcConfig() + .withSecurityGroupIds(description.getSecurityGroupIds()) + .withSubnetIds(description.getSubnetIds())) + .withKMSKeyArn(description.getEncryptionKMSKeyArn()) + .withTracingConfig(description.getTracingConfig()) + .withRuntime(description.getRuntime()); + + if (null != description.getEnvVariables()) { + request.setEnvironment(new Environment().withVariables(description.getEnvVariables())); + } UpdateFunctionConfigurationResult result = client.updateFunctionConfiguration(request); + TagResourceRequest tagResourceRequest = new TagResourceRequest(); + Map objTag = new HashMap<>(); + if (null != description.getTags()) { + + for (Map.Entry entry : description.getTags().entrySet()) { + objTag.put(entry.getKey(), entry.getValue()); + } + } + if (!objTag.isEmpty()) { + + UntagResourceRequest untagResourceRequest = + new UntagResourceRequest().withResource(result.getFunctionArn()); + ListTagsResult existingTags = + client.listTags(new ListTagsRequest().withResource(result.getFunctionArn())); + for (Map.Entry entry : existingTags.getTags().entrySet()) { + untagResourceRequest.getTagKeys().add(entry.getKey()); + } + if (!untagResourceRequest.getTagKeys().isEmpty()) { + client.untagResource(untagResourceRequest); + } + for (Map.Entry entry : objTag.entrySet()) { + tagResourceRequest.addTagsEntry(entry.getKey(), entry.getValue()); + } + tagResourceRequest.setResource(result.getFunctionArn()); + client.tagResource(tagResourceRequest); + } updateTaskStatus("Finished Updating of AWS Lambda Function Configuration Operation..."); + if (StringUtils.isEmpty(description.getTargetGroups())) { + if (!cache.getTargetGroups().isEmpty()) { + AmazonElasticLoadBalancing loadBalancingV2 = getAmazonElasticLoadBalancingClient(); + for (String groupName : cache.getTargetGroups()) { + deregisterTarget( + loadBalancingV2, + cache.getFunctionArn(), + retrieveTargetGroup(loadBalancingV2, groupName).getTargetGroupArn()); + updateTaskStatus("De-registered the target group..."); + } + } + + } else { + AmazonElasticLoadBalancing loadBalancingV2 = getAmazonElasticLoadBalancingClient(); + if (cache.getTargetGroups().isEmpty()) { + registerTarget( + loadBalancingV2, + cache.getFunctionArn(), + retrieveTargetGroup(loadBalancingV2, description.getTargetGroups()) + .getTargetGroupArn()); + updateTaskStatus("Registered the target group..."); + } else { + for (String groupName : cache.getTargetGroups()) { + if (!groupName.equals(description.getTargetGroups())) { + registerTarget( + loadBalancingV2, + cache.getFunctionArn(), + retrieveTargetGroup(loadBalancingV2, description.getTargetGroups()) + .getTargetGroupArn()); + updateTaskStatus("Registered the target group..."); + } + } + } + } return result; } + + private TargetGroup retrieveTargetGroup( + AmazonElasticLoadBalancing loadBalancingV2, String targetGroupName) { + + DescribeTargetGroupsRequest request = + new DescribeTargetGroupsRequest().withNames(targetGroupName); + DescribeTargetGroupsResult describeTargetGroupsResult = + loadBalancingV2.describeTargetGroups(request); + + if (describeTargetGroupsResult.getTargetGroups().size() == 1) { + return describeTargetGroupsResult.getTargetGroups().get(0); + } else if (describeTargetGroupsResult.getTargetGroups().size() > 1) { + throw new IllegalArgumentException( + "There are multiple target groups with the name " + targetGroupName + "."); + } else { + throw new IllegalArgumentException( + "There is no target group with the name " + targetGroupName + "."); + } + } + + private AmazonElasticLoadBalancing getAmazonElasticLoadBalancingClient() { + AWSCredentialsProvider credentialsProvider = getCredentials().getCredentialsProvider(); + NetflixAmazonCredentials credentialAccount = description.getCredentials(); + + return amazonClientProvider.getAmazonElasticLoadBalancingV2( + credentialAccount, getRegion(), false); + } + + private void registerTarget( + AmazonElasticLoadBalancing loadBalancingV2, String functionArn, String targetGroupArn) { + RegisterTargetsResult result = + loadBalancingV2.registerTargets( + new RegisterTargetsRequest() + .withTargets(new TargetDescription().withId(functionArn)) + .withTargetGroupArn(targetGroupArn)); + } + + private void deregisterTarget( + AmazonElasticLoadBalancing loadBalancingV2, String functionArn, String targetGroupArn) { + DeregisterTargetsResult result = + loadBalancingV2.deregisterTargets( + new DeregisterTargetsRequest() + .withTargetGroupArn(targetGroupArn) + .withTargets(new TargetDescription().withId(functionArn))); + } } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/agent/LambdaCachingAgent.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/agent/LambdaCachingAgent.java index e732920f86e..8931d0c8f68 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/agent/LambdaCachingAgent.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/agent/LambdaCachingAgent.java @@ -17,22 +17,17 @@ package com.netflix.spinnaker.clouddriver.lambda.provider.agent; import static com.netflix.spinnaker.cats.agent.AgentDataType.Authority.AUTHORITATIVE; +import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.APPLICATIONS; import static com.netflix.spinnaker.clouddriver.lambda.cache.Keys.Namespace.LAMBDA_FUNCTIONS; +import com.amazonaws.auth.policy.*; import com.amazonaws.services.lambda.AWSLambda; -import com.amazonaws.services.lambda.model.AliasConfiguration; -import com.amazonaws.services.lambda.model.EventSourceMappingConfiguration; -import com.amazonaws.services.lambda.model.FunctionConfiguration; -import com.amazonaws.services.lambda.model.ListAliasesRequest; -import com.amazonaws.services.lambda.model.ListAliasesResult; -import com.amazonaws.services.lambda.model.ListEventSourceMappingsRequest; -import com.amazonaws.services.lambda.model.ListEventSourceMappingsResult; -import com.amazonaws.services.lambda.model.ListFunctionsRequest; -import com.amazonaws.services.lambda.model.ListFunctionsResult; -import com.amazonaws.services.lambda.model.ListVersionsByFunctionRequest; -import com.amazonaws.services.lambda.model.ListVersionsByFunctionResult; +import com.amazonaws.services.lambda.model.*; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.frigga.Names; +import com.netflix.spectator.api.DefaultRegistry; +import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.cats.agent.AccountAware; import com.netflix.spinnaker.cats.agent.AgentDataType; import com.netflix.spinnaker.cats.agent.CacheResult; @@ -41,15 +36,21 @@ import com.netflix.spinnaker.cats.cache.CacheData; import com.netflix.spinnaker.cats.cache.DefaultCacheData; import com.netflix.spinnaker.cats.provider.ProviderCache; +import com.netflix.spinnaker.clouddriver.aws.AmazonCloudProvider; +import com.netflix.spinnaker.clouddriver.aws.data.ArnUtils; import com.netflix.spinnaker.clouddriver.aws.provider.AwsProvider; import com.netflix.spinnaker.clouddriver.aws.security.AmazonClientProvider; import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials; +import com.netflix.spinnaker.clouddriver.cache.OnDemandAgent; +import com.netflix.spinnaker.clouddriver.cache.OnDemandMetricsSupport; import com.netflix.spinnaker.clouddriver.lambda.cache.Keys; import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @Slf4j -public class LambdaCachingAgent implements CachingAgent, AccountAware { +public class LambdaCachingAgent implements CachingAgent, AccountAware, OnDemandAgent { private static final TypeReference> ATTRIBUTES = new TypeReference>() {}; @@ -65,6 +66,8 @@ public class LambdaCachingAgent implements CachingAgent, AccountAware { private final AmazonClientProvider amazonClientProvider; private final NetflixAmazonCredentials account; private final String region; + private OnDemandMetricsSupport metricsSupport; + private final Registry registry; LambdaCachingAgent( ObjectMapper objectMapper, @@ -76,6 +79,12 @@ public class LambdaCachingAgent implements CachingAgent, AccountAware { this.amazonClientProvider = amazonClientProvider; this.account = account; this.region = region; + this.registry = new DefaultRegistry(); + this.metricsSupport = + new OnDemandMetricsSupport( + registry, + this, + AmazonCloudProvider.ID + ":" + AmazonCloudProvider.ID + ":" + OnDemandType.Function); } @Override @@ -113,31 +122,60 @@ public CacheResult loadData(ProviderCache providerCache) { } ListFunctionsResult listFunctionsResult = lambda.listFunctions(listFunctionsRequest); + lstFunction.addAll(listFunctionsResult.getFunctions()); nextMarker = listFunctionsResult.getNextMarker(); } while (nextMarker != null && nextMarker.length() != 0); Collection data = new LinkedList<>(); + Collection appData = new LinkedList<>(); + Map> appRelationships = new HashMap>(); + + Map> cacheResults = new HashMap<>(); for (FunctionConfiguration x : lstFunction) { Map attributes = objectMapper.convertValue(x, ATTRIBUTES); attributes.put("account", account.getName()); attributes.put("region", region); - attributes.put("revisions", listFunctionRevisions(x.getFunctionArn())); attributes.put("aliasConfiguration", listAliasConfiguration(x.getFunctionArn())); attributes.put( "eventSourceMappings", listEventSourceMappingConfiguration(x.getFunctionArn())); + attributes = addConfigAttributes(attributes, x, lambda); + String functionName = x.getFunctionName(); + attributes.put("targetGroups", getTargetGroupNames(lambda, functionName)); + Names names = Names.parseName(functionName); + if (null != names.getApp()) { + String appKey = + com.netflix.spinnaker.clouddriver.aws.data.Keys.getApplicationKey(names.getApp()); + Collection functionKeys = appRelationships.get(appKey); + String functionKey = Keys.getLambdaFunctionKey(account.getName(), region, functionName); + + if (null == functionKeys) { + functionKeys = new ArrayList<>(); + appRelationships.put(appKey, functionKeys); + } + functionKeys.add(functionKey); + } data.add( new DefaultCacheData( Keys.getLambdaFunctionKey(account.getName(), region, x.getFunctionName()), attributes, Collections.emptyMap())); } - + for (String appKey : appRelationships.keySet()) { + appData.add( + new DefaultCacheData( + appKey, + Collections.emptyMap(), + Collections.singletonMap(LAMBDA_FUNCTIONS.ns, appRelationships.get(appKey)))); + } + cacheResults.put(LAMBDA_FUNCTIONS.ns, data); + cacheResults.put( + com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.APPLICATIONS.ns, appData); log.info("Caching {} items in {}", String.valueOf(data.size()), getAgentType()); - return new DefaultCacheResult(Collections.singletonMap(LAMBDA_FUNCTIONS.ns, data)); + return new DefaultCacheResult(cacheResults); } private Map listFunctionRevisions(String functionArn) { @@ -212,4 +250,156 @@ private final List listEventSourceMappingConfig return eventSourceMappingConfigurations; } + + private final Map addConfigAttributes( + Map attributes, FunctionConfiguration x, AWSLambda lambda) { + GetFunctionRequest getFunctionRequest = new GetFunctionRequest(); + getFunctionRequest.setFunctionName(x.getFunctionArn()); + GetFunctionResult getFunctionResult = lambda.getFunction(getFunctionRequest); + attributes.put("vpcConfig", getFunctionResult.getConfiguration().getVpcConfig()); + attributes.put("code", getFunctionResult.getCode()); + attributes.put("tags", getFunctionResult.getTags()); + attributes.put("concurrency", getFunctionResult.getConcurrency()); + return attributes; + } + + @Override + public boolean handles(OnDemandType type, String cloudProvider) { + return type == OnDemandType.Function && cloudProvider.equals(AmazonCloudProvider.ID); + } + + @Override + public OnDemandResult handle(ProviderCache providerCache, Map data) { + if (!validKeys(data)) { + return null; + } + + String appName = (String) data.get("appName"); + String functionName = combineAppDetail(appName, (String) data.get("functionName")); + + String functionKey = + Keys.getLambdaFunctionKey( + (String) data.get("credentials"), (String) data.get("region"), functionName); + + String appKey = com.netflix.spinnaker.clouddriver.aws.data.Keys.getApplicationKey(appName); + + AWSLambda lambda = amazonClientProvider.getAmazonLambda(account, region); + + GetFunctionResult functionResult = null; + try { + functionResult = lambda.getFunction(new GetFunctionRequest().withFunctionName(functionName)); + } catch (ResourceNotFoundException ex) { + log.info("Function {} Not exist", functionName); + } + + Map attributes = new HashMap(); + attributes.put("name", appName); + Map> evictions = Collections.emptyMap(); + + Collection existingFunctionRel = null; + + CacheData application = providerCache.get(APPLICATIONS.ns, appKey); + + if (null != application && null != application.getRelationships()) { + existingFunctionRel = application.getRelationships().get(LAMBDA_FUNCTIONS.ns); + } + + Map> relationships = new HashMap>(); + + if (null != existingFunctionRel && !existingFunctionRel.isEmpty()) { + if (null == functionResult && existingFunctionRel.contains(functionKey)) { + existingFunctionRel.remove(functionKey); + evictions.put(LAMBDA_FUNCTIONS.ns, Collections.singletonList(functionKey)); + } else { + existingFunctionRel.add(functionKey); + } + + } else { + existingFunctionRel = Collections.singletonList(functionKey); + } + + relationships.put(LAMBDA_FUNCTIONS.ns, existingFunctionRel); + DefaultCacheData cacheData = new DefaultCacheData(appKey, attributes, relationships); + DefaultCacheResult defaultCacheresults = + new DefaultCacheResult( + Collections.singletonMap( + com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.APPLICATIONS.ns, + Collections.singletonList(cacheData))); + + return new OnDemandAgent.OnDemandResult(getAgentType(), defaultCacheresults, evictions); + } + + @Override + public Collection pendingOnDemandRequests(ProviderCache providerCache) { + return null; + } + + @Override + public String getOnDemandAgentType() { + return getAgentType() + "-OnDemand"; + } + + @Override + public OnDemandMetricsSupport getMetricsSupport() { + return metricsSupport; + } + + private Boolean validKeys(Map data) { + return (data.containsKey("functionName") + && data.containsKey("credentials") + && data.containsKey("region")); + } + + protected String combineAppDetail(String appName, String functionName) { + Names functionAppName = Names.parseName(functionName); + if (null != functionAppName) { + return functionAppName.getApp().equals(appName) + ? functionName + : (appName + "-" + functionName); + } else { + throw new IllegalArgumentException( + String.format("Function name {%s} contains invlaid charachetrs ", functionName)); + } + } + + private List getTargetGroupNames(AWSLambda lambda, String functionName) { + List targetGroupNames = new ArrayList<>(); + Predicate isAllowStatement = + statement -> statement.getEffect().toString().equals(Statement.Effect.Allow.toString()); + Predicate isLambdaInvokeAction = + statement -> + statement.getActions().stream() + .anyMatch(action -> action.getActionName().equals("lambda:InvokeFunction")); + Predicate isElbPrincipal = + statement -> + statement.getPrincipals().stream() + .anyMatch( + principal -> principal.getId().equals("elasticloadbalancing.amazonaws.com")); + + try { + GetPolicyResult result = + lambda.getPolicy(new GetPolicyRequest().withFunctionName(functionName)); + String json = result.getPolicy(); + Policy policy = Policy.fromJson(json); + + targetGroupNames = + policy.getStatements().stream() + .filter(isAllowStatement.and(isLambdaInvokeAction).and(isElbPrincipal)) + .flatMap(statement -> statement.getConditions().stream()) + .filter( + condition -> + condition.getType().equals("ArnLike") + && condition.getConditionKey().equals("AWS:SourceArn")) + .flatMap(condition -> condition.getValues().stream()) + .filter(value -> ArnUtils.extractTargetGroupName(value).isPresent()) + .map(name -> ArnUtils.extractTargetGroupName(name).get()) + .collect(Collectors.toList()); + + } catch (ResourceNotFoundException ex) { + // ignore the exception. + log.info("No policies exist for {}", functionName); + } + + return targetGroupNames; + } } diff --git a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/view/LambdaFunctionProvider.java b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/view/LambdaFunctionProvider.java index 2ce50847a4f..3dd2a182f95 100644 --- a/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/view/LambdaFunctionProvider.java +++ b/clouddriver-lambda/src/main/java/com/netflix/spinnaker/clouddriver/lambda/provider/view/LambdaFunctionProvider.java @@ -16,23 +16,31 @@ package com.netflix.spinnaker.clouddriver.lambda.provider.view; +import static com.netflix.spinnaker.clouddriver.core.provider.agent.Namespace.APPLICATIONS; +import static com.netflix.spinnaker.clouddriver.lambda.cache.Keys.Namespace.LAMBDA_FUNCTIONS; + import com.netflix.spinnaker.cats.cache.Cache; +import com.netflix.spinnaker.cats.cache.CacheData; import com.netflix.spinnaker.clouddriver.lambda.cache.Keys; import com.netflix.spinnaker.clouddriver.lambda.cache.client.LambdaCacheClient; import com.netflix.spinnaker.clouddriver.model.Function; import com.netflix.spinnaker.clouddriver.model.FunctionProvider; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class LambdaFunctionProvider implements FunctionProvider { private LambdaCacheClient awsLambdaCacheClient; + private final Cache cacheView; @Autowired public LambdaFunctionProvider(Cache cacheView) { this.awsLambdaCacheClient = new LambdaCacheClient(cacheView); + this.cacheView = cacheView; } @Override @@ -44,4 +52,27 @@ public Function getFunction(String account, String region, String functionName) String key = Keys.getLambdaFunctionKey(account, region, functionName); return awsLambdaCacheClient.get(key); } + + public Set getApplicationFunctions(String applicationName) { + + CacheData application = + cacheView.get( + APPLICATIONS.ns, + com.netflix.spinnaker.clouddriver.aws.data.Keys.getApplicationKey(applicationName)); + + Set appFunctions = new HashSet<>(); + if (null != application && null != application.getRelationships()) { + Collection functionRel = application.getRelationships().get(LAMBDA_FUNCTIONS.ns); + if (null != functionRel && !functionRel.isEmpty()) { + functionRel.forEach( + functionKey -> { + Function function = awsLambdaCacheClient.get(functionKey); + if (null != function) { + appFunctions.add(function); + } + }); + } + } + return appFunctions; + } } diff --git a/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/FunctionController.java b/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/FunctionController.java index e3537955495..f36382bb491 100644 --- a/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/FunctionController.java +++ b/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/FunctionController.java @@ -27,11 +27,8 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PostAuthorize; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; @RestController public class FunctionController { @@ -68,4 +65,16 @@ public List list( } } } + + @PreAuthorize("hasPermission(#application, 'APPLICATION', 'READ')") + @PostAuthorize("@authorizationSupport.filterForAccounts(returnObject)") + @RequestMapping(value = "/applications/{application}/functions", method = RequestMethod.GET) + List list(@PathVariable String application) { + List appFunctions = + functionProviders.stream() + .map(functionProvider -> functionProvider.getApplicationFunctions(application)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + return appFunctions; + } }