Skip to content

Commit

Permalink
feat(clouddriver): Add generic cloud operation stage (#3749)
Browse files Browse the repository at this point in the history
This is the first draft of a generic cloud operation stage. The goal of this
stage, and the surrounding work, is to migrate all cloud provider logic from
orca into Clouddriver. The "vision" of this would be that to add a cloud
provider, or new operations for a cloud provider, would no longer require
modification to Orca; only requiring changes to Clouddriver.

There's still a long way to go on this, but it works enough as-is that I feel
comfortable making a PR.
  • Loading branch information
robzienert committed Jun 18, 2020
1 parent 4bc6045 commit 4f61fae
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.spinnaker.orca.clouddriver

import com.netflix.spinnaker.orca.clouddriver.model.OperationContext
import com.netflix.spinnaker.orca.clouddriver.model.Task
import com.netflix.spinnaker.orca.clouddriver.model.TaskId
import io.github.resilience4j.retry.annotation.Retry
Expand Down Expand Up @@ -45,6 +46,12 @@ interface KatoRestService {
@Path("cloudProvider") String cloudProvider,
@Body Collection<? extends Map<String, Map>> operations)

@POST("/{cloudProvider}/ops/{operationName}")
Response submitOperation(@Query("clientRequestId") String clientRequestId,
@Path("cloudProvider") String cloudProvider,
@Path("operationName") String operationName,
@Body OperationContext operation);

@GET("/applications/{app}/jobs/{account}/{region}/{id}")
Response collectJob(@Path("app") String app,
@Path("account") String account,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@

package com.netflix.spinnaker.orca.clouddriver

import com.fasterxml.jackson.databind.ObjectMapper
import com.google.common.hash.Hashing
import com.netflix.spinnaker.kork.core.RetrySupport
import com.netflix.spinnaker.orca.ExecutionContext
import com.netflix.spinnaker.orca.clouddriver.model.OperationContext
import com.netflix.spinnaker.orca.clouddriver.model.SubmitOperationResult
import com.netflix.spinnaker.orca.clouddriver.model.Task
import com.netflix.spinnaker.orca.clouddriver.model.TaskId
import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import retrofit.RetrofitError
import retrofit.client.Response

import javax.annotation.Nonnull
import java.time.Duration
Expand All @@ -34,6 +39,7 @@ class KatoService {
private final KatoRestService katoRestService
private final CloudDriverTaskStatusService cloudDriverTaskStatusService
private final RetrySupport retrySupport
private final ObjectMapper objectMapper

@Autowired
KatoService(KatoRestService katoRestService, CloudDriverTaskStatusService cloudDriverTaskStatusService, RetrySupport retrySupport) {
Expand All @@ -54,6 +60,30 @@ class KatoService {
}, 3, Duration.ofSeconds(1), false)
}

SubmitOperationResult submitOperation(@Nonnull String cloudProvider, OperationContext operation) {
Response response = katoRestService.submitOperation(
requestId(operation),
cloudProvider,
operation.operationType,
operation
)

InputStream body = null
try {
body = response.body.in()
} finally {
body?.close()
}

TaskId taskId = objectMapper.readValue(body, TaskId.class)

SubmitOperationResult result = new SubmitOperationResult()
result.id = taskId.id
result.status = response.status

return result
}

Task lookupTask(String id, boolean skipReplica = false) {
if (skipReplica) {
return katoRestService.lookupTask(id)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2020 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.orca.clouddriver.model;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.netflix.spinnaker.kork.annotations.NonnullByDefault;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.Data;

/** A step towards a more standard cloud operation context object. */
@Data
@NonnullByDefault
public class OperationContext {

private final Map<String, Object> backing = new HashMap<>();

/** The name AtomicOperation type. */
private String operationType;

/** The cloud provider name. */
private String cloudProvider;

/** The credentials name (clouddriver account name). */
private String credentials;

/**
* The operation lifecycle.
*
* <p>This is an internal-only field and should not be set by users. If this property is set by an
* end-user, it will be silently scrubbed.
*/
@Nullable private OperationLifecycle operationLifecycle;

@JsonAnyGetter
public Map<String, Object> getBacking() {
return backing;
}

@JsonAnySetter
public void setBackingValue(String key, @Nullable Object value) {
backing.put(key, value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2020 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.orca.clouddriver.model;

/** Clouddriver operation lifecycle hook points. */
public enum OperationLifecycle {
BEFORE,
AFTER,
FAILURE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2020 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.orca.clouddriver.model;

import lombok.Data;

@Data
public class SubmitOperationResult {
private String id;
private int status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright 2020 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.orca.clouddriver.pipeline;

import static java.lang.String.format;

import com.netflix.spinnaker.orca.api.pipeline.graph.StageDefinitionBuilder;
import com.netflix.spinnaker.orca.api.pipeline.graph.StageGraphBuilder;
import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode;
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution;
import com.netflix.spinnaker.orca.clouddriver.model.OperationLifecycle;
import com.netflix.spinnaker.orca.clouddriver.tasks.MonitorCloudOperationTask;
import com.netflix.spinnaker.orca.clouddriver.tasks.SubmitCloudOperationTask;
import com.netflix.spinnaker.orca.kato.pipeline.Nameable;
import java.util.*;
import javax.annotation.Nonnull;
import lombok.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class CloudOperationStage implements StageDefinitionBuilder, Nameable {

private static final String LIFECYCLE_KEY = "operationLifecycle";

protected TaskGraphConfigurer configureTaskGraph(@Nonnull StageExecution stage) {
return new TaskGraphConfigurer();
}

@Override
public final void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder builder) {
if (isTopLevel(stage)) {
// The lifecycle key is internal-only; it cannot be set by end-users.
stage.getContext().remove(LIFECYCLE_KEY);
}

if (stage.getContext().containsKey(LIFECYCLE_KEY)) {
String lifecycle =
StringUtils.capitalize(stage.getContext().get(LIFECYCLE_KEY).toString().toLowerCase());
builder
.withTask(format("submit%sOperation", lifecycle), SubmitCloudOperationTask.class)
.withTask(format("monitor%sOperation", lifecycle), MonitorCloudOperationTask.class);
} else {
TaskGraphConfigurer configurer = configureTaskGraph(stage);

configurer.beforeTasks.forEach(builder::withTask);

builder
.withTask(configurer.submitOperationTaskName, SubmitCloudOperationTask.class)
.withTask(configurer.monitorOperationTaskName, MonitorCloudOperationTask.class);

configurer.afterTasks.forEach(builder::withTask);
}
}

@Override
public void beforeStages(@Nonnull StageExecution parent, @Nonnull StageGraphBuilder graph) {
if (!isTopLevel(parent)) {
return;
}

graph.append(
it -> {
Map<String, Object> context = new HashMap<>(parent.getContext());
context.put(LIFECYCLE_KEY, OperationLifecycle.BEFORE);

it.setType(getType());
it.setName(format("Before %s", getName()));
it.setContext(context);
});
}

@Override
public void afterStages(@Nonnull StageExecution parent, @Nonnull StageGraphBuilder graph) {
if (!isTopLevel(parent)) {
return;
}

graph.append(
it -> {
Map<String, Object> context = new HashMap<>(parent.getContext());
context.put(LIFECYCLE_KEY, OperationLifecycle.AFTER);

it.setType(getType());
it.setName(format("After %s", getName()));
it.setContext(context);
});
}

@Override
public void onFailureStages(@Nonnull StageExecution stage, @Nonnull StageGraphBuilder graph) {
if (!isTopLevel(stage)) {
return;
}

graph.append(
it -> {
Map<String, Object> context = new HashMap<>(stage.getContext());
context.put(LIFECYCLE_KEY, OperationLifecycle.FAILURE);

it.setType(getType());
it.setName(format("Cleanup %s", getName()));
it.setContext(context);
});
}

@Override
public String getName() {
return "cloudOperation";
}

private static boolean isTopLevel(StageExecution stageExecution) {
return stageExecution.getParentStageId() == null;
}

/** Value object for configuring the Task graph of the stage. */
@Value
public static class TaskGraphConfigurer {

/**
* Tasks that should be added to the {@link TaskNode.Builder} before the submit and monitor
* tasks.
*/
List<TaskNode.TaskDefinition> beforeTasks;

/**
* Tasks that shouild be added to the {@link TaskNode.Builder} after the submit and monitor
* tasks.
*/
List<TaskNode.TaskDefinition> afterTasks;

/** The name of the submitOperation task name. */
String submitOperationTaskName;

/** The name of the monitorOperation task name. */
String monitorOperationTaskName;

/** Defaults. */
public TaskGraphConfigurer() {
beforeTasks = Collections.emptyList();
afterTasks = Collections.emptyList();
submitOperationTaskName = "submitOperation";
monitorOperationTaskName = "monitorOperation";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2020 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.orca.clouddriver.tasks;

import com.netflix.spinnaker.orca.api.pipeline.RetryableTask;
import com.netflix.spinnaker.orca.api.pipeline.TaskResult;
import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution;
import java.time.Duration;
import javax.annotation.Nonnull;
import org.springframework.stereotype.Component;

@Component
public class MonitorCloudOperationTask implements RetryableTask {

@Nonnull
@Override
public TaskResult execute(@Nonnull StageExecution stage) {
// TODO(rz): This would basically be the same as MonitorKatoTask.
return TaskResult.SUCCEEDED;
}

@Override
public long getBackoffPeriod() {
return Duration.ofSeconds(5).toMillis();
}

@Override
public long getTimeout() {
return Duration.ofHours(1).toMillis();
}
}
Loading

0 comments on commit 4f61fae

Please sign in to comment.