Skip to content

Commit

Permalink
fix(echo events/orca): Echo events will be generated when the pipelin…
Browse files Browse the repository at this point in the history
…e is deleted. (spinnaker#4183)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
sanopsmx and mergify[bot] authored Oct 19, 2021
1 parent cd25a1f commit c95880d
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ interface Front50Service {

@DELETE("/pipelines/{applicationName}/{pipelineName}")
Response deletePipeline(@Path("applicationName") String applicationName, @Path("pipelineName") String pipelineName)

@POST('/actions/strategies/reorder')
Response reorderPipelineStrategies(@Body ReorderPipelinesCommand reorderPipelinesCommand)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2021 OpsMx, 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.orca.front50.pipeline;

import com.netflix.spinnaker.orca.api.pipeline.graph.StageDefinitionBuilder;
import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode.Builder;
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution;
import com.netflix.spinnaker.orca.front50.tasks.DeletePipelineTask;
import javax.annotation.Nonnull;
import org.springframework.stereotype.Component;

@Component
public class DeletePipelineStage implements StageDefinitionBuilder {
@Override
public void taskGraph(@Nonnull StageExecution stage, @Nonnull Builder builder) {

builder.withTask("deletePipeline", DeletePipelineTask.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright 2021 OpsMx, 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.orca.front50.tasks;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spinnaker.orca.api.pipeline.RetryableTask;
import com.netflix.spinnaker.orca.api.pipeline.TaskResult;
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus;
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution;
import com.netflix.spinnaker.orca.clouddriver.utils.CloudProviderAware;
import com.netflix.spinnaker.orca.front50.Front50Service;
import com.netflix.spinnaker.orca.front50.PipelineModelMutator;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import retrofit.client.Response;

@Component
public class DeletePipelineTask implements CloudProviderAware, RetryableTask {

private Logger log = LoggerFactory.getLogger(getClass());

@Autowired
DeletePipelineTask(
Optional<Front50Service> front50Service,
Optional<List<PipelineModelMutator>> pipelineModelMutators,
ObjectMapper objectMapper) {
this.front50Service = front50Service.orElse(null);
this.pipelineModelMutators = pipelineModelMutators.orElse(new ArrayList<>());
this.objectMapper = objectMapper;
}

private final Front50Service front50Service;
private final List<PipelineModelMutator> pipelineModelMutators;
ObjectMapper objectMapper;

@Override
public TaskResult execute(StageExecution stage) {
if (front50Service == null) {
throw new UnsupportedOperationException(
"Front50 is not enabled, no way to delete pipeline. Fix this by setting front50.enabled: true");
}

if (!stage.getContext().containsKey("pipeline")) {
throw new IllegalArgumentException("pipeline context must be provided");
}

Map<String, Object> pipeline;
if (!(stage.getContext().get("pipeline") instanceof String)) {
pipeline = (Map<String, Object>) stage.getContext().get("pipeline");
} else {
pipeline = (Map<String, Object>) stage.decodeBase64("/pipeline", Map.class);
}

if (!pipeline.containsKey("index")) {
Map<String, Object> existingPipeline = fetchExistingPipeline(pipeline);
if (existingPipeline != null) {
pipeline.put("index", existingPipeline.get("index"));
}
}
String serviceAccount = (String) stage.getContext().get("pipeline.serviceAccount");
if (serviceAccount != null) {
updateServiceAccount(pipeline, serviceAccount);
}
final Boolean isSavingMultiplePipelines =
(Boolean)
Optional.ofNullable(stage.getContext().get("isSavingMultiplePipelines")).orElse(false);
final Boolean staleCheck =
(Boolean) Optional.ofNullable(stage.getContext().get("staleCheck")).orElse(false);
if (stage.getContext().get("pipeline.id") != null
&& pipeline.get("id") == null
&& !isSavingMultiplePipelines) {
pipeline.put("id", stage.getContext().get("pipeline.id"));

// We need to tell front50 to regenerate cron trigger id's
pipeline.put("regenerateCronTriggerIds", true);
}

pipelineModelMutators.stream()
.filter(m -> m.supports(pipeline))
.forEach(m -> m.mutate(pipeline));

Response response =
front50Service.deletePipeline(
pipeline.get("application").toString(), pipeline.get("name").toString());

Map<String, Object> outputs = new HashMap<>();
outputs.put("notification.type", "deletepipeline");
outputs.put("application", pipeline.get("application"));
outputs.put("pipeline.name", pipeline.get("name"));

try {
Map<String, Object> savedPipeline =
(Map<String, Object>) objectMapper.readValue(response.getBody().in(), Map.class);
outputs.put("pipeline.id", savedPipeline.get("id"));
} catch (Exception e) {
log.error("Unable to deserialize saved pipeline, reason: ", e);

if (pipeline.containsKey("id")) {
outputs.put("pipeline.id", pipeline.get("id"));
}
}

final ExecutionStatus status;
if (response.getStatus() == HttpStatus.OK.value()) {
status = ExecutionStatus.SUCCEEDED;
} else {
if (isSavingMultiplePipelines) {
status = ExecutionStatus.FAILED_CONTINUE;
} else {
status = ExecutionStatus.TERMINAL;
}
}
return TaskResult.builder(status).context(outputs).build();
}

@Override
public long getBackoffPeriod() {
return 1000;
}

@Override
public long getTimeout() {
return TimeUnit.SECONDS.toMillis(30);
}

private void updateServiceAccount(Map<String, Object> pipeline, String serviceAccount) {
if (StringUtils.isEmpty(serviceAccount) || !pipeline.containsKey("triggers")) {
return;
}

List<Map<String, Object>> triggers = (List<Map<String, Object>>) pipeline.get("triggers");
List<String> roles = (List<String>) pipeline.get("roles");
// Managed service acct but no roles; Remove runAsUserFrom triggers
if (roles == null || roles.isEmpty()) {
triggers.forEach(t -> t.remove("runAsUser", serviceAccount));
return;
}
}

private Map<String, Object> fetchExistingPipeline(Map<String, Object> newPipeline) {
String applicationName = (String) newPipeline.get("application");
String newPipelineID = (String) newPipeline.get("id");
if (!StringUtils.isEmpty(newPipelineID)) {
return front50Service.getPipelines(applicationName).stream()
.filter(m -> m.containsKey("id"))
.filter(m -> m.get("id").equals(newPipelineID))
.findFirst()
.orElse(null);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2021 OpsMx, 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.orca.front50.tasks

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.collect.ImmutableMap
import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus
import com.netflix.spinnaker.orca.front50.Front50Service
import com.netflix.spinnaker.orca.front50.PipelineModelMutator
import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl
import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl
import retrofit.client.Response
import spock.lang.Specification
import spock.lang.Subject

class DeletePipelineTaskSpec extends Specification {

Front50Service front50Service = Mock()

PipelineModelMutator mutator = Mock()

ObjectMapper objectMapper = new ObjectMapper()

@Subject
SavePipelineTask sTask = new SavePipelineTask(Optional.of(front50Service), Optional.of([mutator]), objectMapper)

@Subject
DeletePipelineTask task = new DeletePipelineTask(Optional.of(front50Service), Optional.of([mutator]), objectMapper)

def "should delete the pipeline"() {
given:
def pipeline = [
application: 'orca',
name: 'my pipeline',
stages: []
]
def stage = new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "whatever", [
pipeline: Base64.encoder.encodeToString(objectMapper.writeValueAsString(pipeline).bytes)
])

when:
sTask.execute(stage)
def result = task.execute(stage)

then:
2 * mutator.supports(pipeline) >> true
2 * mutator.mutate(pipeline)
1 * front50Service.savePipeline(pipeline, _) >> {
new Response('http://front50', 200, 'OK', [], null)
}
1 * front50Service.deletePipeline(pipeline.application, pipeline.name) >> {
new Response('http://front50', 200, 'OK', [], null)
}
result.status == ExecutionStatus.SUCCEEDED
result.context == ImmutableMap.copyOf([
'notification.type': 'deletepipeline',
'application': 'orca',
'pipeline.name': 'my pipeline'
])
}

def "should fail task when front 50 delete call fails"() {
given:
def pipeline = [
application: 'orca',
name: 'my pipeline',
stages: []
]
def stage = new StageExecutionImpl(PipelineExecutionImpl.newPipeline("orca"), "whatever", [
pipeline: Base64.encoder.encodeToString(objectMapper.writeValueAsString(pipeline).bytes)
])

when:
front50Service.getPipelines(_) >> []
front50Service.deletePipeline(_, _) >> {
new Response('http://front50', 500, 'OK', [], null)
}
def result = task.execute(stage)

then:
result.status == ExecutionStatus.TERMINAL
}
}

0 comments on commit c95880d

Please sign in to comment.