All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.seanroy.plugins.DeployLambdaMojo Maven / Gradle / Ivy

There is a newer version: 2.3.5
Show newest version
package com.github.seanroy.plugins;

import static com.amazonaws.services.lambda.model.EventSourcePosition.LATEST;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;

import com.amazonaws.auth.policy.Policy;
import com.amazonaws.auth.policy.Statement;
import com.amazonaws.services.cloudwatchevents.model.DeleteRuleRequest;
import com.amazonaws.services.cloudwatchevents.model.DescribeRuleRequest;
import com.amazonaws.services.cloudwatchevents.model.DescribeRuleResult;
import com.amazonaws.services.cloudwatchevents.model.ListRuleNamesByTargetRequest;
import com.amazonaws.services.cloudwatchevents.model.PutRuleRequest;
import com.amazonaws.services.cloudwatchevents.model.PutRuleResult;
import com.amazonaws.services.cloudwatchevents.model.PutTargetsRequest;
import com.amazonaws.services.cloudwatchevents.model.RemoveTargetsRequest;
import com.amazonaws.services.cloudwatchevents.model.Target;
import com.amazonaws.services.dynamodbv2.model.DescribeStreamRequest;
import com.amazonaws.services.dynamodbv2.model.ListStreamsRequest;
import com.amazonaws.services.dynamodbv2.model.ListStreamsResult;
import com.amazonaws.services.dynamodbv2.model.Stream;
import com.amazonaws.services.dynamodbv2.model.StreamDescription;
import com.amazonaws.services.lambda.model.AddPermissionRequest;
import com.amazonaws.services.lambda.model.AddPermissionResult;
import com.amazonaws.services.lambda.model.AliasConfiguration;
import com.amazonaws.services.lambda.model.CreateAliasRequest;
import com.amazonaws.services.lambda.model.CreateEventSourceMappingRequest;
import com.amazonaws.services.lambda.model.CreateEventSourceMappingResult;
import com.amazonaws.services.lambda.model.CreateFunctionRequest;
import com.amazonaws.services.lambda.model.CreateFunctionResult;
import com.amazonaws.services.lambda.model.DeleteEventSourceMappingRequest;
import com.amazonaws.services.lambda.model.Environment;
import com.amazonaws.services.lambda.model.EventSourceMappingConfiguration;
import com.amazonaws.services.lambda.model.EventSourcePosition;
import com.amazonaws.services.lambda.model.FunctionCode;
import com.amazonaws.services.lambda.model.GetFunctionRequest;
import com.amazonaws.services.lambda.model.GetFunctionResult;
import com.amazonaws.services.lambda.model.GetPolicyRequest;
import com.amazonaws.services.lambda.model.GetPolicyResult;
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.RemovePermissionRequest;
import com.amazonaws.services.lambda.model.ResourceNotFoundException;
import com.amazonaws.services.lambda.model.UpdateAliasRequest;
import com.amazonaws.services.lambda.model.UpdateEventSourceMappingRequest;
import com.amazonaws.services.lambda.model.UpdateEventSourceMappingResult;
import com.amazonaws.services.lambda.model.UpdateFunctionConfigurationRequest;
import com.amazonaws.services.lambda.model.VpcConfig;
import com.amazonaws.services.lambda.model.VpcConfigResponse;
import com.amazonaws.services.sns.model.CreateTopicRequest;
import com.amazonaws.services.sns.model.CreateTopicResult;
import com.amazonaws.services.sns.model.ListSubscriptionsResult;
import com.amazonaws.services.sns.model.SubscribeRequest;
import com.amazonaws.services.sns.model.SubscribeResult;
import com.amazonaws.services.sns.model.Subscription;
import com.amazonaws.services.sns.model.UnsubscribeRequest;
import com.amazonaws.services.sqs.model.GetQueueAttributesRequest;
import com.amazonaws.services.sqs.model.GetQueueAttributesResult;
import com.amazonaws.services.sqs.model.GetQueueUrlRequest;
import com.amazonaws.services.sqs.model.GetQueueUrlResult;
import com.amazonaws.services.sqs.model.QueueAttributeName;


/**
 * I am a deploy mojo responsible to upload and create or update lambda function in AWS.
 *
 * @author Sean N. Roy, Sean Roy 11/08/16.
 */
@Mojo(name = "deploy-lambda")
public class DeployLambdaMojo extends AbstractLambdaMojo {

    @Override
    public void execute() throws MojoExecutionException {
        super.execute();
        try {
            uploadJarToS3();
            lambdaFunctions.stream().map(f -> {
                getLog().info("---- Create or update " + f.getFunctionName() + " -----");
                return f;
            }).forEach(lf ->
                getFunctionPolicy
                    .andThen(cleanUpOrphans)
                    .andThen(createOrUpdate)
                    .apply(lf));
        } catch (Exception e) {
            getLog().error("Error during processing", e);
            throw new MojoExecutionException(e.getMessage());
        }
    }
    
    private boolean shouldUpdate(LambdaFunction lambdaFunction, GetFunctionResult getFunctionResult) {
        boolean isConfigurationChanged = isConfigurationChanged(lambdaFunction, getFunctionResult);
        if (!isConfigurationChanged) {
            getLog().info("Config hasn't changed for " + lambdaFunction.getFunctionName());
        }
        if (forceUpdate) {
            getLog().info("Forcing update for " + lambdaFunction.getFunctionName());
        }

        return forceUpdate || isConfigurationChanged;
    }
    
