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

br.com.ingenieux.mojo.apigateway.CreateOrUpdateMojo Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2016 ingenieux Labs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package br.com.ingenieux.mojo.apigateway;

import com.google.common.collect.Lists;

import com.amazonaws.services.apigateway.model.CreateDeploymentRequest;
import com.amazonaws.services.apigateway.model.CreateDeploymentResult;
import com.amazonaws.services.apigateway.model.CreateRestApiRequest;
import com.amazonaws.services.apigateway.model.CreateRestApiResult;
import com.amazonaws.services.apigateway.model.PutMode;
import com.amazonaws.services.apigateway.model.PutRestApiRequest;
import com.amazonaws.services.apigateway.model.PutRestApiResult;
import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.flipkart.zjsonpatch.JsonPatch;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import br.com.ingenieux.mojo.apigateway.util.Unthrow;
import br.com.ingenieux.mojo.aws.util.RoleResolver;

import static java.lang.String.format;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang.StringUtils.defaultString;
import static org.apache.commons.lang.StringUtils.isBlank;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

@Mojo(name = "create-or-update", requiresProject = true)
public class CreateOrUpdateMojo extends AbstractAPIGatewayMojo {
  /**
   * Regex to Find Parameter Keys (when importing)
   */
  public static final Pattern KEY_PARAM_REGEX = Pattern.compile("^apigateway\\.param\\.(.+)$");

  /**
   * Regex to Find Stage Parameters
   */
  public static final Pattern KEY_STAGE_VARIABLE_REGEX = Pattern.compile("^apigateway\\.stage\\.([^\\.]{3,}).(.+)$");

  private static final String STR_TEMPLATE_LAMBDA_METHOD = loadResourceAsString("lambda-method.json");

  private static final String STR_TEMPLATE_CORS_METHOD =
    loadResourceAsString("cors-method.json");

  private static String loadResourceAsString(String name) {
    try {
      return IOUtils.toString(CreateOrUpdateMojo.class.getClassLoader().getResource("templates/" + name));
    } catch (Exception exc) {
      throw new IllegalStateException("While loading template: " + name, exc);
    }
  }

  private static final Pattern PATTERN_PARAMETER = Pattern.compile("\\{(\\w+)\\}");

  /**
   * Rest API Description
   */
  @Parameter(property = "apigateway.restApiDescription", required = false, defaultValue = "API for ${project.artifactId}")
  protected String restApiDescription;

  /**
   * Stage Name Description
   */
  @Parameter(property = "apigateway.stageNameDescription", required = true, defaultValue = "${apigateway.stageName} stage")
  protected String stageNameDescription;

  /**
   * Stage Deployment Description
   */
  @Parameter(property = "apigateway.stageDeploymentDescription", required = true, defaultValue = "Updated by apigateway-maven-plugin")
  protected String deploymentDescription;

  /**
   * Deployment File to Use (Swagger)
   */
  @Parameter(
    property = "apigateway.deploymentFile",
    required = true,
    defaultValue = "${project.build.outputDirectory}/META-INF/apigateway/apigateway-swagger.json"
  )
  protected File deploymentFile;

  /**
   * Deployment File to Use (Lambda)
   */
  @Parameter(property = "apigateway.lambdasFile", defaultValue = "${project.build.outputDirectory}/META-INF/lambda-definitions.json")
  protected File lambdasFile;

  /**
   * Cache Cluster Enabled (when creating a new stage)
   */
  @Parameter(property = "apigateway.cacheClusterEnabled", defaultValue = "false")
  protected Boolean cacheClusterEnabled;

  /**
   * Cache Cluster Size (when creating a new stage)
   */
  @Parameter(property = "apigateway.cacheClusterSize")
  protected String cacheClusterSize;

  /**
   * New Stage Definitions
   */
  @Parameter(required = false)
  protected Map> stageVariables = new LinkedHashMap<>();

  /**
   * Overwrite Definitions (otherwise, merge)
   */
  @Parameter(property = "apigateway.overwriteDefinitions", required = true, defaultValue = "false")
  protected Boolean overwriteDefinitions;

  /**
   * Overwrite Definitions (otherwise, merge)
   */
  @Parameter(property = "apigateway.overwriteDefinitions", required = true, defaultValue = "false")
  protected Boolean skipInterpolate;

