Skip to content

Commit

Permalink
Bumped up Stage Instances API to v3 gocd#8681
Browse files Browse the repository at this point in the history
* v3 of the API adds attributes 'created_time' and 'last_transition_time'
* Deprecated v2 of the API
  • Loading branch information
maheshp committed Oct 27, 2020
1 parent b67833a commit 160b5f1
Show file tree
Hide file tree
Showing 20 changed files with 1,662 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,7 @@ api/api-secret-configs-v3/out/
api/api-secret-configs-v3/target/
api/api-secret-configs-v3/logs/
api/api-secret-configs-v3/config/
api/api-stage-instance-v3/out/
api/api-stage-instance-v3/target/
api/api-stage-instance-v3/logs/
api/api-stage-instance-v3/config/
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.thoughtworks.go.server.service.result.HttpOperationResult;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.spark.DeprecatedAPI;
import com.thoughtworks.go.spark.Routes;
import com.thoughtworks.go.spark.spring.SparkSpringController;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -50,6 +51,7 @@
import static spark.Spark.*;

@Component
@DeprecatedAPI(entityName = "Stage Instances", deprecatedApiVersion = ApiVersion.v2, successorApiVersion = ApiVersion.v3, deprecatedIn = "20.9.0", removalIn = "21.1.0")
public class StageInstanceControllerV2 extends ApiController implements SparkSpringController {
private final static String JOB_NAMES_PROPERTY = "jobs";
private final ApiAuthenticationHelper apiAuthenticationHelper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import com.thoughtworks.go.server.service.result.LocalizedOperationResult
import com.thoughtworks.go.serverhealth.HealthStateScope
import com.thoughtworks.go.serverhealth.HealthStateType
import com.thoughtworks.go.spark.ControllerTrait
import com.thoughtworks.go.spark.DeprecatedApiTrait
import com.thoughtworks.go.spark.PipelineAccessSecurity
import com.thoughtworks.go.spark.PipelineGroupOperateUserSecurity
import com.thoughtworks.go.spark.SecurityServiceTrait
Expand All @@ -51,7 +52,7 @@ import static org.mockito.ArgumentMatchers.*
import static org.mockito.Mockito.*
import static org.mockito.MockitoAnnotations.initMocks

class StageInstanceControllerV2Test implements SecurityServiceTrait, ControllerTrait<StageInstanceControllerV2> {
class StageInstanceControllerV2Test implements SecurityServiceTrait, ControllerTrait<StageInstanceControllerV2>, DeprecatedApiTrait {

@Mock
private StageService stageService
Expand Down
26 changes: 26 additions & 0 deletions api/api-stage-instance-v3/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2020 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: 'groovy'

dependencies {
implementation project(':api:api-base')

testImplementation 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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright 2020 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.apiv3.stageinstance;

import com.thoughtworks.go.api.ApiController;
import com.thoughtworks.go.api.ApiVersion;
import com.thoughtworks.go.api.representers.JsonReader;
import com.thoughtworks.go.api.spring.ApiAuthenticationHelper;
import com.thoughtworks.go.api.util.GsonTransformer;
import com.thoughtworks.go.api.util.HaltApiResponses;
import com.thoughtworks.go.apiv3.stageinstance.representers.StageInstancesRepresenter;
import com.thoughtworks.go.apiv3.stageinstance.representers.StageRepresenter;
import com.thoughtworks.go.domain.JobInstance;
import com.thoughtworks.go.domain.NullStage;
import com.thoughtworks.go.domain.PipelineRunIdInfo;
import com.thoughtworks.go.domain.Stage;
import com.thoughtworks.go.presentation.pipelinehistory.StageInstanceModels;
import com.thoughtworks.go.server.service.ScheduleService;
import com.thoughtworks.go.server.service.StageService;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.server.service.result.HttpOperationResult;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.spark.Routes;
import com.thoughtworks.go.spark.spring.SparkSpringController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spark.Request;
import spark.Response;

import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static spark.Spark.*;

@Component
public class StageInstanceControllerV3 extends ApiController implements SparkSpringController {
private final static String JOB_NAMES_PROPERTY = "jobs";
private final ApiAuthenticationHelper apiAuthenticationHelper;
private final StageService stageService;
private final ScheduleService scheduleService;

@Autowired
public StageInstanceControllerV3(ApiAuthenticationHelper apiAuthenticationHelper, StageService stageService, ScheduleService scheduleService) {
super(ApiVersion.v3);
this.apiAuthenticationHelper = apiAuthenticationHelper;
this.stageService = stageService;
this.scheduleService = scheduleService;
}

@Override
public String controllerBasePath() {
return Routes.Stage.BASE;
}

@Override
public void setupRoutes() {
path(controllerBasePath(), () -> {
before("/*", mimeType, this::setContentType);
before("/*", mimeType, this::verifyContentType);

before(Routes.Stage.TRIGGER_FAILED_JOBS_PATH, mimeType, apiAuthenticationHelper::checkPipelineGroupOperateOfPipelineOrGroupInURLUserAnd403);
before(Routes.Stage.TRIGGER_SELECTED_JOBS_PATH, mimeType, apiAuthenticationHelper::checkPipelineGroupOperateOfPipelineOrGroupInURLUserAnd403);
before(Routes.Stage.CANCEL_STAGE_PATH, mimeType, apiAuthenticationHelper::checkPipelineGroupOperateOfPipelineOrGroupInURLUserAnd403);
before(Routes.Stage.INSTANCE_V2, mimeType, apiAuthenticationHelper::checkPipelineViewPermissionsAnd403);
before(Routes.Stage.STAGE_HISTORY, mimeType, apiAuthenticationHelper::checkPipelineViewPermissionsAnd403);

post(Routes.Stage.TRIGGER_FAILED_JOBS_PATH, mimeType, this::rerunFailedJobs);
post(Routes.Stage.TRIGGER_SELECTED_JOBS_PATH, mimeType, this::rerunSelectedJobs);
post(Routes.Stage.CANCEL_STAGE_PATH, mimeType, this::cancelStage);
get(Routes.Stage.INSTANCE_V2, mimeType, this::instanceByCounter);
get(Routes.Stage.STAGE_HISTORY, mimeType, this::history);
});
}

public String rerunSelectedJobs(Request req, Response res) throws IOException {
HttpOperationResult result = new HttpOperationResult();
haltIfRequestBodyDoesNotContainPropertyJobs(req);

JsonReader requestBody = GsonTransformer.getInstance().jsonReaderFrom(req.body());
List<String> requestedJobs = requestBody.readStringArrayIfPresent(JOB_NAMES_PROPERTY).get();

Optional<Stage> optionalStage = getStageFromRequestParam(req, result);
optionalStage.ifPresent(stage -> {
HealthStateType healthStateType = HealthStateType.general(HealthStateScope.forStage(stage.getIdentifier()
.getPipelineName(), stage.getName()));

Set<String> jobsInStage = stage.getJobInstances()
.stream()
.map(JobInstance::getName)
.collect(Collectors.toSet());

List<String> unknownJobs = requestedJobs.stream()
.filter(jobToRun -> !jobsInStage.contains(jobToRun))
.collect(Collectors.toList());

if (unknownJobs.isEmpty()) {
scheduleService.rerunJobs(stage, requestedJobs, result);
} else {
String msg = String.format("Job(s) %s does not exist in stage '%s'.", unknownJobs, stage.getIdentifier().getStageLocator());
result.notFound(msg, "", healthStateType);
}

});

return renderHTTPOperationResult(result, req, res);
}

public String rerunFailedJobs(Request req, Response res) throws IOException {
HttpOperationResult result = new HttpOperationResult();

Optional<Stage> optionalStage = getStageFromRequestParam(req, result);
optionalStage.ifPresent(stage -> scheduleService.rerunFailedJobs(stage, result));
return renderHTTPOperationResult(result, req, res);
}

public String cancelStage(Request req, Response res) throws Exception {
HttpOperationResult result = new HttpOperationResult();
Optional<Stage> optionalStage = getStageFromRequestParam(req, result);
if (!result.isSuccess()) {
return renderHTTPOperationResult(result, req, res);
}

HttpLocalizedOperationResult localizedOperationResult = new HttpLocalizedOperationResult();
scheduleService.cancelAndTriggerRelevantStages(optionalStage.get().getId(), currentUsername(), localizedOperationResult);
return renderHTTPOperationResult(localizedOperationResult, req, res);
}

public String instanceByCounter(Request req, Response res) throws IOException {
String pipelineName = req.params("pipeline_name");
String pipelineCounter = req.params("pipeline_counter");
String stageName = req.params("stage_name");
String stageCounter = req.params("stage_counter");
HttpOperationResult result = new HttpOperationResult();

Stage stageModel = stageService.findStageWithIdentifier(pipelineName,
Integer.parseInt(pipelineCounter),
stageName,
stageCounter,
currentUsername().getUsername().toString(),
result);

if (result.canContinue()) {
return writerForTopLevelObject(req, res, writer -> StageRepresenter.toJSON(writer, stageModel));
} else {
return renderHTTPOperationResult(result, req, res);
}
}

public String history(Request request, Response response) throws IOException {
String pipelineName = request.params("pipeline_name");
String stageName = request.params("stage_name");
Long after = getCursor(request, "after");
Long before = getCursor(request, "before");
int pageSize = getPageSize(request);

StageInstanceModels stageInstanceModels = stageService.findStageHistoryViaCursor(currentUsername(), pipelineName, stageName, after, before, pageSize);
PipelineRunIdInfo latestAndOldestPipelineIds = stageService.getOldestAndLatestStageInstanceId(currentUsername(), pipelineName, stageName);
return writerForTopLevelObject(request, response, writer -> StageInstancesRepresenter.toJSON(writer, stageInstanceModels, latestAndOldestPipelineIds));
}

private Optional<Stage> getStageFromRequestParam(Request request, HttpOperationResult operationResult) {
String pipelineName = request.params("pipeline_name");
String pipelineCounter = request.params("pipeline_counter");
String stageName = request.params("stage_name");
String stageCounter = request.params("stage_counter");

Stage stage = stageService.findStageWithIdentifier(pipelineName,
Integer.parseInt(pipelineCounter),
stageName,
stageCounter,
currentUsername().getUsername().toString(),
operationResult);

if (!operationResult.isSuccess()) {
return Optional.empty();
}

if (stage == null || stage instanceof NullStage) {
String message = String.format("Stage '%s' with counter '%s' not found. Please make sure specified stage or stage run with specified counter exists.", stageName, stageCounter);
operationResult.notFound("Not Found", message, HealthStateType.general(HealthStateScope.GLOBAL));
return Optional.empty();
}

return Optional.ofNullable(stage);
}

private void haltIfRequestBodyDoesNotContainPropertyJobs(Request req) {
JsonReader requestBody = GsonTransformer.getInstance().jsonReaderFrom(req.body());
if (!requestBody.hasJsonObject(JOB_NAMES_PROPERTY)) {
throw HaltApiResponses.haltBecauseOfReason("Could not read property '%s' in request body", JOB_NAMES_PROPERTY);
}
requestBody.readStringArrayIfPresent(JOB_NAMES_PROPERTY);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2020 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.apiv3.stageinstance.representers;

import com.thoughtworks.go.api.base.OutputWriter;
import com.thoughtworks.go.presentation.pipelinehistory.JobHistoryItem;

public class JobHistoryItemRepresenter {
public static void toJSON(OutputWriter jsonWriter, JobHistoryItem jobHistoryItem) {
jsonWriter.add("name", jobHistoryItem.getName());
if (jobHistoryItem.getState() != null) {
jsonWriter.add("state", jobHistoryItem.getState().toString());
}
if (jobHistoryItem.getResult() != null) {
jsonWriter.add("result", jobHistoryItem.getResult().toString());
}
if (jobHistoryItem.getScheduledDate() != null) {
jsonWriter.add("scheduled_date", jobHistoryItem.getScheduledDate().getTime());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2020 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.apiv3.stageinstance.representers;

import com.thoughtworks.go.api.base.OutputWriter;
import com.thoughtworks.go.domain.JobInstance;

public class JobInstanceRepresenter {
public static void toJSON(OutputWriter jsonWriter, JobInstance jobInstance) {
jsonWriter.add("name", jobInstance.getName());
if (jobInstance.getState() != null) {
jsonWriter.add("state", jobInstance.getState().toString());
}
if (jobInstance.getResult() != null) {
jsonWriter.add("result", jobInstance.getResult().toString());
}
if (jobInstance.getScheduledDate() != null) {
jsonWriter.add("scheduled_date", jobInstance.getScheduledDate().getTime());
}
jsonWriter.add("rerun", jobInstance.isRerun());
if (jobInstance.getOriginalJobId() == null) {
jsonWriter.add("original_job_id", (String) null);
}
else {
jsonWriter.add("original_job_id", jobInstance.getOriginalJobId());
}
jsonWriter.addWithDefaultIfBlank("agent_uuid", jobInstance.getAgentUuid(), (String) null);
jsonWriter.add("pipeline_name", (String) null);
jsonWriter.add("pipeline_counter", (String) null);
jsonWriter.add("stage_name", (String) null);
jsonWriter.add("stage_counter", (String) null);
jsonWriter.addChildList("job_state_transitions", jobStateTransitionsWriter -> jobInstance.getTransitions().forEach(
jobStateTransition -> jobStateTransitionsWriter.addChild(
jobStateTransitionWriter -> JobStateTransitionRepresenter.toJSON(jobStateTransitionWriter, jobStateTransition))));

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2020 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.apiv3.stageinstance.representers;

import com.thoughtworks.go.api.base.OutputWriter;
import com.thoughtworks.go.domain.JobStateTransition;

public class JobStateTransitionRepresenter {
public static void toJSON(OutputWriter jsonWriter, JobStateTransition jobStateTransition) {
if (jobStateTransition.getCurrentState() != null) {
jsonWriter.add("state", jobStateTransition.getCurrentState().toString());
}
if (jobStateTransition.getStateChangeTime() != null) {
jsonWriter.add("state_change_time", jobStateTransition.getStateChangeTime().getTime());
}
}
}
Loading

0 comments on commit 160b5f1

Please sign in to comment.