From 5e32d770d9bcbec1b9f04d9c26b8cb3f3014a44e Mon Sep 17 00:00:00 2001 From: Raul Cristian <24556350+ciurescuraul@users.noreply.github.com> Date: Fri, 22 Dec 2023 19:00:25 +0200 Subject: [PATCH] feat(jenkins): Enable Jenkins job triggers for jobs in sub-folders (#1373) - Allow triggering of Jenkins jobs that reside in sub-folders, by treating job names containing slashes as query variables. - Prior to this feature, jobs in sub-folders were not appropriately matched by the Spring framework due to slashes in their path, causing trigger requests to fail. - Include a new feature flag to determine the usage of the existing endpoint (which uses the job name as a path variable) or an updated endpoint (which takes the job name as a query parameter). (cherry picked from commit 18d038db6016233c999ccc24cd4dfd3e1debbd6a) --- .../echo/build/BuildInfoService.java | 20 +++++++++++-- .../echo/config/BuildInfoConfig.java | 9 ++++-- .../config/IgorConfigurationProperties.java | 28 +++++++++++++++++++ .../spinnaker/echo/services/IgorService.java | 7 +++++ .../BuildEventHandlerSpec.groovy | 27 ++++++++++++++++-- 5 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 echo-core/src/main/java/com/netflix/spinnaker/echo/config/IgorConfigurationProperties.java diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java index 608d0a640..056bdccee 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/build/BuildInfoService.java @@ -18,13 +18,18 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.echo.config.IgorConfigurationProperties; import com.netflix.spinnaker.echo.jackson.EchoObjectMapper; import com.netflix.spinnaker.echo.model.Trigger; import com.netflix.spinnaker.echo.model.trigger.BuildEvent; import com.netflix.spinnaker.echo.services.IgorService; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import com.netflix.spinnaker.kork.core.RetrySupport; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; @@ -40,6 +45,7 @@ public class BuildInfoService { private final IgorService igorService; private final RetrySupport retrySupport; + private final IgorConfigurationProperties igorConfigurationProperties; private final ObjectMapper objectMapper = EchoObjectMapper.getInstance(); // Manual triggers try to replicate actual events (and in some cases build events) but rather than @@ -49,7 +55,11 @@ public class BuildInfoService { // an event as with other triggers, but for now we'll see whether we can extract a build event // from the trigger. public BuildEvent getBuildEvent(String master, String job, int buildNumber) { - Map rawBuild = retry(() -> igorService.getBuild(buildNumber, master, job)); + Map rawBuild = + retry( + igorConfigurationProperties.isJobNameAsQueryParameter() + ? () -> igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job) + : () -> igorService.getBuild(buildNumber, master, job)); BuildEvent.Build build = objectMapper.convertValue(rawBuild, BuildEvent.Build.class); BuildEvent.Project project = new BuildEvent.Project(job, build); BuildEvent.Content content = new BuildEvent.Content(project, master); @@ -64,7 +74,11 @@ public Map getBuildInfo(BuildEvent event) { int buildNumber = event.getBuildNumber(); if (StringUtils.isNoneEmpty(master, job)) { - return retry(() -> igorService.getBuild(buildNumber, master, job)); + return retry( + () -> + igorConfigurationProperties.isJobNameAsQueryParameter() + ? igorService.getBuildStatusWithJobQueryParameter(buildNumber, master, job) + : igorService.getBuild(buildNumber, master, job)); } return Collections.emptyMap(); } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/config/BuildInfoConfig.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/config/BuildInfoConfig.java index 851f06b32..ddb939584 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/config/BuildInfoConfig.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/config/BuildInfoConfig.java @@ -21,14 +21,19 @@ import com.netflix.spinnaker.echo.services.IgorService; import com.netflix.spinnaker.kork.core.RetrySupport; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnProperty("igor.enabled") +@EnableConfigurationProperties(IgorConfigurationProperties.class) public class BuildInfoConfig { @Bean - BuildInfoService buildInfoService(IgorService igorService, RetrySupport retrySupport) { - return new BuildInfoService(igorService, retrySupport); + BuildInfoService buildInfoService( + IgorService igorService, + RetrySupport retrySupport, + IgorConfigurationProperties igorConfigurationProperties) { + return new BuildInfoService(igorService, retrySupport, igorConfigurationProperties); } } diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/config/IgorConfigurationProperties.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/config/IgorConfigurationProperties.java new file mode 100644 index 000000000..99b8ea815 --- /dev/null +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/config/IgorConfigurationProperties.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Netflix, 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.netflix.spinnaker.echo.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Setter +@Getter +@ConfigurationProperties(prefix = "feature.igor") +public class IgorConfigurationProperties { + private boolean jobNameAsQueryParameter = false; +} diff --git a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java index b5939075e..063428552 100644 --- a/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java +++ b/echo-core/src/main/java/com/netflix/spinnaker/echo/services/IgorService.java @@ -19,6 +19,7 @@ import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.NotNull; import retrofit.client.Response; import retrofit.http.*; import retrofit.mime.TypedInput; @@ -30,6 +31,12 @@ Map getBuild( @Path("master") String master, @Path(value = "job", encode = false) String job); + @GET("/builds/status/{buildNumber}/{master}") + Map getBuildStatusWithJobQueryParameter( + @NotNull @Path("buildNumber") Integer buildNumber, + @NotNull @Path("master") String master, + @NotNull @Query(value = "job") String job); + @GET("/builds/properties/{buildNumber}/{fileName}/{master}/{job}") Map getPropertyFile( @Path("buildNumber") Integer buildNumber, diff --git a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy index 7f04a071f..c897c51f5 100644 --- a/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy +++ b/echo-pipelinetriggers/src/test/groovy/com/netflix/spinnaker/echo/pipelinetriggers/eventhandlers/BuildEventHandlerSpec.groovy @@ -1,8 +1,8 @@ package com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers -import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spectator.api.NoopRegistry import com.netflix.spinnaker.echo.build.BuildInfoService +import com.netflix.spinnaker.echo.config.IgorConfigurationProperties import com.netflix.spinnaker.echo.jackson.EchoObjectMapper import com.netflix.spinnaker.echo.model.Pipeline import com.netflix.spinnaker.echo.model.trigger.BuildEvent @@ -20,7 +20,7 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { def registry = new NoopRegistry() def objectMapper = EchoObjectMapper.getInstance() def igorService = Mock(IgorService) - def buildInformation = new BuildInfoService(igorService, new RetrySupport()) + def buildInformation = new BuildInfoService(igorService, new RetrySupport(), new IgorConfigurationProperties(jobNameAsQueryParameter: false)) def handlerSupport = new EventHandlerSupport() def fiatPermissionEvaluator = Mock(FiatPermissionEvaluator) @@ -246,6 +246,29 @@ class BuildEventHandlerSpec extends Specification implements RetrofitStubs { outputTrigger.buildInfo.equals(BUILD_INFO) } + def "getBuildInfo method gets job name from query when flag is true"() + { + given: + def mockBuildInfo = BUILD_INFO + def trigger = enabledJenkinsTrigger.withMaster(MASTER_NAME).withBuildNumber(BUILD_NUMBER) + createPipelineWith(enabledJenkinsTrigger).withTrigger(trigger) + def event = getBuildEvent() + + def retrySupport = new RetrySupport() + def configProperties = new IgorConfigurationProperties(jobNameAsQueryParameter: true) + def buildInfoService = new BuildInfoService(igorService, retrySupport, configProperties) + + def permissionEvaluator = fiatPermissionEvaluator + def buildEventHandler = new BuildEventHandler(registry, objectMapper, Optional.of(buildInfoService), permissionEvaluator) + + when: + def outputTrigger = buildEventHandler.buildTrigger(event).apply(trigger) + + then: + 1 * igorService.getBuildStatusWithJobQueryParameter(BUILD_NUMBER, MASTER_NAME, JOB_NAME) >> mockBuildInfo + outputTrigger.buildInfo == mockBuildInfo + } + def "fetches property file if defined"() { given: def trigger = enabledJenkinsTrigger