  /**
   * Parameters
   */
  @Parameter(property = "apigateway.parameters", required = false)
  protected Map parameters = new HashMap<>();

  /**
   * Remove Conflicting Declarations? Disable for advanced usage
   */
  @Parameter(property = "apigateway.removeConflicting", required = true, defaultValue = "true")
  protected boolean removeConflicting;

  /**
   * Resulting Body to use
   */
  protected String body;

  private RoleResolver roleResolver;

  private List lambdaDefinitions = Collections.emptyList();

  private ObjectNode templateChildNode;

  private ObjectNode templateOptionsNode;

  private ObjectNode swaggerDefinition;

  @Override
  protected Object executeInternal() throws Exception {
    this.roleResolver = new RoleResolver(createServiceFor(AmazonIdentityManagementClient.class));

    initConstants();

    initProperties();

    loadLambdaDefinitions();

    createOrUpdateRestApi();

    loadAndInterpolateSwaggerFile();

    importDefinitions();

    cleanupPermissions();

    CreateDeploymentResult result = deploy();

    return result;
  }

  private void initConstants() throws Exception {
    try {
      this.templateChildNode = (ObjectNode) objectMapper.readTree(STR_TEMPLATE_LAMBDA_METHOD);
    } catch (Exception exc) {
      getLog().warn("While building template credentials node:", exc);
    }

    try {
      this.templateChildNode.with("x-amazon-apigateway-integration").put("credentials", roleResolver.lookupRoleGlob("*/apigateway-lambda-invoker"));
    } catch (Exception exc) {
      getLog().warn("While building template credentials node:", exc);
    }

    try {
      this.templateOptionsNode = (ObjectNode) objectMapper.readTree(STR_TEMPLATE_CORS_METHOD);
    } catch (Exception exc) {
      getLog().warn("While building template credentials node:", exc);
    }
  }

  private CreateDeploymentResult deploy() {
    /**
     * Step #1: Doing the stage itself
     */
    CreateDeploymentRequest req =
      new CreateDeploymentRequest()
        .withRestApiId(restApiId)
        .withStageName(stageName)
        .withDescription(deploymentDescription)
        .withStageName(stageName)
        .withStageDescription(stageNameDescription)
        .withCacheClusterEnabled(cacheClusterEnabled)
        .withCacheClusterSize(cacheClusterSize)
        .withVariables(getStageVariables(stageName));

    getLog().info("Creating Deployment Request: " + req);

    CreateDeploymentResult result = getService().createDeployment(req);

    getLog().info("Deployment Request Result: " + result);

    return result;
  }

  private Map getStageVariables(String stageName) {
    if (stageVariables.containsKey(stageName)) {
      return stageVariables.get(stageName);
    }

    return Collections.emptyMap();
  }

  private void cleanupPermissions() {
  }

  private void initProperties() {
    final Properties props = getProperties();

    props
      .entrySet()
      .stream()
      .map(e -> KEY_PARAM_REGEX.matcher(e.getKey() + ""))
      .filter(m -> m.matches())
      .forEach(
        m -> {
          String k = m.group(1);
          String v = curProject.getProperties().getProperty(m.group(0), "");

          if (isEmpty(v) && parameters.containsKey(k)) {
            getLog().info("Removing parameter " + k);

            parameters.remove(k);

          } else {
            getLog().info("Updating parameter " + k + ": " + v);

            parameters.put(k, v);
          }
        });

    props
      .entrySet()
      .stream()
      .map(e -> KEY_STAGE_VARIABLE_REGEX.matcher(e.getKey() + ""))
      .filter(m -> m.matches())
      .forEach(
        m -> {
          String env = m.group(1);
          String k = m.group(2);
          String v = curProject.getProperties().getProperty(m.group(0), "");

          Map stageVariablesForEnv = stageVariables.get(env);

          if (null == stageVariablesForEnv) {
            stageVariablesForEnv = new LinkedHashMap();

            stageVariables.put(env, stageVariablesForEnv);
          }

          if (isEmpty(v) && stageVariablesForEnv.containsKey(k)) {
            getLog().info("Removing stage variable " + k);

            stageVariablesForEnv.remove(k);
          } else {
            getLog().info("Updating stage variable " + k + ": " + v);

            stageVariablesForEnv.put(k, v);
          }
        });

    if (props.containsKey("apigateway.restApiId")) {
      restApiId = props.getProperty("apigateway.restApiId");
    }

    if (props.containsKey("apigateway.restApiName")) {
      restApiName = props.getProperty("apigateway.restApiName");
    }

//    if (props.containsKey("apigateway.restApiHost")) {
//      restApiHost = props.getProperty("apigateway.restApiHost");
//    }
  }

