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

io.linguarobot.aws.cdk.maven.context.VpcNetworkContextProvider Maven / Gradle / Ivy

Go to download

The AWS CDK Maven plugin produces and deploys CloudFormation templates based on the cloud infrastructure defined by means of CDK. The goal of the project is to improve the experience of Java developers while working with CDK by eliminating the need for installing Node.js and interacting with the CDK application by means of CDK Toolkit.

There is a newer version: 0.0.8
Show newest version
package io.linguarobot.aws.cdk.maven.context;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import io.linguarobot.aws.cdk.maven.CdkPluginException;
import io.linguarobot.aws.cdk.maven.MoreCollectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import software.amazon.awscdk.cxapi.VpcSubnet;
import software.amazon.awscdk.cxapi.VpcSubnetGroup;
import software.amazon.awscdk.cxapi.VpcSubnetGroupType;
import software.amazon.awssdk.services.ec2.Ec2Client;
import software.amazon.awssdk.services.ec2.model.DescribeRouteTablesRequest;
import software.amazon.awssdk.services.ec2.model.DescribeRouteTablesResponse;
import software.amazon.awssdk.services.ec2.model.DescribeSubnetsRequest;
import software.amazon.awssdk.services.ec2.model.DescribeSubnetsResponse;
import software.amazon.awssdk.services.ec2.model.DescribeVpcsRequest;
import software.amazon.awssdk.services.ec2.model.DescribeVpcsResponse;
import software.amazon.awssdk.services.ec2.model.DescribeVpnGatewaysRequest;
import software.amazon.awssdk.services.ec2.model.Filter;
import software.amazon.awssdk.services.ec2.model.RouteTable;
import software.amazon.awssdk.services.ec2.model.RouteTableAssociation;
import software.amazon.awssdk.services.ec2.model.Tag;
import software.amazon.awssdk.services.ec2.model.Vpc;
import software.amazon.awssdk.services.ec2.model.VpnGateway;

import javax.json.JsonObject;
import javax.json.JsonString;
import javax.json.JsonValue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;


public class VpcNetworkContextProvider implements ContextProvider {

    public static final String KEY = "vpc-provider";

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
            .registerModule(new JSR353Module())
            .registerModule(new SimpleModule()
                    .addSerializer(VpcSubnetGroupType.class, new VpcSubnetGroupTypeJsonSerializer()));

    private static final String PUBLIC_SUBNET_TYPE = "Public";
    private static final String PRIVATE_SUBNET_TYPE = "Private";
    private static final String ISOLATED_SUBNET_TYPE = "Isolated";
    private static final Set SUBNET_TYPES =
            ImmutableSet.of(PUBLIC_SUBNET_TYPE, PRIVATE_SUBNET_TYPE, ISOLATED_SUBNET_TYPE);

    private final AwsClientProvider awsClientProvider;

    public VpcNetworkContextProvider(AwsClientProvider awsClientProvider) {
        this.awsClientProvider = awsClientProvider;
    }

    @Override
    public JsonValue getContextValue(JsonObject properties) {
        String environment = ContextProviders.buildEnvironment(properties);
        try (Ec2Client ec2Client = awsClientProvider.getClient(Ec2Client.class, environment)) {
            Vpc vpc = getVpc(ec2Client, getFilters(properties));
            VpcContext vpcContext = getVpcContext(ec2Client, vpc, properties);
            return OBJECT_MAPPER.convertValue(vpcContext, JsonObject.class);
        }
    }

    private Vpc getVpc(Ec2Client ec2Client, List filters) {
        DescribeVpcsRequest describeRequest = DescribeVpcsRequest.builder()
                .filters(filters)
                .build();

        List vpcs = Optional.of(ec2Client.describeVpcs(describeRequest))
                .map(DescribeVpcsResponse::vpcs)
                .orElse(Collections.emptyList());

        if (vpcs.size() != 1) {
            throw new CdkPluginException("Found " + vpcs.size() + " or more VPCs matching the criteria while exactly " +
                    "1 is required");
        }

        return vpcs.get(0);
    }

