Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gremlin): Adds a Gremlin stage #2664

Merged
merged 4 commits into from
Mar 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions orca-integrations-gremlin/orca-integrations-gremlin.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apply from: "$rootDir/gradle/kotlin.gradle"

dependencies {
compile project(":orca-core")
compile project(":orca-kotlin")
compile project(":orca-retrofit")

testCompile project(":orca-core-tck")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.netflix.spinnaker.orca.config;

import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature
import com.netflix.spinnaker.orca.gremlin.GremlinConverter
import com.netflix.spinnaker.orca.gremlin.GremlinService
import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper
import com.netflix.spinnaker.orca.retrofit.logging.RetrofitSlf4jLog
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import retrofit.Endpoint
import retrofit.Endpoints
import retrofit.RequestInterceptor
import retrofit.RestAdapter
import retrofit.client.Client

@Configuration
@ConditionalOnProperty("integrations.gremlin.enabled")
@ComponentScan(
"com.netflix.spinnaker.orca.gremlin.pipeline",
"com.netflix.spinnaker.orca.gremlin.tasks"
)
class GremlinConfiguration {

@Bean
fun gremlinEndpoint(
@Value("\${integrations.gremlin.baseUrl}") gremlinBaseUrl: String): Endpoint {
return Endpoints.newFixedEndpoint(gremlinBaseUrl)
}

@Bean
fun gremlinService(
retrofitClient: Client,
gremlinEndpoint: Endpoint,
spinnakerRequestInterceptor: RequestInterceptor
): GremlinService {
val mapper = OrcaObjectMapper
.newInstance()
.setPropertyNamingStrategy(
PropertyNamingStrategy.SNAKE_CASE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // we want Instant serialized as ISO string
return RestAdapter.Builder()
.setRequestInterceptor(spinnakerRequestInterceptor)
.setEndpoint(gremlinEndpoint)
.setClient(retrofitClient)
.setLogLevel(RestAdapter.LogLevel.BASIC)
.setLog(RetrofitSlf4jLog(GremlinService::class.java))
.setConverter(GremlinConverter(mapper))
.build()
.create(GremlinService::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.netflix.spinnaker.orca.gremlin;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import retrofit.converter.ConversionException;
import retrofit.converter.Converter;
import retrofit.mime.TypedByteArray;
import retrofit.mime.TypedInput;
import retrofit.mime.TypedOutput;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.stream.Collectors;

public class GremlinConverter implements Converter {
private static final String MIME_TYPE = "application/json; charset=UTF-8";

private final ObjectMapper objectMapper;

public GremlinConverter() {
this(new ObjectMapper());
}

public GremlinConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

@Override public Object fromBody(TypedInput body, Type type) throws ConversionException {
try {
if (type.getTypeName().equals(String.class.getName())) {
return new BufferedReader(new InputStreamReader(body.in()))
.lines().collect(Collectors.joining("\n"));
} else {
JavaType javaType = objectMapper.getTypeFactory().constructType(type);
return objectMapper.readValue(body.in(), javaType);
}
} catch (final IOException ioe) {
throw new ConversionException(ioe);
}
}

@Override public TypedOutput toBody(Object object) {
try {
String json = objectMapper.writeValueAsString(object);
return new TypedByteArray(MIME_TYPE, json.getBytes("UTF-8"));
} catch (JsonProcessingException | UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.netflix.spinnaker.orca.gremlin

import retrofit.http.*;

interface GremlinService {
@POST("/attacks/new")
@Headers(
"Content-Type: application/json",
"X-Gremlin-Agent: spinnaker/0.1.0"
)
fun create(
@Header("Authorization") authHeader: String,
@Body attackParameters: AttackParameters
): String

@GET("/executions")
@Headers(
"X-Gremlin-Agent: spinnaker/0.1.0"
)
fun getStatus(
@Header("Authorization") authHeader: String,
@Query("taskId") attackGuid: String
): List<AttackStatus>

@DELETE("/attacks/{attackGuid}")
@Headers(
"X-Gremlin-Agent: spinnaker/0.1.0"
)
fun haltAttack(
@Header("Authorization") authHeader: String,
@Path("attackGuid") attackGuid: String
): Void
}

data class AttackParameters(
val command: Map<String, Any>,
val target: Map<String, Any>
)

data class AttackStatus(
val guid: String,
val stage: String,
val stageLifecycle: String,
val endTime: String?,
val output: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.netflix.spinnaker.orca.gremlin.pipeline;

import com.netflix.spinnaker.orca.CancellableStage;
import com.netflix.spinnaker.orca.gremlin.GremlinService;
import com.netflix.spinnaker.orca.gremlin.tasks.LaunchGremlinAttackTask;
import com.netflix.spinnaker.orca.gremlin.tasks.MonitorGremlinAttackTask;
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder;
import com.netflix.spinnaker.orca.pipeline.TaskNode;
import com.netflix.spinnaker.orca.pipeline.model.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.Map;
import java.util.Optional;

@Component
public class GremlinStage implements StageDefinitionBuilder, CancellableStage {
public final static String APIKEY_KEY = "gremlinApiKey";
public final static String COMMAND_TEMPLATE_ID_KEY = "gremlinCommandTemplateId";
public final static String TARGET_TEMPLATE_ID_KEY = "gremlinTargetTemplateId";
public final static String GUID_KEY = "gremlinAttackGuid";
public final static String TERMINAL_KEY = "isGremlinTerminal";

@Autowired
private GremlinService gremlinService;

@Override
public void taskGraph(@Nonnull Stage stage, @Nonnull TaskNode.Builder builder) {
builder
.withTask("launchGremlinAttack", LaunchGremlinAttackTask.class)
.withTask("monitorGremlinAttack", MonitorGremlinAttackTask.class);
}

@Override
public Result cancel(Stage stage) {
final Map<String, Object> ctx = stage.getContext();
final boolean isAttackCompleted = Optional.ofNullable(ctx.get(TERMINAL_KEY))
.map(s -> {
try {
return Boolean.parseBoolean((String) s);
} catch (final Exception ex) {
return false;
}
})
.orElse(false);

if (!isAttackCompleted) {
gremlinService.haltAttack(getApiKey(ctx), getAttackGuid(ctx));
return new CancellableStage.Result(stage, ctx);
}
return null;
}

public static String getApiKey(final Map<String, Object> ctx) {
final String apiKey = (String) ctx.get(APIKEY_KEY);
if (apiKey == null || apiKey.isEmpty()) {
throw new RuntimeException("No API Key provided");
} else {
return "Key " + apiKey;
}
}

public static String getAttackGuid(final Map<String, Object> ctx) {
final String guid = (String) ctx.get(GUID_KEY);
if (guid == null || guid.isEmpty()) {
throw new RuntimeException("Could not find an active Gremlin attack GUID");
} else {
return guid;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.netflix.spinnaker.orca.gremlin.tasks;

import com.netflix.spinnaker.orca.ExecutionStatus;
import com.netflix.spinnaker.orca.Task;
import com.netflix.spinnaker.orca.TaskResult;
import com.netflix.spinnaker.orca.gremlin.AttackParameters;
import com.netflix.spinnaker.orca.gremlin.GremlinService;
import com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage;
import com.netflix.spinnaker.orca.pipeline.model.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.Map;

import static com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage.COMMAND_TEMPLATE_ID_KEY;
import static com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage.GUID_KEY;
import static com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage.TARGET_TEMPLATE_ID_KEY;

@Component
public class LaunchGremlinAttackTask implements Task {
private static final String GREMLIN_TEMPLATE_ID_KEY = "template_id";

@Autowired
private GremlinService gremlinService;

@Nonnull
@Override
public TaskResult execute(@Nonnull Stage stage) {
final Map<String, Object> ctx = stage.getContext();

final String apiKey = GremlinStage.getApiKey(ctx);

final String commandTemplateId = (String) ctx.get(COMMAND_TEMPLATE_ID_KEY);
if (commandTemplateId == null || commandTemplateId.isEmpty()) {
throw new RuntimeException("No command template provided");
}

final String targetTemplateId = (String) ctx.get(TARGET_TEMPLATE_ID_KEY);
if (targetTemplateId == null || targetTemplateId.isEmpty()) {
throw new RuntimeException("No target template provided");
}

final Map<String, Object> commandViaTemplate = new HashMap<>();
commandViaTemplate.put(GREMLIN_TEMPLATE_ID_KEY, commandTemplateId);

final Map<String, Object> targetViaTemplate = new HashMap<>();
targetViaTemplate.put(GREMLIN_TEMPLATE_ID_KEY, targetTemplateId);

final AttackParameters newAttack = new AttackParameters(commandViaTemplate, targetViaTemplate);

final String createdGuid = gremlinService.create(apiKey, newAttack);
final Map<String, Object> responseMap = new HashMap<>();
responseMap.put(GUID_KEY, createdGuid);
return new TaskResult(ExecutionStatus.SUCCEEDED, responseMap);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.netflix.spinnaker.orca.gremlin.tasks;

import com.netflix.spinnaker.orca.ExecutionStatus;
import com.netflix.spinnaker.orca.OverridableTimeoutRetryableTask;
import com.netflix.spinnaker.orca.Task;
import com.netflix.spinnaker.orca.TaskResult;
import com.netflix.spinnaker.orca.gremlin.AttackStatus;
import com.netflix.spinnaker.orca.gremlin.GremlinService;
import com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage;
import com.netflix.spinnaker.orca.pipeline.model.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.netflix.spinnaker.orca.gremlin.pipeline.GremlinStage.TERMINAL_KEY;

@Component
public class MonitorGremlinAttackTask implements OverridableTimeoutRetryableTask, Task {
@Autowired
private GremlinService gremlinService;

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

@Nonnull
@Override
public TaskResult execute(@Nonnull Stage stage) {
final Map<String, Object> ctx = stage.getContext();

final String apiKey = GremlinStage.getApiKey(ctx);
final String attackGuid = GremlinStage.getAttackGuid(ctx);

final List<AttackStatus> statuses = gremlinService.getStatus(apiKey, attackGuid);

boolean foundFailedAttack = false;
String failureType = "";
String failureOutput = "";

for (final AttackStatus status : statuses) {
if (status.getEndTime() == null) {
return new TaskResult(ExecutionStatus.RUNNING, ctx);
}
if (isFailure(status.getStageLifecycle())) {
foundFailedAttack = true;
failureType = status.getStage();
failureOutput = status.getOutput();
}
}
ctx.put(TERMINAL_KEY, "true");
if (foundFailedAttack) {
throw new RuntimeException("Gremlin run failed (" + failureType + ") with output : " + failureOutput);
} else {
return new TaskResult(ExecutionStatus.SUCCEEDED, ctx);
}
}

@Override
public long getBackoffPeriod() {
return TimeUnit.SECONDS.toMillis(10);
}

@Override
public long getTimeout() {
return TimeUnit.MINUTES.toMillis(15);
}

private boolean isFailure(final String gremlinStageName) {
return gremlinStageName.equals("Error");
}
}
5 changes: 5 additions & 0 deletions orca-web/config/orca.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ tasks:
logging:
config: classpath:logback-defaults.xml

integrations:
gremlin:
enabled: false
baseUrl: https://api.gremlin.com/v1

# This configuration lets you configure Webhook stages that will appear as native stages in the Deck UI.
# Properties that are set here will not be displayed in the GUI
#webhook:
Expand Down
1 change: 1 addition & 0 deletions orca-web/orca-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
compile project(":orca-migration")
compile project(":orca-sql")
compile project(":orca-sql-mysql")
compile project(":orca-integrations-gremlin")

runtime spinnaker.dependency("kork")
compile spinnaker.dependency('korkExceptions')
Expand Down
Loading