  protected void loadLambdaDefinitions() throws Exception {
    if (null != lambdasFile && lambdasFile.exists()) {
      getLog().info("Loading lambdas from " + lambdasFile.getPath());

      Map defs = objectMapper.readValue(lambdasFile, new TypeReference>() {
      });

      this.lambdaDefinitions = defs.values().stream().filter(x -> null != x.getApi()).collect(Collectors.toList());

      this.lambdaDefinitions.forEach(x -> x.getApi().methodType = x.getApi().methodType.toLowerCase());
    }
  }

  private PutRestApiResult importDefinitions() {
    getLog().info("Uploading definitions update (overwrite mode?: " + overwriteDefinitions + ")");

    final PutRestApiResult result =
      getService()
        .putRestApi(
          new PutRestApiRequest()
            .withRestApiId(restApiId)
            .withMode(this.overwriteDefinitions ? PutMode.Overwrite : PutMode.Merge)
            .withBody(ByteBuffer.wrap(body.getBytes(DEFAULT_CHARSET)))
            .withParameters(parameters));

    getLog().debug("result: " + result);

    return result;
  }

  private void loadAndInterpolateSwaggerFile() throws Exception {
    String accountId = roleResolver.getAccountId();
    String deploymentFileContents = IOUtils.toString(new FileInputStream(deploymentFile));

    boolean bYamlFile = (deploymentFile.getName().endsWith(".yaml"));

    getLog().info("Loaded deploymentFile contents from " + deploymentFile.getPath());

    // TODO: Consider PluginParameterExpressionEvaluator
    deploymentFileContents = new StrSubstitutor(getProperties()).replace(deploymentFileContents);

    // IMPROVE THIS
    if (!skipInterpolate) {
      deploymentFileContents = deploymentFileContents.replaceAll("\\Qarn:aws:iam:::role/\\E", "arn:aws:iam::" + accountId + ":role/");

      deploymentFileContents =
        deploymentFileContents.replaceAll("\\Qarn:aws:lambda:\\E[\\w\\-]*:\\d*:", "arn:aws:lambda:" + regionName + ":" + accountId + ":");
    }

    getLog().debug("Contents: " + deploymentFileContents);

    swaggerDefinition = (ObjectNode) (bYamlFile ? YAML_OBJECT_MAPPER : objectMapper).readTree(deploymentFileContents);

    swaggerDefinition.with("info").put("title", restApiName).put("description", restApiDescription);

    mergeLambdas(swaggerDefinition);

    ObjectNode objectNode = ObjectNode.class.cast(swaggerDefinition);

    this.body = objectMapper.writeValueAsString(objectNode);

    getLog().debug("Final body content: " + this.body);
  }

  protected void mergeLambdas(ObjectNode swaggerDefinition) throws Exception {
    getLog().info("Loaded " + lambdaDefinitions.size() + " active lambda definitions.");

    if (lambdaDefinitions.isEmpty() || skipInterpolate) {
      getLog().info("Skipping interpolation.");

      return;
    }

    ObjectNode pathNode = swaggerDefinition.with("paths");

    for (LambdaDefinition d : lambdaDefinitions) {
      removeConflictingDeclarations(d, pathNode);

      ObjectNode parentNode = templateChildNode.deepCopy();

      parentNode.with("x-amazon-apigateway-integration").put("httpMethod", "POST").put("uri", getUriFor(d));

      final ArrayNode parametersNode = parentNode.putArray("parameters");

      for (String parameterName : findParametersFor(d.getApi().getPath())) {
        ObjectNode parameterNode = objectMapper.createObjectNode();

        parameterNode.put("name", parameterName);
        parameterNode.put("in", "path");
        parameterNode.put("required", true);
        parameterNode.put("type", "string");

        parametersNode.add(parameterNode);
      }

      ObjectNode result = parentNode;

      {
        ArrayNode patches = getPatches(d.getApi());

        if (null != patches) {
          result = (ObjectNode) JsonPatch.apply(patches, parentNode);
        }
      }

      pathNode.with(d.getApi().getPath()).with(d.getApi().getMethodType()).removeAll();

      pathNode.with(d.getApi().getPath()).with(d.getApi().getMethodType()).setAll(result);
    }

    Map> corsPaths =
      lambdaDefinitions
        .stream()
        .filter(x -> x.api.isCorsEnabled())
        .map(x -> x.getApi())
        .collect(groupingBy(x -> x.getPath(), mapping(k -> k.getMethodType(), toSet())));

    for (Map.Entry> e : corsPaths.entrySet()) {
      String supportedMethods = format("'%s'", StringUtils.join(e.getValue().iterator(), ","));

      ObjectNode optionsNode = pathNode.with(e.getKey()).with("options");

      optionsNode.setAll(templateOptionsNode);

      optionsNode
        .with("x-amazon-apigateway-integration")
        .with("responses")
        .with("default")
        .with("responseParameters")
        .put("method.response.header.Access-Control-Allow-Methods", supportedMethods);
    }
  }

