diff --git a/pom.xml b/pom.xml index c810af8..102970b 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ com.apigee.edge.config apigee-config-maven-plugin - 2.5.1-SNAPSHOT + 2.6.0-SNAPSHOT maven-plugin apigee-config-maven-plugin Plugin to manage configuration in Apigee diff --git a/samples/APIandConfig/HelloWorld/edge.json b/samples/APIandConfig/HelloWorld/edge.json index 757025f..758c4dc 100644 --- a/samples/APIandConfig/HelloWorld/edge.json +++ b/samples/APIandConfig/HelloWorld/edge.json @@ -80,8 +80,8 @@ "description":"Echo Product", "attributes":[ { - "name": "access", - "value": "public" + "name":"access", + "value":"public" } ], "environments":[ @@ -91,33 +91,35 @@ "quota":"10000", "quotaInterval":"1", "quotaTimeUnit":"month", - "operationGroup": { - "operationConfigs": [ - { - "apiSource": "HelloWorld", - "operations": [ - { - "resource": "/", - "methods": [ - "GET" - ] - } + "operationGroup":{ + "operationConfigs":[ + { + "apiSource":"HelloWorld", + "operations":[ + { + "resource":"/", + "methods":[ + "GET" + ] + } ], - "quota": { - "limit": "1000", - "interval": "1", - "timeUnit": "month" + "quota":{ + "limit":"1000", + "interval":"1", + "timeUnit":"month" }, - "attributes": [ - { - "name": "foo", - "value": "bar" - } + "attributes":[ + { + "name":"foo", + "value":"bar" + } ] - } + } ] - }, - "scopes":[] + }, + "scopes":[ + + ] }, { "name":"EchoProduct-legacy", @@ -132,8 +134,8 @@ "value":"Echo Product Legacy" }, { - "name": "access", - "value": "public" + "name":"access", + "value":"public" } ], "description":"Echo Product Legacy", @@ -148,7 +150,9 @@ "quota":"10000", "quotaInterval":"1", "quotaTimeUnit":"month", - "scopes":[] + "scopes":[ + + ] } ], "kvms":[ @@ -253,6 +257,50 @@ } ] }, + "apiCategories":[ + "echo", + "sample", + "foo" + ], + "apiDocs":[ + { + "title":"Sample Doc", + "description":"Sample description", + "anonAllowed":true, + "imageUrl":"", + "requireCallbackUrl":true, + "published":true, + "apiProductName":"weatherProduct", + "categories":[ + "foo" + ], + "oasDocumentation":{ + "spec":{ + "displayName":"sample-doc-spec", + "file":"./specs/openapi.yaml" + } + } + }, + { + "title":"Sample GQL", + "description":"Sample GraphQL", + "anonAllowed":true, + "imageUrl":"", + "requireCallbackUrl":true, + "published":true, + "apiProductName":"demo", + "categories":[ + "bar" + ], + "graphqlDocumentation":{ + "schema":{ + "displayName":"sample-doc-spec", + "file":"./specs/schema.graphql" + }, + "endpointUri":"https://demo.google.com/graphql" + } + } + ], "importKeys":{ "john@example.com":[ { diff --git a/samples/APIandConfig/HelloWorld/specs/openapi.yaml b/samples/APIandConfig/HelloWorld/specs/openapi.yaml new file mode 100644 index 0000000..ab73985 --- /dev/null +++ b/samples/APIandConfig/HelloWorld/specs/openapi.yaml @@ -0,0 +1,144 @@ +openapi: 3.0.0 +info: + description: OpenAPI Specification for the Apigee mock target service endpoint. + version: 1.0.0 + title: Mock Target API +paths: + /: + get: + summary: View personalized greeting + operationId: View a personalized greeting + description: View a personalized greeting for the specified or guest user. + parameters: + - name: user + in: query + description: Your user name. + required: false + schema: + type: string + responses: + "200": + description: Success + /help: + get: + summary: Get help + operationId: Get help + description: View help information about available resources in HTML format. + responses: + "200": + description: Success + /user: + get: + summary: View personalized greeting + operationId: View personalized greeting + description: View a personalized greeting for the specified or guest user. + parameters: + - name: user + in: query + description: Your user name. + required: false + schema: + type: string + responses: + "200": + description: Success + /iloveapis: + get: + summary: View API affirmation + operationId: View API affirmation + description: View API affirmation in HTML format. + responses: + "200": + description: Success + /ip: + get: + summary: View IP address + operationId: View IP address + description: View the IP address of the client in JSON format. + responses: + "200": + description: Success + /xml: + get: + summary: View XML response + operationId: View XML response + description: View a sample response in XML format. + responses: + "200": + description: Success + /json: + get: + summary: View JSON response + operationId: View JSON response + description: View a sample response in JSON format. + responses: + "200": + description: Success + /echo: + get: + summary: View request headers and body + operationId: View request headers and body + description: View the request headers and body in JSON format. + responses: + "200": + description: Success + post: + summary: Send request and view request headers and body + operationId: Send request and view request headers and body + description: "Send a request and view the resulting request headers and body in JSON + format. +The request payload can be specified using one of the + following formats: application/json, application/x-www-form-urlencoded, + or application/xml." + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/request-body" + description: Request payload in application/json, + application/x-www-form-urlencoded, or application/xml format. + required: true + responses: + "200": + description: Success + "/statuscode/{code}": + get: + summary: View status code and message + operationId: View status code and message + description: View status code and message for the specified value. + parameters: + - name: code + in: path + description: HTTP status code. + required: true + schema: + type: string + responses: + "200": + description: Success + /auth: + get: + security: + - basicAuth: [] + summary: Validate access using basic authentication + operationId: Validate access using basic authentication + description: Validate access using basic authentication. + responses: + "200": + description: Success +servers: + - url: http://mocktarget.apigee.net + - url: https://mocktarget.apigee.net +components: + securitySchemes: + basicAuth: + type: http + description: HTTP Basic Authentication. + scheme: basic + schemas: + request-body: + properties: + replace-me: + type: object + description: Replace with request payload in application/json, + application/x-www-form-urlencoded, or application/xml format. \ No newline at end of file diff --git a/samples/APIandConfig/HelloWorld/specs/schema.graphql b/samples/APIandConfig/HelloWorld/specs/schema.graphql new file mode 100644 index 0000000..96da1bb --- /dev/null +++ b/samples/APIandConfig/HelloWorld/specs/schema.graphql @@ -0,0 +1,12 @@ +type Query { + greeting:String + students:[Student] +} + +type Student { + id:ID! + firstName:String + lastName:String + password:String + collegeId:String +} \ No newline at end of file diff --git a/samples/APIandConfig/shared-pom.xml b/samples/APIandConfig/shared-pom.xml index 17c21ec..c5ea568 100644 --- a/samples/APIandConfig/shared-pom.xml +++ b/samples/APIandConfig/shared-pom.xml @@ -69,7 +69,7 @@ com.apigee.edge.config apigee-config-maven-plugin - 2.5.0 + 2.6.0-SNAPSHOT create-config-targetserver @@ -148,6 +148,20 @@ importKeys + + create-portal-categories + install + + apicategories + + + + create-portal-docs + install + + apidocs + + @@ -165,6 +179,7 @@ oauth ${bearer} ${file} + ${siteId} override @@ -179,6 +194,7 @@ oauth ${bearer} ${file} + ${siteId} override diff --git a/samples/EdgeConfig/resources/edge/org/apiCategories.json b/samples/EdgeConfig/resources/edge/org/apiCategories.json new file mode 100644 index 0000000..98d1d28 --- /dev/null +++ b/samples/EdgeConfig/resources/edge/org/apiCategories.json @@ -0,0 +1,4 @@ +[ + "foo", + "bar" +] \ No newline at end of file diff --git a/samples/EdgeConfig/resources/edge/org/apiDocs.json b/samples/EdgeConfig/resources/edge/org/apiDocs.json new file mode 100644 index 0000000..2e5951d --- /dev/null +++ b/samples/EdgeConfig/resources/edge/org/apiDocs.json @@ -0,0 +1,39 @@ +[ + { + "title": "Sample Doc", + "description": "Sample description", + "anonAllowed": true, + "imageUrl": "", + "requireCallbackUrl": true, + "published": true, + "apiProductName": "weatherProduct", + "categories": [ + "foo" + ], + "oasDocumentation": { + "spec": { + "displayName": "sample-openapi", + "file": "./specs/openapi.yaml" + } + } + }, + { + "title": "Sample GQL", + "description": "Sample GraphQL", + "anonAllowed": true, + "imageUrl": "", + "requireCallbackUrl": true, + "published": true, + "apiProductName": "demo", + "categories": [ + "bar" + ], + "graphqlDocumentation": { + "schema": { + "displayName": "sample-graphql", + "file": "./specs/schema.graphql" + }, + "endpointUri": "https://demo.google.com/graphql" + } + } +] \ No newline at end of file diff --git a/samples/EdgeConfig/shared-pom.xml b/samples/EdgeConfig/shared-pom.xml index 1ca96c4..7366632 100644 --- a/samples/EdgeConfig/shared-pom.xml +++ b/samples/EdgeConfig/shared-pom.xml @@ -26,7 +26,7 @@ com.apigee.edge.config apigee-config-maven-plugin - 2.5.0 + 2.6.0-SNAPSHOT create-config-targetserver @@ -112,6 +112,20 @@ importKeys + + create-portal-categories + install + + apicategories + + + + create-portal-docs + install + + apidocs + + @@ -127,6 +141,7 @@ ${env} oauth ${bearer} + ${siteId} ${file} @@ -140,6 +155,7 @@ ${env} oauth ${bearer} + ${siteId} ${file} diff --git a/samples/EdgeConfig/specs/openapi.yaml b/samples/EdgeConfig/specs/openapi.yaml new file mode 100644 index 0000000..ab73985 --- /dev/null +++ b/samples/EdgeConfig/specs/openapi.yaml @@ -0,0 +1,144 @@ +openapi: 3.0.0 +info: + description: OpenAPI Specification for the Apigee mock target service endpoint. + version: 1.0.0 + title: Mock Target API +paths: + /: + get: + summary: View personalized greeting + operationId: View a personalized greeting + description: View a personalized greeting for the specified or guest user. + parameters: + - name: user + in: query + description: Your user name. + required: false + schema: + type: string + responses: + "200": + description: Success + /help: + get: + summary: Get help + operationId: Get help + description: View help information about available resources in HTML format. + responses: + "200": + description: Success + /user: + get: + summary: View personalized greeting + operationId: View personalized greeting + description: View a personalized greeting for the specified or guest user. + parameters: + - name: user + in: query + description: Your user name. + required: false + schema: + type: string + responses: + "200": + description: Success + /iloveapis: + get: + summary: View API affirmation + operationId: View API affirmation + description: View API affirmation in HTML format. + responses: + "200": + description: Success + /ip: + get: + summary: View IP address + operationId: View IP address + description: View the IP address of the client in JSON format. + responses: + "200": + description: Success + /xml: + get: + summary: View XML response + operationId: View XML response + description: View a sample response in XML format. + responses: + "200": + description: Success + /json: + get: + summary: View JSON response + operationId: View JSON response + description: View a sample response in JSON format. + responses: + "200": + description: Success + /echo: + get: + summary: View request headers and body + operationId: View request headers and body + description: View the request headers and body in JSON format. + responses: + "200": + description: Success + post: + summary: Send request and view request headers and body + operationId: Send request and view request headers and body + description: "Send a request and view the resulting request headers and body in JSON + format. +The request payload can be specified using one of the + following formats: application/json, application/x-www-form-urlencoded, + or application/xml." + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/request-body" + description: Request payload in application/json, + application/x-www-form-urlencoded, or application/xml format. + required: true + responses: + "200": + description: Success + "/statuscode/{code}": + get: + summary: View status code and message + operationId: View status code and message + description: View status code and message for the specified value. + parameters: + - name: code + in: path + description: HTTP status code. + required: true + schema: + type: string + responses: + "200": + description: Success + /auth: + get: + security: + - basicAuth: [] + summary: Validate access using basic authentication + operationId: Validate access using basic authentication + description: Validate access using basic authentication. + responses: + "200": + description: Success +servers: + - url: http://mocktarget.apigee.net + - url: https://mocktarget.apigee.net +components: + securitySchemes: + basicAuth: + type: http + description: HTTP Basic Authentication. + scheme: basic + schemas: + request-body: + properties: + replace-me: + type: object + description: Replace with request payload in application/json, + application/x-www-form-urlencoded, or application/xml format. \ No newline at end of file diff --git a/samples/EdgeConfig/specs/schema.graphql b/samples/EdgeConfig/specs/schema.graphql new file mode 100644 index 0000000..96da1bb --- /dev/null +++ b/samples/EdgeConfig/specs/schema.graphql @@ -0,0 +1,12 @@ +type Query { + greeting:String + students:[Student] +} + +type Student { + id:ID! + firstName:String + lastName:String + password:String + collegeId:String +} \ No newline at end of file diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/APICategoriesMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/APICategoriesMojo.java new file mode 100644 index 0000000..46177ef --- /dev/null +++ b/src/main/java/com/apigee/edge/config/mavenplugin/APICategoriesMojo.java @@ -0,0 +1,276 @@ +/** + * Copyright 2023 Google LLC + * + * 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 com.apigee.edge.config.mavenplugin; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.apigee.edge.config.rest.RestUtil; +import com.apigee.edge.config.utils.ServerProfile; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; + +/** ¡¡ + * Goal to create API Categories in Apigee Portal + * scope: org + * + * @author ssvaidyanathan + * @goal apicategories + * @phase install + */ + +public class APICategoriesMojo extends GatewayAbstractMojo +{ + static Logger logger = LogManager.getLogger(APICategoriesMojo.class); + public static final String ____ATTENTION_MARKER____ = + "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private ServerProfile serverProfile; + + public APICategoriesMojo() { + super(); + + } + + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("Apigee Portal API Categories"); + logger.info(____ATTENTION_MARKER____); + + String options=""; + serverProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + logger.debug("Build option " + buildOption.name()); + logger.debug("Base dir " + super.getBaseDirectoryPath()); + if (serverProfile.getPortalSiteId() == null) { + throw new MojoExecutionException( + "Portal Site ID not found in profile"); + } + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + protected void doUpdate(List categories) + throws MojoFailureException { + try { + Map existingCategories = null; + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + logger.info("Retrieving existing API Categories - " + + serverProfile.getEnvironment()); + existingCategories = getCategories(serverProfile); + + for (String category : categories) { + if (existingCategories != null && existingCategories.keySet()!=null + && existingCategories.keySet().contains(category)) { + switch (buildOption) { + case update: + logger.info("API Category \"" + category + + "\" already exists. Skipping."); + break; + case create: + logger.info("API Category \"" + category + + "\" already exists. Skipping."); + break; + case delete: + logger.info("API Category \"" + category + + "\" already exists. Deleting."); + deleteAPICategory(serverProfile, existingCategories.get(category)); + break; + case sync: + logger.info("API Category \"" + category + + "\" already exists. Deleting and recreating."); + deleteAPICategory(serverProfile, existingCategories.get(category)); + logger.info("Creating API Category - " + category); + createAPICategory(serverProfile, category); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info("Creating API Category - " + category); + createAPICategory(serverProfile, category); + break; + case delete: + logger.info("API Category \"" + category + + "\" does not exist. Skipping."); + break; + } + } + } + + } catch (IOException e) { + throw new MojoFailureException("Apigee network call error " + + e.getMessage()); + } catch (RuntimeException e) { + throw e; + } + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + + if (super.isSkip()) { + logger.info("Skipping"); + return; + } + + Logger logger = LogManager.getLogger(APICategoriesMojo.class); + + try { + + init(); + + if (buildOption == OPTIONS.none) { + logger.info("Skipping API Categories (default action)"); + return; + } + + if (serverProfile.getEnvironment() == null) { + throw new MojoExecutionException( + "Apigee environment not found in profile"); + } + + List categories = getOrgConfig(logger, "apiCategories"); + if (categories == null || categories.size() == 0) { + logger.info("No API Categories found."); + return; + } + doUpdate(categories); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } + } + + /*************************************************************************** + * REST call wrappers + **/ + public static String createAPICategory(ServerProfile profile, String category) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + String payload = "{\"name\": \""+category+"\"}"; + HttpResponse response = restUtil.createOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apicategories", + payload); + try { + + logger.info("Response " + response.getContentType() + "\n" + + response.parseAsString()); + if (response.isSuccessStatusCode()) + logger.info("Create Success."); + + } catch (HttpResponseException e) { + logger.error("API Category create error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return ""; + } + + public static String deleteAPICategory(ServerProfile profile, + String categoryId) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + HttpResponse response = restUtil.deleteOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apicategories", + categoryId); + try { + + logger.info("Response " + response.getContentType() + "\n" + + response.parseAsString()); + if (response.isSuccessStatusCode()) + logger.info("Delete Success."); + + } catch (HttpResponseException e) { + logger.error("API Category delete error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return ""; + } + + public static Map getCategories(ServerProfile profile) + throws IOException { + Map categoryMap = new HashMap(); + RestUtil restUtil = new RestUtil(profile); + HttpResponse response = restUtil.getOrgConfig(profile, "sites/"+profile.getPortalSiteId()+"/apicategories"); + if(response == null) return categoryMap; + JSONArray categories = null; + try { + logger.debug("output " + response.getContentType()); + String payload = response.parseAsString(); + logger.debug(payload); + + JSONParser parser = new JSONParser(); + JSONObject obj = (JSONObject)parser.parse(payload); + categories = (JSONArray)obj.get("data"); + for (int i = 0; categories != null && i < categories.size(); i++) { + JSONObject a = (JSONObject) categories.get(i); + categoryMap.put((String)a.get("name"), (String)a.get("id")); + } + } catch (ParseException pe){ + logger.error("Get Categories parse error " + pe.getMessage()); + throw new IOException(pe.getMessage()); + } catch (HttpResponseException e) { + logger.error("Get Categories error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return categoryMap; + } + +} + + + + diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/APIDocsMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/APIDocsMojo.java new file mode 100644 index 0000000..8a837fa --- /dev/null +++ b/src/main/java/com/apigee/edge/config/mavenplugin/APIDocsMojo.java @@ -0,0 +1,466 @@ +/** + * Copyright 2023 Google LLC + * + * 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 com.apigee.edge.config.mavenplugin; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import com.apigee.edge.config.rest.RestUtil; +import com.apigee.edge.config.utils.ServerProfile; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.util.Key; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; + +/** ¡¡ + * Goal to create API Docs in Apigee Portal + * scope: org + * + * @author ssvaidyanathan + * @goal apidocs + * @phase install + */ + +public class APIDocsMojo extends GatewayAbstractMojo +{ + static Logger logger = LogManager.getLogger(APIDocsMojo.class); + public static final String ____ATTENTION_MARKER____ = + "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private ServerProfile serverProfile; + + public APIDocsMojo() { + super(); + + } + + public static class APIDoc { + @Key + public String title; + @Key + public List categories; + } + + protected String getAPIDocName(String payload) throws MojoFailureException { + Gson gson = new Gson(); + try { + APIDoc apiDoc = gson.fromJson(payload, APIDoc.class); + return apiDoc.title; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + protected String updatePayloadWithCategoryId(String payload, ServerProfile profile) throws MojoFailureException, IOException { + Gson gson = new Gson(); + try { + APIDoc apiDoc = gson.fromJson(payload, APIDoc.class); + List categories = apiDoc.categories; + if(categories!=null && categories.size()>0) { + //Fetch existing categories and its id from portal + Map existingCategoryMap = getCategories(profile); + if(existingCategoryMap != null && existingCategoryMap.size()>0) { + JsonArray categoryIds = new JsonArray(); + for (String category : categories) { + if(existingCategoryMap.get(category)!=null) { + JsonPrimitive id = new JsonPrimitive(existingCategoryMap.get(category)); + categoryIds.add(id); + } + } + JsonParser parser = new JsonParser(); + JsonElement jsonElement = parser.parse(payload); + JsonObject apiDocJsonObj = jsonElement.getAsJsonObject(); + apiDocJsonObj.add("categoryIds", categoryIds); + apiDocJsonObj.remove("categories"); //remove the categories + return apiDocJsonObj.toString(); + } + } + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + return payload; + } + + + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("Apigee Portal API Docs"); + logger.info(____ATTENTION_MARKER____); + + String options=""; + serverProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + logger.debug("Build option " + buildOption.name()); + logger.debug("Base dir " + super.getBaseDirectoryPath()); + if (serverProfile.getPortalSiteId() == null) { + throw new MojoExecutionException( + "Portal Site ID not found in profile"); + } + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + protected void doUpdate(List apiDocs) + throws MojoFailureException { + try { + Map existingDocs = null; + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + logger.info("Retrieving existing API Docs - " + + serverProfile.getEnvironment()); + existingDocs = getAPIDocs(serverProfile); + + for (String apiDoc : apiDocs) { + //update category with categoryId + apiDoc = updatePayloadWithCategoryId(apiDoc, serverProfile); + logger.debug("updated doc: "+ apiDoc); + String apiDocName = getAPIDocName(apiDoc); + if (apiDocName == null) { + throw new IllegalArgumentException( + "API Doc does not have a title.\n" + apiDoc + "\n"); + } + if (existingDocs != null && existingDocs.keySet()!=null + && existingDocs.keySet().contains(apiDocName)) { + switch (buildOption) { + case update: + logger.info("Updating API Doc - " + apiDocName); + updateAPIDoc(serverProfile, existingDocs.get(apiDocName), apiDoc); + createAPIDocSpec(serverProfile, existingDocs.get(apiDocName), apiDoc); + break; + case create: + logger.info("API Doc \"" + apiDocName + + "\" already exists. Skipping."); + break; + case delete: + logger.info("API Doc \"" + apiDocName + + "\" already exists. Deleting."); + deleteAPIDoc(serverProfile, existingDocs.get(apiDocName)); + break; + case sync: + logger.info("API Doc \"" + apiDocName + + "\" already exists. Deleting and recreating."); + deleteAPIDoc(serverProfile, existingDocs.get(apiDocName)); + logger.info("Creating API Doc - " + apiDoc); + String apiDocId = createAPIDoc(serverProfile, apiDoc); + createAPIDocSpec(serverProfile, apiDocId, apiDoc); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info("Creating API Doc - " + apiDocName); + String apiDocId = createAPIDoc(serverProfile, apiDoc); + createAPIDocSpec(serverProfile, apiDocId, apiDoc); + break; + case delete: + logger.info("API Doc \"" + apiDocName + + "\" does not exist. Skipping."); + break; + } + } + } + + } catch (IOException e) { + throw new MojoFailureException("Apigee network call error " + + e.getMessage()); + } catch (RuntimeException e) { + throw e; + } + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + + if (super.isSkip()) { + logger.info("Skipping"); + return; + } + + Logger logger = LogManager.getLogger(APIDocsMojo.class); + + try { + + init(); + + if (buildOption == OPTIONS.none) { + logger.info("Skipping API Docs (default action)"); + return; + } + + if (serverProfile.getEnvironment() == null) { + throw new MojoExecutionException( + "Apigee environment not found in profile"); + } + + List apiDocs = getOrgConfig(logger, "apiDocs"); + if (apiDocs == null || apiDocs.size() == 0) { + logger.info("No API Docs found."); + return; + } + + doUpdate(apiDocs); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } + } + + /*************************************************************************** + * REST call wrappers + **/ + public static String createAPIDoc(ServerProfile profile, String apiDocPayload) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + try { + JsonElement jsonObj= new Gson().fromJson(apiDocPayload, JsonElement.class); + jsonObj.getAsJsonObject().remove("oasDocumentation"); + jsonObj.getAsJsonObject().remove("graphqlDocumentation"); + HttpResponse response = restUtil.createOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apidocs", + jsonObj.toString()); + String responseString = response.parseAsString(); + logger.info("Response " + response.getContentType() + "\n" + responseString); + if (response.isSuccessStatusCode()) { + logger.info("Create Success."); + JsonElement respObj= new Gson().fromJson(responseString, JsonElement.class); + String docId = respObj.getAsJsonObject().get("data").getAsJsonObject().get("id").getAsString(); + return docId; + } + } catch (HttpResponseException e) { + logger.error("API Doc create error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + return ""; + } + + public static String createAPIDocSpec(ServerProfile profile, String apiDocId, String apiDocPayload) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + try { + logger.info("API Documentation"); + String specPayload = updatePayloadWithSpecContents(apiDocPayload); + HttpResponse response = restUtil.patchOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apidocs/"+apiDocId+"/documentation", + specPayload); + + logger.info("Response " + response.getContentType() + "\n" + + response.parseAsString()); + if (response.isSuccessStatusCode()) + logger.info("Create Success."); + + } catch (HttpResponseException e) { + logger.error("API Doc create error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return ""; + } + + public static String updateAPIDoc(ServerProfile profile, + String apiDocId, + String apiDocPayload) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + try { + JsonElement jsonObj= new Gson().fromJson(apiDocPayload, JsonElement.class); + jsonObj.getAsJsonObject().remove("oasDocumentation"); + jsonObj.getAsJsonObject().remove("graphqlDocumentation"); + HttpResponse response = restUtil.updateOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apidocs", + apiDocId, + jsonObj.toString()); + logger.info("Response " + response.getContentType() + "\n" + + response.parseAsString()); + if (response.isSuccessStatusCode()) + logger.info("Update Success."); + + } catch (HttpResponseException e) { + logger.error("Target Server update error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + return ""; + } + + public static String deleteAPIDoc(ServerProfile profile, + String apiDocId) + throws IOException { + RestUtil restUtil = new RestUtil(profile); + HttpResponse response = restUtil.deleteOrgConfig(profile, + "sites/"+profile.getPortalSiteId()+"/apidocs", + apiDocId); + try { + + logger.info("Response " + response.getContentType() + "\n" + + response.parseAsString()); + if (response.isSuccessStatusCode()) + logger.info("Delete Success."); + + } catch (HttpResponseException e) { + logger.error("API Doc delete error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return ""; + } + + public static Map getAPIDocs(ServerProfile profile) + throws IOException { + Map apiDocMap = new HashMap(); + RestUtil restUtil = new RestUtil(profile); + HttpResponse response = restUtil.getOrgConfig(profile, "sites/"+profile.getPortalSiteId()+"/apidocs?pageSize=100"); + if(response == null) return apiDocMap; + JSONArray apiDocs = null; + try { + logger.debug("output " + response.getContentType()); + String payload = response.parseAsString(); + logger.debug(payload); + + JSONParser parser = new JSONParser(); + JSONObject obj = (JSONObject)parser.parse(payload); + apiDocs = (JSONArray)obj.get("data"); + for (int i = 0; apiDocs != null && i < apiDocs.size(); i++) { + JSONObject a = (JSONObject) apiDocs.get(i); + apiDocMap.put((String)a.get("title"), (String)a.get("id")); + } + } catch (ParseException pe){ + logger.error("Get API Doc parse error " + pe.getMessage()); + throw new IOException(pe.getMessage()); + } catch (HttpResponseException e) { + logger.error("Get API Doc error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return apiDocMap; + } + + public static Map getCategories(ServerProfile profile) + throws IOException { + Map categoryMap = new HashMap(); + RestUtil restUtil = new RestUtil(profile); + HttpResponse response = restUtil.getOrgConfig(profile, "sites/"+profile.getPortalSiteId()+"/apicategories"); + if(response == null) return categoryMap; + JSONArray categories = null; + try { + logger.debug("output " + response.getContentType()); + String payload = response.parseAsString(); + logger.debug(payload); + + JSONParser parser = new JSONParser(); + JSONObject obj = (JSONObject)parser.parse(payload); + categories = (JSONArray)obj.get("data"); + for (int i = 0; categories != null && i < categories.size(); i++) { + JSONObject a = (JSONObject) categories.get(i); + categoryMap.put((String)a.get("name"), (String)a.get("id")); + } + } catch (ParseException pe){ + logger.error("Get Categories parse error " + pe.getMessage()); + throw new IOException(pe.getMessage()); + } catch (HttpResponseException e) { + logger.error("Get Categories error " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return categoryMap; + } + + public static String updatePayloadWithSpecContents(String payload) throws IOException { + Gson gson = new Gson(); + try { + JsonElement jsonObj= gson.fromJson(payload, JsonElement.class); + JsonElement oas = jsonObj.getAsJsonObject().get("oasDocumentation"); + if(oas!=null) { + JsonObject spec = oas.getAsJsonObject().get("spec").getAsJsonObject(); + spec.addProperty("contents", fileToBase64String(spec.get("file").getAsString())); + spec.remove("file"); + return "{\"oasDocumentation\":"+oas.toString()+"}"; + } + JsonElement gql = jsonObj.getAsJsonObject().get("graphqlDocumentation"); + if(gql!=null) { + + JsonObject schema = gql.getAsJsonObject().get("schema").getAsJsonObject(); + schema.addProperty("contents", fileToBase64String(schema.get("file").getAsString())); + schema.remove("file"); + return "{\"graphqlDocumentation\":"+gql.toString()+"}"; + } + return null; + } catch (JsonParseException e) { + throw new IOException(e.getMessage()); + } + } + + + public static String fileToBase64String (String filePath) throws IOException { + byte[] byteData; + String base64String=null; + try { + byteData = Files.readAllBytes(Paths.get(filePath)); + base64String = Base64.getEncoder().encodeToString(byteData); + } catch (IOException e) { + throw new IOException(e.getMessage()); + } + return base64String; + } + +} + + + + diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/APIProductMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/APIProductMojo.java index 1e20ff2..2624af1 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/APIProductMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/APIProductMojo.java @@ -38,7 +38,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create API Product in Apigee EDGE + * Goal to create API Product in Apigee * scope: org * * @author madhan.sadasivam diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/AppMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/AppMojo.java index e2517cc..a19f37b 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/AppMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/AppMojo.java @@ -42,7 +42,7 @@ import com.google.gson.JsonParser; /** ¡¡ - * Goal to create Apps in Apigee EDGE + * Goal to create Apps in Apigee * scope: org * * @author madhan.sadasivam diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/DeveloperMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/DeveloperMojo.java index dcabebc..f0f44bd 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/DeveloperMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/DeveloperMojo.java @@ -38,7 +38,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create Developer in Apigee EDGE + * Goal to create Developer in Apigee * scope: org * * @author madhan.sadasivam diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/FlowHookMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/FlowHookMojo.java index 2a826a9..2ba99ed 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/FlowHookMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/FlowHookMojo.java @@ -37,7 +37,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to attach flow hooks in Apigee EDGE + * Goal to attach flow hooks in Apigee * scope: env * * @author saisaran.vaidyanathan diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/GatewayAbstractMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/GatewayAbstractMojo.java index 3bfe1e5..9f79fe4 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/GatewayAbstractMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/GatewayAbstractMojo.java @@ -277,6 +277,12 @@ public abstract class GatewayAbstractMojo extends AbstractMojo implements Contex */ private String serviceAccountFilePath; + /** + * apigee portal siteId + * @parameter property="apigee.portal.siteId" + */ + private String portalSiteId; + /** * Parameter to set for DeveloperApp to ignore API Product so that new credentials are not generated for updates (https://github.com/apigee/apigee-config-maven-plugin/issues/128) * @parameter property="apigee.app.ignoreAPIProducts" default-value="false" @@ -335,6 +341,7 @@ public ServerProfile getProfile() { this.buildProfile.setClientSecret(this.clientsecret); this.buildProfile.setKvmOverride(this.kvmOverride); this.buildProfile.setServiceAccountJSONFile(this.serviceAccountFilePath); + this.buildProfile.setPortalSiteId(this.portalSiteId); this.buildProfile.setIgnoreProductsForApp(this.ignoreProductsForApp); // process proxy for management api endpoint @@ -674,7 +681,7 @@ public void contextualize(Context context) throws ContextException { * Get the proxy configuration from the maven settings * * @param settings the maven settings - * @param host the host name of the apigee edge endpoint + * @param host the host name of the apigee endpoint * * @return proxy or null if none was configured or the host was non-proxied */ diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/KVMMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/KVMMojo.java index 4117467..50015d4 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/KVMMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/KVMMojo.java @@ -43,7 +43,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create KVM in Apigee EDGE. + * Goal to create KVM in Apigee. * scope: org, env, api * * @author madhan.sadasivam diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/ReferencesMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/ReferencesMojo.java index 1b1dff2..3b30890 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/ReferencesMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/ReferencesMojo.java @@ -37,7 +37,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create references in Apigee EDGE. + * Goal to create references in Apigee. * scope: env * * @author saisaran.vaidyanathan diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/ResourceFileMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/ResourceFileMojo.java index d461939..562b61a 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/ResourceFileMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/ResourceFileMojo.java @@ -37,7 +37,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create Resource Files in Apigee EDGE. + * Goal to create Resource Files in Apigee. * scope: env * * @author saisaran.vaidyanathan diff --git a/src/main/java/com/apigee/edge/config/mavenplugin/TargetServerMojo.java b/src/main/java/com/apigee/edge/config/mavenplugin/TargetServerMojo.java index ec6664f..d850099 100644 --- a/src/main/java/com/apigee/edge/config/mavenplugin/TargetServerMojo.java +++ b/src/main/java/com/apigee/edge/config/mavenplugin/TargetServerMojo.java @@ -37,7 +37,7 @@ import com.google.gson.JsonParseException; /** ¡¡ - * Goal to create target servers in Apigee EDGE. + * Goal to create target servers in Apigee. * scope: env * * @author madhan.sadasivam diff --git a/src/main/java/com/apigee/edge/config/rest/RestUtil.java b/src/main/java/com/apigee/edge/config/rest/RestUtil.java index 77939e0..b1fa3f2 100644 --- a/src/main/java/com/apigee/edge/config/rest/RestUtil.java +++ b/src/main/java/com/apigee/edge/config/rest/RestUtil.java @@ -509,6 +509,7 @@ public HttpResponse patchEnvConfig(ServerProfile profile, return response; } + /*************************************************************************** * Org Config - get, create, update @@ -754,6 +755,36 @@ public HttpResponse getOrgConfig(ServerProfile profile, return executeAPIGet(profile, importCmd); } + + + public HttpResponse patchOrgConfig(ServerProfile profile, + String resource, + String payload) + throws IOException { + + ByteArrayContent content = new ByteArrayContent("application/json", + payload.getBytes()); + + String importCmd = profile.getHostUrl() + "/" + + profile.getApi_version() + "/organizations/" + + profile.getOrg() + "/" + resource; + + HttpRequest restRequest = APACHE_REQUEST_FACTORY.buildRequest(HttpMethods.PATCH, new GenericUrl(importCmd), content); + restRequest.setReadTimeout(0); + + //logger.info(PrintUtil.formatRequest(restRequest)); + + HttpResponse response; + try { + //response = restRequest.execute(); + response = executeAPI(profile, restRequest); + } catch (HttpResponseException e) { + logger.error("Apigee call failed " + e.getMessage()); + throw new IOException(e.getMessage()); + } + + return response; + } /*************************************************************************** * API Config - get, create, update diff --git a/src/main/java/com/apigee/edge/config/utils/ConfigReader.java b/src/main/java/com/apigee/edge/config/utils/ConfigReader.java index a5a1b6c..dfc7fd8 100644 --- a/src/main/java/com/apigee/edge/config/utils/ConfigReader.java +++ b/src/main/java/com/apigee/edge/config/utils/ConfigReader.java @@ -105,7 +105,10 @@ public static List getOrgConfig(File configFile) if (configs == null) return null; out = new ArrayList(); - for (Object config: configs) { + for (Object config: configs) { + if(config instanceof String) + out.add(config); + else out.add(((JSONObject)config).toJSONString()); } } diff --git a/src/main/java/com/apigee/edge/config/utils/ConsolidatedConfigReader.java b/src/main/java/com/apigee/edge/config/utils/ConsolidatedConfigReader.java index 622900b..13ef82e 100644 --- a/src/main/java/com/apigee/edge/config/utils/ConsolidatedConfigReader.java +++ b/src/main/java/com/apigee/edge/config/utils/ConsolidatedConfigReader.java @@ -120,8 +120,11 @@ public static List getOrgConfig(File configFile, if (configs == null) return null; out = new ArrayList(); - for (Object config: configs) { - out.add(((JSONObject)config).toJSONString()); + for (Object config: configs) { + if(config instanceof String) + out.add(config); + else + out.add(((JSONObject)config).toJSONString()); } } catch(IOException ie) { diff --git a/src/main/java/com/apigee/edge/config/utils/ServerProfile.java b/src/main/java/com/apigee/edge/config/utils/ServerProfile.java index cdb49fe..6409fd5 100644 --- a/src/main/java/com/apigee/edge/config/utils/ServerProfile.java +++ b/src/main/java/com/apigee/edge/config/utils/ServerProfile.java @@ -56,6 +56,7 @@ public class ServerProfile { private String authType; // Mgmt API Auth Type oauth|basic private Boolean kvmOverride = true; //Override kvm only if true (used for update option) private String serviceAccountJSONFile; + private String portalSiteId; //Apigee Integrated Portal Site ID private Boolean ignoreProductsForApp = true; //Ignore API Product for App creation/updates so new credentials are not created @@ -98,6 +99,14 @@ public String getServiceAccountJSONFile() { public void setServiceAccountJSONFile(String serviceAccountJSONFile) { this.serviceAccountJSONFile = serviceAccountJSONFile; } + + public String getPortalSiteId() { + return portalSiteId; + } + + public void setPortalSiteId(String portalSiteId) { + this.portalSiteId = portalSiteId; + } public String getHostURL() { return hostURL;