    private VpcContext getVpcContext(Ec2Client ec2Client, Vpc vpc, JsonObject properties) {
        VpcContext.Builder contextBuilder = VpcContext.builder()
                .vpcId(vpc.vpcId())
                .vpcCidrBlock(vpc.cidrBlock())
                .vpnGatewayId(getVpnGateway(ec2Client, vpc).map(VpnGateway::vpnGatewayId).orElse(null));
        String groupNameTagName = properties.getString("subnetGroupNameTag", "aws-cdk:subnet-name");
        Map>> vpcSubnets = getSubnets(ec2Client, vpc).stream()
                .collect(Collectors.groupingBy(
                        Subnet::getType,
                        LinkedHashMap::new,
                        Collectors.groupingBy(
                                subnet -> subnet.getTags().getOrDefault(groupNameTagName, subnet.getType()),
                                LinkedHashMap::new,
                                MoreCollectors.sorting(Comparator.comparing(Subnet::getAvailabilityZone), Collectors.toList())
                        )
                ));

        boolean asymmetricSubnetsRequested = Optional.ofNullable(properties.get("returnAsymmetricSubnets"))
                .map(v -> Boolean.valueOf(v.toString()))
                .orElse(false);

        if (asymmetricSubnetsRequested) {
            contextBuilder.availabilityZones(ImmutableList.of());
            List subnetGroups = vpcSubnets.entrySet().stream()
                    .flatMap(typeGroupedSubnets -> {
                        VpcSubnetGroupType type = VpcSubnetGroupType.valueOf(typeGroupedSubnets.getKey().toUpperCase());
                        return typeGroupedSubnets.getValue().entrySet().stream()
                                .map(nameGroupedSubnets -> {
                                    List subnets = nameGroupedSubnets.getValue().stream()
                                            .map(this::toVpcSubnet)
                                            .collect(Collectors.toList());

                                    return VpcContextSubnetGroup.builder()
                                            .name(nameGroupedSubnets.getKey())
                                            .type(type)
                                            .subnets(subnets)
                                            .build();
                                });
                    })
                    .collect(Collectors.toList());
            contextBuilder.subnetGroups(subnetGroups);
        } else {
            List> availabilityZones = vpcSubnets.values().stream()
                    .flatMap(groupedSubnets -> groupedSubnets.values().stream())
                    .map(subnets -> subnets.stream()
                            .map(Subnet::getAvailabilityZone)
                            .collect(Collectors.toList()))
                    .distinct()
                    .collect(Collectors.toList());
            if (availabilityZones.size() > 1) {
                throw new CdkPluginException("Not all subnetworks in the VPC have the same availability zones");
            }
            contextBuilder.availabilityZones(Iterables.getOnlyElement(availabilityZones, ImmutableList.of()));
            vpcSubnets.forEach((type, subnets) -> {
                List subnetNames = ImmutableList.copyOf(subnets.keySet());
                List subnetIds = new ArrayList<>();
                List routeTableIds = new ArrayList<>();
                subnets.values().stream()
                        .flatMap(List::stream)
                        .forEach(subnet -> {
                            subnetIds.add(subnet.getId());
                            routeTableIds.add(subnet.getRouteTableId());
                        });

                switch (type) {
                    case ISOLATED_SUBNET_TYPE:
                        contextBuilder.isolatedSubnetIds(subnetIds)
                                .isolatedSubnetNames(subnetNames)
                                .isolatedSubnetRouteTableIds(routeTableIds);
                        break;
                    case PRIVATE_SUBNET_TYPE:
                        contextBuilder.privateSubnetIds(subnetIds)
                                .privateSubnetNames(subnetNames)
                                .privateSubnetRouteTableIds(routeTableIds);
                        break;
                    case PUBLIC_SUBNET_TYPE:
                        contextBuilder.publicSubnetIds(subnetIds)
                                .publicSubnetNames(subnetNames)
                                .publicSubnetRouteTableIds(routeTableIds);
                        break;
                }
            });
        }

        return contextBuilder.build();
    }

    private VpcSubnet toVpcSubnet(Subnet subnet) {
        return VpcContextSubnet.builder()
                .subnetId(subnet.getId())
                .availabilityZone(subnet.getAvailabilityZone())
                .cidr(subnet.getCidrBlock())
                .routeTableId(subnet.getRouteTableId())
                .build();
    }

    private List getRouteTables(Ec2Client ec2Client, Vpc vpc) {
        List routeTables = new ArrayList<>();

        String token = null;
        do {
            DescribeRouteTablesRequest describeRouteTablesRequest = DescribeRouteTablesRequest.builder()
                    .filters(filter("vpc-id", vpc.vpcId()))
                    .nextToken(token)
                    .build();
            DescribeRouteTablesResponse response = ec2Client.describeRouteTables(describeRouteTablesRequest);
            if (response.routeTables() != null) {
                routeTables.addAll(response.routeTables());
            }
            token = response.nextToken();
        } while (token != null);

        return routeTables;
    }

