rest-api/src/jetbrains/buildServer/server/rest/request/BuildQueueRequest.java (466 lines of code) (raw):

/* * Copyright 2000-2024 JetBrains s.r.o. * * 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 jetbrains.buildServer.server.rest.request; import com.intellij.openapi.diagnostic.Logger; import com.intellij.util.Function; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import java.util.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriInfo; import jetbrains.buildServer.ServiceLocator; import jetbrains.buildServer.server.rest.ApiUrlBuilder; import jetbrains.buildServer.server.rest.data.*; import jetbrains.buildServer.server.rest.data.build.TagFinder; import jetbrains.buildServer.server.rest.data.finder.impl.AgentFinder; import jetbrains.buildServer.server.rest.data.finder.impl.BuildPromotionFinder; import jetbrains.buildServer.server.rest.data.finder.impl.QueuedBuildFinder; import jetbrains.buildServer.server.rest.data.finder.impl.UserFinder; import jetbrains.buildServer.server.rest.errors.*; import jetbrains.buildServer.server.rest.errors.BadRequestException; import jetbrains.buildServer.server.rest.errors.NotFoundException; import jetbrains.buildServer.server.rest.model.Fields; import jetbrains.buildServer.server.rest.model.PagerData; import jetbrains.buildServer.server.rest.model.PagerDataImpl; import jetbrains.buildServer.server.rest.model.agent.Agents; import jetbrains.buildServer.server.rest.model.build.Build; import jetbrains.buildServer.server.rest.model.build.BuildCancelRequest; import jetbrains.buildServer.server.rest.model.build.Builds; import jetbrains.buildServer.server.rest.model.build.Tags; import jetbrains.buildServer.server.rest.model.build.approval.ApprovalInfo; import jetbrains.buildServer.server.rest.swagger.constants.LocatorName; import jetbrains.buildServer.server.rest.util.BeanContext; import jetbrains.buildServer.server.rest.util.BuildQueuePositionDescriptor; import jetbrains.buildServer.server.rest.util.BuildQueuePostitionModifier; import jetbrains.buildServer.serverSide.*; import jetbrains.buildServer.serverSide.auth.AccessDeniedException; import jetbrains.buildServer.serverSide.impl.approval.ApprovableBuildManager; import jetbrains.buildServer.tags.TagsManager; import jetbrains.buildServer.users.SUser; import jetbrains.buildServer.util.CollectionsUtil; import jetbrains.buildServer.util.Converter; import jetbrains.buildServer.util.StringUtil; import jetbrains.buildServer.web.util.QueueWebUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * @author Yegor.Yarko * Date: 03.11.13 */ @Path(BuildQueueRequest.API_BUILD_QUEUE_URL) @Api("BuildQueue") public class BuildQueueRequest { private static final Logger LOG = Logger.getInstance(BuildRequest.class.getName()); public static final String API_BUILD_QUEUE_URL = Constants.API_URL + "/buildQueue"; public static final String COMPATIBLE_AGENTS = "/compatibleAgents"; @Context @NotNull private QueuedBuildFinder myQueuedBuildFinder; @Context @NotNull private BuildPromotionFinder myBuildPromotionFinder; @Context @NotNull private AgentFinder myAgentFinder; @Context @NotNull private ApiUrlBuilder myApiUrlBuilder; @Context @NotNull private ServiceLocator myServiceLocator; @Context @NotNull private BeanContext myBeanContext; @NotNull public static String getHref() { return API_BUILD_QUEUE_URL; } @NotNull public static String getQueuedBuildHref(SQueuedBuild build) { return API_BUILD_QUEUE_URL + "/" + QueuedBuildFinder.getLocator(build); } /** * Serves build queue. * * @param locator Build locator to filter builds * @return */ @GET @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get all queued builds.",nickname="getAllQueuedBuilds") public Builds getBuilds(@ApiParam(format = LocatorName.BUILD_QUEUE) @QueryParam("locator") String locator, @QueryParam("fields") String fields, @Context UriInfo uriInfo, @Context HttpServletRequest request) { final PagedSearchResult<SQueuedBuild> result = myQueuedBuildFinder.getItems(locator); final List<BuildPromotion> builds = CollectionsUtil.convertCollection(result.getEntries(), SQueuedBuild::getBuildPromotion); return Builds.createFromPrefilteredBuildPromotions( builds, new PagerDataImpl(uriInfo.getRequestUriBuilder(), request.getContextPath(), result, locator, "locator"), new Fields(fields), myBeanContext ); } /** * Experimental! Deletes the set of builds filtered * * @param locator Build locator to filter builds to delete * @return */ @DELETE @ApiOperation(value="Delete all queued builds.",nickname="deleteAllQueuedBuilds") public void deleteBuildsExperimental(@ApiParam(format = LocatorName.BUILD_QUEUE) @QueryParam("locator") String locator, @QueryParam("fields") String fields, @Context UriInfo uriInfo, @Context HttpServletRequest request) { final PagedSearchResult<SQueuedBuild> result = myQueuedBuildFinder.getItems(locator); final List<Throwable> errors = new ArrayList<Throwable>(); final jetbrains.buildServer.serverSide.BuildQueue buildQueue = myServiceLocator.getSingletonService(jetbrains.buildServer.serverSide.BuildQueue.class); final List<String> itemIds = CollectionsUtil.convertCollection(result.getEntries(), new Converter<String, SQueuedBuild>() { public String createFrom(@NotNull final SQueuedBuild source) { return source.getItemId(); } }); buildQueue.removeItems(itemIds, myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(), null); //TeamCity API issue: TW-34143 for (String itemId : itemIds) { if (buildQueue.findQueued(itemId) != null) { errors.add(new AuthorizationFailedException("Build was not canceled. Probably not sufficient permissions.")); } } //now delete the canceled builds for (SQueuedBuild build : result.getEntries()) { final SBuild associatedBuild = build.getBuildPromotion().getAssociatedBuild(); if (associatedBuild == null) { errors.add(new OperationException("After canceling a build with promotion id '" + build.getBuildPromotion().getId() + "' , no canceled build found to delete.")); } else{ DataProvider.deleteBuild(associatedBuild, myBeanContext.getSingletonService(BuildHistory.class)); } } if (errors.size() >0){ throw new PartialUpdateError("Some builds were not deleted", errors); } } @PUT @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="replaceBuilds",hidden=true) public Builds replaceBuilds(Builds builds, @QueryParam("fields") String fields, @Context UriInfo uriInfo, @Context HttpServletRequest request){ if (builds == null){ throw new BadRequestException("List of builds should be posted."); } if (builds.getSubmittedBuilds() == null){ throw new BadRequestException("Posted element should contain 'builds' sub-element."); } final jetbrains.buildServer.serverSide.BuildQueue buildQueue = myServiceLocator.getSingletonService(jetbrains.buildServer.serverSide.BuildQueue.class); final List<BuildPromotion> queuedBuildPromotions = CollectionsUtil.convertCollection(buildQueue.getItems(), new Converter<BuildPromotion, SQueuedBuild>() { public BuildPromotion createFrom(@NotNull final SQueuedBuild source) { return source.getBuildPromotion(); } }); final List<String> queuedBuildIds = CollectionsUtil.convertCollection(buildQueue.getItems(), new Converter<String, SQueuedBuild>() { public String createFrom(@NotNull final SQueuedBuild source) { return source.getItemId(); } }); buildQueue.removeItems(queuedBuildIds, myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(), null); //todo: consider providing comment here //TeamCity API issue: TW-34143 if (!buildQueue.isQueueEmpty()) { throw new AuthorizationFailedException("Some builds were not canceled. Probably not sufficient permissions."); } //now delete the canceled builds for (BuildPromotion queuedBuildPromotion : queuedBuildPromotions) { final SBuild associatedBuild = queuedBuildPromotion.getAssociatedBuild(); if (associatedBuild == null){ throw new OperationException("After canceling a build with promotion id '" + queuedBuildPromotion.getId() + "' , no canceled build found to delete."); } DataProvider.deleteBuild(associatedBuild, myBeanContext.getSingletonService(BuildHistory.class)); } // now queue final SUser user = myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(); final Map<Long, Long> buildPromotionIdReplacements = new HashMap<Long, Long>(); List<Build> buildsToTrigger = builds.getSubmittedBuilds(); Map<Build, Exception> buildsWithErrors; while (true) { buildsWithErrors = triggerBuilds(buildsToTrigger, user, buildPromotionIdReplacements); if (buildsWithErrors.isEmpty() || buildsToTrigger.size() <= buildsWithErrors.size()) { //no errors or no builds triggered break; } buildsToTrigger = new ArrayList<Build>(buildsWithErrors.keySet()); LOG.info("There was an error triggering " + buildsToTrigger.size() + " builds, will try again." + " Affected build ids: " + listBuildIds(buildsToTrigger)); //repeat (dependnecies order might be relevant) } if (buildsWithErrors.size() != 0) { final StringBuilder buildListDetails = new StringBuilder(); for (Map.Entry<Build, Exception> buildExceptionEntry : buildsWithErrors.entrySet()) { final Build build = buildExceptionEntry.getKey(); //noinspection ThrowableResultOfMethodCallIgnored buildListDetails.append("Not able to add build").append(build.getPromotionIdOfSubmittedBuild() != null ? " with id '" + build.getPromotionIdOfSubmittedBuild() + "'" : "") .append(" to the build queue due to error: ").append(buildExceptionEntry.getValue().toString()); buildListDetails.append("\n"); } throw new BadRequestException("Error triggering " + buildsWithErrors.size() + " out of " + builds.getSubmittedBuilds().size() + " builds: \n" + buildListDetails.substring(0, buildListDetails.length() - "\n".length())); } return getBuilds(null, fields, uriInfo, request); } @NotNull private String listBuildIds(@NotNull final Collection<Build> buildsToTrigger) { return StringUtil.join(buildsToTrigger, new Function<Build, String>() { public String fun(final Build build) { return String.valueOf(build.getPromotionIdOfSubmittedBuild()); } }, ", "); } @NotNull private Map<Build, Exception> triggerBuilds(@NotNull final List<Build> builds, @Nullable final SUser user, @NotNull final Map<Long, Long> buildPromotionIdReplacements) { final Map<Build, Exception> buildsWithErrors = new LinkedHashMap<Build, Exception>(); for (Build build : builds) { try { final SQueuedBuild queuedBuild = build.triggerBuild(user, myServiceLocator, buildPromotionIdReplacements); if (build.getPromotionIdOfSubmittedBuild() != null) { buildPromotionIdReplacements.put(build.getPromotionIdOfSubmittedBuild(), queuedBuild.getBuildPromotion().getId()); } } catch (Exception e) { //noinspection ThrowableResultOfMethodCallIgnored buildsWithErrors.put(build, e); LOG.debug("Got error trying to add build" + (build.getPromotionIdOfSubmittedBuild() != null ? " with id '" + build.getPromotionIdOfSubmittedBuild() + "'" : "") + " to the build queue. Details: " + e.toString(), e); } } return buildsWithErrors; } @GET @Path("/{queuedBuildLocator}") @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get a queued matching build.",nickname="getQueuedBuild") public Build getBuild(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("queuedBuildLocator") String queuedBuildLocator, @QueryParam("fields") String fields, @Context UriInfo uriInfo, @Context HttpServletResponse response) { //also find already started builds BuildPromotion buildPromotion = myQueuedBuildFinder.getBuildPromotionByBuildQueueLocator(queuedBuildLocator); //todo: handle build merges in the queue (TW-33260) return new Build(buildPromotion, new Fields(fields), myBeanContext); } @DELETE @Path("/{queuedBuildLocator}") @ApiOperation(value="Delete a queued matching build.",nickname="deleteQueuedBuild") public void deleteQueuedBuild(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("queuedBuildLocator") String queuedBuildLocator) { SQueuedBuild build = myQueuedBuildFinder.getItem(queuedBuildLocator); cancelQueuedBuild(build, null); //now delete the canceled build final SBuild associatedBuild = build.getBuildPromotion().getAssociatedBuild(); if (associatedBuild == null){ throw new OperationException("After canceling a build with promotion id '" + build.getBuildPromotion().getId() + "' , no canceled build found to delete."); } DataProvider.deleteBuild(associatedBuild, myBeanContext.getSingletonService(BuildHistory.class)); } @GET @ApiOperation(value = "getBuildCancelRequestQueue", hidden = true) @Path("/{buildLocator}/example/buildCancelRequest") @Produces({"application/xml", "application/json"}) public BuildCancelRequest getBuildCancelRequestQueue(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("buildLocator") String buildLocator) { return new BuildCancelRequest("example build cancel comment", false); } @POST @Path("/{queuedBuildLocator}") @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Cancel a queued matching build.",nickname="cancelQueuedBuild") public Build cancelBuild(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("queuedBuildLocator") String queuedBuildLocator, BuildCancelRequest cancelRequest) { if (cancelRequest.readdIntoQueue) { throw new BadRequestException("Restore in queue is not supported for queued builds."); } SQueuedBuild build = myQueuedBuildFinder.getItem(queuedBuildLocator); cancelQueuedBuild(build, cancelRequest.comment); return new Build(build.getBuildPromotion(), Fields.LONG, myBeanContext); } private void cancelQueuedBuild(@NotNull final SQueuedBuild build, @Nullable final String comment) { final jetbrains.buildServer.serverSide.BuildQueue buildQueue = myServiceLocator.getSingletonService(jetbrains.buildServer.serverSide.BuildQueue.class); final String itemId = build.getItemId(); buildQueue.removeItems(Collections.singleton(itemId), myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(), comment); //TeamCity API issue: TW-34143 if (buildQueue.findQueued(itemId) != null) { throw new AuthorizationFailedException("Build was not canceled. Probably not sufficient permissions."); } } @GET @Path("/{queuedBuildLocator}" + COMPATIBLE_AGENTS) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get compatible agents for a queued matching build.",nickname="getCompatibleAgentsForBuild") public Agents serveCompatibleAgents(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("queuedBuildLocator") String queuedBuildLocator, @QueryParam("fields") String fields) { return new Agents(AgentFinder.getCompatibleAgentsLocator(myQueuedBuildFinder.getItem(queuedBuildLocator).getBuildPromotion()), null, new Fields(fields), myBeanContext); } @GET @Path("/{buildLocator}/tags/") @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get tags of the queued matching build.",nickname="getQueuedBuildTags") public Tags serveTags(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("buildLocator") String buildLocator, @QueryParam("locator") String tagLocator, @QueryParam("fields") String fields) { BuildPromotion buildPromotion = myBuildPromotionFinder.getItem(Locator.createLocator(buildLocator, getBuildPromotionLocatorDefaults(), null).getStringRepresentation()); return new Tags(new TagFinder(myBeanContext.getSingletonService(UserFinder.class), buildPromotion).getItems(tagLocator, TagFinder.getDefaultLocator()).getEntries(), new Fields(fields), myBeanContext); } /** * Replaces build's tags. * * @param buildLocator build locator */ @PUT @Path("/{buildLocator}/tags/") @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="replaceTags",hidden=true) public Tags replaceTags(@ApiParam(format = LocatorName.BUILD_QUEUE) @PathParam("buildLocator") String buildLocator, @ApiParam(format = LocatorName.TAG) @QueryParam("locator") String tagLocator, Tags tags, @QueryParam("fields") String fields, @Context HttpServletRequest request) { BuildPromotion buildPromotion = myBuildPromotionFinder.getItem(Locator.createLocator(buildLocator, getBuildPromotionLocatorDefaults(), null).getStringRepresentation()); final TagFinder tagFinder = new TagFinder(myBeanContext.getSingletonService(UserFinder.class), buildPromotion); final TagsManager tagsManager = myBeanContext.getSingletonService(TagsManager.class); tagsManager.removeTagDatas(buildPromotion, tagFinder.getItems(tagLocator, TagFinder.getDefaultLocator()).getEntries()); tagsManager.addTagDatas(buildPromotion, tags.getFromPosted(myBeanContext.getSingletonService(UserFinder.class))); return new Tags(tagFinder.getItems(null, TagFinder.getDefaultLocator()).getEntries(), new Fields(fields), myBeanContext); } /** * Adds a set of tags to a build * * @param buildLocator build locator */ @POST @Path("/{buildLocator}/tags/") @Consumes({"application/xml", "application/json"}) @ApiOperation(value="Add tags to the matching build.",nickname="addTagsToBuild") public void addTags(@ApiParam(format = LocatorName.BUILD) @PathParam("buildLocator") String buildLocator, Tags tags, @Context HttpServletRequest request) { BuildPromotion buildPromotion = myBuildPromotionFinder.getItem(Locator.createLocator(buildLocator, getBuildPromotionLocatorDefaults(), null).getStringRepresentation()); final TagsManager tagsManager = myBeanContext.getSingletonService(TagsManager.class); tagsManager.addTagDatas(buildPromotion, tags.getFromPosted(myBeanContext.getSingletonService(UserFinder.class))); } /** * Adds a single tag to a build * * @param buildLocator build locator * @param tagName name of a tag to add */ @POST @Path("/{buildLocator}/tags/") @Consumes({"text/plain"}) @Produces({"text/plain"}) @ApiOperation(hidden = true, value = "Use addTags instead") public String addTag(@ApiParam(format = LocatorName.BUILD) @PathParam("buildLocator") String buildLocator, String tagName, @Context HttpServletRequest request) { if (StringUtil.isEmpty(tagName)) { //check for empty tags: http://youtrack.jetbrains.com/issue/TW-34426 throw new BadRequestException("Cannot apply empty tag, should have non empty request body"); } BuildPromotion buildPromotion = myBuildPromotionFinder.getItem(Locator.createLocator(buildLocator, getBuildPromotionLocatorDefaults(), null).getStringRepresentation()); final TagsManager tagsManager = myBeanContext.getSingletonService(TagsManager.class); tagsManager.addTagDatas(buildPromotion, Collections.singleton(TagData.createPublicTag(tagName))); return tagName; } @NotNull private Locator getBuildPromotionLocatorDefaults() { Locator defaultLocator = Locator.createEmptyLocator(); defaultLocator.setDimension(BuildPromotionFinder.STATE, BuildPromotionFinder.STATE_QUEUED); defaultLocator.addIgnoreUnusedDimensions(BuildPromotionFinder.STATE); return defaultLocator; } @GET @Path("/{buildLocator}/{field}") @Produces("text/plain") @ApiOperation(value="serveBuildFieldByBuildOnly",hidden=true) public String serveBuildFieldByBuildOnly(@ApiParam(format = LocatorName.BUILD) @PathParam("buildLocator") String buildLocator, @PathParam("field") String field) { final BuildPromotion buildPromotion = myQueuedBuildFinder.getBuildPromotionByBuildQueueLocator(buildLocator); return Build.getFieldValue(buildPromotion, field, myBeanContext); } @POST @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Add a new build to the queue.",nickname="addBuildToQueue") public Build queueNewBuild(Build build, @QueryParam("moveToTop") Boolean moveToTop, @Context HttpServletRequest request, @Context HttpServletResponse response){ if (QueueWebUtil.processLargeQueueCase(request, response, myServiceLocator.getSingletonService(BuildQueue.class))) { return null; } final SUser user = myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(); SQueuedBuild queuedBuild = build.triggerBuild(user, myServiceLocator, new HashMap<Long, Long>()); if (moveToTop != null && moveToTop){ final BuildQueue buildQueue = myServiceLocator.getSingletonService(BuildQueue.class); buildQueue.moveTop(queuedBuild.getItemId()); } return new Build(queuedBuild.getBuildPromotion(), Fields.LONG, myBeanContext); } /** * Experimental ability to reorder the queue */ @PUT @Path("/order") @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Update the build queue order.",nickname="setQueuedBuildsOrder") public Builds setBuildQueueOrder(Builds builds, @QueryParam("fields") String fields) { if (builds.getSubmittedBuilds() == null){ throw new BadRequestException("No new builds order specified. Should post a collection of builds, each with id or locator"); } LinkedHashSet<String> ids = new LinkedHashSet<>(); for (Build build : builds.getSubmittedBuilds()) { try { List<BuildPromotion> items = myBuildPromotionFinder.getItems(build.getLocatorFromPosted(Collections.emptyMap()), new Locator(getBuildPromotionLocatorDefaults())).getEntries(); for (BuildPromotion buildPromotion : items) { SQueuedBuild queuedBuild = buildPromotion.getQueuedBuild(); if (queuedBuild == null) continue; ids.add(String.valueOf(queuedBuild.getItemId())); } } catch (NotFoundException e) { //ignore } } final BuildQueue buildQueue = myServiceLocator.getSingletonService(BuildQueue.class); buildQueue.applyOrder(CollectionsUtil.toArray(ids, String.class)); return Builds.createFromPrefilteredBuildPromotions( myBuildPromotionFinder.getItems(getBuildPromotionLocatorDefaults().getStringRepresentation()).getEntries(), null, new Fields(fields), myBeanContext ); } /** * Experimental ability to get a build at specific queue position */ @GET @Path("/order/{queuePosition}") @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get the queue position of a queued matching build.",nickname="getQueuedBuildPosition") public Build getBuildQueuePosition(@PathParam("queuePosition") String queuePosition, @QueryParam("fields") String fields) { int queuePositionNumber; queuePositionNumber = getQueuePositionNumber(queuePosition); if (queuePositionNumber < 1) { throw new BadRequestException("Unsupported value of queuePosition \"" + queuePosition + "\": should be greater than 0"); } int actualPosition; if (queuePositionNumber == Integer.MAX_VALUE) { final BuildQueue buildQueue = myServiceLocator.getSingletonService(BuildQueue.class); actualPosition = buildQueue.getNumberOfItems() - 1; } else { actualPosition = queuePositionNumber - 1; } Locator locator = getBuildPromotionLocatorDefaults().setDimension(PagerData.START, String.valueOf(actualPosition)); return new Build(myBuildPromotionFinder.getItem(locator.getStringRepresentation()), new Fields(fields), myBeanContext); } private int getQueuePositionNumber(final @PathParam("queuePosition") String queuePosition) { try { if ("first".equals(queuePosition)) return 1; if ("last".equals(queuePosition)) return Integer.MAX_VALUE; return Integer.parseInt(queuePosition); } catch (NumberFormatException e) { throw new BadRequestException("Error parsing queuePosition \"" + queuePosition + "\": should be a number, \"first\" or \"last\""); } } /** * Experimental ability to move to top */ @PUT @Path("/order/{queuePosition}") @Consumes({"application/xml", "application/json"}) @Produces({"application/xml", "application/json"}) @ApiOperation(value="Update the queue position of a queued matching build.",nickname="setQueuedBuildPosition") public Build setBuildQueuePosition(Build build, @PathParam("queuePosition") String queuePosition, @QueryParam("fields") String fields) { BuildPromotion buildToMove = build.getFromPosted(myBuildPromotionFinder, Collections.emptyMap()); SQueuedBuild queuedBuild = buildToMove.getQueuedBuild(); if (queuedBuild == null) { throw new BadRequestException("Cannot move build which is not queued"); } BuildQueuePositionDescriptor newPosition = BuildQueuePositionDescriptor.parse(queuePosition); if(newPosition == null) { throw new BadRequestException("Unsupported value of queuePosition \"" + queuePosition + "\": only \"1\", \"first\" and \"last\" are supported"); } BuildQueuePostitionModifier buildQueuePostitionModifier = myServiceLocator.getSingletonService(BuildQueuePostitionModifier.class); buildQueuePostitionModifier.moveBuild(queuedBuild, newPosition); return new Build(queuedBuild.getBuildPromotion(), new Fields(fields), myBeanContext); } @GET @Path("/{buildLocator}/approvalInfo") @Produces({"application/xml", "application/json"}) @ApiOperation(value="Get approval info of a queued matching build.",nickname="getApprovalInfo") public ApprovalInfo getApprovalInfo( @ApiParam(format = LocatorName.BUILD) @PathParam("buildLocator") String buildLocator, @QueryParam("fields") String fields ) { ApprovableBuildManager approvableBuildManager = myBeanContext.getSingletonService(ApprovableBuildManager.class); BuildPromotionEx buildPromotionEx = (BuildPromotionEx)myBuildPromotionFinder.getBuildPromotion(null, buildLocator); if (approvableBuildManager.shouldBeApproved(buildPromotionEx).requiresApproval()) { return new ApprovalInfo(buildPromotionEx, new Fields(fields), myBeanContext); } else { throw new BadRequestException( "Trying to access approval status for a queued build that does not have approval configuration" ); } } @POST @Path("/{buildLocator}/approve") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @ApiOperation(value = "Approve queued build with approval feature enabled.", nickname = "approveQueuedBuild") public ApprovalInfo approveQueuedBuild(@ApiParam(format = LocatorName.BUILD) @PathParam("buildLocator") String buildLocator, String requestor, @QueryParam("fields") String fields) { final ApprovableBuildManager approvableBuildManager = myServiceLocator.getSingletonService(ApprovableBuildManager.class); BuildPromotionEx buildPromotionEx = (BuildPromotionEx)myBuildPromotionFinder.getBuildPromotion(null, buildLocator); ApprovalInfo.ApprovalStatus status = ApprovalInfo.ApprovalStatus.resolve(buildPromotionEx, approvableBuildManager); if (status != ApprovalInfo.ApprovalStatus.waitingForApproval) { throw new BadRequestException("This build cannot be approved, approval status is: " + status); } SUser user = myServiceLocator.getSingletonService(UserFinder.class).getCurrentUser(); try { approvableBuildManager.addApprovedByUser(buildPromotionEx, user); } catch (IllegalStateException | AccessDeniedException e) { throw new BadRequestException(e.getMessage()); } return new ApprovalInfo(buildPromotionEx, new Fields(fields), myBeanContext); } }