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

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

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