diff --git a/api/api-elastic-profile-operation-v1/build.gradle b/api/api-elastic-profile-operation-v1/build.gradle new file mode 100644 index 00000000000..6ef146e0458 --- /dev/null +++ b/api/api-elastic-profile-operation-v1/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2018 ThoughtWorks, 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. + */ + +apply plugin: 'jacoco' +apply plugin: 'groovy' + +dependencies { + compile project(':api:api-base') + + testCompile project(path: ':api:api-base', configuration: 'testOutput') + + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: project.versions.junit5 + testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: project.versions.junit5 +} diff --git a/api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1.java b/api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1.java new file mode 100644 index 00000000000..34378f5c820 --- /dev/null +++ b/api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 ThoughtWorks, 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.thoughtworks.go.apiv1.elasticprofileoperation; + +import com.thoughtworks.go.api.ApiController; +import com.thoughtworks.go.api.ApiVersion; +import com.thoughtworks.go.api.spring.ApiAuthenticationHelper; +import com.thoughtworks.go.apiv1.elasticprofileoperation.representers.ElasticProfileUsageRepresenter; +import com.thoughtworks.go.config.exceptions.RecordNotFoundException; +import com.thoughtworks.go.domain.ElasticProfileUsage; +import com.thoughtworks.go.server.service.ElasticProfileService; +import com.thoughtworks.go.spark.Routes; +import com.thoughtworks.go.spark.spring.SparkSpringController; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import spark.Request; +import spark.Response; + +import java.util.Collection; + +import static spark.Spark.*; + +@Component +public class ElasticProfileOperationControllerV1 extends ApiController implements SparkSpringController { + private static final String PROFILE_ID_PARAM = "profile_id"; + private ElasticProfileService elasticProfileService; + private final ApiAuthenticationHelper apiAuthenticationHelper; + + @Autowired + public ElasticProfileOperationControllerV1(ElasticProfileService elasticProfileService, ApiAuthenticationHelper apiAuthenticationHelper) { + super(ApiVersion.v1); + this.elasticProfileService = elasticProfileService; + this.apiAuthenticationHelper = apiAuthenticationHelper; + } + + @Override + public String controllerBasePath() { + return Routes.ElasticProfileAPI.INTERNAL_BASE; + } + + @Override + public void setupRoutes() { + path(controllerBasePath(), () -> { + before("", this::setContentType); + before("/*", this::setContentType); + before("", mimeType, apiAuthenticationHelper::checkAdminUserOrGroupAdminUserAnd403); + before("/*", mimeType, apiAuthenticationHelper::checkAdminUserOrGroupAdminUserAnd403); + + get(Routes.ElasticProfileAPI.ID + Routes.ElasticProfileAPI.USAGES, mimeType, this::usages); + exception(RecordNotFoundException.class, this::notFound); + }); + } + + public String usages(Request request, Response response) { + final String elasticProfileId = StringUtils.stripToEmpty(request.params(PROFILE_ID_PARAM)); + final Collection jobsUsingElasticProfile = elasticProfileService.getUsageInformation(elasticProfileId); + return ElasticProfileUsageRepresenter.toJSON(jobsUsingElasticProfile); + } +} diff --git a/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenter.java b/api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenter.java similarity index 94% rename from api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenter.java rename to api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenter.java index 37103a7970c..2f1b851cd94 100644 --- a/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenter.java +++ b/api/api-elastic-profile-operation-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.go.apiv1.elasticprofile.representers; +package com.thoughtworks.go.apiv1.elasticprofileoperation.representers; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; diff --git a/api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1Test.groovy b/api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1Test.groovy new file mode 100644 index 00000000000..38ddcf2d9f9 --- /dev/null +++ b/api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/ElasticProfileOperationControllerV1Test.groovy @@ -0,0 +1,118 @@ +/* + * Copyright 2018 ThoughtWorks, 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.thoughtworks.go.apiv1.elasticprofileoperation + +import com.thoughtworks.go.api.SecurityTestTrait +import com.thoughtworks.go.api.spring.ApiAuthenticationHelper +import com.thoughtworks.go.config.exceptions.RecordNotFoundException +import com.thoughtworks.go.domain.ElasticProfileUsage +import com.thoughtworks.go.server.service.ElasticProfileService +import com.thoughtworks.go.spark.ControllerTrait +import com.thoughtworks.go.spark.GroupAdminUserSecurity +import com.thoughtworks.go.spark.SecurityServiceTrait +import groovy.json.JsonBuilder +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.mockito.Mock + +import static org.mockito.Mockito.when +import static org.mockito.MockitoAnnotations.initMocks + +class ElasticProfileOperationControllerV1Test implements SecurityServiceTrait, ControllerTrait { + + @Mock + private ElasticProfileService elasticProfileService + + @BeforeEach + void setup() { + initMocks(this) + } + + @Override + ElasticProfileOperationControllerV1 createControllerInstance() { + new ElasticProfileOperationControllerV1(elasticProfileService, new ApiAuthenticationHelper(securityService, goConfigService)) + } + + + @Nested + class Usages { + @Nested + class Security implements SecurityTestTrait, GroupAdminUserSecurity { + + @Override + String getControllerMethodUnderTest() { + return "usages" + } + + @Override + void makeHttpCall() { + getWithApiHeader(controller.controllerPath("/docker/usages")) + } + } + + @Nested + class AsAGroupAdmin { + + @BeforeEach + void setUp() { + enableSecurity() + loginAsGroupAdmin() + } + + @Test + void 'should list jobs associated with a profile id'() { + def elasticProfileUsages = Arrays.asList( + new ElasticProfileUsage("LinuxPR", "build", "compile", "linux-pr"), + new ElasticProfileUsage("LinuxPR", "build", "test", "linux-pr"), + + new ElasticProfileUsage("WindowsPR", "clean", "clean-dirs"), + new ElasticProfileUsage("WindowsPR", "clean", "clean-artifacts") + ) + + when(elasticProfileService.getUsageInformation("docker")).thenReturn(elasticProfileUsages) + + getWithApiHeader(controller.controllerPath("/docker/usages")) + + def expectedResponse = [ + [pipeline_name: "LinuxPR", stage_name: "build", job_name: "compile", template_name: "linux-pr"], + [pipeline_name: "LinuxPR", stage_name: "build", job_name: "test", template_name: "linux-pr"], + + [pipeline_name: "WindowsPR", stage_name: "clean", job_name: "clean-dirs"], + [pipeline_name: "WindowsPR", stage_name: "clean", job_name: "clean-artifacts"] + ] + + assertThatResponse() + .isOk() + .hasContentType(controller.mimeType) + .hasBodyWithJson(new JsonBuilder(expectedResponse).toString()) + } + + @Test + void 'should return 404 when profile with id does not exist'() { + when(elasticProfileService.getUsageInformation("docker")).thenThrow(new RecordNotFoundException("docker")) + + getWithApiHeader(controller.controllerPath("/docker/usages")) + + assertThatResponse() + .isNotFound() + .hasContentType(controller.mimeType) + .hasJsonMessage("Either the resource you requested was not found, or you are not authorized to perform this action.") + } + } + } +} diff --git a/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenterTest.groovy b/api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenterTest.groovy similarity index 96% rename from api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenterTest.groovy rename to api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenterTest.groovy index ff9d6c7a0fe..37a6c9a4913 100644 --- a/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/representers/ElasticProfileUsageRepresenterTest.groovy +++ b/api/api-elastic-profile-operation-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofileoperation/representers/ElasticProfileUsageRepresenterTest.groovy @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.go.apiv1.elasticprofile.representers +package com.thoughtworks.go.apiv1.elasticprofileoperation.representers import com.thoughtworks.go.domain.ElasticProfileUsage import org.junit.jupiter.api.Test diff --git a/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1.java b/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1.java index 8b58fd8702b..54d1b529fe8 100644 --- a/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1.java +++ b/api/api-elastic-profile-v1/src/main/java/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1.java @@ -23,12 +23,10 @@ import com.thoughtworks.go.api.spring.ApiAuthenticationHelper; import com.thoughtworks.go.api.util.GsonTransformer; import com.thoughtworks.go.apiv1.elasticprofile.representers.ElasticProfileRepresenter; -import com.thoughtworks.go.apiv1.elasticprofile.representers.ElasticProfileUsageRepresenter; import com.thoughtworks.go.apiv1.elasticprofile.representers.ElasticProfilesRepresenter; import com.thoughtworks.go.config.PluginProfiles; import com.thoughtworks.go.config.elastic.ElasticProfile; import com.thoughtworks.go.config.exceptions.RecordNotFoundException; -import com.thoughtworks.go.domain.ElasticProfileUsage; import com.thoughtworks.go.server.service.ElasticProfileService; import com.thoughtworks.go.server.service.EntityHashingService; import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult; @@ -41,7 +39,6 @@ import spark.Response; import java.io.IOException; -import java.util.Collection; import java.util.function.Consumer; import static com.thoughtworks.go.api.util.HaltApiResponses.*; @@ -78,7 +75,7 @@ public void setupRoutes() { get("", mimeType, this::index); get(Routes.ElasticProfileAPI.ID, mimeType, this::show); - get(Routes.ElasticProfileAPI.ID + Routes.ElasticProfileAPI.USAGES, mimeType, this::usages); + post("", mimeType, this::create); put(Routes.ElasticProfileAPI.ID, mimeType, this::update); delete(Routes.ElasticProfileAPI.ID, mimeType, this::destroy); @@ -141,11 +138,6 @@ public String destroy(Request request, Response response) throws IOException { return renderHTTPOperationResult(result, request, response); } - public String usages(Request request, Response response) { - final String elasticProfileId = StringUtils.stripToEmpty(request.params(PROFILE_ID_PARAM)); - final Collection jobsUsingElasticProfile = elasticProfileService.getUsageInformation(elasticProfileId); - return ElasticProfileUsageRepresenter.toJSON(jobsUsingElasticProfile); - } private boolean isRenameAttempt(String profileIdFromRequestParam, String profileIdFromRequestBody) { diff --git a/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1Test.groovy b/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1Test.groovy index 5c4c05541b8..0b4eea64ed0 100644 --- a/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1Test.groovy +++ b/api/api-elastic-profile-v1/src/test/groovy/com/thoughtworks/go/apiv1/elasticprofile/ElasticProfileControllerV1Test.groovy @@ -23,8 +23,6 @@ import com.thoughtworks.go.apiv1.elasticprofile.representers.ElasticProfileRepre import com.thoughtworks.go.apiv1.elasticprofile.representers.ElasticProfilesRepresenter import com.thoughtworks.go.config.elastic.ElasticProfile import com.thoughtworks.go.config.elastic.ElasticProfiles -import com.thoughtworks.go.config.exceptions.RecordNotFoundException -import com.thoughtworks.go.domain.ElasticProfileUsage import com.thoughtworks.go.i18n.LocalizedMessage import com.thoughtworks.go.server.domain.Username import com.thoughtworks.go.server.service.ElasticProfileService @@ -538,64 +536,4 @@ class ElasticProfileControllerV1Test implements SecurityServiceTrait, Controller } } } - - @Nested - class Usages { - @Nested - class Security implements SecurityTestTrait, GroupAdminUserSecurity { - - @Override - String getControllerMethodUnderTest() { - return "usages" - } - - @Override - void makeHttpCall() { - getWithApiHeader(controller.controllerPath("/docker/usages")) - } - } - - @Nested - class AsAGroupAdmin { - @Test - void 'should list jobs associated with a profile id'() { - def elasticProfileUsages = Arrays.asList( - new ElasticProfileUsage("LinuxPR", "build", "compile", "linux-pr"), - new ElasticProfileUsage("LinuxPR", "build", "test", "linux-pr"), - - new ElasticProfileUsage("WindowsPR", "clean", "clean-dirs"), - new ElasticProfileUsage("WindowsPR", "clean", "clean-artifacts") - ) - - when(elasticProfileService.getUsageInformation("docker")).thenReturn(elasticProfileUsages) - - getWithApiHeader(controller.controllerPath("/docker/usages")) - - def expectedResponse = [ - [pipeline_name: "LinuxPR", stage_name: "build", job_name: "compile", template_name: "linux-pr"], - [pipeline_name: "LinuxPR", stage_name: "build", job_name: "test", template_name: "linux-pr"], - - [pipeline_name: "WindowsPR", stage_name: "clean", job_name: "clean-dirs"], - [pipeline_name: "WindowsPR", stage_name: "clean", job_name: "clean-artifacts"] - ] - - assertThatResponse() - .isOk() - .hasContentType(controller.mimeType) - .hasBodyWithJson(new JsonBuilder(expectedResponse).toString()) - } - - @Test - void 'should return 404 when profile with id does not exist'() { - when(elasticProfileService.getUsageInformation("docker")).thenThrow(new RecordNotFoundException("docker")) - - getWithApiHeader(controller.controllerPath("/docker/usages")) - - assertThatResponse() - .isNotFound() - .hasContentType(controller.mimeType) - .hasJsonMessage("Either the resource you requested was not found, or you are not authorized to perform this action.") - } - } - } } diff --git a/server/webapp/WEB-INF/applicationContext-global.xml b/server/webapp/WEB-INF/applicationContext-global.xml index 786c1a4289f..d1946d2488b 100644 --- a/server/webapp/WEB-INF/applicationContext-global.xml +++ b/server/webapp/WEB-INF/applicationContext-global.xml @@ -27,6 +27,7 @@ + diff --git a/server/webapp/WEB-INF/rails/webpack/helpers/spark_routes.ts b/server/webapp/WEB-INF/rails/webpack/helpers/spark_routes.ts index 00ed5deb9ce..e4455ffedb0 100644 --- a/server/webapp/WEB-INF/rails/webpack/helpers/spark_routes.ts +++ b/server/webapp/WEB-INF/rails/webpack/helpers/spark_routes.ts @@ -123,7 +123,7 @@ export default class { } static elasticProfileUsagePath(profileId: string): string { - return `/go/api/elastic/profiles/${profileId}/usages`; + return `/go/api/internal/elastic/profiles/${profileId}/usages`; } static agentsPath(uuid?: string): string { diff --git a/server/webapp/WEB-INF/urlrewrite.xml b/server/webapp/WEB-INF/urlrewrite.xml index 36cd0d3892d..eb7a677611f 100644 --- a/server/webapp/WEB-INF/urlrewrite.xml +++ b/server/webapp/WEB-INF/urlrewrite.xml @@ -57,6 +57,12 @@ true + + Spark Elastic Profiles Usage API + ^/api/internal/elastic/profiles/(.*)/usages$ + /spark/api/internal/elastic/profiles/${escape:$1}/usages + + Spark Artifact Store Index API ^/api/admin/artifact_stores(/?)$ diff --git a/settings.gradle b/settings.gradle index c154c26f145..eb416d85f28 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ include ':api:api-dashboard-v2' include ':api:api-data-sharing-reporting-v2' include ':api:api-data-sharing-settings-v1' include ':api:api-data-sharing-usage-data-v3' +include ':api:api-elastic-profile-operation-v1' include ':api:api-elastic-profile-v1' include ':api:api-encryption-v1' include ':api:api-material-search-v1' diff --git a/spark/spark-base/src/main/java/com/thoughtworks/go/spark/Routes.java b/spark/spark-base/src/main/java/com/thoughtworks/go/spark/Routes.java index e06e954f450..766ac2f93a3 100644 --- a/spark/spark-base/src/main/java/com/thoughtworks/go/spark/Routes.java +++ b/spark/spark-base/src/main/java/com/thoughtworks/go/spark/Routes.java @@ -17,8 +17,6 @@ package com.thoughtworks.go.spark; import com.google.common.net.UrlEscapers; -import com.thoughtworks.go.api.base.OutputLinkWriter; -import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.text.StrSubstitutor; import static com.google.common.collect.ImmutableMap.of; @@ -303,6 +301,7 @@ public static String name(String name) { public static class ElasticProfileAPI { public static final String BASE = "/api/elastic/profiles"; + public static final String INTERNAL_BASE = "/api/internal/elastic/profiles"; public static final String ID = "/:profile_id"; public static final String DOC = "https://api.gocd.org/current/#elastic-agent-profiles"; public static final String USAGES = "/usages";