    /*
     *  Get the existing policy function (on updates) and assign it to the lambdaFunction.
     */
    private Function getFunctionPolicy = (LambdaFunction lambdaFunction) -> {
        try {
            lambdaFunction.setExistingPolicy(Policy.fromJson(lambdaClient.getPolicy(new GetPolicyRequest()
                .withFunctionName(lambdaFunction.getFunctionName())
                .withQualifier(lambdaFunction.getQualifier())).getPolicy()));
        } catch (ResourceNotFoundException rnfe3) {
            getLog().debug("Probably creating a new function, policy doesn't exist yet: " + rnfe3.getMessage());
        } 
        
        return lambdaFunction;
    };

    private Function updateFunctionConfig = (LambdaFunction lambdaFunction) -> {
        getLog().info("About to update functionConfig for " + lambdaFunction.getFunctionName());
        UpdateFunctionConfigurationRequest updateFunctionRequest = new UpdateFunctionConfigurationRequest()
                .withFunctionName(lambdaFunction.getFunctionName())
                .withDescription(lambdaFunction.getDescription())
                .withHandler(lambdaFunction.getHandler())
                .withRole(lambdaFunction.getLambdaRoleArn())
                .withTimeout(lambdaFunction.getTimeout())
                .withMemorySize(lambdaFunction.getMemorySize())
                .withRuntime(runtime)
                .withVpcConfig(getVpcConfig(lambdaFunction))
                .withEnvironment(new Environment().withVariables(lambdaFunction.getEnvironmentVariables()));
        lambdaClient.updateFunctionConfiguration(updateFunctionRequest);
        return lambdaFunction;
    };


    private Function createOrUpdateAliases = (LambdaFunction lambdaFunction) -> {
        lambdaFunction.getAliases().forEach(alias -> {
            UpdateAliasRequest updateAliasRequest = new UpdateAliasRequest()
                    .withFunctionName(lambdaFunction.getFunctionName())
                    .withFunctionVersion(lambdaFunction.getVersion())
                    .withName(alias);
            try {
                lambdaClient.updateAlias(updateAliasRequest
                        );
                getLog().info("Alias " + alias + " updated for " + lambdaFunction.getFunctionName() + " with version " + lambdaFunction.getVersion());
            } catch (ResourceNotFoundException ignored) {
                CreateAliasRequest createAliasRequest = new CreateAliasRequest()
                        .withFunctionName(lambdaFunction.getFunctionName())
                        .withFunctionVersion(lambdaFunction.getVersion())
                        .withName(alias);
                lambdaClient.createAlias(createAliasRequest);
                getLog().info("Alias " + alias + " created for " + lambdaFunction.getFunctionName() + " with version " + lambdaFunction.getVersion());
            }
        });
        return lambdaFunction;
    };

    private BiFunction createOrUpdateSNSTopicSubscription = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        getLog().info("About to create or update " + trigger.getIntegration() + " trigger for " + trigger.getSNSTopic());
        CreateTopicRequest createTopicRequest = new CreateTopicRequest()
                .withName(trigger.getSNSTopic());
        CreateTopicResult createTopicResult = snsClient.createTopic(createTopicRequest);
        getLog().info("Topic " + createTopicResult.getTopicArn() + " created");

        SubscribeRequest subscribeRequest = new SubscribeRequest()
                .withTopicArn(createTopicResult.getTopicArn())
                .withEndpoint(lambdaFunction.getUnqualifiedFunctionArn())
                .withProtocol("lambda");
        SubscribeResult subscribeResult = snsClient.subscribe(subscribeRequest);
        getLog().info("Lambda function " + lambdaFunction.getFunctionName() + " subscribed to " + createTopicResult.getTopicArn());
        getLog().info("Created " + trigger.getIntegration() + " trigger " + subscribeResult.getSubscriptionArn());


        Optional statementOpt;
        try {
            GetPolicyRequest getPolicyRequest = new GetPolicyRequest()
                    .withFunctionName(lambdaFunction.getFunctionName());
            GetPolicyResult GetPolicyResult = lambdaClient.getPolicy(getPolicyRequest);
            statementOpt = Policy.fromJson(GetPolicyResult.getPolicy()).getStatements().stream()
                                                     .filter(statement -> statement.getActions().stream().anyMatch(e -> PERM_LAMBDA_INVOKE.equals(e.getActionName())) &&
                                                             statement.getPrincipals().stream().anyMatch(principal -> PRINCIPAL_SNS.equals(principal.getId())) &&
                                                             statement.getConditions().stream().anyMatch(condition -> condition.getValues().stream().anyMatch(s -> Objects.equals(createTopicResult.getTopicArn(), s)))
                                                     ).findAny();
        } catch (ResourceNotFoundException ignored) {
            // no policy found
            statementOpt = empty();
        }

