From 048c853883fb482701bc3348036dfab8b1bb5a7b Mon Sep 17 00:00:00 2001 From: Sai Saran Vaidyanathan Date: Mon, 16 Sep 2024 15:37:42 -0700 Subject: [PATCH] feat: API, Spec, Version, Attributes, Dependencies --- README.md | 76 ++++ pom.xml | 5 + samples/README.md | 16 +- samples/apiVersions.json | 53 +++ samples/apis.json | 73 ++++ samples/attributes.json | 52 +++ samples/dependencies.json | 28 ++ samples/deployments.json | 87 ++++ samples/externalApis.json | 7 + samples/pom.xml | 37 +- samples/specs.json | 30 ++ .../config/mavenplugin/ApiVersionsMojo.java | 389 ++++++++++++++++++ .../apihub/config/mavenplugin/ApisMojo.java | 379 +++++++++++++++++ .../config/mavenplugin/AttributesMojo.java | 11 +- .../config/mavenplugin/DependenciesMojo.java | 367 +++++++++++++++++ .../config/mavenplugin/DeploymentsMojo.java | 358 ++++++++++++++++ .../config/mavenplugin/ExternalApisMojo.java | 58 +-- .../apihub/config/mavenplugin/SpecsMojo.java | 366 ++++++++++++++++ .../config/utils/ApiHubClientSingleton.java | 60 ++- .../apihub/config/utils/FQDNHelper.java | 194 +++++++++ 20 files changed, 2580 insertions(+), 66 deletions(-) create mode 100644 samples/apiVersions.json create mode 100644 samples/apis.json create mode 100644 samples/dependencies.json create mode 100644 samples/deployments.json create mode 100644 samples/specs.json create mode 100644 src/main/java/com/apigee/apihub/config/mavenplugin/ApiVersionsMojo.java create mode 100644 src/main/java/com/apigee/apihub/config/mavenplugin/ApisMojo.java create mode 100644 src/main/java/com/apigee/apihub/config/mavenplugin/DependenciesMojo.java create mode 100644 src/main/java/com/apigee/apihub/config/mavenplugin/DeploymentsMojo.java create mode 100644 src/main/java/com/apigee/apihub/config/mavenplugin/SpecsMojo.java create mode 100644 src/main/java/com/apigee/apihub/config/utils/FQDNHelper.java diff --git a/README.md b/README.md index 1f5654e..b685502 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,84 @@ The code is distributed under the Apache License 2.0. The [samples folder](./samples) provides a README with Getting Started steps and commands to hit the ground quickly. +## Prerequisites +You will need the following to run the samples: +- Apigee Edge developer account (in an Apigee hybrid org) +- [Java SDK >= 8](http://www.oracle.com/technetwork/java/javase/downloads/index.html) +- [Maven 3.x](https://maven.apache.org/) + +## Plugin Usage + +To use the plugin, add the following dependency to your pom + +```xml + + com.apigee.apihub.config + apigee-apihub-maven-plugin + 1.x.x + +``` + +### Command + +``` +mvn install -P -Dapigee.apihub.config.dir=$path -Dapigee.apihub.config.options=$option +``` + +#### Options + +``` +-P + Pick a profile in the parent pom.xml (shared-pom.xml in the example). + Apigee org and env information comes from the profile. + +-Dapigee.apihub.config.options + none - No action (default) + create - Create when not found. Pre-existing config is NOT updated even if it is different. + update - Update when found; create when not found + delete - Delete when found + sync - Delete and recreate. + +-Dapigee.apihub.config.dir + path to the directory containing the configuration +``` + +#### Individual goals + +To execute individual goals, you can use the prefix `apigee-apihub:`, for example `apigee-apihub:attributes` + +The list of goals available are: +- apis +- apiversions +- specs +- attributes +- dependencies +- externalapis +- deployments + +An example to just configure attributes will look like + +``` +mvn apigee-apihub:attributes -Pdev -Dapigee.apihub.config.options=create -Dapigee.apihub.config.dir=./config +``` + +**NOTE:** The config files must be in a single directory and should match the below naming conventions: + +| Goal | File name | +| -------- | ------- | +| apis | apis.json | +| apiversions | apiVersions.json | +| specs | specs.json | +| attributes | attributes.json | +| externalapis | externalApis.json | +| dependencies | dependencies.json | +| deployments | deployments.json | + ## Support * Please send feature requests using [issues](https://github.com/apigee/apigee-apihub-maven-plugin/issues) * Post a question in [Apigee community](https://community.apigee.com/index.html) * Create an [issue](https://github.com/apigee/apigee-apihub-maven-plugin/issues/new) + +## Disclaimer +This is not an officially supported Google product. diff --git a/pom.xml b/pom.xml index 73f872f..3966134 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,11 @@ gson 2.8.9 + + com.jayway.jsonpath + json-path + 2.9.0 + org.apache.logging.log4j log4j-api diff --git a/samples/README.md b/samples/README.md index b3ce399..18a4cb1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -8,17 +8,17 @@ mvn install -P{profile} -DprojectId=${project} -Dfile={path} -P Pick a profile in the pom.xml. - Apigee APi Hub location, config file, option are picked from the profile. + Apigee API Hub location, config directory, option are picked from the profile. -Dapigee.apihub.config.options none - No action (default) - create - Creates the API in the Apigee API Hub - update - Updates the API in the Apigee API Hub - delete - Deletes the API in the Apigee API Hub - sync - executes delete and update options mentioned above + create - Create when not found. Pre-existing config is NOT updated even if it is different. + update - Update when found; create when not found + delete - Delete when found + sync - Delete and recreate. - -Dfile - path to the service account key file that has the appropriate Apigee API Hub permissions + -Dapigee.apihub.config.dir + path to the directory containing the configuration -Dbearer access token. Service Account file takes precedence @@ -34,7 +34,7 @@ mvn install -P{profile} -DprojectId=${project} -Dfile={path} ## API Configuration -- Check out the samples that includes the structure of the API, specs, deployment and artifact objects needed to push an API to the API Hub +- Check out the samples that includes the structure of the API, specs, deployment and other config objects needed to push an API to the API Hub ### Basic Implementation diff --git a/samples/apiVersions.json b/samples/apiVersions.json new file mode 100644 index 0000000..d389f31 --- /dev/null +++ b/samples/apiVersions.json @@ -0,0 +1,53 @@ +[ + { + "name":"api1/versions/version-1", + "displayName":"version-1", + "description":"version-1", + "documentation":{ + "externalUri":"https://api-docs.example.com" + }, + "deployments":[ + "deployment1" + ], + "lifecycle":{ + "attribute":"system-lifecycle", + "enumValues":{ + "values":[ + { + "id":"concept" + } + ] + } + }, + "attributes":{ + "sample-attribute1":{ + "enumValues":{ + "values":[ + { + "id":"foo0" + } + ] + } + } + }, + "selectedDeployment":"deployment1" + }, + { + "name":"api1/versions/version-2", + "displayName":"version-2", + "description":"version-2", + "documentation":{ + "externalUri":"https://api-docs.example.com" + }, + "lifecycle":{ + "attribute":"system-lifecycle", + "enumValues":{ + "values":[ + { + "id":"design" + } + ] + } + } + } +] \ No newline at end of file diff --git a/samples/apis.json b/samples/apis.json new file mode 100644 index 0000000..15b30e8 --- /dev/null +++ b/samples/apis.json @@ -0,0 +1,73 @@ +[ + { + "name":"api1", + "displayName":"API 1", + "description":"API 1", + "documentation":{ + "externalUri":"https://api-docs.example.com" + }, + "owner":{ + "displayName":"API Owner", + "email":"api1-owner@example.com" + }, + "targetUser":{ + "attribute":"system-target-user", + "enumValues":{ + "values":[ + { + "id":"team" + } + ] + } + }, + "team":{ + "attribute":"system-team", + "enumValues":{ + "values":[ + { + "id":"example-team" + } + ] + } + }, + "businessUnit":{ + "attribute":"system-business-unit", + "enumValues":{ + "values":[ + { + "id":"example-business-unit" + } + ] + } + }, + "maturityLevel":{ + "attribute":"system-maturity-level", + "enumValues":{ + "values":[ + { + "id":"level-1" + } + ] + } + }, + "apiStyle":{ + "attribute":"system-api-style", + "enumValues":{ + "values":[ + { + "id":"rest" + } + ] + } + }, + "attributes":{ + "sample-attribute10":{ + "stringValues":{ + "values":[ + "Test" + ] + } + } + } + } +] \ No newline at end of file diff --git a/samples/attributes.json b/samples/attributes.json index bee81b8..c5b65d5 100644 --- a/samples/attributes.json +++ b/samples/attributes.json @@ -58,5 +58,57 @@ "scope": "EXTERNAL_API", "dataType": "STRING", "cardinality": 1 + }, + { + "name": "sample-attribute7", + "displayName": "sample-attribute7", + "description": "Sample Attribute 7", + "scope": "EXTERNAL_API", + "dataType": "STRING", + "cardinality": 1 + }, + { + "name": "sample-attribute8", + "displayName": "sample-attribute8", + "description": "Sample Attribute 8", + "scope": "DEPENDENCY", + "dataType": "STRING", + "cardinality": 1 + }, + { + "name": "sample-attribute9", + "displayName": "sample-attribute9", + "description": "Sample Attribute 9", + "scope": "DEPENDENCY", + "dataType": "STRING", + "cardinality": 1 + }, + { + "name": "sample-attribute10", + "displayName": "sample-attribute10", + "description": "Sample Attribute 10", + "scope": "API", + "dataType": "STRING", + "cardinality": 1 + }, + { + "name": "sample-attribute11", + "displayName": "sample-attribute11", + "description": "Sample Attribute 11", + "scope": "EXTERNAL_API", + "dataType": "ENUM", + "allowedValues": [ + { + "id": "foo0", + "displayName": "foo", + "description": "bar" + }, + { + "id": "url0", + "displayName": "url", + "description": "https://google.com" + } + ], + "cardinality": 1 } ] \ No newline at end of file diff --git a/samples/dependencies.json b/samples/dependencies.json new file mode 100644 index 0000000..cbda6ae --- /dev/null +++ b/samples/dependencies.json @@ -0,0 +1,28 @@ +[ + { + "name":"dependency1", + "description": "Dependency 1", + "consumer":{ + "externalApiResourceName":"externalApis/externalApi1" + }, + "supplier":{ + "externalApiResourceName":"externalApis/externalApi2" + }, + "attributes":{ + "sample-attribute8":{ + "stringValues":{ + "values":[ + "dependency1" + ] + } + }, + "sample-attribute9":{ + "stringValues":{ + "values":[ + "dependency2" + ] + } + } + } + } +] \ No newline at end of file diff --git a/samples/deployments.json b/samples/deployments.json new file mode 100644 index 0000000..bc722f4 --- /dev/null +++ b/samples/deployments.json @@ -0,0 +1,87 @@ +[ + { + "name":"deployment1", + "displayName":"Deployment 1", + "description":"Deployment 1", + "documentation":{ + "externalUri":"https://api-test.docs.example.com" + }, + "deploymentType": { + "attribute": "system-deployment-type", + "enumValues": { + "values": [ + { + "id": "apigee-hybrid" + } + ] + } + }, + "resourceUri":"https://api-test.example.com", + "endpoints":[ + "https://foo.com", + "https://bar.com" + ], + "slo": { + "attribute": "system-slo", + "enumValues": { + "values": [ + { + "id":"99-95" + } + ] + } + }, + "environment": { + "attribute": "system-environment", + "enumValues": { + "values": [ + { + "id":"test" + } + ] + } + }, + "attributes":{ + "sample-attribute3":{ + "stringValues":{ + "values":[ + "Test" + ] + } + } + } + }, + { + "name":"deployment2", + "displayName":"Deployment 2", + "description":"Deployment 2", + "documentation":{ + "externalUri":"https://api-staging.docs.example.com" + }, + "deploymentType": { + "attribute": "system-deployment-type", + "enumValues": { + "values": [ + { + "id": "apigee" + } + ] + } + }, + "environment": { + "attribute": "system-environment", + "enumValues": { + "values": [ + { + "id":"staging" + } + ] + } + }, + "resourceUri":"https://api-staging.example.com", + "endpoints":[ + "https://foo.com", + "https://bar.com" + ] + } +] \ No newline at end of file diff --git a/samples/externalApis.json b/samples/externalApis.json index 2abee5b..b0b5c3e 100644 --- a/samples/externalApis.json +++ b/samples/externalApis.json @@ -19,6 +19,13 @@ "extValue" ] } + }, + "sample-attribute7":{ + "stringValues":{ + "values":[ + "extValue" + ] + } } } }, diff --git a/samples/pom.xml b/samples/pom.xml index aa05ae0..5984229 100755 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -58,6 +58,41 @@ externalapis + + apigee-apihub-dependencies + install + + dependencies + + + + apigee-apihub-deployments + install + + deployments + + + + apigee-apihub-apis + install + + apis + + + + apigee-apihub-apiversions + install + + apiversions + + + + apigee-apihub-specs + install + + specs + + @@ -76,4 +111,4 @@ - + \ No newline at end of file diff --git a/samples/specs.json b/samples/specs.json new file mode 100644 index 0000000..c618540 --- /dev/null +++ b/samples/specs.json @@ -0,0 +1,30 @@ +[ + { + "name":"api1/versions/version-1/specs/spec1", + "displayName":"API Spec 1", + "specType":{ + "attribute":"system-spec-type", + "enumValues":{ + "values":[ + { + "id":"openapi" + } + ] + } + }, + "contents":{ + "contents": "LS0tCnN3YWdnZXI6ICIyLjAiCmluZm86CiAgZGVzY3JpcHRpb246ICJUaGlzIGlzIGEgc2FtcGxlIHNlcnZlciBQZXRzdG9yZSBzZXJ2ZXIuICBZb3UgY2FuIGZpbmQgb3V0IG1vcmUgYWJvdXRcCiAgICBcIFN3YWdnZXIgYXQgW2h0dHA6Ly9zd2FnZ2VyLmlvXShodHRwOi8vc3dhZ2dlci5pbykgb3Igb24gW2lyYy5mcmVlbm9kZS5uZXQsICNzd2FnZ2VyXShodHRwOi8vc3dhZ2dlci5pby9pcmMvKS5cCiAgICBcICBGb3IgdGhpcyBzYW1wbGUsIHlvdSBjYW4gdXNlIHRoZSBhcGkga2V5IGBzcGVjaWFsLWtleWAgdG8gdGVzdCB0aGUgYXV0aG9yaXphdGlvblwKICAgIFwgZmlsdGVycy4iCiAgdmVyc2lvbjogIjEuMC4wIgogIHRpdGxlOiAiU3dhZ2dlciBQZXRzdG9yZSIKICB0ZXJtc09mU2VydmljZTogImh0dHA6Ly9zd2FnZ2VyLmlvL3Rlcm1zLyIKICBjb250YWN0OgogICAgZW1haWw6ICJhcGl0ZWFtQHN3YWdnZXIuaW8iCiAgbGljZW5zZToKICAgIG5hbWU6ICJBcGFjaGUgMi4wIgogICAgdXJsOiAiaHR0cDovL3d3dy5hcGFjaGUub3JnL2xpY2Vuc2VzL0xJQ0VOU0UtMi4wLmh0bWwiCmhvc3Q6ICJwZXRzdG9yZS5zd2FnZ2VyLmlvIgpiYXNlUGF0aDogIi92MiIKdGFnczoKLSBuYW1lOiAicGV0IgogIGRlc2NyaXB0aW9uOiAiRXZlcnl0aGluZyBhYm91dCB5b3VyIFBldHMiCiAgZXh0ZXJuYWxEb2NzOgogICAgZGVzY3JpcHRpb246ICJGaW5kIG91dCBtb3JlIgogICAgdXJsOiAiaHR0cDovL3N3YWdnZXIuaW8iCi0gbmFtZTogInN0b3JlIgogIGRlc2NyaXB0aW9uOiAiQWNjZXNzIHRvIFBldHN0b3JlIG9yZGVycyIKLSBuYW1lOiAidXNlciIKICBkZXNjcmlwdGlvbjogIk9wZXJhdGlvbnMgYWJvdXQgdXNlciIKICBleHRlcm5hbERvY3M6CiAgICBkZXNjcmlwdGlvbjogIkZpbmQgb3V0IG1vcmUgYWJvdXQgb3VyIHN0b3JlIgogICAgdXJsOiAiaHR0cDovL3N3YWdnZXIuaW8iCnNjaGVtZXM6Ci0gImh0dHAiCnBhdGhzOgogIC9wZXQ6CiAgICBwb3N0OgogICAgICB0YWdzOgogICAgICAtICJwZXQiCiAgICAgIHN1bW1hcnk6ICJBZGQgYSBuZXcgcGV0IHRvIHRoZSBzdG9yZSIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAiYWRkUGV0IgogICAgICBjb25zdW1lczoKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIGluOiAiYm9keSIKICAgICAgICBuYW1lOiAiYm9keSIKICAgICAgICBkZXNjcmlwdGlvbjogIlBldCBvYmplY3QgdGhhdCBuZWVkcyB0byBiZSBhZGRlZCB0byB0aGUgc3RvcmUiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICBzY2hlbWE6CiAgICAgICAgICAkcmVmOiAiIy9kZWZpbml0aW9ucy9QZXQiCiAgICAgIHJlc3BvbnNlczoKICAgICAgICA0MDU6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgaW5wdXQiCiAgICAgIHNlY3VyaXR5OgogICAgICAtIHBldHN0b3JlX2F1dGg6CiAgICAgICAgLSAid3JpdGU6cGV0cyIKICAgICAgICAtICJyZWFkOnBldHMiCiAgICBwdXQ6CiAgICAgIHRhZ3M6CiAgICAgIC0gInBldCIKICAgICAgc3VtbWFyeTogIlVwZGF0ZSBhbiBleGlzdGluZyBwZXQiCiAgICAgIGRlc2NyaXB0aW9uOiAiIgogICAgICBvcGVyYXRpb25JZDogInVwZGF0ZVBldCIKICAgICAgY29uc3VtZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL2pzb24iCiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBpbjogImJvZHkiCiAgICAgICAgbmFtZTogImJvZHkiCiAgICAgICAgZGVzY3JpcHRpb246ICJQZXQgb2JqZWN0IHRoYXQgbmVlZHMgdG8gYmUgYWRkZWQgdG8gdGhlIHN0b3JlIgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgc2NoZW1hOgogICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvUGV0IgogICAgICByZXNwb25zZXM6CiAgICAgICAgNDAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJJbnZhbGlkIElEIHN1cHBsaWVkIgogICAgICAgIDQwNDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiUGV0IG5vdCBmb3VuZCIKICAgICAgICA0MDU6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIlZhbGlkYXRpb24gZXhjZXB0aW9uIgogICAgICBzZWN1cml0eToKICAgICAgLSBwZXRzdG9yZV9hdXRoOgogICAgICAgIC0gIndyaXRlOnBldHMiCiAgICAgICAgLSAicmVhZDpwZXRzIgogIC9wZXQvZmluZEJ5U3RhdHVzOgogICAgZ2V0OgogICAgICB0YWdzOgogICAgICAtICJwZXQiCiAgICAgIHN1bW1hcnk6ICJGaW5kcyBQZXRzIGJ5IHN0YXR1cyIKICAgICAgZGVzY3JpcHRpb246ICJNdWx0aXBsZSBzdGF0dXMgdmFsdWVzIGNhbiBiZSBwcm92aWRlZCB3aXRoIGNvbW1hIHNlcGFyYXRlZCBzdHJpbmdzIgogICAgICBvcGVyYXRpb25JZDogImZpbmRQZXRzQnlTdGF0dXMiCiAgICAgIHByb2R1Y2VzOgogICAgICAtICJhcHBsaWNhdGlvbi94bWwiCiAgICAgIC0gImFwcGxpY2F0aW9uL2pzb24iCiAgICAgIHBhcmFtZXRlcnM6CiAgICAgIC0gbmFtZTogInN0YXR1cyIKICAgICAgICBpbjogInF1ZXJ5IgogICAgICAgIGRlc2NyaXB0aW9uOiAiU3RhdHVzIHZhbHVlcyB0aGF0IG5lZWQgdG8gYmUgY29uc2lkZXJlZCBmb3IgZmlsdGVyIgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogImFycmF5IgogICAgICAgIGl0ZW1zOgogICAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgICAgIGVudW06CiAgICAgICAgICAtICJhdmFpbGFibGUiCiAgICAgICAgICAtICJwZW5kaW5nIgogICAgICAgICAgLSAic29sZCIKICAgICAgICAgIGRlZmF1bHQ6ICJhdmFpbGFibGUiCiAgICAgICAgY29sbGVjdGlvbkZvcm1hdDogIm11bHRpIgogICAgICByZXNwb25zZXM6CiAgICAgICAgMjAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJzdWNjZXNzZnVsIG9wZXJhdGlvbiIKICAgICAgICAgIHNjaGVtYToKICAgICAgICAgICAgdHlwZTogImFycmF5IgogICAgICAgICAgICBpdGVtczoKICAgICAgICAgICAgICAkcmVmOiAiIy9kZWZpbml0aW9ucy9QZXQiCiAgICAgICAgNDAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJJbnZhbGlkIHN0YXR1cyB2YWx1ZSIKICAgICAgc2VjdXJpdHk6CiAgICAgIC0gcGV0c3RvcmVfYXV0aDoKICAgICAgICAtICJ3cml0ZTpwZXRzIgogICAgICAgIC0gInJlYWQ6cGV0cyIKICAvcGV0L2ZpbmRCeVRhZ3M6CiAgICBnZXQ6CiAgICAgIHRhZ3M6CiAgICAgIC0gInBldCIKICAgICAgc3VtbWFyeTogIkZpbmRzIFBldHMgYnkgdGFncyIKICAgICAgZGVzY3JpcHRpb246ICJNdWxpcGxlIHRhZ3MgY2FuIGJlIHByb3ZpZGVkIHdpdGggY29tbWEgc2VwYXJhdGVkIHN0cmluZ3MuIFVzZVwKICAgICAgICBcIHRhZzEsIHRhZzIsIHRhZzMgZm9yIHRlc3RpbmcuIgogICAgICBvcGVyYXRpb25JZDogImZpbmRQZXRzQnlUYWdzIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIG5hbWU6ICJ0YWdzIgogICAgICAgIGluOiAicXVlcnkiCiAgICAgICAgZGVzY3JpcHRpb246ICJUYWdzIHRvIGZpbHRlciBieSIKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIHR5cGU6ICJhcnJheSIKICAgICAgICBpdGVtczoKICAgICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICAgICAgY29sbGVjdGlvbkZvcm1hdDogIm11bHRpIgogICAgICByZXNwb25zZXM6CiAgICAgICAgMjAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJzdWNjZXNzZnVsIG9wZXJhdGlvbiIKICAgICAgICAgIHNjaGVtYToKICAgICAgICAgICAgdHlwZTogImFycmF5IgogICAgICAgICAgICBpdGVtczoKICAgICAgICAgICAgICAkcmVmOiAiIy9kZWZpbml0aW9ucy9QZXQiCiAgICAgICAgNDAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJJbnZhbGlkIHRhZyB2YWx1ZSIKICAgICAgc2VjdXJpdHk6CiAgICAgIC0gcGV0c3RvcmVfYXV0aDoKICAgICAgICAtICJ3cml0ZTpwZXRzIgogICAgICAgIC0gInJlYWQ6cGV0cyIKICAgICAgZGVwcmVjYXRlZDogdHJ1ZQogIC9wZXQve3BldElkfToKICAgIGdldDoKICAgICAgdGFnczoKICAgICAgLSAicGV0IgogICAgICBzdW1tYXJ5OiAiRmluZCBwZXQgYnkgSUQiCiAgICAgIGRlc2NyaXB0aW9uOiAiUmV0dXJucyBhIHNpbmdsZSBwZXQiCiAgICAgIG9wZXJhdGlvbklkOiAiZ2V0UGV0QnlJZCIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBuYW1lOiAicGV0SWQiCiAgICAgICAgaW46ICJwYXRoIgogICAgICAgIGRlc2NyaXB0aW9uOiAiSUQgb2YgcGV0IHRvIHJldHVybiIKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICByZXNwb25zZXM6CiAgICAgICAgMjAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJzdWNjZXNzZnVsIG9wZXJhdGlvbiIKICAgICAgICAgIHNjaGVtYToKICAgICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvUGV0IgogICAgICAgIDQwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiSW52YWxpZCBJRCBzdXBwbGllZCIKICAgICAgICA0MDQ6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIlBldCBub3QgZm91bmQiCiAgICAgIHNlY3VyaXR5OgogICAgICAtIGFwaV9rZXk6IFtdCiAgICBwb3N0OgogICAgICB0YWdzOgogICAgICAtICJwZXQiCiAgICAgIHN1bW1hcnk6ICJVcGRhdGVzIGEgcGV0IGluIHRoZSBzdG9yZSB3aXRoIGZvcm0gZGF0YSIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAidXBkYXRlUGV0V2l0aEZvcm0iCiAgICAgIGNvbnN1bWVzOgogICAgICAtICJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQiCiAgICAgIHByb2R1Y2VzOgogICAgICAtICJhcHBsaWNhdGlvbi94bWwiCiAgICAgIC0gImFwcGxpY2F0aW9uL2pzb24iCiAgICAgIHBhcmFtZXRlcnM6CiAgICAgIC0gbmFtZTogInBldElkIgogICAgICAgIGluOiAicGF0aCIKICAgICAgICBkZXNjcmlwdGlvbjogIklEIG9mIHBldCB0aGF0IG5lZWRzIHRvIGJlIHVwZGF0ZWQiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiAiaW50ZWdlciIKICAgICAgICBmb3JtYXQ6ICJpbnQ2NCIKICAgICAgLSBuYW1lOiAibmFtZSIKICAgICAgICBpbjogImZvcm1EYXRhIgogICAgICAgIGRlc2NyaXB0aW9uOiAiVXBkYXRlZCBuYW1lIG9mIHRoZSBwZXQiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgLSBuYW1lOiAic3RhdHVzIgogICAgICAgIGluOiAiZm9ybURhdGEiCiAgICAgICAgZGVzY3JpcHRpb246ICJVcGRhdGVkIHN0YXR1cyBvZiB0aGUgcGV0IgogICAgICAgIHJlcXVpcmVkOiBmYWxzZQogICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICAgIHJlc3BvbnNlczoKICAgICAgICA0MDU6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgaW5wdXQiCiAgICAgIHNlY3VyaXR5OgogICAgICAtIHBldHN0b3JlX2F1dGg6CiAgICAgICAgLSAid3JpdGU6cGV0cyIKICAgICAgICAtICJyZWFkOnBldHMiCiAgICBkZWxldGU6CiAgICAgIHRhZ3M6CiAgICAgIC0gInBldCIKICAgICAgc3VtbWFyeTogIkRlbGV0ZXMgYSBwZXQiCiAgICAgIGRlc2NyaXB0aW9uOiAiIgogICAgICBvcGVyYXRpb25JZDogImRlbGV0ZVBldCIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBuYW1lOiAiYXBpX2tleSIKICAgICAgICBpbjogImhlYWRlciIKICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICAtIG5hbWU6ICJwZXRJZCIKICAgICAgICBpbjogInBhdGgiCiAgICAgICAgZGVzY3JpcHRpb246ICJQZXQgaWQgdG8gZGVsZXRlIgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogImludGVnZXIiCiAgICAgICAgZm9ybWF0OiAiaW50NjQiCiAgICAgIHJlc3BvbnNlczoKICAgICAgICA0MDA6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgSUQgc3VwcGxpZWQiCiAgICAgICAgNDA0OgogICAgICAgICAgZGVzY3JpcHRpb246ICJQZXQgbm90IGZvdW5kIgogICAgICBzZWN1cml0eToKICAgICAgLSBwZXRzdG9yZV9hdXRoOgogICAgICAgIC0gIndyaXRlOnBldHMiCiAgICAgICAgLSAicmVhZDpwZXRzIgogIC9wZXQve3BldElkfS91cGxvYWRJbWFnZToKICAgIHBvc3Q6CiAgICAgIHRhZ3M6CiAgICAgIC0gInBldCIKICAgICAgc3VtbWFyeTogInVwbG9hZHMgYW4gaW1hZ2UiCiAgICAgIGRlc2NyaXB0aW9uOiAiIgogICAgICBvcGVyYXRpb25JZDogInVwbG9hZEZpbGUiCiAgICAgIGNvbnN1bWVzOgogICAgICAtICJtdWx0aXBhcnQvZm9ybS1kYXRhIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBuYW1lOiAicGV0SWQiCiAgICAgICAgaW46ICJwYXRoIgogICAgICAgIGRlc2NyaXB0aW9uOiAiSUQgb2YgcGV0IHRvIHVwZGF0ZSIKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICAtIG5hbWU6ICJhZGRpdGlvbmFsTWV0YWRhdGEiCiAgICAgICAgaW46ICJmb3JtRGF0YSIKICAgICAgICBkZXNjcmlwdGlvbjogIkFkZGl0aW9uYWwgZGF0YSB0byBwYXNzIHRvIHNlcnZlciIKICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICAtIG5hbWU6ICJmaWxlIgogICAgICAgIGluOiAiZm9ybURhdGEiCiAgICAgICAgZGVzY3JpcHRpb246ICJmaWxlIHRvIHVwbG9hZCIKICAgICAgICByZXF1aXJlZDogZmFsc2UKICAgICAgICB0eXBlOiAiZmlsZSIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIDIwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAic3VjY2Vzc2Z1bCBvcGVyYXRpb24iCiAgICAgICAgICBzY2hlbWE6CiAgICAgICAgICAgICRyZWY6ICIjL2RlZmluaXRpb25zL0FwaVJlc3BvbnNlIgogICAgICBzZWN1cml0eToKICAgICAgLSBwZXRzdG9yZV9hdXRoOgogICAgICAgIC0gIndyaXRlOnBldHMiCiAgICAgICAgLSAicmVhZDpwZXRzIgogIC9zdG9yZS9pbnZlbnRvcnk6CiAgICBnZXQ6CiAgICAgIHRhZ3M6CiAgICAgIC0gInN0b3JlIgogICAgICBzdW1tYXJ5OiAiUmV0dXJucyBwZXQgaW52ZW50b3JpZXMgYnkgc3RhdHVzIgogICAgICBkZXNjcmlwdGlvbjogIlJldHVybnMgYSBtYXAgb2Ygc3RhdHVzIGNvZGVzIHRvIHF1YW50aXRpZXMiCiAgICAgIG9wZXJhdGlvbklkOiAiZ2V0SW52ZW50b3J5IgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczogW10KICAgICAgcmVzcG9uc2VzOgogICAgICAgIDIwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAic3VjY2Vzc2Z1bCBvcGVyYXRpb24iCiAgICAgICAgICBzY2hlbWE6CiAgICAgICAgICAgIHR5cGU6ICJvYmplY3QiCiAgICAgICAgICAgIGFkZGl0aW9uYWxQcm9wZXJ0aWVzOgogICAgICAgICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgICAgICAgIGZvcm1hdDogImludDMyIgogICAgICBzZWN1cml0eToKICAgICAgLSBhcGlfa2V5OiBbXQogIC9zdG9yZS9vcmRlcjoKICAgIHBvc3Q6CiAgICAgIHRhZ3M6CiAgICAgIC0gInN0b3JlIgogICAgICBzdW1tYXJ5OiAiUGxhY2UgYW4gb3JkZXIgZm9yIGEgcGV0IgogICAgICBkZXNjcmlwdGlvbjogIiIKICAgICAgb3BlcmF0aW9uSWQ6ICJwbGFjZU9yZGVyIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIGluOiAiYm9keSIKICAgICAgICBuYW1lOiAiYm9keSIKICAgICAgICBkZXNjcmlwdGlvbjogIm9yZGVyIHBsYWNlZCBmb3IgcHVyY2hhc2luZyB0aGUgcGV0IgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgc2NoZW1hOgogICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvT3JkZXIiCiAgICAgIHJlc3BvbnNlczoKICAgICAgICAyMDA6CiAgICAgICAgICBkZXNjcmlwdGlvbjogInN1Y2Nlc3NmdWwgb3BlcmF0aW9uIgogICAgICAgICAgc2NoZW1hOgogICAgICAgICAgICAkcmVmOiAiIy9kZWZpbml0aW9ucy9PcmRlciIKICAgICAgICA0MDA6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgT3JkZXIiCiAgL3N0b3JlL29yZGVyL3tvcmRlcklkfToKICAgIGdldDoKICAgICAgdGFnczoKICAgICAgLSAic3RvcmUiCiAgICAgIHN1bW1hcnk6ICJGaW5kIHB1cmNoYXNlIG9yZGVyIGJ5IElEIgogICAgICBkZXNjcmlwdGlvbjogIkZvciB2YWxpZCByZXNwb25zZSB0cnkgaW50ZWdlciBJRHMgd2l0aCB2YWx1ZSA+PSAxIGFuZCA8PSAxMC5cCiAgICAgICAgXCBPdGhlciB2YWx1ZXMgd2lsbCBnZW5lcmF0ZWQgZXhjZXB0aW9ucyIKICAgICAgb3BlcmF0aW9uSWQ6ICJnZXRPcmRlckJ5SWQiCiAgICAgIHByb2R1Y2VzOgogICAgICAtICJhcHBsaWNhdGlvbi94bWwiCiAgICAgIC0gImFwcGxpY2F0aW9uL2pzb24iCiAgICAgIHBhcmFtZXRlcnM6CiAgICAgIC0gbmFtZTogIm9yZGVySWQiCiAgICAgICAgaW46ICJwYXRoIgogICAgICAgIGRlc2NyaXB0aW9uOiAiSUQgb2YgcGV0IHRoYXQgbmVlZHMgdG8gYmUgZmV0Y2hlZCIKICAgICAgICByZXF1aXJlZDogdHJ1ZQogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIG1heGltdW06IDEwLjAKICAgICAgICBtaW5pbXVtOiAxLjAKICAgICAgICBmb3JtYXQ6ICJpbnQ2NCIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIDIwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAic3VjY2Vzc2Z1bCBvcGVyYXRpb24iCiAgICAgICAgICBzY2hlbWE6CiAgICAgICAgICAgICRyZWY6ICIjL2RlZmluaXRpb25zL09yZGVyIgogICAgICAgIDQwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiSW52YWxpZCBJRCBzdXBwbGllZCIKICAgICAgICA0MDQ6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIk9yZGVyIG5vdCBmb3VuZCIKICAgIGRlbGV0ZToKICAgICAgdGFnczoKICAgICAgLSAic3RvcmUiCiAgICAgIHN1bW1hcnk6ICJEZWxldGUgcHVyY2hhc2Ugb3JkZXIgYnkgSUQiCiAgICAgIGRlc2NyaXB0aW9uOiAiRm9yIHZhbGlkIHJlc3BvbnNlIHRyeSBpbnRlZ2VyIElEcyB3aXRoIHBvc2l0aXZlIGludGVnZXIgdmFsdWUuXAogICAgICAgIFwgTmVnYXRpdmUgb3Igbm9uLWludGVnZXIgdmFsdWVzIHdpbGwgZ2VuZXJhdGUgQVBJIGVycm9ycyIKICAgICAgb3BlcmF0aW9uSWQ6ICJkZWxldGVPcmRlciIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBuYW1lOiAib3JkZXJJZCIKICAgICAgICBpbjogInBhdGgiCiAgICAgICAgZGVzY3JpcHRpb246ICJJRCBvZiB0aGUgb3JkZXIgdGhhdCBuZWVkcyB0byBiZSBkZWxldGVkIgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogImludGVnZXIiCiAgICAgICAgbWluaW11bTogMS4wCiAgICAgICAgZm9ybWF0OiAiaW50NjQiCiAgICAgIHJlc3BvbnNlczoKICAgICAgICA0MDA6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgSUQgc3VwcGxpZWQiCiAgICAgICAgNDA0OgogICAgICAgICAgZGVzY3JpcHRpb246ICJPcmRlciBub3QgZm91bmQiCiAgL3VzZXI6CiAgICBwb3N0OgogICAgICB0YWdzOgogICAgICAtICJ1c2VyIgogICAgICBzdW1tYXJ5OiAiQ3JlYXRlIHVzZXIiCiAgICAgIGRlc2NyaXB0aW9uOiAiVGhpcyBjYW4gb25seSBiZSBkb25lIGJ5IHRoZSBsb2dnZWQgaW4gdXNlci4iCiAgICAgIG9wZXJhdGlvbklkOiAiY3JlYXRlVXNlciIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBpbjogImJvZHkiCiAgICAgICAgbmFtZTogImJvZHkiCiAgICAgICAgZGVzY3JpcHRpb246ICJDcmVhdGVkIHVzZXIgb2JqZWN0IgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgc2NoZW1hOgogICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvVXNlciIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIGRlZmF1bHQ6CiAgICAgICAgICBkZXNjcmlwdGlvbjogInN1Y2Nlc3NmdWwgb3BlcmF0aW9uIgogIC91c2VyL2NyZWF0ZVdpdGhBcnJheToKICAgIHBvc3Q6CiAgICAgIHRhZ3M6CiAgICAgIC0gInVzZXIiCiAgICAgIHN1bW1hcnk6ICJDcmVhdGVzIGxpc3Qgb2YgdXNlcnMgd2l0aCBnaXZlbiBpbnB1dCBhcnJheSIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAiY3JlYXRlVXNlcnNXaXRoQXJyYXlJbnB1dCIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczoKICAgICAgLSBpbjogImJvZHkiCiAgICAgICAgbmFtZTogImJvZHkiCiAgICAgICAgZGVzY3JpcHRpb246ICJMaXN0IG9mIHVzZXIgb2JqZWN0IgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgc2NoZW1hOgogICAgICAgICAgdHlwZTogImFycmF5IgogICAgICAgICAgaXRlbXM6CiAgICAgICAgICAgICRyZWY6ICIjL2RlZmluaXRpb25zL1VzZXIiCiAgICAgIHJlc3BvbnNlczoKICAgICAgICBkZWZhdWx0OgogICAgICAgICAgZGVzY3JpcHRpb246ICJzdWNjZXNzZnVsIG9wZXJhdGlvbiIKICAvdXNlci9jcmVhdGVXaXRoTGlzdDoKICAgIHBvc3Q6CiAgICAgIHRhZ3M6CiAgICAgIC0gInVzZXIiCiAgICAgIHN1bW1hcnk6ICJDcmVhdGVzIGxpc3Qgb2YgdXNlcnMgd2l0aCBnaXZlbiBpbnB1dCBhcnJheSIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAiY3JlYXRlVXNlcnNXaXRoTGlzdElucHV0IgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIGluOiAiYm9keSIKICAgICAgICBuYW1lOiAiYm9keSIKICAgICAgICBkZXNjcmlwdGlvbjogIkxpc3Qgb2YgdXNlciBvYmplY3QiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICBzY2hlbWE6CiAgICAgICAgICB0eXBlOiAiYXJyYXkiCiAgICAgICAgICBpdGVtczoKICAgICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvVXNlciIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIGRlZmF1bHQ6CiAgICAgICAgICBkZXNjcmlwdGlvbjogInN1Y2Nlc3NmdWwgb3BlcmF0aW9uIgogIC91c2VyL2xvZ2luOgogICAgZ2V0OgogICAgICB0YWdzOgogICAgICAtICJ1c2VyIgogICAgICBzdW1tYXJ5OiAiTG9ncyB1c2VyIGludG8gdGhlIHN5c3RlbSIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAibG9naW5Vc2VyIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIG5hbWU6ICJ1c2VybmFtZSIKICAgICAgICBpbjogInF1ZXJ5IgogICAgICAgIGRlc2NyaXB0aW9uOiAiVGhlIHVzZXIgbmFtZSBmb3IgbG9naW4iCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICAtIG5hbWU6ICJwYXNzd29yZCIKICAgICAgICBpbjogInF1ZXJ5IgogICAgICAgIGRlc2NyaXB0aW9uOiAiVGhlIHBhc3N3b3JkIGZvciBsb2dpbiBpbiBjbGVhciB0ZXh0IgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIDIwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAic3VjY2Vzc2Z1bCBvcGVyYXRpb24iCiAgICAgICAgICBzY2hlbWE6CiAgICAgICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICAgICAgICBoZWFkZXJzOgogICAgICAgICAgICBYLVJhdGUtTGltaXQ6CiAgICAgICAgICAgICAgdHlwZTogImludGVnZXIiCiAgICAgICAgICAgICAgZm9ybWF0OiAiaW50MzIiCiAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJjYWxscyBwZXIgaG91ciBhbGxvd2VkIGJ5IHRoZSB1c2VyIgogICAgICAgICAgICBYLUV4cGlyZXMtQWZ0ZXI6CiAgICAgICAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgICAgICAgICBmb3JtYXQ6ICJkYXRlLXRpbWUiCiAgICAgICAgICAgICAgZGVzY3JpcHRpb246ICJkYXRlIGluIFVUQyB3aGVuIHRva2VuIGV4cGlyZXMiCiAgICAgICAgNDAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJJbnZhbGlkIHVzZXJuYW1lL3Bhc3N3b3JkIHN1cHBsaWVkIgogIC91c2VyL2xvZ291dDoKICAgIGdldDoKICAgICAgdGFnczoKICAgICAgLSAidXNlciIKICAgICAgc3VtbWFyeTogIkxvZ3Mgb3V0IGN1cnJlbnQgbG9nZ2VkIGluIHVzZXIgc2Vzc2lvbiIKICAgICAgZGVzY3JpcHRpb246ICIiCiAgICAgIG9wZXJhdGlvbklkOiAibG9nb3V0VXNlciIKICAgICAgcHJvZHVjZXM6CiAgICAgIC0gImFwcGxpY2F0aW9uL3htbCIKICAgICAgLSAiYXBwbGljYXRpb24vanNvbiIKICAgICAgcGFyYW1ldGVyczogW10KICAgICAgcmVzcG9uc2VzOgogICAgICAgIGRlZmF1bHQ6CiAgICAgICAgICBkZXNjcmlwdGlvbjogInN1Y2Nlc3NmdWwgb3BlcmF0aW9uIgogIC91c2VyL3t1c2VybmFtZX06CiAgICBnZXQ6CiAgICAgIHRhZ3M6CiAgICAgIC0gInVzZXIiCiAgICAgIHN1bW1hcnk6ICJHZXQgdXNlciBieSB1c2VyIG5hbWUiCiAgICAgIGRlc2NyaXB0aW9uOiAiIgogICAgICBvcGVyYXRpb25JZDogImdldFVzZXJCeU5hbWUiCiAgICAgIHByb2R1Y2VzOgogICAgICAtICJhcHBsaWNhdGlvbi94bWwiCiAgICAgIC0gImFwcGxpY2F0aW9uL2pzb24iCiAgICAgIHBhcmFtZXRlcnM6CiAgICAgIC0gbmFtZTogInVzZXJuYW1lIgogICAgICAgIGluOiAicGF0aCIKICAgICAgICBkZXNjcmlwdGlvbjogIlRoZSBuYW1lIHRoYXQgbmVlZHMgdG8gYmUgZmV0Y2hlZC4gVXNlIHVzZXIxIGZvciB0ZXN0aW5nLiAiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICByZXNwb25zZXM6CiAgICAgICAgMjAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJzdWNjZXNzZnVsIG9wZXJhdGlvbiIKICAgICAgICAgIHNjaGVtYToKICAgICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvVXNlciIKICAgICAgICA0MDA6CiAgICAgICAgICBkZXNjcmlwdGlvbjogIkludmFsaWQgdXNlcm5hbWUgc3VwcGxpZWQiCiAgICAgICAgNDA0OgogICAgICAgICAgZGVzY3JpcHRpb246ICJVc2VyIG5vdCBmb3VuZCIKICAgIHB1dDoKICAgICAgdGFnczoKICAgICAgLSAidXNlciIKICAgICAgc3VtbWFyeTogIlVwZGF0ZWQgdXNlciIKICAgICAgZGVzY3JpcHRpb246ICJUaGlzIGNhbiBvbmx5IGJlIGRvbmUgYnkgdGhlIGxvZ2dlZCBpbiB1c2VyLiIKICAgICAgb3BlcmF0aW9uSWQ6ICJ1cGRhdGVVc2VyIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIG5hbWU6ICJ1c2VybmFtZSIKICAgICAgICBpbjogInBhdGgiCiAgICAgICAgZGVzY3JpcHRpb246ICJuYW1lIHRoYXQgbmVlZCB0byBiZSB1cGRhdGVkIgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgLSBpbjogImJvZHkiCiAgICAgICAgbmFtZTogImJvZHkiCiAgICAgICAgZGVzY3JpcHRpb246ICJVcGRhdGVkIHVzZXIgb2JqZWN0IgogICAgICAgIHJlcXVpcmVkOiB0cnVlCiAgICAgICAgc2NoZW1hOgogICAgICAgICAgJHJlZjogIiMvZGVmaW5pdGlvbnMvVXNlciIKICAgICAgcmVzcG9uc2VzOgogICAgICAgIDQwMDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiSW52YWxpZCB1c2VyIHN1cHBsaWVkIgogICAgICAgIDQwNDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiVXNlciBub3QgZm91bmQiCiAgICBkZWxldGU6CiAgICAgIHRhZ3M6CiAgICAgIC0gInVzZXIiCiAgICAgIHN1bW1hcnk6ICJEZWxldGUgdXNlciIKICAgICAgZGVzY3JpcHRpb246ICJUaGlzIGNhbiBvbmx5IGJlIGRvbmUgYnkgdGhlIGxvZ2dlZCBpbiB1c2VyLiIKICAgICAgb3BlcmF0aW9uSWQ6ICJkZWxldGVVc2VyIgogICAgICBwcm9kdWNlczoKICAgICAgLSAiYXBwbGljYXRpb24veG1sIgogICAgICAtICJhcHBsaWNhdGlvbi9qc29uIgogICAgICBwYXJhbWV0ZXJzOgogICAgICAtIG5hbWU6ICJ1c2VybmFtZSIKICAgICAgICBpbjogInBhdGgiCiAgICAgICAgZGVzY3JpcHRpb246ICJUaGUgbmFtZSB0aGF0IG5lZWRzIHRvIGJlIGRlbGV0ZWQiCiAgICAgICAgcmVxdWlyZWQ6IHRydWUKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICByZXNwb25zZXM6CiAgICAgICAgNDAwOgogICAgICAgICAgZGVzY3JpcHRpb246ICJJbnZhbGlkIHVzZXJuYW1lIHN1cHBsaWVkIgogICAgICAgIDQwNDoKICAgICAgICAgIGRlc2NyaXB0aW9uOiAiVXNlciBub3QgZm91bmQiCnNlY3VyaXR5RGVmaW5pdGlvbnM6CiAgcGV0c3RvcmVfYXV0aDoKICAgIHR5cGU6ICJvYXV0aDIiCiAgICBhdXRob3JpemF0aW9uVXJsOiAiaHR0cDovL3BldHN0b3JlLnN3YWdnZXIuaW8vb2F1dGgvZGlhbG9nIgogICAgZmxvdzogImltcGxpY2l0IgogICAgc2NvcGVzOgogICAgICB3cml0ZTpwZXRzOiAibW9kaWZ5IHBldHMgaW4geW91ciBhY2NvdW50IgogICAgICByZWFkOnBldHM6ICJyZWFkIHlvdXIgcGV0cyIKICBhcGlfa2V5OgogICAgdHlwZTogImFwaUtleSIKICAgIG5hbWU6ICJhcGlfa2V5IgogICAgaW46ICJoZWFkZXIiCmRlZmluaXRpb25zOgogIE9yZGVyOgogICAgdHlwZTogIm9iamVjdCIKICAgIHByb3BlcnRpZXM6CiAgICAgIGlkOgogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICBwZXRJZDoKICAgICAgICB0eXBlOiAiaW50ZWdlciIKICAgICAgICBmb3JtYXQ6ICJpbnQ2NCIKICAgICAgcXVhbnRpdHk6CiAgICAgICAgdHlwZTogImludGVnZXIiCiAgICAgICAgZm9ybWF0OiAiaW50MzIiCiAgICAgIHNoaXBEYXRlOgogICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICAgICAgZm9ybWF0OiAiZGF0ZS10aW1lIgogICAgICBzdGF0dXM6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgICBkZXNjcmlwdGlvbjogIk9yZGVyIFN0YXR1cyIKICAgICAgICBlbnVtOgogICAgICAgIC0gInBsYWNlZCIKICAgICAgICAtICJhcHByb3ZlZCIKICAgICAgICAtICJkZWxpdmVyZWQiCiAgICAgIGNvbXBsZXRlOgogICAgICAgIHR5cGU6ICJib29sZWFuIgogICAgICAgIGRlZmF1bHQ6IGZhbHNlCiAgICB4bWw6CiAgICAgIG5hbWU6ICJPcmRlciIKICBVc2VyOgogICAgdHlwZTogIm9iamVjdCIKICAgIHByb3BlcnRpZXM6CiAgICAgIGlkOgogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICB1c2VybmFtZToKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICBmaXJzdE5hbWU6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgbGFzdE5hbWU6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgZW1haWw6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgcGFzc3dvcmQ6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgcGhvbmU6CiAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgdXNlclN0YXR1czoKICAgICAgICB0eXBlOiAiaW50ZWdlciIKICAgICAgICBmb3JtYXQ6ICJpbnQzMiIKICAgICAgICBkZXNjcmlwdGlvbjogIlVzZXIgU3RhdHVzIgogICAgeG1sOgogICAgICBuYW1lOiAiVXNlciIKICBDYXRlZ29yeToKICAgIHR5cGU6ICJvYmplY3QiCiAgICBwcm9wZXJ0aWVzOgogICAgICBpZDoKICAgICAgICB0eXBlOiAiaW50ZWdlciIKICAgICAgICBmb3JtYXQ6ICJpbnQ2NCIKICAgICAgbmFtZToKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgeG1sOgogICAgICBuYW1lOiAiQ2F0ZWdvcnkiCiAgVGFnOgogICAgdHlwZTogIm9iamVjdCIKICAgIHByb3BlcnRpZXM6CiAgICAgIGlkOgogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICBuYW1lOgogICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICB4bWw6CiAgICAgIG5hbWU6ICJUYWciCiAgUGV0OgogICAgdHlwZTogIm9iamVjdCIKICAgIHJlcXVpcmVkOgogICAgLSAibmFtZSIKICAgIC0gInBob3RvVXJscyIKICAgIHByb3BlcnRpZXM6CiAgICAgIGlkOgogICAgICAgIHR5cGU6ICJpbnRlZ2VyIgogICAgICAgIGZvcm1hdDogImludDY0IgogICAgICBjYXRlZ29yeToKICAgICAgICAkcmVmOiAiIy9kZWZpbml0aW9ucy9DYXRlZ29yeSIKICAgICAgbmFtZToKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICAgIGV4YW1wbGU6ICJkb2dnaWUiCiAgICAgIHBob3RvVXJsczoKICAgICAgICB0eXBlOiAiYXJyYXkiCiAgICAgICAgeG1sOgogICAgICAgICAgbmFtZTogInBob3RvVXJsIgogICAgICAgICAgd3JhcHBlZDogdHJ1ZQogICAgICAgIGl0ZW1zOgogICAgICAgICAgdHlwZTogInN0cmluZyIKICAgICAgdGFnczoKICAgICAgICB0eXBlOiAiYXJyYXkiCiAgICAgICAgeG1sOgogICAgICAgICAgbmFtZTogInRhZyIKICAgICAgICAgIHdyYXBwZWQ6IHRydWUKICAgICAgICBpdGVtczoKICAgICAgICAgICRyZWY6ICIjL2RlZmluaXRpb25zL1RhZyIKICAgICAgc3RhdHVzOgogICAgICAgIHR5cGU6ICJzdHJpbmciCiAgICAgICAgZGVzY3JpcHRpb246ICJwZXQgc3RhdHVzIGluIHRoZSBzdG9yZSIKICAgICAgICBlbnVtOgogICAgICAgIC0gImF2YWlsYWJsZSIKICAgICAgICAtICJwZW5kaW5nIgogICAgICAgIC0gInNvbGQiCiAgICB4bWw6CiAgICAgIG5hbWU6ICJQZXQiCiAgQXBpUmVzcG9uc2U6CiAgICB0eXBlOiAib2JqZWN0IgogICAgcHJvcGVydGllczoKICAgICAgY29kZToKICAgICAgICB0eXBlOiAiaW50ZWdlciIKICAgICAgICBmb3JtYXQ6ICJpbnQzMiIKICAgICAgdHlwZToKICAgICAgICB0eXBlOiAic3RyaW5nIgogICAgICBtZXNzYWdlOgogICAgICAgIHR5cGU6ICJzdHJpbmciCmV4dGVybmFsRG9jczoKICBkZXNjcmlwdGlvbjogIkZpbmQgb3V0IG1vcmUgYWJvdXQgU3dhZ2dlciIKICB1cmw6ICJodHRwOi8vc3dhZ2dlci5pbyIK", + "mimeType": "application/yaml" + }, + "attributes":{ + "sample-attribute2":{ + "jsonValues":{ + "values":[ + "{\"foo\":\"bar\"}" + ] + } + } + }, + "parsingMode":"RELAXED" + } +] \ No newline at end of file diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/ApiVersionsMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/ApiVersionsMojo.java new file mode 100644 index 0000000..d7ea17d --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/ApiVersionsMojo.java @@ -0,0 +1,389 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.mavenplugin; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.parser.ParseException; + +import com.apigee.apihub.config.utils.ApiHubClientSingleton; +import com.apigee.apihub.config.utils.BuildProfile; +import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; +import com.apigee.apihub.config.utils.ProtoJsonUtil; +import com.google.api.client.util.Key; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.apihub.v1.ApiHubClient; +import com.google.cloud.apihub.v1.ApiName; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.protobuf.FieldMask; + +/** + * Goal to configure API Versions in Apigee API Hub + * + * @author ssvaidyanathan + * @goal apiversions + * @phase install + */ +public class ApiVersionsMojo extends ApiHubAbstractMojo { + static Logger logger = LogManager.getLogger(ApiVersionsMojo.class); + + public static final String ____ATTENTION_MARKER____ = "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private BuildProfile buildProfile; + + /** + * Constructor. + */ + public ApiVersionsMojo() { + super(); + } + + + public static class ApiVersion { + @Key + public String name; + } + + protected String getApiVersionName(String payload) + throws MojoFailureException { + Gson gson = new Gson(); + try { + ApiVersion apiVersion = gson.fromJson(payload, ApiVersion.class); + return apiVersion.name; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * Initilization + * @throws MojoExecutionException + * @throws MojoFailureException + */ + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("API Hub API Versions"); + logger.info(____ATTENTION_MARKER____); + + String options = ""; + buildProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + if (buildOption == OPTIONS.none) { + logger.info("Skipping API Version (default action)"); + return; + } + + logger.debug("Build option " + buildOption.name()); + + + if (buildProfile.getProjectId() == null) { + throw new MojoExecutionException("Apigee API hub Project ID is missing"); + } + if (buildProfile.getLocation() == null) { + throw new MojoExecutionException("Apigee API hub Location is missing"); + } + if (buildProfile.getServiceAccountFilePath() == null && buildProfile.getBearer() == null) { + throw new MojoExecutionException("Service Account file path or Bearer token is missing"); + } + if (buildProfile.getConfigDir() == null) { + throw new MojoExecutionException("API Confile Dir is missing"); + } + + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (super.isSkip()) { + getLog().info("Skipping"); + return; + } + + try { + init(); + logger.info(format("Fetching apiVersions.json file from %s directory", buildProfile.getConfigDir())); + List apiVersions = ConfigReader.parseConfig(buildProfile.getConfigDir()+"/apiVersions.json"); + processApiVersions(apiVersions); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (ParseException e) { + throw new MojoFailureException(e.getMessage()); + } catch (IOException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * + * @param apiVersions + * @throws MojoExecutionException + */ + public void processApiVersions(List apiVersions) throws MojoExecutionException { + try { + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + for (String apiVersion : apiVersions) { + String apiVersionName = getApiVersionName(apiVersion); + String pattern = "^([a-zA-Z0-9-_]+)\\/versions\\/([a-zA-Z0-9-_]+)$"; //{api}/versions/{version} + Pattern p = Pattern.compile(pattern); + Matcher m = p.matcher(apiVersionName); + if (apiVersionName == null) { + throw new IllegalArgumentException("Api Version does not have a name"); + } + else if(apiVersionName != null && !m.matches()) { + throw new IllegalArgumentException(format("Api Version should be in %s format", pattern)); + } + if (doesApiVersionExist(buildProfile, apiVersionName)) { + switch (buildOption) { + case create: + logger.info(format("Api Version \"%s\" already exists. Skipping.", apiVersionName)); + break; + case update: + logger.info(format("Api Version \"%s\" already exists. Updating.", apiVersionName)); + //update + doUpdate(buildProfile, apiVersionName, apiVersion); + break; + case delete: + logger.info(format("Api Version \"%s\" already exists. Deleting.", apiVersionName)); + //delete + doDelete(buildProfile, apiVersionName); + break; + case sync: + logger.info(format("Api Version \"%s\" already exists. Deleting and recreating.", apiVersionName)); + //delete + doDelete(buildProfile, apiVersionName); + logger.info(format("Creating Api Version - %s", apiVersionName)); + //create + doCreate(buildProfile, apiVersionName, apiVersion); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info(format("Creating Api Version - %s", apiVersionName)); + //create + doCreate(buildProfile, apiVersionName, apiVersion); + break; + case delete: + logger.info(format("Api Version \"%s\" does not exist. Skipping.", apiVersionName)); + break; + } + } + } + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Create Api Version + * @param apiVersionName + * @param apiVersionStr + * @throws MojoExecutionException + */ + public void doCreate(BuildProfile profile, String apiVersionName, String apiVersionStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //parse the {api}/versions/{version} + String pattern = "^([a-zA-Z0-9-_]+)\\/versions\\/([a-zA-Z0-9-_]+)$"; + Pattern p = Pattern.compile(pattern); + Matcher m = p.matcher(apiVersionName); + if(m.matches()) { + String apiName = m.group(1); + String version = m.group(2); + + ApiName parent = ApiName.of(profile.getProjectId(), profile.getLocation(), apiName); + + //replace the name field from {api}/versions/{version} to {version} + apiVersionStr = FQDNHelper.replaceFQDNJsonValue("$.name", version, apiVersionStr); + + //update attributes with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", apiVersionStr); + + //update deployments ARRAY with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonStringArray("$.deployments", format("projects/%s/locations/%s/deployments", profile.getProjectId(), profile.getLocation()), apiVersionStr); + + //update lifecycle with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.lifecycle.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update compliance with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.compliance.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update accreditation with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.accreditation.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update selectedVersion with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.selectedDeployment", format("projects/%s/locations/%s/deployments", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + logger.debug("after modifying: "+ apiVersionStr); + + com.google.cloud.apihub.v1.Version apiVersionObj = ProtoJsonUtil.fromJson(apiVersionStr, com.google.cloud.apihub.v1.Version.class); + apiHubClient.createVersion(parent, apiVersionObj, version); + logger.info("Create success"); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create failure: " + e.getMessage()); + } + } + + /** + * Delete Api Version + * @param profile + * @param apiVersionName + * @throws MojoExecutionException + */ + public void doDelete(BuildProfile profile, String apiVersionName) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + apiHubClient.deleteVersion(format("projects/%s/locations/%s/apis/%s", profile.getProjectId(), profile.getLocation(), apiVersionName)); + logger.info("Delete success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Delete failure: " + e.getMessage()); + } + } + + /** + * Update Api Version + * @param profile + * @param apiVersionName + * @param apiVersionStr + * @throws MojoExecutionException + */ + public void doUpdate(BuildProfile profile, String apiVersionName, String apiVersionStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //updating the name field in the api object to projects/{project}/locations/{location}/apis/{api}/versions/{version} format as its required by the updateVersion method + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/apis", profile.getProjectId(), profile.getLocation()), + apiVersionStr); + + //update attributes with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", apiVersionStr); + + //update deployments ARRAY with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonStringArray("$.deployments", format("projects/%s/locations/%s/deployments", profile.getProjectId(), profile.getLocation()), apiVersionStr); + + //update lifecycle with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.lifecycle.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update compliance with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.compliance.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update accreditation with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.accreditation.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + //update selectedVersion with FQDN if exist + apiVersionStr = FQDNHelper.updateFQDNJsonValue("$.selectedDeployment", format("projects/%s/locations/%s/deployments", profile.getProjectId(), profile.getLocation()),apiVersionStr); + + logger.debug("after modifying: "+ apiVersionStr); + + com.google.cloud.apihub.v1.Version apiVersionObj = ProtoJsonUtil.fromJson(apiVersionStr, com.google.cloud.apihub.v1.Version.class); + List fieldMaskValues = new ArrayList<>(); + fieldMaskValues.add("display_name"); + fieldMaskValues.add("description"); + fieldMaskValues.add("documentation"); + fieldMaskValues.add("deployments"); + fieldMaskValues.add("lifecycle"); + fieldMaskValues.add("compliance"); + fieldMaskValues.add("accreditation"); + fieldMaskValues.add("attributes"); + FieldMask updateMask = FieldMask.newBuilder().addAllPaths(fieldMaskValues).build(); + apiHubClient.updateVersion(apiVersionObj, updateMask); + logger.info("Update success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Update failure: " + e.getMessage()); + } + } + + /** + * Check if an Api Version exist + * + * @param profile + * @param apiVersionName + * @return + * @throws IOException + */ + public static boolean doesApiVersionExist(BuildProfile profile, String apiVersionName) + throws IOException { + try { + logger.info("Checking if Api Version - " +apiVersionName + " exist"); + ApiHubClient apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + com.google.cloud.apihub.v1.Version apiVersionResponse = apiHubClient.getVersion(format("projects/%s/locations/%s/apis/%s", profile.getProjectId(), profile.getLocation(), apiVersionName)); + if(apiVersionResponse == null) + return false; + } + catch (ApiException e) { + if(e.getStatusCode().getCode().equals(Code.NOT_FOUND)) { + return false; + } + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + return true; + } + +} diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/ApisMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/ApisMojo.java new file mode 100644 index 0000000..281b4b9 --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/ApisMojo.java @@ -0,0 +1,379 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.mavenplugin; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.parser.ParseException; + +import com.apigee.apihub.config.utils.ApiHubClientSingleton; +import com.apigee.apihub.config.utils.BuildProfile; +import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; +import com.apigee.apihub.config.utils.ProtoJsonUtil; +import com.google.api.client.util.Key; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.apihub.v1.ApiHubClient; +import com.google.cloud.apihub.v1.ApiName; +import com.google.cloud.apihub.v1.LocationName; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.protobuf.FieldMask; + +/** + * Goal to configure APIs in Apigee API Hub + * + * @author ssvaidyanathan + * @goal apis + * @phase install + */ +public class ApisMojo extends ApiHubAbstractMojo { + static Logger logger = LogManager.getLogger(ApisMojo.class); + + public static final String ____ATTENTION_MARKER____ = "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private BuildProfile buildProfile; + + /** + * Constructor. + */ + public ApisMojo() { + super(); + } + + + public static class Api { + @Key + public String name; + } + + protected String getApiName(String payload) + throws MojoFailureException { + Gson gson = new Gson(); + try { + Api api = gson.fromJson(payload, Api.class); + return api.name; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * Initilization + * @throws MojoExecutionException + * @throws MojoFailureException + */ + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("API Hub APIs"); + logger.info(____ATTENTION_MARKER____); + + String options = ""; + buildProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + if (buildOption == OPTIONS.none) { + logger.info("Skipping APIs (default action)"); + return; + } + + logger.debug("Build option " + buildOption.name()); + + + if (buildProfile.getProjectId() == null) { + throw new MojoExecutionException("Apigee API hub Project ID is missing"); + } + if (buildProfile.getLocation() == null) { + throw new MojoExecutionException("Apigee API hub Location is missing"); + } + if (buildProfile.getServiceAccountFilePath() == null && buildProfile.getBearer() == null) { + throw new MojoExecutionException("Service Account file path or Bearer token is missing"); + } + if (buildProfile.getConfigDir() == null) { + throw new MojoExecutionException("API Confile Dir is missing"); + } + + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (super.isSkip()) { + getLog().info("Skipping"); + return; + } + + try { + init(); + logger.info(format("Fetching apis.json file from %s directory", buildProfile.getConfigDir())); + List apis = ConfigReader.parseConfig(buildProfile.getConfigDir()+"/apis.json"); + processApis(apis); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (ParseException e) { + throw new MojoFailureException(e.getMessage()); + } catch (IOException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * + * @param apis + * @throws MojoExecutionException + */ + public void processApis(List apis) throws MojoExecutionException { + try { + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + for (String api : apis) { + String apiName = getApiName(api); + if (apiName == null) { + throw new IllegalArgumentException("Api does not have a name"); + } + if (doesApiExist(buildProfile, apiName)) { + switch (buildOption) { + case create: + logger.info(format("Api \"%s\" already exists. Skipping.", apiName)); + break; + case update: + logger.info(format("Api \"%s\" already exists. Updating.", apiName)); + //update + doUpdate(buildProfile, apiName, api); + break; + case delete: + logger.info(format("Api \"%s\" already exists. Deleting.", apiName)); + //delete + doDelete(buildProfile, apiName); + break; + case sync: + logger.info(format("Api \"%s\" already exists. Deleting and recreating.", apiName)); + //delete + doDelete(buildProfile, apiName); + logger.info(format("Creating Api - %s", apiName)); + //create + doCreate(buildProfile, apiName, api); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info(format("Creating Api - %s", apiName)); + //create + doCreate(buildProfile, apiName, api); + break; + case delete: + logger.info(format("Api \"%s\" does not exist. Skipping.", apiName)); + break; + } + } + } + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Create Api + * @param apiName + * @param apiStr + * @throws MojoExecutionException + */ + public void doCreate(BuildProfile profile, String apiName, String apiStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + LocationName parent = LocationName.of(profile.getProjectId(), profile.getLocation()); + + //update attributes with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", apiStr); + + //update targetUser with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.targetUser.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update team with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.team.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update businessUnit with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.businessUnit.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update maturityLevel with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.maturityLevel.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update apiStyle with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.apiStyle.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update selectedVersion with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.selectedVersion", format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()),apiStr); + + logger.debug("after modifying: "+ apiStr); + + com.google.cloud.apihub.v1.Api apiObj = ProtoJsonUtil.fromJson(apiStr, com.google.cloud.apihub.v1.Api.class); + apiHubClient.createApi(parent, apiObj, apiName); + logger.info("Create success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create failure: " + e.getMessage()); + } + } + + /** + * Delete Api + * @param profile + * @param apiName + * @throws MojoExecutionException + */ + public void doDelete(BuildProfile profile, String apiName) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + ApiName name = ApiName.of(profile.getProjectId(), profile.getLocation(), apiName); + apiHubClient.deleteApi(name); + logger.info("Delete success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Delete failure: " + e.getMessage()); + } + } + + /** + * Update Api + * @param profile + * @param apiName + * @param apiStr + * @throws MojoExecutionException + */ + public void doUpdate(BuildProfile profile, String apiName, String apiStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //updating the name field in the api object to projects/{project}/locations/{location}/apis/{api} format as its required by the updateApi method + apiStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/apis", profile.getProjectId(), profile.getLocation()), + apiStr); + + //update attributes with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", apiStr); + + //update targetUser with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.targetUser.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update team with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.team.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update businessUnit with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.businessUnit.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update maturityLevel with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.maturityLevel.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update apiStyle with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.apiStyle.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),apiStr); + + //update selectedVersion with FQDN if exist + apiStr = FQDNHelper.updateFQDNJsonValue("$.selectedVersion", format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()),apiStr); + + logger.debug("after modifying: "+ apiStr); + + com.google.cloud.apihub.v1.Api apiObj = ProtoJsonUtil.fromJson(apiStr, com.google.cloud.apihub.v1.Api.class); + List fieldMaskValues = new ArrayList<>(); + fieldMaskValues.add("display_name"); + fieldMaskValues.add("description"); + fieldMaskValues.add("owner"); + fieldMaskValues.add("documentation"); + fieldMaskValues.add("target_user"); + fieldMaskValues.add("team"); + fieldMaskValues.add("business_unit"); + fieldMaskValues.add("maturity_level"); + fieldMaskValues.add("api_style"); + fieldMaskValues.add("attributes"); + FieldMask updateMask = FieldMask.newBuilder().addAllPaths(fieldMaskValues).build(); + apiHubClient.updateApi(apiObj, updateMask); + logger.info("Update success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Update failure: " + e.getMessage()); + } + } + + /** + * Check if an Api exist + * + * @param profile + * @param apiName + * @return + * @throws IOException + */ + public static boolean doesApiExist(BuildProfile profile, String apiName) + throws IOException { + try { + logger.info("Checking if Api - " +apiName + " exist"); + ApiHubClient apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + ApiName name = ApiName.of(profile.getProjectId(), profile.getLocation(), apiName); + com.google.cloud.apihub.v1.Api apiResponse = apiHubClient.getApi(name); + if(apiResponse == null) + return false; + } + catch (ApiException e) { + if(e.getStatusCode().getCode().equals(Code.NOT_FOUND)) { + return false; + } + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + return true; + } + +} diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/AttributesMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/AttributesMojo.java index 3bebe63..95f39e2 100644 --- a/src/main/java/com/apigee/apihub/config/mavenplugin/AttributesMojo.java +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/AttributesMojo.java @@ -31,6 +31,7 @@ import com.apigee.apihub.config.utils.ApiHubClientSingleton; import com.apigee.apihub.config.utils.BuildProfile; import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; import com.apigee.apihub.config.utils.ProtoJsonUtil; import com.google.api.client.util.Key; import com.google.api.gax.rpc.ApiException; @@ -105,7 +106,7 @@ public void init() throws MojoExecutionException, MojoFailureException { buildOption = OPTIONS.valueOf(options); } if (buildOption == OPTIONS.none) { - logger.info("Skipping Artifact (default action)"); + logger.info("Skipping Attributes (default action)"); return; } @@ -272,9 +273,13 @@ public void doUpdate(BuildProfile profile, String attributeName, String attribut ApiHubClient apiHubClient = null; try { apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); - com.google.cloud.apihub.v1.Attribute attributeObj = ProtoJsonUtil.fromJson(attributeStr, com.google.cloud.apihub.v1.Attribute.class); + //updating the name field in the attribute object to projects/{project}/locations/{location}/attributes/{attribute} format as its required by the updateAttribute method - attributeObj = attributeObj.toBuilder().setName(format("projects/%s/locations/%s/attributes/%s", profile.getProjectId(), profile.getLocation(), attributeName)).build(); + attributeStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()), + attributeStr); + + com.google.cloud.apihub.v1.Attribute attributeObj = ProtoJsonUtil.fromJson(attributeStr, com.google.cloud.apihub.v1.Attribute.class); List fieldMaskValues = new ArrayList<>(); fieldMaskValues.add("display_name"); fieldMaskValues.add("description"); diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/DependenciesMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/DependenciesMojo.java new file mode 100644 index 0000000..16f5cce --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/DependenciesMojo.java @@ -0,0 +1,367 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.mavenplugin; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.parser.ParseException; + +import com.apigee.apihub.config.utils.ApiHubClientSingleton; +import com.apigee.apihub.config.utils.BuildProfile; +import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; +import com.apigee.apihub.config.utils.ProtoJsonUtil; +import com.google.api.client.util.Key; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.apihub.v1.ApiHubDependenciesClient; +import com.google.cloud.apihub.v1.DependencyName; +import com.google.cloud.apihub.v1.LocationName; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.protobuf.FieldMask; + +/** + * Goal to configure Dependencies in Apigee API Hub + * + * @author ssvaidyanathan + * @goal dependencies + * @phase install + */ +public class DependenciesMojo extends ApiHubAbstractMojo { + static Logger logger = LogManager.getLogger(DependenciesMojo.class); + + public static final String ____ATTENTION_MARKER____ = "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private BuildProfile buildProfile; + + /** + * Constructor. + */ + public DependenciesMojo() { + super(); + } + + + public static class Dependency { + @Key + public String name; + } + + protected String getDependencyName(String payload) + throws MojoFailureException { + Gson gson = new Gson(); + try { + Dependency dependency = gson.fromJson(payload, Dependency.class); + return dependency.name; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * Initilization + * @throws MojoExecutionException + * @throws MojoFailureException + */ + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("API Hub Dependency"); + logger.info(____ATTENTION_MARKER____); + + String options = ""; + buildProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + if (buildOption == OPTIONS.none) { + logger.info("Skipping Dependency (default action)"); + return; + } + + logger.debug("Build option " + buildOption.name()); + + + if (buildProfile.getProjectId() == null) { + throw new MojoExecutionException("Apigee API hub Project ID is missing"); + } + if (buildProfile.getLocation() == null) { + throw new MojoExecutionException("Apigee API hub Location is missing"); + } + if (buildProfile.getServiceAccountFilePath() == null && buildProfile.getBearer() == null) { + throw new MojoExecutionException("Service Account file path or Bearer token is missing"); + } + if (buildProfile.getConfigDir() == null) { + throw new MojoExecutionException("API Confile Dir is missing"); + } + + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (super.isSkip()) { + getLog().info("Skipping"); + return; + } + + try { + init(); + logger.info(format("Fetching dependencies.json file from %s directory", buildProfile.getConfigDir())); + List dependencies = ConfigReader.parseConfig(buildProfile.getConfigDir()+"/dependencies.json"); + processDependencies(dependencies); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (ParseException e) { + throw new MojoFailureException(e.getMessage()); + } catch (IOException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * + * @param dependencies + * @throws MojoExecutionException + */ + public void processDependencies(List dependencies) throws MojoExecutionException { + try { + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + for (String dependency : dependencies) { + String dependencyName = getDependencyName(dependency); + if (dependencyName == null) { + throw new IllegalArgumentException("Dependency does not have a name"); + } + if (doesDependencyExist(buildProfile, dependencyName)) { + switch (buildOption) { + case create: + logger.info(format("Dependency \"%s\" already exists. Skipping.", dependencyName)); + break; + case update: + logger.info(format("Dependency \"%s\" already exists. Updating.", dependencyName)); + //update + doUpdate(buildProfile, dependencyName, dependency); + break; + case delete: + logger.info(format("Dependency \"%s\" already exists. Deleting.", dependencyName)); + //delete + doDelete(buildProfile, dependencyName); + break; + case sync: + logger.info(format("Dependency \"%s\" already exists. Deleting and recreating.", dependencyName)); + //delete + doDelete(buildProfile, dependencyName); + logger.info(format("Creating Dependency - %s", dependencyName)); + //create + doCreate(buildProfile, dependencyName, dependency); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info(format("Creating Dependency - %s", dependencyName)); + //create + doCreate(buildProfile, dependencyName, dependency); + break; + case delete: + logger.info(format("Dependency \"%s\" does not exist. Skipping.", dependencyName)); + break; + } + } + } + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Create Dependency + * @param dependencyName + * @param dependencyStr + * @throws MojoExecutionException + */ + public void doCreate(BuildProfile profile, String dependencyName, String dependencyStr) throws MojoExecutionException { + ApiHubDependenciesClient apiHubDependenciesClient = null; + try { + apiHubDependenciesClient = ApiHubClientSingleton.getDependenciesInstance(profile).getApiHubDependenciesClient(); + LocationName parent = LocationName.of(profile.getProjectId(), profile.getLocation()); + + //update attributes with FQDN if exist + dependencyStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", dependencyStr); + + //update externalApiResourceName for consumer and supplier + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.consumer.externalApiResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.supplier.externalApiResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + + //update operationResourceName for consumer and supplier + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.consumer.operationResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.supplier.operationResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + + logger.debug("after modifying: "+ dependencyStr); + + + com.google.cloud.apihub.v1.Dependency dependencyObj = ProtoJsonUtil.fromJson(dependencyStr, com.google.cloud.apihub.v1.Dependency.class); + apiHubDependenciesClient.createDependency(parent, dependencyObj, dependencyName); + logger.info("Create success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create failure: " + e.getMessage()); + } + } + + /** + * Delete Dependency + * @param profile + * @param dependencyName + * @throws MojoExecutionException + */ + public void doDelete(BuildProfile profile, String dependencyName) throws MojoExecutionException { + ApiHubDependenciesClient apiHubDependenciesClient = null; + try { + apiHubDependenciesClient = ApiHubClientSingleton.getDependenciesInstance(profile).getApiHubDependenciesClient(); + DependencyName name = DependencyName.of(profile.getProjectId(), profile.getLocation(), dependencyName); + apiHubDependenciesClient.deleteDependency(name); + logger.info("Delete success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Delete failure: " + e.getMessage()); + } + } + + /** + * Update Dependency + * @param profile + * @param dependencyName + * @param dependencyStr + * @throws MojoExecutionException + */ + public void doUpdate(BuildProfile profile, String dependencyName, String dependencyStr) throws MojoExecutionException { + ApiHubDependenciesClient apiHubDependenciesClient = null; + try { + apiHubDependenciesClient = ApiHubClientSingleton.getDependenciesInstance(profile).getApiHubDependenciesClient(); + + //updating the name field in the dependency object to projects/{project}/locations/{location}/dependencies/{dependency} format as its required by the updateDependency method + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/dependencies", profile.getProjectId(), profile.getLocation()), + dependencyStr); + + //update attributes with FQDN if exist + dependencyStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", dependencyStr); + + //update externalApiResourceName for consumer and supplier + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.consumer.externalApiResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.supplier.externalApiResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + + //update operationResourceName for consumer and supplier + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.consumer.operationResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + dependencyStr = FQDNHelper.updateFQDNJsonValue("$.supplier.operationResourceName", + format("projects/%s/locations/%s", profile.getProjectId(), profile.getLocation()), + dependencyStr); + + logger.debug("after modifying: "+ dependencyStr); + + com.google.cloud.apihub.v1.Dependency dependencyObj = ProtoJsonUtil.fromJson(dependencyStr, com.google.cloud.apihub.v1.Dependency.class); + List fieldMaskValues = new ArrayList<>(); + fieldMaskValues.add("description"); + FieldMask updateMask = FieldMask.newBuilder().addAllPaths(fieldMaskValues).build(); + apiHubDependenciesClient.updateDependency(dependencyObj, updateMask); + logger.info("Update success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Update failure: " + e.getMessage()); + } + } + + /** + * Check if an Dependency exist + * + * @param profile + * @param dependencyName + * @return + * @throws IOException + */ + public static boolean doesDependencyExist(BuildProfile profile, String dependencyName) + throws IOException { + try { + logger.info("Checking if Dependency - " +dependencyName + " exist"); + ApiHubDependenciesClient apiHubDependenciesClient = ApiHubClientSingleton.getDependenciesInstance(profile).getApiHubDependenciesClient(); + DependencyName name = DependencyName.of(profile.getProjectId(), profile.getLocation(), dependencyName); + com.google.cloud.apihub.v1.Dependency dependencyResponse = apiHubDependenciesClient.getDependency(name); + if(dependencyResponse == null) + return false; + } + catch (ApiException e) { + if(e.getStatusCode().getCode().equals(Code.NOT_FOUND)) { + return false; + } + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + return true; + } + +} diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/DeploymentsMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/DeploymentsMojo.java new file mode 100644 index 0000000..3454179 --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/DeploymentsMojo.java @@ -0,0 +1,358 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.mavenplugin; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.parser.ParseException; + +import com.apigee.apihub.config.utils.ApiHubClientSingleton; +import com.apigee.apihub.config.utils.BuildProfile; +import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; +import com.apigee.apihub.config.utils.ProtoJsonUtil; +import com.google.api.client.util.Key; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.apihub.v1.ApiHubClient; +import com.google.cloud.apihub.v1.DeploymentName; +import com.google.cloud.apihub.v1.LocationName; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.protobuf.FieldMask; + +/** + * Goal to configure Deployments in Apigee API Hub + * + * @author ssvaidyanathan + * @goal deployments + * @phase install + */ +public class DeploymentsMojo extends ApiHubAbstractMojo { + static Logger logger = LogManager.getLogger(DeploymentsMojo.class); + + public static final String ____ATTENTION_MARKER____ = "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private BuildProfile buildProfile; + + /** + * Constructor. + */ + public DeploymentsMojo() { + super(); + } + + + public static class Deployment { + @Key + public String name; + } + + protected String getDeploymentName(String payload) + throws MojoFailureException { + Gson gson = new Gson(); + try { + Deployment deployment = gson.fromJson(payload, Deployment.class); + return deployment.name; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * Initilization + * @throws MojoExecutionException + * @throws MojoFailureException + */ + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("API Hub Deployments"); + logger.info(____ATTENTION_MARKER____); + + String options = ""; + buildProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + if (buildOption == OPTIONS.none) { + logger.info("Skipping Deployment (default action)"); + return; + } + + logger.debug("Build option " + buildOption.name()); + + + if (buildProfile.getProjectId() == null) { + throw new MojoExecutionException("Apigee API hub Project ID is missing"); + } + if (buildProfile.getLocation() == null) { + throw new MojoExecutionException("Apigee API hub Location is missing"); + } + if (buildProfile.getServiceAccountFilePath() == null && buildProfile.getBearer() == null) { + throw new MojoExecutionException("Service Account file path or Bearer token is missing"); + } + if (buildProfile.getConfigDir() == null) { + throw new MojoExecutionException("API Confile Dir is missing"); + } + + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (super.isSkip()) { + getLog().info("Skipping"); + return; + } + + try { + init(); + logger.info(format("Fetching deployments.json file from %s directory", buildProfile.getConfigDir())); + List deployments = ConfigReader.parseConfig(buildProfile.getConfigDir()+"/deployments.json"); + processDeployments(deployments); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (ParseException e) { + throw new MojoFailureException(e.getMessage()); + } catch (IOException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * + * @param deployments + * @throws MojoExecutionException + */ + public void processDeployments(List deployments) throws MojoExecutionException { + try { + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + for (String deployment : deployments) { + String deploymentName = getDeploymentName(deployment); + if (deploymentName == null) { + throw new IllegalArgumentException("Deployment does not have a name"); + } + if (doesDeploymentExist(buildProfile, deploymentName)) { + switch (buildOption) { + case create: + logger.info(format("Deployment \"%s\" already exists. Skipping.", deploymentName)); + break; + case update: + logger.info(format("Deployment \"%s\" already exists. Updating.", deploymentName)); + //update + doUpdate(buildProfile, deploymentName, deployment); + break; + case delete: + logger.info(format("Deployment \"%s\" already exists. Deleting.", deploymentName)); + //delete + doDelete(buildProfile, deploymentName); + break; + case sync: + logger.info(format("Deployment \"%s\" already exists. Deleting and recreating.", deploymentName)); + //delete + doDelete(buildProfile, deploymentName); + logger.info(format("Creating Deployment - %s", deploymentName)); + //create + doCreate(buildProfile, deploymentName, deployment); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info(format("Creating Deployment - %s", deploymentName)); + //create + doCreate(buildProfile, deploymentName, deployment); + break; + case delete: + logger.info(format("Deployment \"%s\" does not exist. Skipping.", deploymentName)); + break; + } + } + } + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Create Deployment + * @param deploymentName + * @param deploymentStr + * @throws MojoExecutionException + */ + public void doCreate(BuildProfile profile, String deploymentName, String deploymentStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + LocationName parent = LocationName.of(profile.getProjectId(), profile.getLocation()); + + //update attributes with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", deploymentStr); + + //update slo with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.slo.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + + //update environment with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.environment.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + + //update deploymentType with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.deploymentType.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + logger.debug("after modifying: "+ deploymentStr); + + com.google.cloud.apihub.v1.Deployment deploymentObj = ProtoJsonUtil.fromJson(deploymentStr, com.google.cloud.apihub.v1.Deployment.class); + apiHubClient.createDeployment(parent, deploymentObj, deploymentName); + logger.info("Create success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create failure: " + e.getMessage()); + } + } + + /** + * Delete Deployment + * @param profile + * @param deploymentName + * @throws MojoExecutionException + */ + public void doDelete(BuildProfile profile, String deploymentName) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + DeploymentName name = DeploymentName.of(profile.getProjectId(), profile.getLocation(), deploymentName); + apiHubClient.deleteDeployment(name); + logger.info("Delete success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Delete failure: " + e.getMessage()); + } + } + + /** + * Update Deployment + * @param profile + * @param deploymentName + * @param deploymentStr + * @throws MojoExecutionException + */ + public void doUpdate(BuildProfile profile, String deploymentName, String deploymentStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //updating the name field in the deployment object to projects/{project}/locations/{location}/deployments/{deployment} format as its required by the updateDeployment method + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/deployments", profile.getProjectId(), profile.getLocation()), + deploymentStr); + + //update attributes with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", deploymentStr); + + //update slo with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.slo.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + + //update environment with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.environment.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + + //update deploymentType with FQDN if exist + deploymentStr = FQDNHelper.updateFQDNJsonValue("$.deploymentType.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),deploymentStr); + logger.debug("after modifying: "+ deploymentStr); + + com.google.cloud.apihub.v1.Deployment deploymentObj = ProtoJsonUtil.fromJson(deploymentStr, com.google.cloud.apihub.v1.Deployment.class); + List fieldMaskValues = new ArrayList<>(); + fieldMaskValues.add("display_name"); + fieldMaskValues.add("description"); + fieldMaskValues.add("documentation"); + fieldMaskValues.add("deployment_type"); + fieldMaskValues.add("resource_uri"); + fieldMaskValues.add("endpoints"); + fieldMaskValues.add("slo"); + fieldMaskValues.add("environment"); + fieldMaskValues.add("attributes"); + FieldMask updateMask = FieldMask.newBuilder().addAllPaths(fieldMaskValues).build(); + apiHubClient.updateDeployment(deploymentObj, updateMask); + logger.info("Update success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Update failure: " + e.getMessage()); + } + } + + /** + * Check if an Deployment exist + * + * @param profile + * @param deploymentName + * @return + * @throws IOException + */ + public static boolean doesDeploymentExist(BuildProfile profile, String deploymentName) + throws IOException { + try { + logger.info("Checking if Deployment - " +deploymentName + " exist"); + ApiHubClient apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + DeploymentName name = DeploymentName.of(profile.getProjectId(), profile.getLocation(), deploymentName); + com.google.cloud.apihub.v1.Deployment deploymentResponse = apiHubClient.getDeployment(name); + if(deploymentResponse == null) + return false; + } + catch (ApiException e) { + if(e.getStatusCode().getCode().equals(Code.NOT_FOUND)) { + return false; + } + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + return true; + } + +} diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/ExternalApisMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/ExternalApisMojo.java index bd8396c..47e5ca7 100644 --- a/src/main/java/com/apigee/apihub/config/mavenplugin/ExternalApisMojo.java +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/ExternalApisMojo.java @@ -21,8 +21,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -33,6 +31,7 @@ import com.apigee.apihub.config.utils.ApiHubClientSingleton; import com.apigee.apihub.config.utils.BuildProfile; import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; import com.apigee.apihub.config.utils.ProtoJsonUtil; import com.google.api.client.util.Key; import com.google.api.gax.rpc.ApiException; @@ -41,8 +40,6 @@ import com.google.cloud.apihub.v1.ExternalApiName; import com.google.cloud.apihub.v1.LocationName; import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.protobuf.FieldMask; @@ -109,7 +106,7 @@ public void init() throws MojoExecutionException, MojoFailureException { buildOption = OPTIONS.valueOf(options); } if (buildOption == OPTIONS.none) { - logger.info("Skipping Artifact (default action)"); + logger.info("Skipping External API (default action)"); return; } @@ -237,7 +234,11 @@ public void doCreate(BuildProfile profile, String externalApiName, String extern try { apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); LocationName parent = LocationName.of(profile.getProjectId(), profile.getLocation()); - com.google.cloud.apihub.v1.ExternalApi externalApipObj = ProtoJsonUtil.fromJson(updateAttributeFQDN(profile, externalApiName, externalApiStr), com.google.cloud.apihub.v1.ExternalApi.class); + + //update attributes with FQDN if exist + externalApiStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", externalApiStr); + + com.google.cloud.apihub.v1.ExternalApi externalApipObj = ProtoJsonUtil.fromJson(externalApiStr, com.google.cloud.apihub.v1.ExternalApi.class); apiHubClient.createExternalApi(parent, externalApipObj, externalApiName); logger.info("Create success"); } catch (Exception e) { @@ -276,9 +277,17 @@ public void doUpdate(BuildProfile profile, String externalApiName, String extern ApiHubClient apiHubClient = null; try { apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); - com.google.cloud.apihub.v1.ExternalApi externalApiObj = ProtoJsonUtil.fromJson(updateAttributeFQDN(profile, externalApiName, externalApiStr), com.google.cloud.apihub.v1.ExternalApi.class); - //updating the name field in the externalApi object to projects/{project}/locations/{location}/externalApis/{externalApi} format as its required by the updateExternalApi method - externalApiObj = externalApiObj.toBuilder().setName(format("projects/%s/locations/%s/externalApis/%s", profile.getProjectId(), profile.getLocation(), externalApiName)).build(); + + //updating the name field in the attribute object to projects/{project}/locations/{location}/attributes/{attribute} format as its required by the updateAttribute method + externalApiStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/externalApis", profile.getProjectId(), profile.getLocation()), + externalApiStr); + + //update attributes with FQDN if exist + externalApiStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", externalApiStr); + + + com.google.cloud.apihub.v1.ExternalApi externalApiObj = ProtoJsonUtil.fromJson(externalApiStr, com.google.cloud.apihub.v1.ExternalApi.class); List fieldMaskValues = new ArrayList<>(); fieldMaskValues.add("display_name"); fieldMaskValues.add("description"); @@ -323,35 +332,4 @@ public static boolean doesExternalApiExist(BuildProfile profile, String external return true; } - /** - * TO update the attributes with FQDN - * @param profile - * @param externalApiName - * @param externalApiStr - * @return - * @throws IOException - */ - public static String updateAttributeFQDN (BuildProfile profile, String externalApiName, String externalApiStr) throws IOException { - try { - JsonElement je = new Gson().fromJson(externalApiStr, JsonElement.class); - JsonObject jo = je.getAsJsonObject(); - JsonElement attributes = jo.get("attributes"); - if(attributes!=null) { - logger.debug("attributes exist, replacing with FQDN"); - JsonObject attributesObj = attributes.getAsJsonObject(); - Set> entries = attributesObj.entrySet(); - for(Map.Entry entry: entries) { - ((JsonObject) attributes).add(format("projects/%s/locations/%s/attributes/%s", profile.getProjectId(), profile.getLocation(), entry.getKey()), entry.getValue()); - attributesObj.remove(entry.getKey()); - } - logger.debug("updated json:"+je); - return je.toString(); - } - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("updateAttributeFQDN failure: " + e.getMessage()); - } - return externalApiStr; - } - } diff --git a/src/main/java/com/apigee/apihub/config/mavenplugin/SpecsMojo.java b/src/main/java/com/apigee/apihub/config/mavenplugin/SpecsMojo.java new file mode 100644 index 0000000..8e82249 --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/mavenplugin/SpecsMojo.java @@ -0,0 +1,366 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.mavenplugin; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.parser.ParseException; + +import com.apigee.apihub.config.utils.ApiHubClientSingleton; +import com.apigee.apihub.config.utils.BuildProfile; +import com.apigee.apihub.config.utils.ConfigReader; +import com.apigee.apihub.config.utils.FQDNHelper; +import com.apigee.apihub.config.utils.ProtoJsonUtil; +import com.google.api.client.util.Key; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.cloud.apihub.v1.ApiHubClient; +import com.google.cloud.apihub.v1.VersionName; +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.protobuf.FieldMask; + +/** + * Goal to configure Spec in Apigee API Hub + * + * @author ssvaidyanathan + * @goal specs + * @phase install + */ +public class SpecsMojo extends ApiHubAbstractMojo { + static Logger logger = LogManager.getLogger(SpecsMojo.class); + + public static final String ____ATTENTION_MARKER____ = "************************************************************************"; + + enum OPTIONS { + none, create, update, delete, sync + } + + OPTIONS buildOption = OPTIONS.none; + + private BuildProfile buildProfile; + + /** + * Constructor. + */ + public SpecsMojo() { + super(); + } + + + public static class Spec { + @Key + public String name; + } + + protected String getSpecName(String payload) + throws MojoFailureException { + Gson gson = new Gson(); + try { + Spec spec = gson.fromJson(payload, Spec.class); + return spec.name; + } catch (JsonParseException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * Initilization + * @throws MojoExecutionException + * @throws MojoFailureException + */ + public void init() throws MojoExecutionException, MojoFailureException { + try { + logger.info(____ATTENTION_MARKER____); + logger.info("API Hub Specs"); + logger.info(____ATTENTION_MARKER____); + + String options = ""; + buildProfile = super.getProfile(); + + options = super.getOptions(); + if (options != null) { + buildOption = OPTIONS.valueOf(options); + } + if (buildOption == OPTIONS.none) { + logger.info("Skipping Spec (default action)"); + return; + } + + logger.debug("Build option " + buildOption.name()); + + + if (buildProfile.getProjectId() == null) { + throw new MojoExecutionException("Apigee API hub Project ID is missing"); + } + if (buildProfile.getLocation() == null) { + throw new MojoExecutionException("Apigee API hub Location is missing"); + } + if (buildProfile.getServiceAccountFilePath() == null && buildProfile.getBearer() == null) { + throw new MojoExecutionException("Service Account file path or Bearer token is missing"); + } + if (buildProfile.getConfigDir() == null) { + throw new MojoExecutionException("API Confile Dir is missing"); + } + + + } catch (IllegalArgumentException e) { + throw new RuntimeException("Invalid apigee.option provided"); + } catch (RuntimeException e) { + throw e; + } + + } + + /** + * Entry point for the mojo. + */ + public void execute() throws MojoExecutionException, MojoFailureException { + if (super.isSkip()) { + getLog().info("Skipping"); + return; + } + + try { + init(); + logger.info(format("Fetching specs.json file from %s directory", buildProfile.getConfigDir())); + List specs = ConfigReader.parseConfig(buildProfile.getConfigDir()+"/specs.json"); + processSpecs(specs); + + } catch (MojoFailureException e) { + throw e; + } catch (RuntimeException e) { + throw e; + } catch (ParseException e) { + throw new MojoFailureException(e.getMessage()); + } catch (IOException e) { + throw new MojoFailureException(e.getMessage()); + } + } + + /** + * + * @param specs + * @throws MojoExecutionException + */ + public void processSpecs(List specs) throws MojoExecutionException { + try { + if (buildOption != OPTIONS.update && + buildOption != OPTIONS.create && + buildOption != OPTIONS.delete && + buildOption != OPTIONS.sync) { + return; + } + for (String spec : specs) { + String specName = getSpecName(spec); + String pattern = "^([a-zA-Z0-9-_]+)\\/versions\\/([a-zA-Z0-9-_]+)\\/specs\\/([a-zA-Z0-9-_]+)$"; //{api}/versions/{version}/specs/{spec} + Pattern p = Pattern.compile(pattern); + Matcher m = p.matcher(specName); + if (specName == null) { + throw new IllegalArgumentException("Spec does not have a name"); + } + else if(specName != null && !m.matches()) { + throw new IllegalArgumentException(format("Spec should be in %s format", pattern)); + } + if (doesSpecExist(buildProfile, specName)) { + switch (buildOption) { + case create: + logger.info(format("Spec \"%s\" already exists. Skipping.", specName)); + break; + case update: + logger.info(format("Spec \"%s\" already exists. Updating.", specName)); + //update + doUpdate(buildProfile, specName, spec); + break; + case delete: + logger.info(format("Spec \"%s\" already exists. Deleting.", specName)); + //delete + doDelete(buildProfile, specName); + break; + case sync: + logger.info(format("Spec \"%s\" already exists. Deleting and recreating.", specName)); + //delete + doDelete(buildProfile, specName); + logger.info(format("Creating Spec - %s", specName)); + //create + doCreate(buildProfile, specName, spec); + break; + } + } else { + switch (buildOption) { + case create: + case sync: + case update: + logger.info(format("Creating Spec - %s", specName)); + //create + doCreate(buildProfile, specName, spec); + break; + case delete: + logger.info(format("Spec \"%s\" does not exist. Skipping.", specName)); + break; + } + } + } + }catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Create Spec + * @param specName + * @param specStr + * @throws MojoExecutionException + */ + public void doCreate(BuildProfile profile, String specName, String specStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //parse the {api}/versions/{version}/specs/{spec} + String pattern = "^([a-zA-Z0-9-_]+)\\/versions\\/([a-zA-Z0-9-_]+)\\/specs\\/([a-zA-Z0-9-_]+)$"; //{api}/versions/{version}/specs/{spec} + Pattern p = Pattern.compile(pattern); + Matcher m = p.matcher(specName); + if(m.matches()) { + String apiName = m.group(1); + String version = m.group(2); + String specId = m.group(3); + + VersionName parent = VersionName.of(profile.getProjectId(), profile.getLocation(), apiName, version); + + //replace the name field from {api}/versions/{version}/specs/{spec} to {spec} + specStr = FQDNHelper.replaceFQDNJsonValue("$.name", specId, specStr); + + //update attributes with FQDN if exist + specStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", specStr); + + //update specType with FQDN if exist + specStr = FQDNHelper.updateFQDNJsonValue("$.specType.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),specStr); + + + com.google.cloud.apihub.v1.Spec specObj = ProtoJsonUtil.fromJson(specStr, com.google.cloud.apihub.v1.Spec.class); + apiHubClient.createSpec(parent, specObj, specId); + logger.info("Create success"); + + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Create failure: " + e.getMessage()); + } + } + + /** + * Delete Spec + * @param profile + * @param specName + * @throws MojoExecutionException + */ + public void doDelete(BuildProfile profile, String specName) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + apiHubClient.deleteSpec(format("projects/%s/locations/%s/apis/%s", profile.getProjectId(), profile.getLocation(), specName)); + logger.info("Delete success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Delete failure: " + e.getMessage()); + } + } + + /** + * Update Spec + * @param profile + * @param specName + * @param specStr + * @throws MojoExecutionException + */ + public void doUpdate(BuildProfile profile, String specName, String specStr) throws MojoExecutionException { + ApiHubClient apiHubClient = null; + try { + apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + + //updating the name field in the spec object to projects/{project}/locations/{location}/apis/{api}/versions/{version}/specs/{spec} format as its required by the updateSpec method + specStr = FQDNHelper.updateFQDNJsonValue("$.name", + format("projects/%s/locations/%s/apis", profile.getProjectId(), profile.getLocation()), + specStr); + + //update attributes with FQDN if exist + specStr = FQDNHelper.updateFQDNJsonKey(profile, "attributes", "projects/%s/locations/%s/attributes/%s", specStr); + + //update specType with FQDN if exist + specStr = FQDNHelper.updateFQDNJsonValue("$.specType.attribute", format("projects/%s/locations/%s/attributes", profile.getProjectId(), profile.getLocation()),specStr); + + logger.debug("after modifying: "+ specStr); + + com.google.cloud.apihub.v1.Spec specObj = ProtoJsonUtil.fromJson(specStr, com.google.cloud.apihub.v1.Spec.class); + List fieldMaskValues = new ArrayList<>(); + fieldMaskValues.add("display_name"); + if(FQDNHelper.checkIfJsonElementExist("$.sourceUri", specStr)) + fieldMaskValues.add("source_uri"); + fieldMaskValues.add("lint_response"); + fieldMaskValues.add("attributes"); + if(FQDNHelper.checkIfJsonElementExist("$.contents", specStr)) + fieldMaskValues.add("contents"); + fieldMaskValues.add("spec_type");; + FieldMask updateMask = FieldMask.newBuilder().addAllPaths(fieldMaskValues).build(); + apiHubClient.updateSpec(specObj, updateMask); + logger.info("Update success"); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Update failure: " + e.getMessage()); + } + } + + /** + * Check if Spec exist + * + * @param profile + * @param specName + * @return + * @throws IOException + */ + public static boolean doesSpecExist(BuildProfile profile, String specName) + throws IOException { + try { + logger.info("Checking if Spec - " +specName + " exist"); + ApiHubClient apiHubClient = ApiHubClientSingleton.getInstance(profile).getApiHubClient(); + com.google.cloud.apihub.v1.Spec specResponse = apiHubClient.getSpec(format("projects/%s/locations/%s/apis/%s", profile.getProjectId(), profile.getLocation(), specName)); + if(specResponse == null) + return false; + } + catch (ApiException e) { + if(e.getStatusCode().getCode().equals(Code.NOT_FOUND)) { + return false; + } + } + catch (Exception e) { + throw new IOException(e.getMessage()); + } + return true; + } + +} diff --git a/src/main/java/com/apigee/apihub/config/utils/ApiHubClientSingleton.java b/src/main/java/com/apigee/apihub/config/utils/ApiHubClientSingleton.java index 306f21a..13008e7 100644 --- a/src/main/java/com/apigee/apihub/config/utils/ApiHubClientSingleton.java +++ b/src/main/java/com/apigee/apihub/config/utils/ApiHubClientSingleton.java @@ -25,6 +25,8 @@ import com.google.auth.oauth2.AccessToken; import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.apihub.v1.ApiHubClient; +import com.google.cloud.apihub.v1.ApiHubDependenciesClient; +import com.google.cloud.apihub.v1.ApiHubDependenciesSettings; import com.google.cloud.apihub.v1.ApiHubSettings; public class ApiHubClientSingleton { @@ -33,18 +35,13 @@ public class ApiHubClientSingleton { // Static variable reference of apiHubClient of type ApiHubClientSingleton private static ApiHubClientSingleton apiHubClientObj = null; + // Static variable reference of apiHubClient of type ApiHubClientSingleton + private static ApiHubClientSingleton apiHubDependenciesClientObj = null; private ApiHubClient apiHubClient; - - public void setApiHubClient(ApiHubClient apiHubClient) { - this.apiHubClient = apiHubClient; - } - - public ApiHubClient getApiHubClient() { - return apiHubClient; - } + private ApiHubDependenciesClient apiHubDependenciesClient; - private ApiHubClientSingleton(BuildProfile profile) throws Exception { + private ApiHubClientSingleton(BuildProfile profile, String clientType) throws Exception { GoogleCredentials credentials = null; try { if(profile.getServiceAccountFilePath() == null && profile.getBearer() == null) { @@ -59,20 +56,55 @@ else if(profile.getServiceAccountFilePath()!=null) { logger.info("Using the bearer token"); credentials = GoogleCredentials.newBuilder().setAccessToken(new AccessToken(profile.getBearer(), null)).build(); } - ApiHubSettings hubSettings = ApiHubSettings.newHttpJsonBuilder() - .setCredentialsProvider(FixedCredentialsProvider.create(credentials)).build(); - setApiHubClient(ApiHubClient.create(hubSettings)); + //apihub + if(clientType!=null && clientType.equals("apis")) { + ApiHubSettings hubSettings = ApiHubSettings.newHttpJsonBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)).build(); + setApiHubClient(ApiHubClient.create(hubSettings)); + } + //dependencies + if(clientType!=null && clientType.equals("dependencies")) { + ApiHubDependenciesSettings hubDependenciesSettings = ApiHubDependenciesSettings.newHttpJsonBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)).build(); + setApiHubDependenciesClient(ApiHubDependenciesClient.create(hubDependenciesSettings)); + } + } catch (Exception e) { throw e; } } - // Static method to create instance of ApiHubClientSingleton class + // Static method to create instance of ApiHubClient class public static ApiHubClientSingleton getInstance(BuildProfile profile) throws Exception { if (apiHubClientObj == null) - apiHubClientObj = new ApiHubClientSingleton(profile); + apiHubClientObj = new ApiHubClientSingleton(profile, "apis"); return apiHubClientObj; } + + // Static method to create instance of ApiHubDependenciesClient class + public static ApiHubClientSingleton getDependenciesInstance(BuildProfile profile) throws Exception + { + if (apiHubDependenciesClientObj == null) + apiHubDependenciesClientObj = new ApiHubClientSingleton(profile, "dependencies"); + + return apiHubDependenciesClientObj; + } + + public void setApiHubClient(ApiHubClient apiHubClient) { + this.apiHubClient = apiHubClient; + } + + public ApiHubClient getApiHubClient() { + return apiHubClient; + } + + public void setApiHubDependenciesClient(ApiHubDependenciesClient apiHubDependenciesClient) { + this.apiHubDependenciesClient = apiHubDependenciesClient; + } + + public ApiHubDependenciesClient getApiHubDependenciesClient() { + return apiHubDependenciesClient; + } } diff --git a/src/main/java/com/apigee/apihub/config/utils/FQDNHelper.java b/src/main/java/com/apigee/apihub/config/utils/FQDNHelper.java new file mode 100644 index 0000000..de7378a --- /dev/null +++ b/src/main/java/com/apigee/apihub/config/utils/FQDNHelper.java @@ -0,0 +1,194 @@ +/** + * Copyright 2024 Google Inc. + * + * 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.apihub.config.utils; + +import static java.lang.String.format; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; + +public class FQDNHelper { + + static Logger logger = LogManager.getLogger(FQDNHelper.class); + + /** + * To update the FQDN + * @param profile + * @param resource + * @param resourceFQDN + * @param resourceStr + * @return + * @throws IOException + */ + public static String updateFQDNJsonKey (BuildProfile profile, String resource, String resourceFQDN, String resourceStr) throws IOException { + try { + JsonObject originalObject = new Gson().fromJson(resourceStr, JsonObject.class); + if (originalObject.has(resource)) { + logger.debug(format("%s exist, replacing with FQDN", resource)); + JsonObject obj = originalObject.getAsJsonObject(resource); + JsonObject newObj = new JsonObject(); + for (String key : obj.keySet()) { + newObj.add(format(resourceFQDN, profile.getProjectId(), profile.getLocation(), key), obj.get(key)); + } + originalObject.remove(resource); + originalObject.add(resource, newObj); + + return originalObject.toString(); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("updateFQDNJsonKey failure for " + resource + " : " + e.getMessage()); + } + return resourceStr; + } + + + /** + * To update the Json Value with FQDN + * @param jsonPath + * @param newVal + * @param resourceStr + * @return + * @throws IOException + */ + public static String updateFQDNJsonValue (String jsonPath, String newVal, String resourceStr) throws IOException { + try { + if(checkIfJsonElementExist(jsonPath, resourceStr)) { + Configuration configuration = Configuration.builder().options(Option.DEFAULT_PATH_LEAF_TO_NULL).build(); + String attrVal = JsonPath.parse(resourceStr, configuration).read(jsonPath); + if(attrVal!= null) { + String newJson = JsonPath.parse(resourceStr).set(jsonPath, newVal+"/"+attrVal).jsonString(); + return newJson; + } + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("updateFQDNJsonValue failure: " + e.getMessage()); + } + return resourceStr; + } + + /** + * To replace the Json Value with new value + * @param jsonPath + * @param newVal + * @param resourceStr + * @return + * @throws IOException + */ + public static String replaceFQDNJsonValue (String jsonPath, String newVal, String resourceStr) throws IOException { + try { + if(checkIfJsonElementExist(jsonPath, resourceStr)) { + Configuration configuration = Configuration.builder().options(Option.DEFAULT_PATH_LEAF_TO_NULL).build(); + String attrVal = JsonPath.parse(resourceStr, configuration).read(jsonPath); + if(attrVal!= null) { + String newJson = JsonPath.parse(resourceStr).set(jsonPath, newVal).jsonString(); + return newJson; + } + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("replaceFQDNJsonValue failure: " + e.getMessage()); + } + return resourceStr; + } + + /** + * To update the Json String Array with FQDN + * @param jsonPath + * @param newVal + * @param resourceStr + * @return + * @throws IOException + */ + public static String updateFQDNJsonStringArray (String jsonPath, String newVal, String resourceStr) throws IOException { + try { + if(checkIfJsonArrayEmpty(jsonPath, resourceStr)) { + Configuration configuration = Configuration.builder().options(Option.DEFAULT_PATH_LEAF_TO_NULL).build(); + List values = JsonPath.parse(resourceStr, configuration).read(jsonPath); + List updValues= new ArrayList(); + if(values!= null && values.size()>0) { + for (String value : values) { + System.out.println("value: "+ value); + updValues.add(format("%s/%s", newVal, value)); + } + String newJson = JsonPath.parse(resourceStr).set(jsonPath, updValues).jsonString(); + return newJson; + } + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("updateFQDNJsonValue failure: " + e.getMessage()); + } + return resourceStr; + } + + /** + * Check if a json attribute exist using its jsonpath + * @param jsonPath + * @param str + * @return + * @throws Exception + */ + public static boolean checkIfJsonElementExist (String jsonPath, String str) throws Exception { + try { + Configuration configuration = Configuration.builder().options(Option.SUPPRESS_EXCEPTIONS).build(); + Object attrVal = JsonPath.parse(str, configuration).read(jsonPath); + if(attrVal!= null) { + return true; + } + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("checkIfJsonElementExist failure: " + e.getMessage()); + } + return false; + } + + /** + * Check if a json array is empty using its jsonpath + * @param jsonPath + * @param str + * @return + * @throws Exception + */ + public static boolean checkIfJsonArrayEmpty (String jsonPath, String str) throws Exception { + try { + Configuration configuration = Configuration.builder().options(Option.SUPPRESS_EXCEPTIONS).build(); + List attrVal = JsonPath.parse(str, configuration).read(jsonPath); + if(attrVal!= null && attrVal.size()>0) { + return true; + } + } + catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("checkIfJsonArrayEmpty failure: " + e.getMessage()); + } + return false; + } + +}