Skip to content

Commit f1d0a77

Browse files
fix(artifacts): separate resolve artifacts for trigger of type pipeline
Co-authored-by: Nemesis Osorio <nemesis211_6@hotmail.com>
1 parent 342dcc6 commit f1d0a77

File tree

5 files changed

+174
-110
lines changed

5 files changed

+174
-110
lines changed

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

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package com.netflix.spinnaker.orca.pipeline.util;
1818

1919
import static com.google.common.collect.ImmutableList.toImmutableList;
20-
import static java.lang.String.format;
2120

2221
import com.google.common.base.Suppliers;
2322
import com.google.common.collect.ImmutableList;
@@ -84,6 +83,10 @@ public static ArtifactResolver getInstance(
8483
return new ArtifactResolver(currentArtifacts, ImmutableList::of, requireUniqueMatches);
8584
}
8685

86+
public ResolveResult resolveExpectedArtifacts(Iterable<ExpectedArtifact> expectedArtifacts) {
87+
return resolveExpectedArtifacts(expectedArtifacts, true);
88+
}
89+
8790
/**
8891
* Resolves the input expected artifacts, returning the result of the resolution as a {@link
8992
* ResolveResult}.
@@ -111,7 +114,8 @@ public static ArtifactResolver getInstance(
111114
* @param expectedArtifacts The expected artifacts to resolve
112115
* @return The result of the artifact resolution
113116
*/
114-
public ResolveResult resolveExpectedArtifacts(Iterable<ExpectedArtifact> expectedArtifacts) {
117+
public ResolveResult resolveExpectedArtifacts(
118+
Iterable<ExpectedArtifact> expectedArtifacts, boolean throwsExceptionIfNotFound) {
115119
// We keep track of resolved artifacts in an ImmutableSet.Builder so that duplicates are not
116120
// added (in the case that an artifact matches more than one expected artifact). An ImmutableSet
117121
// iterates in the order elements were added (including via the builder), so calling asList()
@@ -120,17 +124,22 @@ public ResolveResult resolveExpectedArtifacts(Iterable<ExpectedArtifact> expecte
120124
ImmutableList.Builder<ExpectedArtifact> boundExpectedArtifacts = ImmutableList.builder();
121125

122126
for (ExpectedArtifact expectedArtifact : expectedArtifacts) {
123-
Artifact resolved =
124-
resolveSingleArtifact(expectedArtifact)
125-
.orElseThrow(
126-
() ->
127-
new InvalidRequestException(
128-
format(
129-
"Unmatched expected artifact %s could not be resolved.",
130-
expectedArtifact)));
131-
resolvedArtifacts.add(resolved);
132-
boundExpectedArtifacts.add(expectedArtifact.toBuilder().boundArtifact(resolved).build());
127+
Optional<Artifact> resolvedOptional = resolveSingleArtifact(expectedArtifact);
128+
129+
if (resolvedOptional.isEmpty() && throwsExceptionIfNotFound) {
130+
throw new InvalidRequestException(
131+
String.format(
132+
"Unmatched expected artifact %s could not be resolved.", expectedArtifact));
133+
}
134+
135+
if (resolvedOptional.isPresent()) {
136+
Artifact resolvedArtifact = resolvedOptional.get();
137+
resolvedArtifacts.add(resolvedArtifact);
138+
boundExpectedArtifacts.add(
139+
expectedArtifact.toBuilder().boundArtifact(resolvedArtifact).build());
140+
}
133141
}
142+
134143
return new ResolveResult(resolvedArtifacts.build().asList(), boundExpectedArtifacts.build());
135144
}
136145

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

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@
5252
import javax.annotation.CheckReturnValue;
5353
import javax.annotation.Nonnull;
5454
import javax.annotation.Nullable;
55-
import org.apache.commons.lang3.ObjectUtils;
5655
import org.apache.commons.lang3.StringUtils;
5756
import org.slf4j.Logger;
5857
import org.slf4j.LoggerFactory;
@@ -144,31 +143,7 @@ private List<Artifact> getAllArtifacts(
144143
contextParameterProcessor.process(
145144
boundArtifactMap, contextParameterProcessor.buildExecutionContext(stage), true);
146145

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-
}
146+
return objectMapper.convertValue(evaluatedBoundArtifactMap, Artifact.class);
172147
}
173148

174149
public @Nullable Artifact getBoundArtifactForId(StageExecution stage, @Nullable String id) {
@@ -230,6 +205,107 @@ public List<Artifact> getArtifactsForPipelineIdWithoutStageRef(
230205
.orElse(Collections.emptyList());
231206
}
232207

208+
/**
209+
* The intention is to throw an exception if all the expected artifacts are not provided in the
210+
* trigger <br>
211+
* For now this is only for triggers of type pipeline
212+
*
213+
* @param pipeline
214+
* @throws InvalidRequestException if the trigger does not provide all the expected artifacts
215+
*/
216+
public void validateArtifactConstraintsFromTrigger(Map<String, Object> pipeline) {
217+
Map<String, Object> trigger = (Map<String, Object>) pipeline.get("trigger");
218+
List<?> expectedArtifactIds =
219+
(List<?>) trigger.getOrDefault("expectedArtifactIds", emptyList());
220+
221+
ImmutableList<ExpectedArtifact> expectedArtifacts =
222+
Optional.ofNullable((List<?>) pipeline.get("expectedArtifacts"))
223+
.map(Collection::stream)
224+
.orElse(Stream.empty())
225+
.map(it -> objectMapper.convertValue(it, ExpectedArtifact.class))
226+
.filter(
227+
artifact ->
228+
expectedArtifactIds.contains(artifact.getId())
229+
|| artifact.isUseDefaultArtifact()
230+
|| artifact.isUsePriorArtifact())
231+
.collect(toImmutableList());
232+
233+
ImmutableSet<Artifact> receivedArtifacts = concatReceivedArtifacts(pipeline, trigger);
234+
235+
ArtifactResolver.getInstance(
236+
receivedArtifacts, () -> getPriorArtifacts(pipeline), /* requireUniqueMatches= */ true)
237+
.resolveExpectedArtifacts(expectedArtifacts);
238+
}
239+
240+
private ImmutableSet<Artifact> concatReceivedArtifacts(
241+
Map<String, Object> pipeline, Map<String, Object> trigger) {
242+
return Stream.concat(
243+
Optional.ofNullable((List<?>) pipeline.get("receivedArtifacts"))
244+
.map(Collection::stream)
245+
.orElse(Stream.empty()),
246+
Optional.ofNullable((List<?>) trigger.get("artifacts"))
247+
.map(Collection::stream)
248+
.orElse(Stream.empty()))
249+
.map(it -> objectMapper.convertValue(it, Artifact.class))
250+
.collect(toImmutableSet());
251+
}
252+
253+
/**
254+
* This will only resolve the expected artifacts that match received artifacts. <br>
255+
* If the expected artifact is not present in received artifacts, it is not resolved. <br>
256+
* This resolves as many artifacts as possible, because there is no standard way to include all
257+
* the stages using expected artifacts. <br>
258+
* The best scenario is: the trigger pipeline provides all the expected artifacts. <br>
259+
* The worst scenario is: the pipeline will fail until the stage using the expected artifact is
260+
* executed. <br>
261+
* For now this is only for triggers of type pipeline
262+
*
263+
* @param pipeline
264+
*/
265+
public void resolveArtifactsFromTrigger(Map pipeline) {
266+
Map<String, Object> trigger = (Map<String, Object>) pipeline.get("trigger");
267+
ImmutableList<ExpectedArtifact> expectedArtifacts =
268+
Optional.ofNullable((List<?>) pipeline.get("expectedArtifacts"))
269+
.map(Collection::stream)
270+
.orElse(Stream.empty())
271+
.map(it -> objectMapper.convertValue(it, ExpectedArtifact.class))
272+
.collect(toImmutableList());
273+
274+
ImmutableSet<Artifact> receivedArtifacts = concatReceivedArtifacts(pipeline, trigger);
275+
276+
ArtifactResolver.ResolveResult resolveResult =
277+
ArtifactResolver.getInstance(
278+
receivedArtifacts,
279+
() -> getPriorArtifacts(pipeline),
280+
/* requireUniqueMatches= */ true)
281+
.resolveExpectedArtifacts(expectedArtifacts, false);
282+
283+
ImmutableSet<Artifact> allArtifacts =
284+
ImmutableSet.<Artifact>builder()
285+
.addAll(receivedArtifacts)
286+
.addAll(resolveResult.getResolvedArtifacts())
287+
.build();
288+
289+
try {
290+
trigger.put(
291+
"artifacts",
292+
objectMapper.readValue(objectMapper.writeValueAsString(allArtifacts), List.class));
293+
trigger.put(
294+
"expectedArtifacts",
295+
objectMapper.readValue(
296+
objectMapper.writeValueAsString(resolveResult.getResolvedExpectedArtifacts()),
297+
List.class));
298+
trigger.put(
299+
"resolvedExpectedArtifacts",
300+
objectMapper.readValue(
301+
objectMapper.writeValueAsString(resolveResult.getResolvedExpectedArtifacts()),
302+
List.class)); // Add the actual expectedArtifacts we included in the ids.
303+
} catch (IOException e) {
304+
throw new ArtifactResolutionException(
305+
"Failed to store artifacts in trigger: " + e.getMessage(), e);
306+
}
307+
}
308+
233309
private List<String> getExpectedArtifactIdsFromMap(Map<String, Object> trigger) {
234310
List<String> expectedArtifactIds = (List<String>) trigger.get("expectedArtifactIds");
235311
return (expectedArtifactIds != null) ? expectedArtifactIds : emptyList();

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

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,29 +82,6 @@ class ArtifactUtilsSpec extends Specification {
8282
artifact.name == 'build/libs/my-jar-100.jar'
8383
}
8484

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-
10885
def "should find upstream artifacts in small pipeline"() {
10986
when:
11087
def desired = execution.getStages().find { it.name == "desired" }

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,6 @@ class DependentPipelineStarter implements ApplicationContextAware {
8585
it.expectedArtifactIds ?: []
8686
}
8787

88-
// we are following a similar approach as triggers above
89-
// expectedArtifacts can be used in triggers and stages
90-
// for now we identified DeployManifestStage
91-
// in ResolveDeploySourceManifestTask using ManifestEvaluator.getRequiredArtifacts
92-
def requiredArtifactIds = pipelineConfig.get("stages", []).collectMany {
93-
it.requiredArtifactIds ?: []
94-
}
95-
expectedArtifactIds.addAll(requiredArtifactIds)
96-
9788
pipelineConfig.trigger = [
9889
type : "pipeline",
9990
user : authenticationDetails?.user ?: user ?: "[anonymous]",
@@ -102,7 +93,7 @@ class DependentPipelineStarter implements ApplicationContextAware {
10293
parameters : [:],
10394
strategy : suppliedParameters.strategy == true,
10495
correlationId : "${parentPipeline.id}_${parentPipelineStageId}_${pipelineConfig.id}_${parentPipeline.startTime}".toString(),
105-
expectedArtifactIds : expectedArtifactIds.toSet().toList()
96+
expectedArtifactIds : expectedArtifactIds
10697
]
10798
/* correlationId is added so that two pipelines aren't triggered when a pipeline is canceled.
10899
* parentPipelineStageId is added so that a child pipeline (via pipeline stage)
@@ -144,7 +135,8 @@ class DependentPipelineStarter implements ApplicationContextAware {
144135
def artifactError = null
145136

146137
try {
147-
artifactUtils?.resolveArtifacts(pipelineConfig)
138+
artifactUtils?.validateArtifactConstraintsFromTrigger(pipelineConfig)
139+
artifactUtils?.resolveArtifactsFromTrigger(pipelineConfig)
148140
} catch (Exception e) {
149141
artifactError = e
150142
}

0 commit comments

Comments
 (0)