        if (!statementOpt.isPresent()) {
            AddPermissionRequest addPermissionRequest = new AddPermissionRequest()
                    .withAction(PERM_LAMBDA_INVOKE)
                    .withPrincipal(PRINCIPAL_SNS)
                    .withSourceArn(createTopicResult.getTopicArn())
                    .withFunctionName(lambdaFunction.getFunctionName())
                    .withStatementId(UUID.randomUUID().toString());
            AddPermissionResult addPermissionResult = lambdaClient.addPermission(addPermissionRequest);
            getLog().debug("Added permission to lambda function " + addPermissionResult.toString());
        }
        return trigger;
    };

    /**
     * TODO: Much of this code can be factored out into an addPermission function.
     */
    private BiFunction addAlexaSkillsKitPermission = (Trigger trigger, LambdaFunction lambdaFunction) -> { 
        if (!ofNullable(lambdaFunction.getExistingPolicy()).orElse(new Policy()).getStatements().stream().anyMatch(s ->
                s.getId().equals(getAlexaPermissionStatementId()))) {
            getLog().info("Granting invoke permission to " + trigger.getIntegration());    
            AddPermissionRequest addPermissionRequest = new AddPermissionRequest()
                    .withAction(PERM_LAMBDA_INVOKE)
                    .withPrincipal(PRINCIPAL_ALEXA)
                    .withFunctionName(lambdaFunction.getFunctionName())
                    .withQualifier(lambdaFunction.getQualifier())
                    .withStatementId(getAlexaPermissionStatementId());
    
            AddPermissionResult addPermissionResult = lambdaClient.addPermission(addPermissionRequest);
        }

        return trigger;
    };
    private String getAlexaPermissionStatementId() {
        return "lambda-maven-plugin-alexa-" + regionName + "-permission";
    }
  
    /**
     * TODO: Much of this code can be factored out into an addPermission function.
     */
    private BiFunction addLexPermission = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        if (!ofNullable(lambdaFunction.getExistingPolicy()).orElse(new Policy()).getStatements().stream().anyMatch(s ->
                 s.getId().equals(getLexPermissionStatementId(trigger.getLexBotName())))) {
            getLog().info("Granting invoke permission to Lex bot " + trigger.getLexBotName());
            AddPermissionRequest addPermissionRequest = new AddPermissionRequest()
                .withAction(PERM_LAMBDA_INVOKE)
                .withPrincipal(PRINCIPAL_LEX)
                .withFunctionName(lambdaFunction.getFunctionName())
                .withQualifier(lambdaFunction.getQualifier())
                .withStatementId(getLexPermissionStatementId(trigger.getLexBotName()));
    
            AddPermissionResult addPermissionResult = lambdaClient.addPermission(addPermissionRequest);
        }
        return trigger;
    };
    private String getLexPermissionStatementId(String botName) {
        return "lambda-maven-plugin-lex-" + regionName + "-permission-" + botName;
    }

    private BiFunction createOrUpdateScheduledRule = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        // TODO: I hate that these checks are done twice, but for the time being it beats updates that just didn't work.
        if ( isScheduleRuleChanged(lambdaFunction) || isKeepAliveChanged(lambdaFunction)) {
            getLog().info("About to create or update " + trigger.getIntegration() + " trigger for " + trigger.getRuleName());
            PutRuleRequest putRuleRequest = new PutRuleRequest()
                    .withName(trigger.getRuleName())
                    .withDescription(trigger.getRuleDescription())
                    .withScheduleExpression(trigger.getScheduleExpression());
            PutRuleResult putRuleResult = eventsClient.putRule(putRuleRequest);
            getLog().info("Created " + trigger.getIntegration() + " trigger " + putRuleResult.getRuleArn());

            AddPermissionRequest addPermissionRequest = new AddPermissionRequest()
                    .withAction(PERM_LAMBDA_INVOKE)
                    .withPrincipal(PRINCIPAL_EVENTS)
                    .withSourceArn(putRuleResult.getRuleArn())
                    .withFunctionName(lambdaFunction.getFunctionName())
                    .withStatementId(UUID.randomUUID().toString());
            AddPermissionResult addPermissionResult = lambdaClient.addPermission(addPermissionRequest);
            getLog().debug("Added permission to lambda function " + addPermissionResult.toString());

            PutTargetsRequest putTargetsRequest = new PutTargetsRequest()
                    .withRule(trigger.getRuleName())
                    .withTargets(new Target().withId("1").withArn(lambdaFunction.getUnqualifiedFunctionArn()));
            eventsClient.putTargets(putTargetsRequest);
        }
        return trigger;
    };
    
    
    private Function createOrUpdateKeepAlive = (LambdaFunction lambdaFunction) -> {
        if (isKeepAliveChanged(lambdaFunction)) {
            ofNullable(lambdaFunction.getKeepAlive()).flatMap(f -> {
               if ( f > 0 ) { 
                   getLog().info("Setting keepAlive to " + f + " minutes.");
                   
                   createOrUpdateScheduledRule.apply(new Trigger()
                       .withIntegration("Function Keep Alive")
                       .withDescription(String.format("This feature pings function %s every %d %s.",
                                                       lambdaFunction.getFunctionName(), f,
                                                       f > 1 ? "minutes" : "minute"))
                       .withRuleName(lambdaFunction.getKeepAliveRuleName())        
                       .withScheduleExpression(lambdaFunction.getKeepAliveScheduleExpression()),
                       lambdaFunction);
               }
               
               return Optional.of(f);
            });  
        }
        return lambdaFunction;
    };

    private BiFunction createOrUpdateDynamoDBTrigger = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        getLog().info("About to create or update " + trigger.getIntegration() + " trigger for " + trigger.getDynamoDBTable());
        ListStreamsRequest listStreamsRequest = new ListStreamsRequest().withTableName(trigger.getDynamoDBTable());
        ListStreamsResult listStreamsResult = dynamoDBStreamsClient.listStreams(listStreamsRequest);

        String streamArn = listStreamsResult.getStreams().stream()
                                            .filter(s -> Objects.equals(trigger.getDynamoDBTable(), s.getTableName()))
                                            .findFirst()
                                            .map(Stream::getStreamArn)
                                            .orElseThrow(() -> new IllegalArgumentException("Unable to find stream for table " + trigger.getDynamoDBTable()));

        return findorUpdateMappingConfiguration(trigger, lambdaFunction, streamArn);
    };
    
    
    private BiFunction createOrUpdateSQSTrigger = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        getLog().info("About to create or update " + trigger.getIntegration() + " trigger for " + trigger.getStandardQueue());
        String queueArn = null;
        
        Optional getQueueUrlOptionalResult = ofNullable(sqsClient.getQueueUrl(new GetQueueUrlRequest()
    			.withQueueName(trigger.getStandardQueue())));
        
        if (getQueueUrlOptionalResult.isPresent()) {
        	String queueUrl = getQueueUrlOptionalResult.get().getQueueUrl();
			GetQueueAttributesResult getQueueAttributesResult = sqsClient.getQueueAttributes( new GetQueueAttributesRequest()
	    			.withQueueUrl(queueUrl).withAttributeNames(QueueAttributeName.QueueArn));
	    	
	    	queueArn = getQueueAttributesResult.getAttributes().get(QueueAttributeName.QueueArn.name());

        } else {
        	throw new IllegalArgumentException("Unable to find queue " + trigger.getStandardQueue());
        }
        
		
        return findorUpdateMappingConfiguration(trigger, lambdaFunction, queueArn);
    };

    private BiFunction createOrUpdateKinesisStream = (Trigger trigger, LambdaFunction lambdaFunction) -> {
        getLog().info("About to create or update " + trigger.getIntegration() + " trigger for " + trigger.getKinesisStream());

        try {
            return findorUpdateMappingConfiguration(trigger, lambdaFunction, 
                    kinesisClient.describeStream(trigger.getKinesisStream()).getStreamDescription().getStreamARN());
        } catch (Exception rnfe) {
            getLog().info(rnfe.getMessage());
            throw new IllegalArgumentException("Unable to find stream with name " + trigger.getKinesisStream());
        }        
    };

    private Trigger findorUpdateMappingConfiguration(Trigger trigger, LambdaFunction lambdaFunction, String streamArn) {
        ListEventSourceMappingsRequest listEventSourceMappingsRequest = new ListEventSourceMappingsRequest()
                .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn());
        ListEventSourceMappingsResult listEventSourceMappingsResult = lambdaClient.listEventSourceMappings(listEventSourceMappingsRequest);

        Optional eventSourceMappingConfiguration = listEventSourceMappingsResult.getEventSourceMappings().stream()
                .filter(stream -> {
                    boolean isSameFunctionArn = Objects.equals(stream.getFunctionArn(), lambdaFunction.getUnqualifiedFunctionArn());
                    boolean isSameSourceArn = Objects.equals(stream.getEventSourceArn(), streamArn);
                    return isSameFunctionArn && isSameSourceArn;
                })
                .findFirst();

        if (eventSourceMappingConfiguration.isPresent()) {
            UpdateEventSourceMappingRequest updateEventSourceMappingRequest = new UpdateEventSourceMappingRequest()
                    .withUUID(eventSourceMappingConfiguration.get().getUUID())
                    .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn())
                    .withBatchSize(ofNullable(trigger.getBatchSize()).orElse(10))
                    .withEnabled(ofNullable(trigger.getEnabled()).orElse(true));
            UpdateEventSourceMappingResult updateEventSourceMappingResult = lambdaClient.updateEventSourceMapping(updateEventSourceMappingRequest);
            trigger.withTriggerArn(updateEventSourceMappingResult.getEventSourceArn());
            getLog().info("Updated " + trigger.getIntegration() + " trigger " + trigger.getTriggerArn());
        } else {
        	
        	CreateEventSourceMappingRequest createEventSourceMappingRequest = new CreateEventSourceMappingRequest()
                    .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn())
                    .withEventSourceArn(streamArn)
                    .withBatchSize(ofNullable(trigger.getBatchSize()).orElse(10))
                    .withEnabled(ofNullable(trigger.getEnabled()).orElse(true));
        	// For SQS starting position is not valid
        	if (!streamArn.contains(":sqs:")) {
        		createEventSourceMappingRequest.setStartingPosition(EventSourcePosition.fromValue(ofNullable(trigger.getStartingPosition()).orElse(LATEST.toString())));
        	}
            
            CreateEventSourceMappingResult createEventSourceMappingResult = lambdaClient.createEventSourceMapping(createEventSourceMappingRequest);
            trigger.withTriggerArn(createEventSourceMappingResult.getEventSourceArn());
            getLog().info("Created " + trigger.getIntegration() + " trigger " + trigger.getTriggerArn());
        }

        return trigger;
    }

    private Function createOrUpdateTriggers = (LambdaFunction lambdaFunction) -> {
        lambdaFunction.getTriggers().forEach(trigger -> {
            if (TRIG_INT_LABEL_CLOUDWATCH_EVENTS.equals(trigger.getIntegration())) {
                createOrUpdateScheduledRule.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_DYNAMO_DB.equals(trigger.getIntegration())) {
                createOrUpdateDynamoDBTrigger.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_KINESIS.equals(trigger.getIntegration())) {
                createOrUpdateKinesisStream.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_SNS.equals(trigger.getIntegration())) {
                createOrUpdateSNSTopicSubscription.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_ALEXA_SK.equals(trigger.getIntegration())) {
                addAlexaSkillsKitPermission.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_LEX.equals(trigger.getIntegration())) {
                addLexPermission.apply(trigger, lambdaFunction);
            } else if (TRIG_INT_LABEL_SQS.equals(trigger.getIntegration())) {
                createOrUpdateSQSTrigger.apply(trigger, lambdaFunction);
            } else {
                throw new IllegalArgumentException("Unknown integration for trigger " + trigger.getIntegration() + ". Correct your configuration");
            }
        });
        return lambdaFunction;
    };

    private GetFunctionResult getFunction(LambdaFunction lambdaFunction) {
        return lambdaClient.getFunction(new GetFunctionRequest().withFunctionName(lambdaFunction.getFunctionName()));
    }

    private boolean isConfigurationChanged(LambdaFunction lambdaFunction, GetFunctionResult function) {
        BiPredicate isChangeStr = (s0, s1) -> !Objects.equals(s0, s1);
        BiPredicate isChangeInt = (i0, i1) -> !Objects.equals(i0, i1);
        BiPredicate, List> isChangeList = (l0, l1) -> !(l0.containsAll(l1) && l1.containsAll(l0));
        return of(function.getConfiguration())
                .map(config -> {
                    VpcConfigResponse vpcConfig = config.getVpcConfig();
                    if (vpcConfig == null) {
                        vpcConfig = new VpcConfigResponse();
                    }
                    boolean isDescriptionChanged = isChangeStr.test(config.getDescription(), lambdaFunction.getDescription());
                    boolean isHandlerChanged = isChangeStr.test(config.getHandler(), lambdaFunction.getHandler());
                    boolean isRoleChanged = isChangeStr.test(config.getRole(), lambdaFunction.getLambdaRoleArn());
                    boolean isTimeoutChanged = isChangeInt.test(config.getTimeout(), lambdaFunction.getTimeout());
                    boolean isMemoryChanged = isChangeInt.test(config.getMemorySize(), lambdaFunction.getMemorySize());
                    boolean isSecurityGroupIdsChanged = isChangeList.test(vpcConfig.getSecurityGroupIds(), lambdaFunction.getSecurityGroupIds());
                    boolean isVpcSubnetIdsChanged = isChangeList.test(vpcConfig.getSubnetIds(), lambdaFunction.getSubnetIds());
                    return isDescriptionChanged || isHandlerChanged || isRoleChanged || isTimeoutChanged || isMemoryChanged || 
                           isSecurityGroupIdsChanged || isVpcSubnetIdsChanged || isAliasesChanged(lambdaFunction) || isKeepAliveChanged(lambdaFunction) ||
                           isScheduleRuleChanged(lambdaFunction);
                })
                .orElse(true);
    }
    
    private boolean isKeepAliveChanged(LambdaFunction lambdaFunction) {
        try {
            return ofNullable(lambdaFunction.getKeepAlive()).map( ka -> {
                DescribeRuleResult res = eventsClient.describeRule(new DescribeRuleRequest().withName(lambdaFunction.getKeepAliveRuleName()));
                return !Objects.equals(res.getScheduleExpression(), lambdaFunction.getKeepAliveScheduleExpression());
            }).orElse(false);
            
        } catch( com.amazonaws.services.cloudwatchevents.model.ResourceNotFoundException ignored ) {
            return true;
        }
    }
    
    private boolean isScheduleRuleChanged(LambdaFunction lambdaFunction) {
        try {
            return lambdaFunction.getTriggers().stream().filter(t -> TRIG_INT_LABEL_CLOUDWATCH_EVENTS.equals(t.getIntegration())).anyMatch(trigger -> {
                DescribeRuleResult res = eventsClient.describeRule(new DescribeRuleRequest().withName(trigger.getRuleName()));
                return !(Objects.equals(res.getName(), trigger.getRuleName()) &&
                        Objects.equals(res.getDescription(), trigger.getRuleDescription()) &&
                        Objects.equals(res.getScheduleExpression(), trigger.getScheduleExpression()));
            });
        } catch( com.amazonaws.services.cloudwatchevents.model.ResourceNotFoundException ignored ) {
            return true;
        }
    }

    private boolean isAliasesChanged(LambdaFunction lambdaFunction) {
        try {
            ListAliasesResult listAliasesResult = lambdaClient.listAliases(new ListAliasesRequest()
                    .withFunctionName(lambdaFunction.getFunctionName()));
            List configuredAliases = listAliasesResult.getAliases().stream()
                                                              .map(AliasConfiguration::getName)
                                                              .collect(toList());
            return !configuredAliases.containsAll(lambdaFunction.getAliases());
        } catch (ResourceNotFoundException ignored) {
            return true;
        }
    }

    private Function createFunction = (LambdaFunction lambdaFunction) -> {
        getLog().info("About to create function " + lambdaFunction.getFunctionName());
        CreateFunctionRequest createFunctionRequest = new CreateFunctionRequest()
                .withDescription(lambdaFunction.getDescription())
                .withRole(lambdaFunction.getLambdaRoleArn())
                .withFunctionName(lambdaFunction.getFunctionName())
                .withHandler(lambdaFunction.getHandler())
                .withRuntime(runtime)
                .withTimeout(ofNullable(lambdaFunction.getTimeout()).orElse(timeout))   
                .withMemorySize(ofNullable(lambdaFunction.getMemorySize()).orElse(memorySize))
                .withVpcConfig(getVpcConfig(lambdaFunction))
                .withCode(new FunctionCode()
                        .withS3Bucket(s3Bucket)
                        .withS3Key(fileName))
                .withEnvironment(new Environment().withVariables(lambdaFunction.getEnvironmentVariables()));

        CreateFunctionResult createFunctionResult = lambdaClient.createFunction(createFunctionRequest);
        lambdaFunction.withVersion(createFunctionResult.getVersion())
                      .withFunctionArn(createFunctionResult.getFunctionArn());
        getLog().info("Function " + createFunctionResult.getFunctionName() + " created. Function Arn: " + createFunctionResult.getFunctionArn());

        
        return lambdaFunction;
    };

    private VpcConfig getVpcConfig(LambdaFunction lambdaFunction) {
        return new VpcConfig()
                .withSecurityGroupIds(lambdaFunction.getSecurityGroupIds())
                .withSubnetIds(lambdaFunction.getSubnetIds());
    }

    /**
     * Remove orphaned kinesis stream triggers.
     * TODO: Combine with cleanUpOrphanedDynamoDBTriggers.
     */
    Function cleanUpOrphanedKinesisTriggers = lambdaFunction -> { 
        ListEventSourceMappingsResult listEventSourceMappingsResult = 
                lambdaClient.listEventSourceMappings(new ListEventSourceMappingsRequest()
                        .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn()));

        
        List streamNames = new ArrayList();
        
        // This nonsense is to prevent cleanupOrphanedDynamoDBTriggers from removing DynamoDB triggers
        // and vice versa.  Unfortunately this assumes that stream names won't be the same as table names.
        lambdaFunction.getTriggers().stream().forEach(t -> {
            ofNullable(t.getKinesisStream()).ifPresent(x -> streamNames.add(x));
            ofNullable(t.getDynamoDBTable()).ifPresent(x -> streamNames.add(x));
        });

        listEventSourceMappingsResult.getEventSourceMappings().stream().forEach(s -> {
            if ( s.getEventSourceArn().contains(":kinesis:") ) {                
                if ( ! streamNames.contains(kinesisClient.describeStream(new com.amazonaws.services.kinesis.model.DescribeStreamRequest()
                        .withStreamName(s.getEventSourceArn().substring(s.getEventSourceArn().lastIndexOf('/')+1)))
                        .getStreamDescription()
                        .getStreamName()) ){
                    getLog().info("    Removing orphaned Kinesis trigger for stream " + s.getEventSourceArn());
                    try {
                        lambdaClient.deleteEventSourceMapping(new DeleteEventSourceMappingRequest().withUUID(s.getUUID()));
                    } catch(Exception e8) {
                        getLog().error("    Error removing orphaned Kinesis trigger for stream " + s.getEventSourceArn());
                    }
                }
            }
        });
        
        return lambdaFunction;  
     };
    
    /**
     * Removes orphaned sns triggers.
     */
    Function cleanUpOrphanedSNSTriggers = lambdaFunction -> {
        
        List subscriptions = new ArrayList();
        ListSubscriptionsResult result = snsClient.listSubscriptions();
        
        do {
            subscriptions.addAll(result.getSubscriptions().stream().filter( sub -> {
                return sub.getEndpoint().equals(lambdaFunction.getFunctionArn());
            }).collect(Collectors.toList()));
            
            result = snsClient.listSubscriptions(result.getNextToken());
        } while( result.getNextToken() != null );
        
        if (subscriptions.size() > 0 ) {
            List snsTopicNames = lambdaFunction.getTriggers().stream().map(t -> {
                return ofNullable(t.getSNSTopic()).orElse("");
            }).collect(Collectors.toList());
            
            subscriptions.stream().forEach(s -> {
                String topicName = s.getTopicArn().substring(s.getTopicArn().lastIndexOf(":")+1);
                if (!snsTopicNames.contains(topicName)) {
                    getLog().info("    Removing orphaned SNS trigger for topic " + topicName);
                    try {
                        snsClient.unsubscribe(new UnsubscribeRequest().withSubscriptionArn(s.getSubscriptionArn()));
                        
                        ofNullable(lambdaFunction.getExistingPolicy()).flatMap( policy -> {
                            policy.getStatements().stream()
                                .filter(
                                    stmt -> stmt.getActions().stream().anyMatch( e -> PERM_LAMBDA_INVOKE.equals(e.getActionName())) &&
                                    stmt.getPrincipals().stream().anyMatch(principal -> PRINCIPAL_SNS.equals(principal.getId())) &&
                                    stmt.getResources().stream().anyMatch(r -> r.getId().equals(lambdaFunction.getFunctionArn()))
                                ).forEach( st -> {
                                    if( st.getConditions().stream().anyMatch(condition -> condition.getValues().contains(s.getTopicArn())) ) {
                                        getLog().info("      Removing invoke permission for SNS trigger");       
                                        try {
                                            lambdaClient.removePermission(new RemovePermissionRequest()
                                                .withFunctionName(lambdaFunction.getFunctionName())
                                                .withQualifier(lambdaFunction.getQualifier())
                                                .withStatementId(st.getId()));
                                        } catch (Exception e7) {
                                            getLog().error("      Error removing invoke permission for SNS trigger");
                                        }
                                    }
                                });
                            return of(policy);
                        });
                        
                    } catch(Exception e5) {
                        getLog().error("    Error removing SNS trigger for topic " + topicName);
                    }
                }
            });
        }
        
        return lambdaFunction; 
    };

    
    /**
     * Removes orphaned SQS triggers.
     */
    Function cleanUpOrphanedSQSTriggers = lambdaFunction -> {
        ListEventSourceMappingsResult listEventSourceMappingsResult = 
                lambdaClient.listEventSourceMappings(new ListEventSourceMappingsRequest()
                        .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn()));

        
        List standardQueues = new ArrayList();
        
        lambdaFunction.getTriggers().stream().forEach(t -> {
            ofNullable(t.getStandardQueue()).ifPresent(x -> standardQueues.add(x));
        });
        
        listEventSourceMappingsResult.getEventSourceMappings().stream().forEach(s -> {
            if ( s.getEventSourceArn().contains(":sqs:")) {
            	// This API hit may not required, added here only for double check or cross verification
            	Optional getQueueUrlOptionalResult = ofNullable(sqsClient.getQueueUrl(new GetQueueUrlRequest()
            			.withQueueName(s.getEventSourceArn().substring(s.getEventSourceArn().lastIndexOf(':')+1))));
            	
            	getQueueUrlOptionalResult.ifPresent(queue -> {
            			String queueName = queue.getQueueUrl().substring(queue.getQueueUrl().lastIndexOf('/')+1);
	        			if ( ! standardQueues.contains(queueName) ) {    
	                        getLog().info("    Removing orphaned SQS trigger for queue " + queueName);
	                        try {    
	                            lambdaClient.deleteEventSourceMapping(new DeleteEventSourceMappingRequest().withUUID(s.getUUID()));
	                        } catch (Exception exp) {
	                            getLog().error("    Error removing SQS trigger for queue " + queueName + ", Error Message :" + exp.getMessage());
	                        }
	                    }
	            	}
            			
            	);
            }
        });
        
        return lambdaFunction; 
     };
    
    /**
     * Removes orphaned dynamo db triggers.
     * TODO: Combine with cleanUpOrphanedKinesisTriggers
     */
    Function cleanUpOrphanedDynamoDBTriggers = lambdaFunction -> {
        ListEventSourceMappingsResult listEventSourceMappingsResult = 
                lambdaClient.listEventSourceMappings(new ListEventSourceMappingsRequest()
                        .withFunctionName(lambdaFunction.getUnqualifiedFunctionArn()));

        
        List tableNames = new ArrayList();
        
        // This nonsense is to prevent cleanupOrphanedDynamoDBTriggers from removing DynamoDB triggers
        // and vice versa.  Unfortunately this assumes that stream names won't be the same as table names.
        lambdaFunction.getTriggers().stream().forEach(t -> {
            ofNullable(t.getKinesisStream()).ifPresent(x -> tableNames.add(x));
            ofNullable(t.getDynamoDBTable()).ifPresent(x -> tableNames.add(x));
        });
        
        listEventSourceMappingsResult.getEventSourceMappings().stream().forEach(s -> {
            if ( s.getEventSourceArn().contains(":dynamodb:")) {
                StreamDescription sd = dynamoDBStreamsClient.describeStream(new DescribeStreamRequest()
                    .withStreamArn(s.getEventSourceArn())).getStreamDescription();
                
                if ( ! tableNames.contains(sd.getTableName()) ) {    
                    getLog().info("    Removing orphaned DynamoDB trigger for table " + sd.getTableName());
                    try {    
                        lambdaClient.deleteEventSourceMapping(new DeleteEventSourceMappingRequest().withUUID(s.getUUID()));
                    } catch (Exception e4) {
                        getLog().error("    Error removing DynamoDB trigger for table " + sd.getTableName());
                    }
                }
            }
        });
        
        return lambdaFunction; 
     };

    
    /**
     * Removes the Alexa permission if it isn't found in the current configuration.
     * TODO: Factor out code common with other orphan clean up functions.
     */
    Function cleanUpOrphanedAlexaSkillsTriggers = lambdaFunction -> {
        ofNullable(lambdaFunction.getExistingPolicy()).flatMap( policy -> {
            policy.getStatements().stream()
                .filter(
                    stmt -> stmt.getActions().stream().anyMatch( e -> PERM_LAMBDA_INVOKE.equals(e.getActionName())) &&
                    stmt.getPrincipals().stream().anyMatch(principal -> PRINCIPAL_ALEXA.equals(principal.getId())) &&
                    !lambdaFunction.getTriggers().stream().anyMatch( t -> t.getIntegration().equals(TRIG_INT_LABEL_ALEXA_SK)))
                .forEach( s -> {    
                    try {
                        getLog().info("    Removing orphaned Alexa permission " + s.getId());
                        lambdaClient.removePermission(new RemovePermissionRequest()
                            .withFunctionName(lambdaFunction.getFunctionName())
                            .withQualifier(lambdaFunction.getQualifier())
                            .withStatementId(s.getId()));
                    } catch (ResourceNotFoundException rnfe1) {
                        getLog().error("    Error removing permission for " + s.getId() + ": " + rnfe1.getMessage());
                    }
                });
            return of(policy);
        });
                    
        return lambdaFunction; 
    };
    
    /**
     * Removes any Lex permissions that aren't found in the current configuration.
     * TODO: Factor out code common with other orphan clean up functions.
     */
    Function cleanUpOrphanedLexSkillsTriggers = lambdaFunction -> {
        ofNullable(lambdaFunction.getExistingPolicy()).flatMap( policy -> {
            policy.getStatements().stream()
                .filter(stmt -> stmt.getActions().stream().anyMatch( e -> PERM_LAMBDA_INVOKE.equals(e.getActionName())) &&
                        stmt.getPrincipals().stream().anyMatch(principal -> PRINCIPAL_LEX.equals(principal.getId())) &&
                        !lambdaFunction.getTriggers().stream().anyMatch( t -> stmt.getId().contains(ofNullable(t.getLexBotName()).orElse(""))))
                .forEach( s -> {    
                    try {
                        getLog().info("    Removing orphaned Lex permission " + s.getId());
                        lambdaClient.removePermission(new RemovePermissionRequest()
                            .withFunctionName(lambdaFunction.getFunctionName())
                            .withQualifier(lambdaFunction.getQualifier())
                            .withStatementId(s.getId()));
                    } catch (Exception ign2) { 
                        getLog().error("   Error removing permission for " + s.getId() + ign2.getMessage() ); 
                    }
                });
            return of(policy);
        });
        
        return lambdaFunction;
    };
    
    Function cleanUpOrphanedCloudWatchEventRules = lambdaFunction -> {
        // Get the list of cloudwatch event rules defined for this function (if any).
        List existingRuleNames = cloudWatchEventsClient.listRuleNamesByTarget(new ListRuleNamesByTargetRequest()
        .withTargetArn(lambdaFunction.getFunctionArn())).getRuleNames();
    
        // Get the list of cloudwatch event rules to be defined for this function (if any).
        List definedRuleNames = lambdaFunction.getTriggers().stream().filter(
                t -> TRIG_INT_LABEL_CLOUDWATCH_EVENTS.equals(t.getIntegration())).map(t -> {
                    return t.getRuleName();
                }).collect(toList());
        
        // Add the keep alive rule name if the user has disabled keep alive for the function.
        ofNullable(lambdaFunction.getKeepAlive()).ifPresent(ka -> {
           if ( ka > 0 ) {
               definedRuleNames.add(lambdaFunction.getKeepAliveRuleName());
           }
        });
    
        // Remove all of the rules that will be defined from the list of existing rules.
        // The remainder is a set of event rules which should no longer be associated to this
        // function.
        existingRuleNames.removeAll(definedRuleNames);
        
        // For each remaining rule, remove the function as a target and attempt to delete
        // the rule.
        existingRuleNames.stream().forEach(ern -> {
            getLog().info("    Removing CloudWatch Event Rule: " + ern);
            cloudWatchEventsClient.removeTargets(new RemoveTargetsRequest()
                .withIds("1")
                .withRule(ern));
            try {
                cloudWatchEventsClient.deleteRule(new DeleteRuleRequest().withName(ern));
            } catch (Exception e) {
                getLog().error("    Error removing orphaned rule: " + e.getMessage());
            }
        });
        
        return lambdaFunction;  
    };
    
    Function cleanUpOrphans = lambdaFunction -> {
        try {
            lambdaFunction.setFunctionArn(lambdaClient.getFunction(
                    new GetFunctionRequest().withFunctionName(lambdaFunction.getFunctionName())).getConfiguration().getFunctionArn());
            
            getLog().info("Cleaning up orphaned triggers.");
            
            // Add clean up orphaned trigger functions for each integration here:
            cleanUpOrphanedCloudWatchEventRules
                .andThen(cleanUpOrphanedDynamoDBTriggers)
                .andThen(cleanUpOrphanedKinesisTriggers)
                .andThen(cleanUpOrphanedSNSTriggers)
                .andThen(cleanUpOrphanedAlexaSkillsTriggers)
                .andThen(cleanUpOrphanedLexSkillsTriggers)
                .andThen(cleanUpOrphanedSQSTriggers)
                .apply(lambdaFunction);
            
        } catch (ResourceNotFoundException ign1) {
            getLog().debug("Assuming function has no orphan triggers to clean up since it doesn't exist yet.");
        }
            
        return lambdaFunction;  
    };

    Function createOrUpdate = lambdaFunction -> {
      try {
          lambdaFunction.setFunctionArn(lambdaClient.getFunction(
                  new GetFunctionRequest().withFunctionName(lambdaFunction.getFunctionName())).getConfiguration().getFunctionArn());
          of(getFunction(lambdaFunction))
                  .filter(getFunctionResult -> shouldUpdate(lambdaFunction, getFunctionResult))
                  .map(getFujnctionResult ->
                  updateFunctionCode
                          .andThen(updateFunctionConfig)
                          .andThen(createOrUpdateAliases)
                          .andThen(createOrUpdateTriggers)
                          .andThen(createOrUpdateKeepAlive)
                          .apply(lambdaFunction));
      } catch (ResourceNotFoundException ign) {
          createFunction.andThen(createOrUpdateAliases)
                        .andThen(createOrUpdateTriggers)
                        .apply(lambdaFunction);
      }
                      
      return lambdaFunction;
    };
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy