Skip to content
This repository was archived by the owner on Oct 2, 2025. It is now read-only.

Commit 645059d

Browse files
fix(artifacts): Automated triggers with artifact constraints are broken if you have 2 or more of the same type (#4579)
* refactor(artifacts): partially reverting #4397 * refactor(artifacts): reverted #4489 * refactor(artifacts): reverted #4526 * test(artifacts): added test when parent pipeline does not provide expected artifacts * test(artifacts): resolve artifacts for default and prior artifacts --------- Co-authored-by: Cameron Motevasselani <[email protected]>
1 parent a5d151f commit 645059d

File tree

4 files changed

+142
-186
lines changed

4 files changed

+142
-186
lines changed

orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/util/ArtifactUtils.java

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,26 @@
2828
import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
2929
import com.netflix.spinnaker.kork.artifacts.model.Artifact;
3030
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact;
31-
import com.netflix.spinnaker.kork.web.exceptions.InvalidRequestException;
3231
import com.netflix.spinnaker.orca.api.pipeline.models.PipelineExecution;
3332
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution;
3433
import com.netflix.spinnaker.orca.pipeline.model.StageContext;
3534
import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl;
3635
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository;
3736
import com.netflix.spinnaker.orca.pipeline.persistence.ExecutionRepository.ExecutionCriteria;
3837
import java.io.IOException;
39-
import java.util.ArrayList;
4038
import java.util.Collection;
4139
import java.util.Collections;
4240
import java.util.Comparator;
43-
import java.util.HashSet;
4441
import java.util.List;
4542
import java.util.Map;
4643
import java.util.Objects;
4744
import java.util.Optional;
48-
import java.util.Set;
4945
import java.util.function.Predicate;
5046
import java.util.stream.Collectors;
5147
import java.util.stream.Stream;
5248
import javax.annotation.CheckReturnValue;
5349
import javax.annotation.Nonnull;
5450
import javax.annotation.Nullable;
55-
import org.apache.commons.lang3.ObjectUtils;
5651
import org.apache.commons.lang3.StringUtils;
5752
import org.slf4j.Logger;
5853
import org.slf4j.LoggerFactory;
@@ -144,31 +139,7 @@ private List<Artifact> getAllArtifacts(
144139
contextParameterProcessor.process(
145140
boundArtifactMap, contextParameterProcessor.buildExecutionContext(stage), true);
146141

147-
Artifact evaluatedArtifact =
148-
objectMapper.convertValue(evaluatedBoundArtifactMap, Artifact.class);
149-
return getBoundInlineArtifact(evaluatedArtifact, stage.getExecution())
150-
.orElse(evaluatedArtifact);
151-
}
152-
153-
private Optional<Artifact> getBoundInlineArtifact(
154-
@Nullable Artifact artifact, PipelineExecution execution) {
155-
if (ObjectUtils.anyNull(
156-
artifact, execution.getTrigger(), execution.getTrigger().getArtifacts())) {
157-
return Optional.empty();
158-
}
159-
try {
160-
ExpectedArtifact expectedArtifact =
161-
ExpectedArtifact.builder().matchArtifact(artifact).build();
162-
return ArtifactResolver.getInstance(execution.getTrigger().getArtifacts(), true)
163-
.resolveExpectedArtifacts(List.of(expectedArtifact))
164-
.getResolvedExpectedArtifacts()
165-
.stream()
166-
.findFirst()
167-
.flatMap(this::getBoundArtifact);
168-
} catch (InvalidRequestException e) {
169-
log.debug("Could not match inline artifact with trigger bound artifacts", e);
170-
return Optional.empty();
171-
}
142+
return objectMapper.convertValue(evaluatedBoundArtifactMap, Artifact.class);
172143
}
173144

174145
public @Nullable Artifact getBoundArtifactForId(StageExecution stage, @Nullable String id) {
@@ -230,38 +201,10 @@ public List<Artifact> getArtifactsForPipelineIdWithoutStageRef(
230201
.orElse(Collections.emptyList());
231202
}
232203

233-
private List<String> getExpectedArtifactIdsFromMap(Map<String, Object> trigger) {
234-
List<String> expectedArtifactIds = (List<String>) trigger.get("expectedArtifactIds");
235-
return (expectedArtifactIds != null) ? expectedArtifactIds : emptyList();
236-
}
237-
238204
public void resolveArtifacts(Map pipeline) {
239205
Map<String, Object> trigger = (Map<String, Object>) pipeline.get("trigger");
240-
List<?> triggers = Optional.ofNullable((List<?>) pipeline.get("triggers")).orElse(emptyList());
241-
Set<String> expectedArtifactIdsListConcat =
242-
new HashSet<>(getExpectedArtifactIdsFromMap(trigger));
243-
244-
// Due to 8df68b79cf1 getBoundArtifactForStage now does resolution which
245-
// can potentially return null artifact back, which will throw an exception
246-
// for stages that expect a non-null value. Before this commit, when
247-
// getBoundArtifactForStage was called, the method would just retrieve the
248-
// bound artifact from the stage context, and return the appropriate
249-
// artifact back. This change prevents tasks like CreateBakeManifestTask
250-
// from working properly, if null is returned.
251-
//
252-
// reference: https://github.com/spinnaker/orca/pull/4397
253-
triggers.stream()
254-
.map(it -> (Map<String, Object>) it)
255-
// This filter prevents multiple triggers from adding its
256-
// expectedArtifactIds unless it is the expected trigger type that was
257-
// triggered
258-
//
259-
// reference: https://github.com/spinnaker/orca/pull/4322
260-
.filter(it -> trigger.getOrDefault("type", "").equals(it.get("type")))
261-
.map(this::getExpectedArtifactIdsFromMap)
262-
.forEach(expectedArtifactIdsListConcat::addAll);
263-
264-
final List<String> expectedArtifactIds = new ArrayList<>(expectedArtifactIdsListConcat);
206+
List<?> expectedArtifactIds =
207+
(List<?>) trigger.getOrDefault("expectedArtifactIds", emptyList());
265208
ImmutableList<ExpectedArtifact> expectedArtifacts =
266209
Optional.ofNullable((List<?>) pipeline.get("expectedArtifacts"))
267210
.map(Collection::stream)

orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/util/ArtifactUtilsSpec.groovy

Lines changed: 95 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package com.netflix.spinnaker.orca.pipeline.util
1919

2020
import com.fasterxml.jackson.core.type.TypeReference
2121
import com.fasterxml.jackson.databind.ObjectMapper
22-
import com.netflix.spinnaker.kork.artifacts.ArtifactTypes
2322
import com.netflix.spinnaker.kork.artifacts.model.Artifact
2423
import com.netflix.spinnaker.kork.artifacts.model.ExpectedArtifact
2524
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus
@@ -82,29 +81,6 @@ class ArtifactUtilsSpec extends Specification {
8281
artifact.name == 'build/libs/my-jar-100.jar'
8382
}
8483

85-
def "should bind stage-inlined artifacts to trigger artifacts"() {
86-
setup:
87-
def execution = pipeline {
88-
stage {
89-
name = "upstream stage"
90-
type = "stage1"
91-
refId = "1"
92-
}
93-
}
94-
95-
execution.trigger = new DefaultTrigger('manual')
96-
execution.trigger.artifacts.add(Artifact.builder().type('http/file').name('build/libs/my-jar-100.jar').build())
97-
98-
when:
99-
def artifact = makeArtifactUtils().getBoundArtifactForStage(execution.stages[0], null, Artifact.builder()
100-
.type('http/file')
101-
.name('build/libs/my-jar-\\d+.jar')
102-
.build())
103-
104-
then:
105-
artifact.name == 'build/libs/my-jar-100.jar'
106-
}
107-
10884
def "should find upstream artifacts in small pipeline"() {
10985
when:
11086
def desired = execution.getStages().find { it.name == "desired" }
@@ -516,81 +492,118 @@ class ArtifactUtilsSpec extends Specification {
516492
initialArtifacts == finalArtifacts
517493
}
518494

519-
def "should find artifact if triggers is present in pipeline"() {
495+
def "resolve expected artifact using default artifact"() {
520496
given:
521-
def defaultArtifact = Artifact.builder()
522-
.customKind(true)
497+
def matchArtifact = Artifact
498+
.builder()
499+
.name("my-artifact")
500+
.artifactAccount("embedded-artifact")
501+
.type("embedded/base64")
523502
.build()
524-
525-
def matchArtifact = Artifact.builder()
526-
.name("my-pipeline-artifact")
503+
def defaultArtifact = Artifact
504+
.builder()
505+
.name("default-artifact")
506+
.artifactAccount("embedded-artifact")
527507
.type("embedded/base64")
528-
.reference("aGVsbG8gd29ybGQK")
508+
.reference("bmVtZXNpcwo=")
529509
.build()
530-
531-
def expectedArtifact = ExpectedArtifact.builder()
532-
.usePriorArtifact(false)
533-
.useDefaultArtifact(false)
534-
.id("my-id")
535-
.defaultArtifact(defaultArtifact)
510+
def expectedArtifact = ExpectedArtifact
511+
.builder()
536512
.matchArtifact(matchArtifact)
513+
.defaultArtifact(defaultArtifact)
514+
.useDefaultArtifact(true)
537515
.build()
538516

539-
def expectedArtifact2 = ExpectedArtifact.builder()
540-
.usePriorArtifact(false)
541-
.useDefaultArtifact(false)
542-
.id("my-id-2")
543-
.defaultArtifact(defaultArtifact)
517+
def pipeline = [
518+
id : "01HE3GXEJX05143Y7JSGTRRB40",
519+
trigger : [
520+
type: "manual",
521+
// not passing artifacts in trigger
522+
],
523+
expectedArtifacts: [expectedArtifact],
524+
]
525+
def artifactUtils = makeArtifactUtils()
526+
527+
when:
528+
artifactUtils.resolveArtifacts(pipeline)
529+
List<ExpectedArtifact> resolvedArtifacts = objectMapper.convertValue(
530+
pipeline.trigger.resolvedExpectedArtifacts,
531+
new TypeReference<List<ExpectedArtifact>>() {}
532+
)
533+
534+
then:
535+
pipeline.trigger.artifacts.size() == 1
536+
pipeline.trigger.expectedArtifacts.size() == 1
537+
pipeline.trigger.resolvedExpectedArtifacts.size() == 1
538+
resolvedArtifacts*.getBoundArtifact() == [defaultArtifact]
539+
}
540+
541+
def "resolve expected artifact using prior artifact"() {
542+
given:
543+
def artifactName = "my-artifact-name"
544+
def priorArtifact = Artifact
545+
.builder()
546+
.name(artifactName)
547+
.artifactAccount("embedded-artifact")
548+
.type("embedded/base64")
549+
.reference("b3NvcmlvCg==")
550+
.build()
551+
552+
def pipelineId = "01HE3GXEJX05143Y7JSGTRRB41"
553+
def priorExecution = pipeline {
554+
id:
555+
pipelineId
556+
status:
557+
ExecutionStatus.SUCCEEDED
558+
stage {
559+
refId = "1"
560+
outputs.artifacts = [priorArtifact]
561+
}
562+
}
563+
564+
ExecutionRepository.ExecutionCriteria criteria = new ExecutionRepository.ExecutionCriteria();
565+
criteria.setPageSize(1);
566+
criteria.setSortType(ExecutionRepository.ExecutionComparator.START_TIME_OR_ID);
567+
568+
def executionRepositoryMock = Mock(ExecutionRepository) {
569+
retrievePipelinesForPipelineConfigId(pipelineId, criteria) >> Observable.just(priorExecution)
570+
}
571+
572+
def matchArtifact = Artifact
573+
.builder()
574+
.name(artifactName)
575+
.artifactAccount("embedded-artifact")
576+
.type("embedded/base64")
577+
.build()
578+
def expectedArtifact = ExpectedArtifact
579+
.builder()
544580
.matchArtifact(matchArtifact)
581+
.usePriorArtifact(true)
545582
.build()
546583

547584
def pipeline = [
548-
"id": "abc",
549-
"stages": [
550-
stage {
551-
expectedArtifacts: [expectedArtifact]
552-
inputArtifacts: [
553-
"id": "my-id"
554-
]
555-
}
585+
id : pipelineId,
586+
trigger : [
587+
type: "manual",
588+
// not passing artifacts in trigger
556589
],
557-
expectedArtifacts: [
558-
expectedArtifact
559-
],
560-
trigger: [
561-
artifacts: [
562-
Artifact.builder()
563-
.type(ArtifactTypes.EMBEDDED_BASE64.getMimeType())
564-
.name(matchArtifact.getName())
565-
.reference(matchArtifact.getReference())
566-
.build()
567-
],
568-
type: "some-type"
569-
],
570-
triggers: [
571-
[
572-
enabled: true,
573-
expectedArtifactIds: [
574-
expectedArtifact.getId()
575-
],
576-
type: "some-type"
577-
],
578-
[
579-
enabled: true,
580-
expectedArtifactIds: [
581-
expectedArtifact2.getId()
582-
],
583-
type: "some-other-type"
584-
]
585-
]
590+
expectedArtifacts: [expectedArtifact],
586591
]
587592

588-
def pipelineMap = getObjectMapper().convertValue(pipeline, Map.class)
593+
def artifactUtils = makeArtifactUtilsWithStub(executionRepositoryMock)
594+
589595
when:
590-
makeArtifactUtils().resolveArtifacts(pipelineMap)
596+
artifactUtils.resolveArtifacts(pipeline)
597+
List<ExpectedArtifact> resolvedArtifacts = objectMapper.convertValue(
598+
pipeline.trigger.resolvedExpectedArtifacts,
599+
new TypeReference<List<ExpectedArtifact>>() {}
600+
)
591601

592602
then:
593-
pipelineMap.trigger.resolvedExpectedArtifacts.size() == 1
603+
pipeline.trigger.artifacts.size() == 1
604+
pipeline.trigger.expectedArtifacts.size() == 1
605+
pipeline.trigger.resolvedExpectedArtifacts.size() == 1
606+
resolvedArtifacts*.getBoundArtifact() == [priorArtifact]
594607
}
595608

596609
private List<Artifact> extractTriggerArtifacts(Map<String, Object> trigger) {

orca-front50/src/main/groovy/com/netflix/spinnaker/orca/front50/DependentPipelineStarter.groovy

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,6 @@ class DependentPipelineStarter implements ApplicationContextAware {
8787
it.expectedArtifactIds ?: []
8888
}
8989

90-
// we are following a similar approach as triggers above
91-
// expectedArtifacts can be used in triggers and stages
92-
// for now we identified DeployManifestStage
93-
// in ResolveDeploySourceManifestTask using ManifestEvaluator.getRequiredArtifacts
94-
def requiredArtifactIds = pipelineConfig.get("stages", []).collectMany {
95-
it.requiredArtifactIds ?: []
96-
}
97-
expectedArtifactIds.addAll(requiredArtifactIds)
98-
9990
pipelineConfig.trigger = [
10091
type : "pipeline",
10192
user : authenticationDetails?.user ?: user ?: "[anonymous]",

0 commit comments

Comments
 (0)