  private ArrayNode getPatches(LambdaDefinition.Api api) {
    if (null == api.getPatches() || 0 == api.getPatches().length) return null;

    ArrayNode result = objectMapper.createArrayNode();

    result.addAll(
      Arrays.stream(api.getPatches())
        .map(
          p ->
            Unthrow.wrap(
              patch -> {
                ObjectNode resultNode = objectMapper.createObjectNode();

                resultNode.put("op", patch.getOp().toLowerCase());

                resultNode.put("path", patch.getPath());

                // TODO: Interpolate patch.value
                if (isNotBlank(patch.getValue())) {
                  JsonNode value = null;

                  String sourceValue = patch.getValue();

                  if (-1 != "[{\"".indexOf(sourceValue.charAt(0))) {
                    value = objectMapper.readTree(sourceValue);
                  } else {
                    value = resultNode.textNode(sourceValue);
                  }

                  resultNode.set("value", value);
                } else if (isNotBlank(patch.getFrom())) {
                  JsonNode value = null;

                  String sourceValue = patch.getFrom();

                  if (-1 != "[{\"".indexOf(sourceValue.charAt(0))) {
                    value = objectMapper.readTree(sourceValue);
                  } else {
                    value = resultNode.textNode(sourceValue);
                  }

                  resultNode.set("from", value);
                }

                return resultNode;
              },
              p))
        .collect(Collectors.toList()));

    return result;
  }

  private String getUriFor(LambdaDefinition d) {
    // "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1::function:do_validateBot/invocations"

    return format("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/%s/invocations", d.getArn());
  }

  private void removeConflictingDeclarations(LambdaDefinition d, ObjectNode pathNode) {
    if (!removeConflicting) return;
    String normalizedPath = normalizePath(d.getApi().getPath());

    outer:
    do {
      for (String path : Lists.newArrayList(pathNode.fieldNames())) {
        String normalizedExistingPath = normalizePath(path);

        boolean hasMatchingPath = normalizedExistingPath.equals(normalizedPath);

        if (hasMatchingPath) {
          getLog().info("Renaming possibly conflicting path " + path + " to " + d.getApi().getPath() + " (overrides will apply)");

          ObjectNode childNodesFromExisting = pathNode.with(path);

          pathNode.with(d.getApi().getPath()).setAll(childNodesFromExisting);

          pathNode.remove(path);
          continue outer;
        }
      }
    } while (false);
  }

  private String normalizePath(String path) {
    return path.replaceAll("\\{\\w+\\}", "{}");
  }

  protected Set findParametersFor(String path) {
    Set result = new LinkedHashSet<>();
    Matcher m = PATTERN_PARAMETER.matcher(path);

    while (m.find()) {
      result.add(m.group(1));
    }

    return result;
  }

  private void createOrUpdateRestApi() throws Exception {
    super.lookupIds();

    if (isBlank(restApiId)) {
      CreateRestApiRequest req = new CreateRestApiRequest();

      req.withName(restApiName);
      req.withDescription(defaultString(restApiDescription));

      final CreateRestApiResult result = getService().createRestApi(req);

      getLog().info("Created restApi " + req + ": " + result);

      super.lookupIds();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy