Skip to content

Commit 6948d47

Browse files
author
Chris Wiechmann
authored
Merge pull request #122 from Axway-API-Management-Plus/api-upgrade
Adds API-Upgrade feature to the CLI
2 parents e754ee6 + cebb912 commit 6948d47

11 files changed

Lines changed: 488 additions & 93 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
88
### Fixed
99
- New API-Manager 7.7-September release settings are ignored during import (See issue [#119](https://github.com/Axway-API-Management-Plus/apim-cli/issues/119))
1010

11+
### Added
12+
- Support to upgrade one or multiple APIs (See issue [#120](https://github.com/Axway-API-Management-Plus/apim-cli/issues/120))
13+
1114
## [1.3.2] 2020-11-25
1215
### Fixed
13-
- Application-Subscription not restored, when API is Republished to be updated and no applications given the API-Config (See issue [#117](https://github.com/Axway-API-Management-Plus/apim-cli/issues/113))
16+
- Application-Subscription not restored, when API is Republished to be updated and no applications given the API-Config (See issue [#117](https://github.com/Axway-API-Management-Plus/apim-cli/issues/117))
1417

1518
## [1.3.1] 2020-11-24
1619
### Fixed

modules/apim-adapter/src/main/java/com/axway/apim/adapter/apis/APIManagerAPIAdapter.java

Lines changed: 73 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -814,72 +814,30 @@ private JsonNode importFromSwagger(API api) throws URISyntaxException, AppExcept
814814
}
815815
}
816816

817-
public void upgradeAccessToNewerAPI(API newAPI, API oldAPI) throws AppException {
818-
if(newAPI.getState().equals(API.STATE_UNPUBLISHED)) {
819-
LOG.debug("No need to grant access to newly created API, as desired state of API is unpublished.");
820-
return;
821-
}
822-
LOG.info("Upgrade access & subscriptions to newly created API.");
823-
817+
public void upgradeAccessToNewerAPI(API apiToUpgrade, API referenceAPI) throws AppException {
818+
upgradeAccessToNewerAPI(apiToUpgrade, referenceAPI, null, null, null);
819+
// Existing applications now got access to the new API, hence we have to update the internal state
820+
// APIManagerAdapter.getInstance().addClientApplications(inTransitState, actualState);
821+
// Additionally we need to preserve existing (maybe manually created) application quotas
824822
URI uri;
825823
HttpEntity entity;
826824
RestAPICall request;
827825
HttpResponse httpResponse = null;
828-
829-
try {
830-
uri = new URIBuilder(cmd.getAPIManagerURL()).setPath(RestAPICall.API_VERSION+"/proxies/upgrade/"+oldAPI.getId()).build();
831-
832-
List<NameValuePair> params = new Vector<NameValuePair>();
833-
params.add(new BasicNameValuePair("upgradeApiId", newAPI.getId()));
834-
835-
entity = new UrlEncodedFormEntity(params, "UTF-8");
836-
837-
request = new POSTRequest(entity, uri, true);
838-
request.setContentType("application/x-www-form-urlencoded");
839-
840-
httpResponse = request.execute();
841-
int statusCode = httpResponse.getStatusLine().getStatusCode();
842-
if(statusCode != 204){
843-
String response = EntityUtils.toString(httpResponse.getEntity());
844-
if(statusCode==403 && response.contains("Unknown API")) {
845-
LOG.warn("Got unexpected error: 'Unknown API' while granting access to newer API ... Try again in 1 second.");
846-
Thread.sleep(1000);
847-
httpResponse = request.execute();
848-
statusCode = httpResponse.getStatusLine().getStatusCode();
849-
if(statusCode != 204) {
850-
LOG.error("Error upgrading access to newer API. Received Status-Code: " +statusCode + ", Response: " + EntityUtils.toString(httpResponse.getEntity()));
851-
throw new AppException("Error upgrading access to newer API. Received Status-Code: " +statusCode, ErrorCode.CANT_CREATE_BE_API);
852-
} else {
853-
LOG.info("Successfully granted access to newer API on retry. Received Status-Code: " +statusCode );
854-
}
855-
}
856-
}
857-
} catch (Exception e) {
858-
throw new AppException("Can't upgrade access to newer API!", ErrorCode.CANT_UPGRADE_API_ACCESS, e);
859-
} finally {
860-
try {
861-
if(httpResponse!=null)
862-
((CloseableHttpResponse)httpResponse).close();
863-
} catch (Exception ignore) {}
864-
}
865-
// Existing applications now got access to the new API, hence we have to update the internal state
866-
// APIManagerAdapter.getInstance().addClientApplications(inTransitState, actualState);
867-
// Additionally we need to preserve existing (maybe manually created) application quotas
868826
boolean updateAppQuota = false;
869-
if(oldAPI.getApplications().size()!=0) {
870-
LOG.debug("Found: "+oldAPI.getApplications().size()+" subscribed applications for this API. Taking over potentially configured quota configuration.");
871-
for(ClientApplication app : oldAPI.getApplications()) {
827+
if(referenceAPI.getApplications().size()!=0) {
828+
LOG.debug("Found: "+referenceAPI.getApplications().size()+" subscribed applications for this API. Taking over potentially configured quota configuration.");
829+
for(ClientApplication app : referenceAPI.getApplications()) {
872830
if(app.getAppQuota()==null) continue;
873831
// REST-API for App-Quota is also returning Default-Quotas, but we have to ignore them here!
874832
if(app.getAppQuota().getId().equals(APIManagerAdapter.APPLICATION_DEFAULT_QUOTA) || app.getAppQuota().getId().equals(APIManagerAdapter.SYSTEM_API_QUOTA)) continue;
875833
for(QuotaRestriction restriction : app.getAppQuota().getRestrictions()) {
876-
if(restriction.getApi().equals(oldAPI.getId())) { // This application has a restriction for this specific API
834+
if(restriction.getApi().equals(referenceAPI.getId())) { // This application has a restriction for this specific API
877835
updateAppQuota = true;
878-
restriction.setApi(newAPI.getId()); // Take over the quota config to new API
836+
restriction.setApi(apiToUpgrade.getId()); // Take over the quota config to new API
879837
if(!restriction.getMethod().equals("*")) { // The restriction is for a specific method
880-
String originalMethodName = APIManagerAdapter.getInstance().methodAdapter.getMethodForId(oldAPI.getId(), restriction.getMethod()).getName();
838+
String originalMethodName = APIManagerAdapter.getInstance().methodAdapter.getMethodForId(referenceAPI.getId(), restriction.getMethod()).getName();
881839
// Try to find the same operation for the newly created API based on the name
882-
String newMethodId = APIManagerAdapter.getInstance().methodAdapter.getMethodForName(newAPI.getId(), originalMethodName).getId();
840+
String newMethodId = APIManagerAdapter.getInstance().methodAdapter.getMethodForName(apiToUpgrade.getId(), originalMethodName).getId();
883841
restriction.setMethod(newMethodId);
884842
}
885843
}
@@ -911,6 +869,67 @@ public void upgradeAccessToNewerAPI(API newAPI, API oldAPI) throws AppException
911869
}
912870
}
913871

872+
public boolean upgradeAccessToNewerAPI(API apiToUpgrade, API referenceAPI, Boolean deprecateRefApi, Boolean retireRefApi, Long retirementDateRefAPI) throws AppException {
873+
if(apiToUpgrade.getState().equals(API.STATE_UNPUBLISHED)) {
874+
LOG.info("API to upgrade has state unpublished.");
875+
return false;
876+
}
877+
if(apiToUpgrade.getId().equals(referenceAPI.getId())) {
878+
LOG.warn("API to upgrade: "+Utils.getAPILogString(apiToUpgrade)+" and "
879+
+ "reference/old API: "+Utils.getAPILogString(referenceAPI)+" are the same. Skip upgrade access to newer API.");
880+
return false;
881+
}
882+
LOG.debug("Upgrade access & subscriptions to API: " + apiToUpgrade.getName() + " " + apiToUpgrade.getVersion() + "("+apiToUpgrade.getId()+")");
883+
884+
URI uri;
885+
HttpEntity entity;
886+
RestAPICall request;
887+
HttpResponse httpResponse = null;
888+
try {
889+
uri = new URIBuilder(cmd.getAPIManagerURL()).setPath(RestAPICall.API_VERSION+"/proxies/upgrade/"+referenceAPI.getId()).build();
890+
891+
List<NameValuePair> params = new Vector<NameValuePair>();
892+
params.add(new BasicNameValuePair("upgradeApiId", apiToUpgrade.getId()));
893+
if(deprecateRefApi != null) params.add(new BasicNameValuePair("deprecate", deprecateRefApi.toString()));
894+
if(retireRefApi != null) params.add(new BasicNameValuePair("retire", retireRefApi.toString()));
895+
if(retirementDateRefAPI != null) params.add(new BasicNameValuePair("retirementDate", formatRetirementDate(retirementDateRefAPI)));
896+
897+
entity = new UrlEncodedFormEntity(params, "UTF-8");
898+
899+
request = new POSTRequest(entity, uri, true);
900+
request.setContentType("application/x-www-form-urlencoded");
901+
902+
httpResponse = request.execute();
903+
int statusCode = httpResponse.getStatusLine().getStatusCode();
904+
if(statusCode != 204){
905+
String response = EntityUtils.toString(httpResponse.getEntity());
906+
if(statusCode==403 && response.contains("Unknown API")) {
907+
LOG.warn("Got unexpected error: 'Unknown API' while granting access to newer API ... Try again in 1 second.");
908+
Thread.sleep(1000);
909+
httpResponse = request.execute();
910+
statusCode = httpResponse.getStatusLine().getStatusCode();
911+
if(statusCode != 204) {
912+
LOG.error("Error upgrading access to newer API. Received Status-Code: " +statusCode + ", Response: " + EntityUtils.toString(httpResponse.getEntity()));
913+
throw new AppException("Error upgrading access to newer API. Received Status-Code: " +statusCode, ErrorCode.CANT_CREATE_BE_API);
914+
} else {
915+
LOG.info("Successfully granted access to newer API on retry. Received Status-Code: " +statusCode );
916+
}
917+
} else {
918+
LOG.error("Error upgrading access to newer API. Received Status-Code: " +statusCode + ", Response: " + response);
919+
throw new AppException("Error upgrading access to newer API. Received Status-Code: " +statusCode, ErrorCode.CANT_CREATE_BE_API);
920+
}
921+
}
922+
return true;
923+
} catch (Exception e) {
924+
throw new AppException("Can't upgrade access to newer API!", ErrorCode.CANT_UPGRADE_API_ACCESS, e);
925+
} finally {
926+
try {
927+
if(httpResponse!=null)
928+
((CloseableHttpResponse)httpResponse).close();
929+
} catch (Exception ignore) {}
930+
}
931+
}
932+
914933
public void grantClientOrganization(List<Organization> grantAccessToOrgs, API api, boolean allOrgs) throws AppException {
915934
URI uri;
916935
HttpEntity entity;

modules/apim-adapter/src/main/java/com/axway/apim/api/API.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.axway.apim.api.model.TagMap;
2323
import com.axway.apim.api.model.apps.ClientApplication;
2424
import com.axway.apim.lib.APIPropertyAnnotation;
25-
import com.axway.apim.lib.errorHandling.AppException;
2625
import com.fasterxml.jackson.annotation.JsonAlias;
2726
import com.fasterxml.jackson.annotation.JsonFilter;
2827
import com.fasterxml.jackson.annotation.JsonIgnore;

modules/apim-adapter/src/main/java/com/axway/apim/lib/utils/Utils.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,27 @@
77
import java.net.URLDecoder;
88
import java.nio.charset.StandardCharsets;
99
import java.nio.file.Files;
10+
import java.text.ParseException;
11+
import java.text.SimpleDateFormat;
12+
import java.time.ZoneId;
13+
import java.util.Arrays;
14+
import java.util.Date;
1015
import java.util.HashMap;
1116
import java.util.Iterator;
1217
import java.util.LinkedHashMap;
1318
import java.util.List;
19+
import java.util.Locale;
1420
import java.util.Map;
1521
import java.util.Scanner;
22+
import java.util.TimeZone;
1623

1724
import org.apache.commons.text.StringSubstitutor;
1825
import org.slf4j.Logger;
1926
import org.slf4j.LoggerFactory;
2027

2128
import com.axway.apim.adapter.APIManagerAdapter;
2229
import com.axway.apim.api.model.CustomProperties.Type;
30+
import com.axway.apim.api.API;
2331
import com.axway.apim.api.model.CustomPropertiesEntity;
2432
import com.axway.apim.api.model.CustomProperty;
2533
import com.axway.apim.api.model.CustomProperty.Option;
@@ -239,4 +247,31 @@ public static void addCustomPropertiesForEntity(List<? extends CustomPropertiesE
239247
entity.setCustomProperties((customProperties.size()==0) ? null : customProperties);
240248
}
241249
}
250+
251+
public static Long getParsedDate(String date) throws AppException {
252+
List<String> dateFormats = Arrays.asList("dd.MM.yyyy", "dd/MM/yyyy", "yyyy-MM-dd", "dd-MM-yyyy");
253+
SimpleDateFormat format;
254+
Date retDate = null;
255+
for (String dateFormat : dateFormats) {
256+
format = new SimpleDateFormat(dateFormat, Locale.US);
257+
format.setTimeZone(TimeZone.getTimeZone(ZoneId.of("Z")));
258+
try {
259+
retDate = format.parse(date);
260+
} catch (ParseException e) { }
261+
if(retDate!=null && retDate.after(new Date())) {
262+
LOG.info("Parsed retirementDate: '"+date+"' using format: '"+dateFormat+"' to: '"+retDate+"'");
263+
break;
264+
}
265+
}
266+
if(retDate==null || retDate.before(new Date())) {
267+
ErrorState.getInstance().setError("Unable to parse the given retirementDate using the following formats: " + dateFormats, ErrorCode.CANT_READ_CONFIG_FILE, false);
268+
throw new AppException("Cannnot parse given retirementDate", ErrorCode.CANT_READ_CONFIG_FILE);
269+
}
270+
return retDate.getTime();
271+
}
272+
273+
public static String getAPILogString(API api) {
274+
if(api==null) return "N/A";
275+
return api.getName() + " " + api.getVersion() + " ("+api.getVersion()+")";
276+
}
242277
}

modules/apis/src/main/java/com/axway/apim/APIExportApp.java

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
import com.axway.apim.api.API;
1111
import com.axway.apim.api.export.impl.APIResultHandler;
1212
import com.axway.apim.api.export.impl.APIResultHandler.APIListImpl;
13-
import com.axway.apim.api.export.lib.cli.CLIAPIApproveOptions;
1413
import com.axway.apim.api.export.lib.cli.CLIAPIDeleteOptions;
1514
import com.axway.apim.api.export.lib.cli.CLIAPIExportOptions;
1615
import com.axway.apim.api.export.lib.cli.CLIAPIUnpublishOptions;
16+
import com.axway.apim.api.export.lib.cli.CLIAPIUpgradeOptions;
1717
import com.axway.apim.api.export.lib.cli.CLIChangeAPIOptions;
18-
import com.axway.apim.api.export.lib.params.APIApproveParams;
1918
import com.axway.apim.api.export.lib.params.APIChangeParams;
2019
import com.axway.apim.api.export.lib.params.APIExportParams;
20+
import com.axway.apim.api.export.lib.params.APIUpgradeParams;
2121
import com.axway.apim.cli.APIMCLIServiceProvider;
2222
import com.axway.apim.cli.CLIServiceMethod;
23+
import com.axway.apim.lib.ExportResult;
2324
import com.axway.apim.lib.errorHandling.AppException;
2425
import com.axway.apim.lib.errorHandling.ErrorCode;
2526
import com.axway.apim.lib.errorHandling.ErrorCodeMapper;
@@ -34,6 +35,8 @@ public class APIExportApp implements APIMCLIServiceProvider {
3435

3536
private static Logger LOG = LoggerFactory.getLogger(APIExportApp.class);
3637

38+
static ErrorCodeMapper errorCodeMapper = new ErrorCodeMapper();
39+
3740
private static ErrorState errorState = ErrorState.getInstance();
3841

3942
public static void main(String args[]) {
@@ -74,10 +77,10 @@ public int exportAPI(APIExportParams params) {
7477
if(errorState.hasError()) {
7578
errorState.logErrorMessages(LOG);
7679
if(errorState.isLogStackTrace()) LOG.error(e.getMessage(), e);
77-
return new ErrorCodeMapper().getMapedErrorCode(errorState.getErrorCode()).getCode();
80+
return errorCodeMapper.getMapedErrorCode(errorState.getErrorCode()).getCode();
7881
} else {
7982
LOG.error(e.getMessage(), e);
80-
return new ErrorCodeMapper().getMapedErrorCode(e.getErrorCode()).getCode();
83+
return errorCodeMapper.getMapedErrorCode(e.getErrorCode()).getCode();
8184
}
8285
} catch (Exception e) {
8386
LOG.error(e.getMessage(), e);
@@ -138,7 +141,7 @@ public static int approve(String args[]) {
138141
try {
139142
deleteInstances();
140143

141-
APIApproveParams params = (APIApproveParams) CLIAPIApproveOptions.create(args).getParams();
144+
APIUpgradeParams params = (APIUpgradeParams) CLIAPIUpgradeOptions.create(args).getParams();
142145

143146
return execute(params, APIListImpl.API_APPROVE_HANDLER);
144147
} catch (Exception e) {
@@ -147,6 +150,22 @@ public static int approve(String args[]) {
147150
}
148151
}
149152

153+
@CLIServiceMethod(name = "upgrade", description = "Upgrades one or more APIs based on a given 'old/reference' API.")
154+
public static int upgrade(String args[]) {
155+
try {
156+
deleteInstances();
157+
158+
APIUpgradeParams params = (APIUpgradeParams) CLIAPIUpgradeOptions.create(args).getParams();
159+
160+
APIExportApp app = new APIExportApp();
161+
ExportResult result = app.uprgradeAPI(params, APIListImpl.API_UPGRADE_HANDLE);
162+
return result.getRc();
163+
} catch (Exception e) {
164+
LOG.error(e.getMessage(), e);
165+
return ErrorCode.UNXPECTED_ERROR.getCode();
166+
}
167+
}
168+
150169
private static int execute(APIExportParams params, APIListImpl resultHandlerImpl) {
151170
try {
152171

@@ -194,6 +213,64 @@ private static int execute(APIExportParams params, APIListImpl resultHandlerImpl
194213
}
195214
}
196215

216+
public ExportResult uprgradeAPI(APIUpgradeParams params, APIListImpl resultHandlerImpl) {
217+
ExportResult result = new ExportResult();
218+
try {
219+
params.validateRequiredParameters();
220+
// We need to clean some Singleton-Instances, as tests are running in the same JVM
221+
APIManagerAdapter.deleteInstance();
222+
ErrorState.deleteInstance();
223+
APIMHttpClient.deleteInstances();
224+
225+
APIManagerAdapter apimanagerAdapter = APIManagerAdapter.getInstance();
226+
if(!APIManagerAdapter.hasAdminAccount()) {
227+
LOG.error("Upgrading API-Access needs admin access.");
228+
result.setRc(ErrorCode.NO_ADMIN_ROLE_USER.getCode());
229+
return result;
230+
}
231+
// Get the reference API from API-Manager
232+
API referenceAPI = apimanagerAdapter.apiAdapter.getAPI(params.getReferenceAPIFilter(), true);
233+
if(referenceAPI == null) {
234+
LOG.info("Published reference API for upgrade not found using filter: " + params.getReferenceAPIFilter());
235+
return result;
236+
}
237+
params.setReferenceAPI(referenceAPI);
238+
// Get all APIs to be upgraded
239+
APIResultHandler resultHandler = APIResultHandler.create(resultHandlerImpl, params);
240+
APIFilter filter = resultHandler.getFilter();
241+
List<API> apis = apimanagerAdapter.apiAdapter.getAPIs(filter, true);
242+
243+
if(apis.size()==0) {
244+
LOG.info("No published APIs found using filter: " + filter);
245+
} else {
246+
LOG.info(apis.size() + " API(s) selected.");
247+
resultHandler.execute(apis);
248+
if(resultHandler.hasError()) {
249+
LOG.info("");
250+
LOG.error("Please check the log. At least one error was recorded.");
251+
} else {
252+
LOG.debug("Successfully selected " + apis.size() + " API(s).");
253+
}
254+
}
255+
return result;
256+
} catch (AppException ap) {
257+
ErrorState errorState = ErrorState.getInstance();
258+
if(errorState.hasError()) {
259+
errorState.logErrorMessages(LOG);
260+
if(errorState.isLogStackTrace()) LOG.error(ap.getMessage(), ap);
261+
result.setRc(errorCodeMapper.getMapedErrorCode(errorState.getErrorCode()).getCode());
262+
} else {
263+
LOG.error(ap.getMessage(), ap);
264+
result.setRc(errorCodeMapper.getMapedErrorCode(ap.getErrorCode()).getCode());
265+
}
266+
return result;
267+
} catch (Exception e) {
268+
LOG.error(e.getMessage(), e);
269+
result.setRc(ErrorCode.UNXPECTED_ERROR.getCode());
270+
return result;
271+
}
272+
}
273+
197274
private static void deleteInstances() throws AppException {
198275
// We need to clean some Singleton-Instances, as tests are running in the same JVM
199276
APIManagerAdapter.deleteInstance();

modules/apis/src/main/java/com/axway/apim/api/export/impl/APIResultHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.axway.apim.api.export.ExportAPI;
2323
import com.axway.apim.api.export.lib.params.APIExportParams;
2424
import com.axway.apim.api.model.CustomProperties.Type;
25-
import com.axway.apim.api.model.CustomProperty;
2625
import com.axway.apim.api.model.DeviceType;
2726
import com.axway.apim.api.model.InboundProfile;
2827
import com.axway.apim.api.model.Organization;
@@ -49,7 +48,8 @@ public enum APIListImpl {
4948
API_PUBLISH_HANDLER(PublishAPIHandler.class),
5049
API_UNPUBLISH_HANDLER(UnpublishAPIHandler.class),
5150
API_CHANGE_HANDLER(APIChangeHandler.class),
52-
API_APPROVE_HANDLER(ApproveAPIHandler.class);
51+
API_APPROVE_HANDLER(ApproveAPIHandler.class),
52+
API_UPGRADE_HANDLE(UpgradeAPIHandler.class);
5353

5454
private final Class<APIResultHandler> implClass;
5555

0 commit comments

Comments
 (0)