diff --git a/CHANGELOG.md b/CHANGELOG.md index 179463118..b92b688d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added +- API import supports `-safeUpdate` to prevent updates when an existing API path belongs to a different API name [#556](https://github.com/Axway-API-Management-Plus/apim-cli/issues/556) - Allow deletion of application credentials like oauth/apikey [#583](https://github.com/Axway-API-Management-Plus/apim-cli/issues/583) - Extend the functionality of "override_certificates" environment property [#565](https://github.com/Axway-API-Management-Plus/apim-cli/issues/565) @@ -213,7 +214,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Support of All / Global quotas for API and application. (See issue [#362](https://github.com/Axway-API-Management-Plus/apim-cli/issues/362)) -- Host docker cli images on github docker registry (See issue [#373](https://github.com/Axway-API-Management-Plus/apim-cli/issues/373) +- Host docker cli images on github docker registry (See issue [#373](https://github.com/Axway-API-Management-Plus/apim-cli/issues/373)) ## [1.13.5] 2023-03-15 diff --git a/modules/apim-adapter/src/main/java/com/axway/apim/lib/error/ErrorCode.java b/modules/apim-adapter/src/main/java/com/axway/apim/lib/error/ErrorCode.java index bf19606ca..7751018ec 100644 --- a/modules/apim-adapter/src/main/java/com/axway/apim/lib/error/ErrorCode.java +++ b/modules/apim-adapter/src/main/java/com/axway/apim/lib/error/ErrorCode.java @@ -70,7 +70,8 @@ public enum ErrorCode { GRANT_ACCESS_APPLICATION_ERR(102, "Error granting application access to API."), REVOKE_ACCESS_APPLICATION_ERR(103, "Error revoking application access to API."), INVALID_SECURITY_PROFILE_CONFIG(104, "The given security profile is invalid.", false), - ERR_IMPORTING_API_DAT_FILE(105, "Error importing API-Dat file.", false); + ERR_IMPORTING_API_DAT_FILE(105, "Error importing API-Dat file.", false), + API_PATH_IN_USE_BY_DIFFERENT_API(106, "Configured API path is already used by a different API.", false); private final int code; diff --git a/modules/apis/src/main/java/com/axway/apim/APIImportApp.java b/modules/apis/src/main/java/com/axway/apim/APIImportApp.java index 0e4d620d1..1bfec23f6 100644 --- a/modules/apis/src/main/java/com/axway/apim/APIImportApp.java +++ b/modules/apis/src/main/java/com/axway/apim/APIImportApp.java @@ -126,6 +126,7 @@ public int importAPI(APIImportParams params) { .useFEAPIDefinition(params.isUseFEAPIDefinition()) // Should API-Definition load from the FE-API? .build(); API actualAPI = apimAdapter.getApiAdapter().getAPI(filter, true); + validateSafeUpdate(actualAPI, desiredAPI, params); APIChangeState changes = new APIChangeState(actualAPI, desiredAPI, params); new APIImportManager().applyChanges(changes, params.isForceUpdate(), params.isUpdateOnly()); APIPropertiesExport.getInstance().store(); @@ -146,6 +147,20 @@ public int importAPI(APIImportParams params) { } } + void validateSafeUpdate(API actualAPI, API desiredAPI, APIImportParams params) throws AppException { + if (!params.isSafeUpdate()) return; + if (actualAPI == null) return; + if (actualAPI.getName() == null || desiredAPI.getName() == null) return; + if (actualAPI.getName().equals(desiredAPI.getName())) return; + if (desiredAPI.getId() != null && desiredAPI.getId().equals(actualAPI.getId())) return; + + throw new AppException( + "Safe update blocked: Path '" + desiredAPI.getPath() + "' is already used by API '" + actualAPI.getName() + + "' (ID: " + actualAPI.getId() + "). Desired API name is '" + desiredAPI.getName() + "'.", + ErrorCode.API_PATH_IN_USE_BY_DIFFERENT_API + ); + } + @Override public String getGroupId() { return "api"; diff --git a/modules/apis/src/main/java/com/axway/apim/apiimport/lib/cli/CLIAPIImportOptions.java b/modules/apis/src/main/java/com/axway/apim/apiimport/lib/cli/CLIAPIImportOptions.java index e8ffdbbbc..b407e0389 100644 --- a/modules/apis/src/main/java/com/axway/apim/apiimport/lib/cli/CLIAPIImportOptions.java +++ b/modules/apis/src/main/java/com/axway/apim/apiimport/lib/cli/CLIAPIImportOptions.java @@ -92,6 +92,10 @@ public void addOptions() { option.setRequired(false); addOption(option); + option = new Option("safeUpdate", "If set, import fails when the same API path is already used by a different API name (unless apiId matches)."); + option.setRequired(false); + addOption(option); + option = new Option("zeroDowntimeUpdate", true,"Always update a published APIs by creating a new API and switch clients to it. Defaults to false"); option.setRequired(false); addOption(option); @@ -131,6 +135,7 @@ public Parameters getParams() { params.setConfig(getValue("config")); params.setApiDefinition(getValue("apidefinition")); params.setForceUpdate(hasOption("forceUpdate")); + params.setSafeUpdate(hasOption("safeUpdate")); params.setChangeOrganization(hasOption("changeOrganization")); params.setUseFEAPIDefinition(hasOption("useFEAPIDefinition")); params.setIgnoreQuotas(hasOption("ignoreQuotas")); diff --git a/modules/apis/src/main/java/com/axway/apim/apiimport/lib/params/APIImportParams.java b/modules/apis/src/main/java/com/axway/apim/apiimport/lib/params/APIImportParams.java index 2d907bcfc..ca9560a9f 100644 --- a/modules/apis/src/main/java/com/axway/apim/apiimport/lib/params/APIImportParams.java +++ b/modules/apis/src/main/java/com/axway/apim/apiimport/lib/params/APIImportParams.java @@ -7,6 +7,7 @@ public class APIImportParams extends StandardImportParams implements Parameters { private boolean forceUpdate; + private boolean safeUpdate; private boolean useFEAPIDefinition; private boolean validateRemoteHost; private boolean updateOnly; @@ -32,6 +33,12 @@ public boolean isForceUpdate() { public void setForceUpdate(boolean forceUpdate) { this.forceUpdate = forceUpdate; } + public boolean isSafeUpdate() { + return safeUpdate; + } + public void setSafeUpdate(boolean safeUpdate) { + this.safeUpdate = safeUpdate; + } public boolean isValidateRemoteHost() { return validateRemoteHost; } diff --git a/modules/apis/src/test/java/com/axway/apim/APIImportAppTest.java b/modules/apis/src/test/java/com/axway/apim/APIImportAppTest.java index 39dce378b..061e6dc27 100644 --- a/modules/apis/src/test/java/com/axway/apim/APIImportAppTest.java +++ b/modules/apis/src/test/java/com/axway/apim/APIImportAppTest.java @@ -8,6 +8,9 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import com.axway.apim.lib.error.AppException; +import com.axway.apim.lib.error.ErrorCode; + public class APIImportAppTest extends WiremockWrapper { @BeforeClass @@ -59,4 +62,59 @@ public void testEmptyPassword() throws Exception { Assert.assertEquals("",desiredAPI.getAuthenticationProfiles().get(0).getParameters().get("password")); } + + @Test + public void safeUpdateAllowsSameName() throws AppException { + APIImportApp app = new APIImportApp(); + APIImportParams params = new APIImportParams(); + params.setSafeUpdate(true); + + API actualApi = new API(); + actualApi.setId("actual-id"); + actualApi.setName("Order API"); + + API desiredApi = new API(); + desiredApi.setId("desired-id"); + desiredApi.setName("Order API"); + desiredApi.setPath("/api/orders/v1"); + + app.validateSafeUpdate(actualApi, desiredApi, params); + } + + @Test + public void safeUpdateAllowsNameMismatchWhenApiIdMatches() throws AppException { + APIImportApp app = new APIImportApp(); + APIImportParams params = new APIImportParams(); + params.setSafeUpdate(true); + + API actualApi = new API(); + actualApi.setId("same-id"); + actualApi.setName("Order API"); + + API desiredApi = new API(); + desiredApi.setId("same-id"); + desiredApi.setName("Inventory API"); + desiredApi.setPath("/api/orders/v1"); + + app.validateSafeUpdate(actualApi, desiredApi, params); + } + + @Test + public void safeUpdateBlocksNameMismatchForSamePath() { + APIImportApp app = new APIImportApp(); + APIImportParams params = new APIImportParams(); + params.setSafeUpdate(true); + + API actualApi = new API(); + actualApi.setId("actual-id"); + actualApi.setName("Order API"); + + API desiredApi = new API(); + desiredApi.setId("different-id"); + desiredApi.setName("Inventory API"); + desiredApi.setPath("/api/orders/v1"); + + AppException exception = Assert.expectThrows(AppException.class, () -> app.validateSafeUpdate(actualApi, desiredApi, params)); + Assert.assertEquals(exception.getError(), ErrorCode.API_PATH_IN_USE_BY_DIFFERENT_API); + } } diff --git a/modules/apis/src/test/java/com/axway/apim/apiimport/lib/cli/APIImportCLIOptionsTest.java b/modules/apis/src/test/java/com/axway/apim/apiimport/lib/cli/APIImportCLIOptionsTest.java index 31bb7ea89..7aa58e7b4 100644 --- a/modules/apis/src/test/java/com/axway/apim/apiimport/lib/cli/APIImportCLIOptionsTest.java +++ b/modules/apis/src/test/java/com/axway/apim/apiimport/lib/cli/APIImportCLIOptionsTest.java @@ -48,11 +48,12 @@ public void testAPIImportParameter() throws AppException { @Test public void testToggles() throws AppException { - String[] args = {"-s", "prod", "-c", "myConfig.json", "-rollback", "true", "-force", "-forceUpdate", "-ignoreCache", "-useFEAPIDefinition", "-changeOrganization", "-ignoreQuotas", "-updateOnly"}; + String[] args = {"-s", "prod", "-c", "myConfig.json", "-rollback", "true", "-force", "-forceUpdate", "-safeUpdate", "-ignoreCache", "-useFEAPIDefinition", "-changeOrganization", "-ignoreQuotas", "-updateOnly"}; CLIOptions options = CLIAPIImportOptions.create(args); APIImportParams params = (APIImportParams) options.getParams(); Assert.assertTrue(params.isForce()); Assert.assertTrue(params.isForceUpdate()); + Assert.assertTrue(params.isSafeUpdate()); Assert.assertTrue(params.isIgnoreCache()); Assert.assertTrue(params.isUpdateOnly()); Assert.assertTrue(params.isChangeOrganization()); @@ -96,4 +97,12 @@ public void testRetireFlagAndDate() throws AppException { Assert.assertTrue(params.isReferenceAPIRetire()); Assert.assertEquals(params.getReferenceAPIRetirementDate(), "31.12.2027"); } + + @Test + public void testSafeUpdateDefault() throws AppException { + String[] args = {"-s", "prod", "-c", "myConfig.json"}; + CLIOptions options = CLIAPIImportOptions.create(args); + APIImportParams params = (APIImportParams) options.getParams(); + Assert.assertFalse(params.isSafeUpdate()); + } }