    private List getSubnets(Ec2Client ec2Client, Vpc vpc) {
        List routeTables = getRouteTables(ec2Client, vpc);
        RouteTable mainRouteTable = routeTables.stream()
                .filter(routeTable -> getStream(routeTable.associations())
                        .anyMatch(association -> association.main() != null && association.main()))
                .findAny()
                .orElse(null);

        Map subnetRouteTables = routeTables.stream()
                .flatMap(routeTable -> getStream(routeTable.associations())
                        .map(RouteTableAssociation::subnetId)
                        .filter(Objects::nonNull)
                        .map(subnetId -> Pair.of(subnetId, routeTable)))
                .collect(Collectors.toMap(Pair::getKey, Pair::getValue));

        String token = null;

        List subnets = new ArrayList<>();
        do {
            DescribeSubnetsRequest request = DescribeSubnetsRequest.builder()
                    .filters(filter("vpc-id", vpc.vpcId()))
                    .nextToken(token)
                    .build();
            DescribeSubnetsResponse response = ec2Client.describeSubnets(request);
            if (response.subnets() != null) {
                response.subnets().forEach(subnet -> {
                    RouteTable routeTable = subnetRouteTables.getOrDefault(subnet.subnetId(), mainRouteTable);
                    if (routeTable == null) {
                        throw new CdkPluginException("The subnet '" + subnet.subnetId() + "' doesn't have an associated " +
                                "route table");
                    }

                    Subnet result = new Subnet();
                    result.setId(subnet.subnetId());
                    result.setAvailabilityZone(subnet.availabilityZone());
                    result.setCidrBlock(subnet.cidrBlock());

                    Map tags = getStream(subnet.tags())
                            .collect(Collectors.toMap(Tag::key, Tag::value, (a, b) -> a));

                    String type = Optional.ofNullable(tags.get("aws-cdk:subnet-type"))
                            .orElseGet(() -> {
                                if (subnet.mapPublicIpOnLaunch() != null && subnet.mapPublicIpOnLaunch()) {
                                    return PUBLIC_SUBNET_TYPE;
                                }

                                return hasInternetGateway(routeTable) ? PUBLIC_SUBNET_TYPE : PRIVATE_SUBNET_TYPE;
                            });

                    if (!SUBNET_TYPES.contains(type)) {
                        throw new CdkPluginException("The subnet '" + subnet.subnetId() + "' has invalid type '" +
                                type + "'. The type must be one of the following values: " + String.join(", ", SUBNET_TYPES));
                    }

                    result.setType(type);
                    result.setTags(tags);
                    result.setRouteTableId(routeTable.routeTableId());
                    subnets.add(result);
                });
            }
            token = response.nextToken();
        } while (token != null);

        return subnets;
    }

    private boolean hasInternetGateway(RouteTable routeTable) {
        return getStream(routeTable.routes())
                .anyMatch(route -> route.gatewayId() != null && route.gatewayId().startsWith("igw-"));
    }

    private Optional getVpnGateway(Ec2Client ec2Client, Vpc vpc) {
        DescribeVpnGatewaysRequest request = DescribeVpnGatewaysRequest.builder()
                .filters(ImmutableList.of(
                        filter("attachment.vpc-id", vpc.vpcId()),
                        filter("attachment.state", "attached"),
                        filter("state", "available")
                ))
                .build();

        return Optional.of(ec2Client.describeVpnGateways(request))
                .filter(response -> response.vpnGateways() != null && response.vpnGateways().size() == 1)
                .map(response -> response.vpnGateways().get(0));
    }

    private List getFilters(JsonObject properties) {
        if (!properties.containsKey("filter") || properties.isNull("filter")) {
            return ImmutableList.of();
        }

        return properties.getJsonObject("filter").entrySet().stream()
                .map(filter -> filter(filter.getKey(), ((JsonString) filter.getValue()).getString()))
                .collect(Collectors.toList());
    }

    private Filter filter(String name, String... values) {
        return Filter.builder()
                .name(name)
                .values(values)
                .build();
    }

    private  Stream getStream(List values) {
        return values != null ? values.stream() : Stream.empty();
    }

    private static class Subnet {

        private String id;
        private String type;
        private String availabilityZone;
        private String cidrBlock;
        private Map tags;
        private String routeTableId;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getAvailabilityZone() {
            return availabilityZone;
        }

        public void setAvailabilityZone(String availabilityZone) {
            this.availabilityZone = availabilityZone;
        }

        public String getCidrBlock() {
            return cidrBlock;
        }

        public void setCidrBlock(String cidrBlock) {
            this.cidrBlock = cidrBlock;
        }

        public Map getTags() {
            return tags;
        }

        public void setTags(Map tags) {
            this.tags = tags;
        }

        public String getRouteTableId() {
            return routeTableId;
        }

        public void setRouteTableId(String routeTableId) {
            this.routeTableId = routeTableId;
        }
    }

    private static class VpcSubnetGroupTypeJsonSerializer extends JsonSerializer {

        @Override
        public void serialize(VpcSubnetGroupType value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
            generator.writeString(StringUtils.capitalize(value.toString().toLowerCase()));
        }

    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy