tech.greenfield.aws.route53.EventHandler.java Source code

Java tutorial

Introduction

Here is the source code for tech.greenfield.aws.route53.EventHandler.java

Source

package tech.greenfield.aws.route53;

import static tech.greenfield.aws.Clients.ec2;
import static tech.greenfield.aws.Clients.route53;

import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import com.amazonaws.SdkBaseException;
import com.amazonaws.services.ec2.model.DescribeInstancesRequest;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.route53.model.*;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Handler for a single SNS event that was submitted to the lambda implementation
 * @author odeda
 *
 *     Copyright (C) 2016  GreenfieldTech
 * 
 *     This library is free software; you can redistribute it and/or
 *     modify it under the terms of the GNU Lesser General Public
 *     License as published by the Free Software Foundation; either
 *     version 2.1 of the License, or (at your option) any later version.
 * 
 *     This library is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *     Lesser General Public License for more details.
 * 
 *     You should have received a copy of the GNU Lesser General Public
 *     License along with this library; if not, write to the Free Software
 *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
public class EventHandler {

    @FunctionalInterface
    public interface Route53UpdateTask {
        void run() throws NoIpException;
    }

    static private ObjectMapper s_mapper = new ObjectMapper();

    protected Logger logger = Logger.getLogger(getClass().getName());
    private EventType eventType;
    private String ec2instanceId;
    private String autoScalingGroupName;
    private Route53Message message;

    static {
        s_mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
    }

    protected EventHandler(Context context, EventType eventType, String ec2InstanceId, String autoScalingGroupName,
            Route53Message message) {
        this.eventType = Objects.requireNonNull(eventType, "Missing event type");
        this.ec2instanceId = ec2InstanceId;
        this.autoScalingGroupName = autoScalingGroupName;
        this.message = message;
    }

    /**
     * Event handler entry point
     */
    public void handle() {
        try {
            switch (eventType) {
            case EC2_INSTANCE_LAUNCH:
                retryIfThrottled(() -> registerInstance(ec2instanceId));
                break;
            case EC2_INSTANCE_TERMINATE:
            case EC2_INSTANCE_TERMINATE_ERROR:
                retryIfThrottled(() -> deregisterInstance(ec2instanceId));
                break;
            default: // do nothing in case of launch error or test notification
            }
        } catch (NoIpException e) {
            logger.warning("Error: " + e.getMessage());
            logger.warning("No IP was found, starting plan B - update all instances");
            rebuildAllRRs(this.autoScalingGroupName);
        } catch (SilentFailure | SdkBaseException e) {
            Tools.logException(logger, "Silently failing Route53 update", e);
        }
    }

    private void rebuildAllRRs(String asgName) {
        List<Instance> instances = Tools.getASGInstances(asgName).stream()
                .filter(i -> i.getHealthStatus().equals("Healthy")).map(Tools::asInstanceToEC2)
                .filter(Objects::nonNull).collect(Collectors.toList());

        ChangeBatch changes = instances.isEmpty() ? message.getDeleteChanges()
                : message.getUpsertChanges(instances);
        if (Route53Message.isDebug())
            logger.info("Sending DNS change request: " + changes);
        try {
            ChangeResourceRecordSetsRequest req = new ChangeResourceRecordSetsRequest(
                    Route53Message.getHostedZoneId(), changes);
            Tools.waitFor(route53().changeResourceRecordSets(req));
        } catch (IllegalArgumentException e) {
            Tools.logException(logger, "Error in submitting Route53 update", e);
            throw new SdkBaseException(e);
        }
    }

    /**
     * Start an DNS registration for the launched instance
     * @param ec2InstanceId instance ID of instance that needs to be registered
     * @param ttl TTL in seconds to use when creating a new record
     */
    private void registerInstance(String ec2InstanceId) throws NoIpException {
        logger.info("Registering " + ec2InstanceId);
        ChangeBatch cb = message.getUpsertChanges(Collections.singletonList(getInstance(ec2InstanceId)));

        if (Route53Message.isDebug())
            logger.fine("Adding instance with addresses: " + cb);

        for (Change c : cb.getChanges()) {
            ResourceRecordSet rr = c.getResourceRecordSet();
            ResourceRecordSet oldrr = Tools.getRecordSet(rr.getName(), rr.getType());
            if (Objects.nonNull(oldrr))
                oldrr.getResourceRecords().forEach(r -> rr.withResourceRecords(r));
        }
        ChangeResourceRecordSetsRequest req = new ChangeResourceRecordSetsRequest(Route53Message.getHostedZoneId(),
                cb);
        if (Route53Message.isDebug())
            logger.fine("Sending rr change request: " + req);
        Tools.waitFor(route53().changeResourceRecordSets(req));
    }

    /**
     * Start a DNS re-registration for the terminated instance
     * @param ec2InstanceId instance ID of instance that needs to be de-registered
     * @param ttl TTL in seconds to use when creating a new record
     * @throws NoIpException 
     */
    private void deregisterInstance(String ec2InstanceId) throws NoIpException {
        logger.info("Deregistering " + ec2InstanceId);
        ChangeBatch changes = message.getRemoveChanges(getInstance(ec2InstanceId));
        if (changes.getChanges().isEmpty()) {
            logger.info("Nothing to remove");
            return;
        }
        ChangeResourceRecordSetsRequest req = new ChangeResourceRecordSetsRequest(Route53Message.getHostedZoneId(),
                changes);
        if (Route53Message.isDebug())
            logger.fine("Sending rr change request: " + req);
        Tools.waitFor(route53().changeResourceRecordSets(req));
    }

    /**
     * Helper method to resolve an instance ID to an EC2 instance object
     * @param ec2InstanceId instance Id to resolve
     * @return EC2 instance found
     * @throws RuntimeException in case no instance with the specified ID was found
     */
    private Instance getInstance(String ec2InstanceId) {
        return ec2().describeInstances(new DescribeInstancesRequest().withInstanceIds(ec2InstanceId))
                .getReservations().stream().flatMap(r -> r.getInstances().stream()).findFirst()
                .orElseThrow(() -> new RuntimeException("Failed to locate instance " + ec2InstanceId));
    }

    private void retryIfThrottled(Route53UpdateTask action) throws NoIpException {
        while (true) {
            try {
                action.run();
                return;
            } catch (AmazonRoute53Exception e) {
                // retry in case of 
                if (e.getMessage().contains("Rate exceeded")) {
                    logger.info("Throttled: " + e);
                    try {
                        Thread.sleep(2000);
                        logger.info("Retrying...");
                    } catch (InterruptedException e1) {
                    }
                    continue;
                } else
                    throw e;
            }
        }
    }

}