From 5344bc4f07288842bb850145bcf80a084bd94114 Mon Sep 17 00:00:00 2001 From: wxq Date: Wed, 30 Aug 2023 20:24:30 +0800 Subject: [PATCH] feat(openapi): create app (#32) * feat(openapi): create app * feat(openapi): create app * fix: remove create app by env * feat: support init admins when create app * refactor: path "apps/create" -> "apps" * test: move to IntegrationTest * Update AppOpenApiServiceTest.java * refactor: use OpenCreateAppDTO instead of OpenAppDTO * test: testCreateAppThenCreateNamespaceThenRelease * Update CHANGES.md * add assignAppRoleToSelf to mark role permission * testCreateAppButHaveNoAppRole * refactor: use composite instead of extend OpenAppDTO * Update OpenCreateAppDTO.java * test: make test more complex. create the cluster too --- CHANGES.md | 10 +- .../apollo/openapi/api/AppOpenApiService.java | 5 + .../openapi/client/ApolloOpenApiClient.java | 4 + .../service/AbstractOpenApiService.java | 4 + .../client/service/AppOpenApiService.java | 19 ++ .../apollo/openapi/dto/OpenCreateAppDTO.java | 71 ++++++ .../ApolloOpenApiClientIntegrationTest.java | 227 ++++++++++++++++++ .../client/service/AppOpenApiServiceTest.java | 80 ++++++ 8 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenCreateAppDTO.java create mode 100644 apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java diff --git a/CHANGES.md b/CHANGES.md index 4344deb4..5742c0a2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,9 +5,11 @@ Release Notes. Apollo Java 2.2.0 ------------------ -[refactor(apollo-client): Optimize the exception message when failing to retrieve configuration information.](https://github.com/apolloconfig/apollo-java/pull/22) -[Add JUnit5 extension support for apollo mock server.](https://github.com/apolloconfig/apollo-java/pull/25) -[Support concurrent loading of Config for different namespaces.](https://github.com/apolloconfig/apollo-java/pull/31) -[Fix snakeyaml 2.x compatibility issues](https://github.com/apolloconfig/apollo-java/pull/35) +* [refactor(apollo-client): Optimize the exception message when failing to retrieve configuration information.](https://github.com/apolloconfig/apollo-java/pull/22) +* [Add JUnit5 extension support for apollo mock server.](https://github.com/apolloconfig/apollo-java/pull/25) +* [Support concurrent loading of Config for different namespaces.](https://github.com/apolloconfig/apollo-java/pull/31) +* [Fix snakeyaml 2.x compatibility issues](https://github.com/apolloconfig/apollo-java/pull/35) +* [feat(openapi): create app](https://github.com/apolloconfig/apollo-java/pull/32) + ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/2?closed=1) \ No newline at end of file diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/api/AppOpenApiService.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/api/AppOpenApiService.java index 2b4d1c9d..81365264 100644 --- a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/api/AppOpenApiService.java +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/api/AppOpenApiService.java @@ -17,6 +17,7 @@ package com.ctrip.framework.apollo.openapi.api; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.dto.OpenEnvClusterDTO; import java.util.List; @@ -25,6 +26,10 @@ */ public interface AppOpenApiService { + default void createApp(OpenCreateAppDTO req) { + throw new UnsupportedOperationException(); + } + List getEnvClusterInfo(String appId); List getAllApps(); diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java index a0688238..6981397e 100644 --- a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java @@ -65,6 +65,10 @@ private ApolloOpenApiClient(String portalUrl, String token, RequestConfig reques releaseService = new ReleaseOpenApiService(client, baseUrl, GSON); } + public void createApp(OpenCreateAppDTO req) { + appService.createApp(req); + } + /** * Get the environment and cluster information */ diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AbstractOpenApiService.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AbstractOpenApiService.java index 811ee890..464bb1f9 100644 --- a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AbstractOpenApiService.java +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AbstractOpenApiService.java @@ -97,6 +97,10 @@ private void checkHttpResponseStatus(HttpResponse response) { throw new ApolloOpenApiException(status.getStatusCode(), status.getReasonPhrase(), message); } + protected void checkNotNull(Object value, String name) { + Preconditions.checkArgument(null != value, name + " should not be null"); + } + protected void checkNotEmpty(String value, String name) { Preconditions.checkArgument(!Strings.isNullOrEmpty(value), name + " should not be null or empty"); } diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiService.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiService.java index 4fe38911..856217d1 100644 --- a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiService.java +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiService.java @@ -18,6 +18,7 @@ import com.ctrip.framework.apollo.openapi.client.url.OpenApiPathBuilder; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO; import com.ctrip.framework.apollo.openapi.dto.OpenEnvClusterDTO; import com.google.common.base.Joiner; import com.google.gson.Gson; @@ -39,6 +40,24 @@ public AppOpenApiService(CloseableHttpClient client, String baseUrl, Gson gson) super(client, baseUrl, gson); } + @Override + public void createApp(OpenCreateAppDTO req) { + OpenAppDTO app = req.getApp(); + checkNotNull(app, "App"); + checkNotEmpty(app.getAppId(), "App id"); + checkNotEmpty(app.getName(), "App name"); + OpenApiPathBuilder pathBuilder = OpenApiPathBuilder.newBuilder() + .customResource("apps"); + + try (CloseableHttpResponse response = post(pathBuilder, req)) { + gson.fromJson(EntityUtils.toString(response.getEntity()), void.class); + } catch (Throwable ex) { + throw new RuntimeException( + String.format("Create app: %s for appId: %s failed", app.getName(), + app.getAppId()), ex); + } + } + @Override public List getEnvClusterInfo(String appId) { checkNotEmpty(appId, "App id"); diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenCreateAppDTO.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenCreateAppDTO.java new file mode 100644 index 00000000..5b09a711 --- /dev/null +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenCreateAppDTO.java @@ -0,0 +1,71 @@ +/* + * Copyright 2022 Apollo Authors + * + * 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.ctrip.framework.apollo.openapi.dto; + +import java.util.Set; + +public class OpenCreateAppDTO { + + /** + * when {@code assignAppRoleToSelf} is true, + * you can do anything with the app by current token! + */ + private boolean assignAppRoleToSelf; + + /** + * The application owner has project administrator permission by default. + *

+ * Administrators can create namespace, cluster, and assign user permissions + */ + private Set admins; + + private OpenAppDTO app; + + public boolean isAssignAppRoleToSelf() { + return assignAppRoleToSelf; + } + + public void setAssignAppRoleToSelf(boolean assignAppRoleToSelf) { + this.assignAppRoleToSelf = assignAppRoleToSelf; + } + + public Set getAdmins() { + return admins; + } + + public void setAdmins(Set admins) { + this.admins = admins; + } + + public OpenAppDTO getApp() { + return app; + } + + public void setApp(OpenAppDTO app) { + this.app = app; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("OpenCreateAppDTO{"); + sb.append("assignAppRoleToSelf='").append(assignAppRoleToSelf).append('\''); + sb.append(", admins='").append(admins).append('\''); + sb.append(", app=").append(app); + sb.append('}'); + return sb.toString(); + } +} diff --git a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java new file mode 100644 index 00000000..9d931ba2 --- /dev/null +++ b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2022 Apollo Authors + * + * 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.ctrip.framework.apollo.openapi.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenClusterDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenItemDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenNamespaceDTO; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Do not run in 'mvn clean test', + * left code here for develop test, + * you can run the method by ide. + */ +class ApolloOpenApiClientIntegrationTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final ApolloOpenApiClient client = newClient(); + + private final String env = "DEV"; + private final String clusterName = "default"; + + ApolloOpenApiClient newClient() { + String someUrl = "http://localhost:8070"; +// String someToken = "0627b87948c30517157e8b2a9565e473b5a97323a50128f584838ed10559d3fd"; + String someToken = "9d0a241e9cb2300f302a875b1195340b2b6f56373cf5ca5d006a3f4e1a46b3ef"; + + return ApolloOpenApiClient.newBuilder() + .withPortalUrl(someUrl) + .withToken(someToken) + .withReadTimeout(2000 * 1000) + .withConnectTimeout(2000 * 1000) + .build(); + } + + void createApp(String appId, String ownerName, boolean assignAppRoleToSelf, String ... admins) { + { + OpenAppDTO app = new OpenAppDTO(); + app.setName("openapi create app 测试名字 " + appId); + app.setAppId(appId); + app.setOwnerName(ownerName); + app.setOwnerEmail(ownerName + "@apollo.apollo"); + app.setOrgId("orgIdFromOpenapi"); + app.setOrgName("orgNameFromOpenapi"); + OpenCreateAppDTO req = new OpenCreateAppDTO(); + req.setApp(app); + req.setAdmins(new HashSet<>(Arrays.asList(admins))); + req.setAssignAppRoleToSelf(assignAppRoleToSelf); + log.info("create app {}, ownerName {} assignAppRoleToSelf {}", appId, ownerName, assignAppRoleToSelf); + client.createApp(req); + } + } + + @Test + @Disabled("only for integration test") + public void testCreateApp() { + final String appId = "openapi-create-app1"; + final String ownerName = "user-test-xxx1"; + createApp(appId, ownerName, false, "user-test-xxx2", "user3"); + + List list = client.getAppsByIds(Collections.singletonList(appId)); + assertEquals(1, list.size()); + OpenAppDTO openAppDTO = list.get(0); + log.info("{}", openAppDTO); + assertEquals(appId, openAppDTO.getAppId()); + assertEquals(ownerName, openAppDTO.getOwnerName()); + } + + + @Test + @Disabled("only for integration test") + public void testCreateAppButHaveNoAppRole() { + // create app + final String appIdSuffix = LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyyMMdd-HH-mm-ss") + ); + final String appId = "openapi-create-app-" + appIdSuffix; + final String ownerName = "test-create-release1"; + createApp(appId, ownerName, false, "user-test-xxx1", "user-test-xxx2"); + + { + List list = client.getAppsByIds(Collections.singletonList(appId)); + assertEquals(1, list.size()); + OpenAppDTO openAppDTO = list.get(0); + log.info("{}", openAppDTO); + assertEquals(appId, openAppDTO.getAppId()); + assertEquals(ownerName, openAppDTO.getOwnerName()); + } + + // create namespace + final String namespaceName = "openapi-create-namespace"; + { + OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); + dto.setName(namespaceName); + dto.setAppId(appId); + dto.setComment("create from openapi"); + dto.setDataChangeCreatedBy(ownerName); + log.info("create namespace {} should fail because have no app role", namespaceName); + assertThrows(RuntimeException.class, () -> client.createAppNamespace(dto)); + } + } + + @Test + @Disabled("only for integration test") + public void testCreateAppThenCreateClusterCreateNamespaceThenRelease() { + // create app + final String appIdSuffix = LocalDateTime.now().format( + DateTimeFormatter.ofPattern("yyyyMMdd-HH-mm-ss") + ); + final String appId = "openapi-create-app-" + appIdSuffix; + final String ownerName = "test-create-release1"; + createApp(appId, ownerName, true, "user-test-xxx1", "user-test-xxx2"); + + { + List list = client.getAppsByIds(Collections.singletonList(appId)); + assertEquals(1, list.size()); + OpenAppDTO openAppDTO = list.get(0); + log.info("{}", openAppDTO); + assertEquals(appId, openAppDTO.getAppId()); + assertEquals(ownerName, openAppDTO.getOwnerName()); + } + + // create cluster + final String clusterName = "cluster-openapi"; + { + OpenClusterDTO dto = new OpenClusterDTO(); + dto.setAppId(appId); + dto.setName(clusterName); + dto.setDataChangeCreatedBy(ownerName); + log.info("create cluster {}", clusterName); + client.createCluster(env, dto); + } + + // create namespace + final String namespaceName = "openapi-create-namespace"; + { + OpenAppNamespaceDTO dto = new OpenAppNamespaceDTO(); + dto.setName(namespaceName); + dto.setAppId(appId); + dto.setComment("create from openapi"); + dto.setDataChangeCreatedBy(ownerName); + log.info("create namespace {}", namespaceName); + client.createAppNamespace(dto); + } + + // modify + // k1=v1 + { + OpenItemDTO itemDTO = new OpenItemDTO(); + itemDTO.setKey("k1"); + itemDTO.setValue("v1"); + itemDTO.setDataChangeCreatedBy(ownerName); + client.createOrUpdateItem( + appId, env, clusterName, namespaceName, itemDTO + ); + } + // k2=v2 + { + OpenItemDTO itemDTO = new OpenItemDTO(); + itemDTO.setKey("k2"); + itemDTO.setValue("v2"); + itemDTO.setDataChangeCreatedBy(ownerName); + client.createOrUpdateItem( + appId, env, clusterName, namespaceName, itemDTO + ); + } + + // release namespace + { + NamespaceReleaseDTO dto = new NamespaceReleaseDTO(); + dto.setReleaseTitle("openapi-release"); + dto.setReleasedBy(ownerName); + dto.setReleaseComment("test openapi release in " + LocalDateTime.now()); + log.info("release namespace {}", namespaceName); + client.publishNamespace(appId, env, clusterName, namespaceName, dto); + } + + // read then namespace + { + OpenNamespaceDTO namespaceDTO + = client.getNamespace(appId, env, clusterName, namespaceName); + List items = namespaceDTO.getItems(); + Map map = new HashMap<>(16); + for (OpenItemDTO item : items) { + map.put(item.getKey(), item.getValue()); + } + assertEquals(2, map.size()); + assertEquals("v1", map.get("k1")); + assertEquals("v2", map.get("k2")); + log.info("create app {} namespace {} and release {} success", appId, namespaceName, map); + } + } + +} diff --git a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiServiceTest.java b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiServiceTest.java index ef3e9b7e..428b377e 100644 --- a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiServiceTest.java +++ b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/service/AppOpenApiServiceTest.java @@ -17,11 +17,19 @@ package com.ctrip.framework.apollo.openapi.client.service; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; +import com.ctrip.framework.apollo.openapi.dto.OpenCreateAppDTO; +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.HashSet; import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.BasicHttpEntity; import org.apache.http.entity.StringEntity; import org.junit.Before; import org.junit.Test; @@ -65,4 +73,76 @@ public void testGetEnvClusterInfoWithError() throws Exception { appOpenApiService.getEnvClusterInfo(someAppId); } + + @Test(expected = RuntimeException.class) + public void testCreateAppNullApp() throws Exception { + OpenCreateAppDTO req = new OpenCreateAppDTO(); + appOpenApiService.createApp(req); + } + + @Test(expected = RuntimeException.class) + public void testCreateAppEmptyAppId() throws Exception { + OpenCreateAppDTO req = new OpenCreateAppDTO(); + req.setApp(new OpenAppDTO()); + appOpenApiService.createApp(req); + } + + @Test(expected = RuntimeException.class) + public void testCreateAppEmptyAppName() throws Exception { + OpenAppDTO app = new OpenAppDTO(); + app.setAppId("appId1"); + + OpenCreateAppDTO req = new OpenCreateAppDTO(); + req.setApp(app); + appOpenApiService.createApp(req); + } + + @Test(expected = RuntimeException.class) + public void testCreateAppFail() throws Exception { + OpenAppDTO app = new OpenAppDTO(); + app.setAppId("appId1"); + app.setName("name1"); + + OpenCreateAppDTO req = new OpenCreateAppDTO(); + req.setApp(app); + req.setAdmins(new HashSet<>(Arrays.asList("user1", "user2"))); + + when(statusLine.getStatusCode()).thenReturn(400); + + appOpenApiService.createApp(req); + } + + + @Test + public void testCreateAppSuccess() throws Exception { + OpenAppDTO app = new OpenAppDTO(); + app.setAppId("appId1"); + app.setName("name1"); + + OpenCreateAppDTO req = new OpenCreateAppDTO(); + req.setApp(app); + req.setAdmins(new HashSet<>(Arrays.asList("user1", "user2"))); + + when(statusLine.getStatusCode()).thenReturn(200); + { + BasicHttpEntity httpEntity = new BasicHttpEntity(); + httpEntity.setContentLength(0L); + httpEntity.setContent(new ByteArrayInputStream(new byte[0])); + when(someHttpResponse.getEntity()).thenReturn(httpEntity); + } + + appOpenApiService.createApp(req); + + verify(someHttpResponse, atLeastOnce()).getEntity(); + verify(httpClient, atLeastOnce()).execute(argThat(request -> { + if (!"POST".equals(request.getMethod())) { + return false; + } + if (!request.getURI().toString().endsWith("apps")) { + return false; + } + return true; + })); + + } }