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

Proof-of-concept port of ApplicationAbleFlight using annotations #1698

Draft
wants to merge 1 commit into
base: stairway_poc
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package bio.terra.workspace.common.stairway;

import bio.terra.buffer.model.ErrorReport;
import bio.terra.stairway.FlightContext;
import bio.terra.stairway.Step;
import bio.terra.stairway.StepResult;
import bio.terra.stairway.exception.RetryException;
import com.google.common.annotations.VisibleForTesting;
import org.springframework.http.HttpStatus;

public abstract class BaseStep implements Step {
@StepOutput protected Object response;
@StepOutput protected HttpStatus statusCode;

private FlightContext context;

@Override
public final StepResult doStep(FlightContext context)
throws InterruptedException, RetryException {
this.context = context;
StepUtils.readInputs(this, context);
try {
return perform();
} finally {
StepUtils.writeOutputs(this, context);
}
}

@Override
public final StepResult undoStep(FlightContext context) throws InterruptedException {
this.context = context;
StepUtils.readInputs(this, context);
return undo();
}

public abstract StepResult perform() throws InterruptedException, RetryException;

public StepResult undo() throws InterruptedException {
// Many steps aren't undoable.
return StepResult.getStepResultSuccess();
}

protected void setErrorResponse(String message, HttpStatus responseStatus) {
ErrorReport errorModel = new ErrorReport().message(message);
setResponse(errorModel, responseStatus);
}

protected void setResponse(Object responseObject, HttpStatus responseStatus) {
response = responseObject;
statusCode = responseStatus;
}

protected void setResponse(Object responseObject) {
response = responseObject;
statusCode = HttpStatus.OK;
}

protected FlightContext getContext() {
return context;
}

protected String getFlightId() {
return context.getFlightId();
}

@VisibleForTesting
public Object getResponse() {
return response;
}

@VisibleForTesting
public HttpStatus getStatusCode() {
return statusCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bio.terra.workspace.common.stairway;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StepInput {
String USE_DEFAULT_NAME = "";

String value() default USE_DEFAULT_NAME;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package bio.terra.workspace.common.stairway;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StepOutput {
String USE_DEFAULT_NAME = "";

String value() default USE_DEFAULT_NAME;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package bio.terra.workspace.common.stairway;

import bio.terra.stairway.Flight;
import bio.terra.stairway.FlightContext;
import bio.terra.stairway.FlightMap;
import bio.terra.stairway.Step;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

// Suppress sonar warnings about reflection API usage. Reflection APIs must be used to read and
// write fields in a Step.
@SuppressWarnings({"java:S3011"})
public class StepUtils {

public static class MissingStepInputException extends RuntimeException {
public MissingStepInputException(String key) {
super("No flight value found for StepInput key '" + key + "'");
}
}

public static class IllegalSetException extends RuntimeException {
public IllegalSetException(Throwable cause) {
super(cause);
}
}

public static class IllegalGetException extends RuntimeException {
public IllegalGetException(Throwable cause) {
super(cause);
}
}

private StepUtils() {}

public static String keyFromField(Field field) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By default, the inputs and outputs are mapped to flight map (or flight input map) elements by using the annotated field's name. You can also provide an annotation argument to use a different name instead.

var input = field.getAnnotation(StepInput.class);
if (input != null && !input.value().isEmpty()) {
return input.value();
}
var output = field.getAnnotation(StepOutput.class);
if (output != null && !output.value().isEmpty()) {
return output.value();
}
return field.getName();
}

private static List<Field> collect(Step step, Class<? extends Annotation> annotation) {
List<Field> inputs = new ArrayList<>();
for (Class<?> clazz = step.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(annotation)) {
inputs.add(field);
}
}
}
return inputs;
}

public static void readInputs(Step step, FlightContext context) throws MissingStepInputException {
collect(step, StepInput.class)
.forEach(
field -> {
String key = keyFromField(field);
if (context.getInputParameters().containsKey(key)) {
setField(step, context.getInputParameters(), field, key);
} else if (context.getWorkingMap().containsKey(key)) {
setField(step, context.getWorkingMap(), field, key);
} else if (!field.isAnnotationPresent(StepOutput.class)) {
// If the field is only used as an input, report an error if there's no value for
// it.
throw new MissingStepInputException(key);
}
});
}

private static void setField(Step step, FlightMap map, Field field, String key) {
field.setAccessible(true);
try {
field.set(step, map.get(key, field.getType()));
} catch (IllegalAccessException e) {
throw new IllegalSetException(e);
}
}

public static void writeOutputs(Step step, FlightContext context) {
collect(step, StepOutput.class)
.forEach(
field -> {
field.setAccessible(true);
final Object value;
try {
value = field.get(step);
} catch (IllegalAccessException e) {
throw new IllegalGetException(e);
}
// An unset output can occur if an exception is thrown inside the run() operation.
if (value != null) {
context.getWorkingMap().put(keyFromField(field), value);
}
});
}

public record StepData(String stepName, List<String> inputs, List<String> outputs) {}

static StepData getStepData(Step step) {
return new StepData(
step.getClass().getSimpleName(),
collect(step, StepInput.class).stream().map(StepUtils::keyFromField).toList(),
collect(step, StepOutput.class).stream().map(StepUtils::keyFromField).toList());
}

// Using inputs and outputs, generate a flow analysis report
public static List<StepData> generateFlowAnalysisReport(Flight flight) {
return flight.getSteps().stream().map(StepUtils::getStepData).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package bio.terra.workspace.service.workspace.flight.application;

import bio.terra.stairway.StepResult;
import bio.terra.stairway.exception.RetryException;
import bio.terra.workspace.common.stairway.BaseStep;
import bio.terra.workspace.common.stairway.StepInput;
import bio.terra.workspace.db.ApplicationDao;
import bio.terra.workspace.service.workspace.model.WsmWorkspaceApplication;
import java.util.UUID;

public class ApplicationAbleDaoStepV3 extends BaseStep {
private final ApplicationDao applicationDao;
private final String applicationId;
@StepInput private UUID workspaceId;
@StepInput private AbleEnum applicationAbleEnum;
@StepInput private boolean applicationAbleDao;

public ApplicationAbleDaoStepV3(ApplicationDao applicationDao, String applicationId) {
this.applicationDao = applicationDao;
this.applicationId = applicationId;
}

@Override
public StepResult perform() throws InterruptedException, RetryException {

// if the application was in the correct database state in precheck, we do nothing
if (applicationAbleDao) {
return StepResult.getStepResultSuccess();
}

WsmWorkspaceApplication wsmApp;
if (applicationAbleEnum == AbleEnum.ENABLE) {
wsmApp = applicationDao.enableWorkspaceApplication(workspaceId, applicationId);
} else {
wsmApp = applicationDao.disableWorkspaceApplication(workspaceId, applicationId);
}
setResponse(wsmApp);
return StepResult.getStepResultSuccess();
}

@Override
public StepResult undo() throws InterruptedException {
// if the application was in the correct database state in precheck, we do nothing
if (applicationAbleDao) {
return StepResult.getStepResultSuccess();
}

if (applicationAbleEnum == AbleEnum.ENABLE) {
applicationDao.disableWorkspaceApplication(workspaceId, applicationId);
} else {
applicationDao.enableWorkspaceApplication(workspaceId, applicationId);
}
return StepResult.getStepResultSuccess();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package bio.terra.workspace.service.workspace.flight.application;

import bio.terra.stairway.Flight;
import bio.terra.stairway.FlightMap;
import bio.terra.workspace.common.utils.FlightBeanBag;
import bio.terra.workspace.service.workspace.flight.WorkspaceFlightMapKeys;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;

public class ApplicationAbleFlightV3 extends Flight {
public ApplicationAbleFlightV3(FlightMap inputParameters, Object applicationContext) {
super(inputParameters, applicationContext);

FlightBeanBag beanBag = FlightBeanBag.getFromObject(applicationContext);

// get data from inputs that steps need

List<String> applicationIdList =
inputParameters.get(WorkspaceFlightMapKeys.APPLICATION_IDS, new TypeReference<>() {});

for (String applicationId : applicationIdList) {
addStep(
new ApplicationAblePrecheckStepV3(
beanBag.getApplicationDao(), beanBag.getSamService(), applicationId));

addStep(new ApplicationAbleIamStepV3(beanBag.getSamService()));

addStep(new ApplicationAbleDaoStepV3(beanBag.getApplicationDao(), applicationId));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package bio.terra.workspace.service.workspace.flight.application;

import bio.terra.stairway.StepResult;
import bio.terra.stairway.exception.RetryException;
import bio.terra.workspace.common.stairway.BaseStep;
import bio.terra.workspace.common.stairway.StepInput;
import bio.terra.workspace.service.iam.AuthenticatedUserRequest;
import bio.terra.workspace.service.iam.SamService;
import bio.terra.workspace.service.iam.model.WsmIamRole;
import bio.terra.workspace.service.workspace.model.WsmApplication;
import java.util.UUID;

public class ApplicationAbleIamStepV3 extends BaseStep {
private final SamService samService;
@StepInput private AuthenticatedUserRequest authUserInfo;
@StepInput private UUID workspaceId;
@StepInput private AbleEnum applicationAbleEnum;
@StepInput private WsmApplication wsmApplication;
@StepInput private boolean applicationAbleSam;

public ApplicationAbleIamStepV3(SamService samService) {
this.samService = samService;
}

@Override
public StepResult perform() throws InterruptedException, RetryException {
// if the application was in the correct Sam state in precheck, then we do nothing
if (applicationAbleSam) {
return StepResult.getStepResultSuccess();
}

if (applicationAbleEnum == AbleEnum.ENABLE) {
samService.grantWorkspaceRole(
workspaceId, authUserInfo, WsmIamRole.APPLICATION, wsmApplication.getServiceAccount());
} else {
samService.removeWorkspaceRole(
workspaceId, authUserInfo, WsmIamRole.APPLICATION, wsmApplication.getServiceAccount());
}

return StepResult.getStepResultSuccess();
}

@Override
public StepResult undo() throws InterruptedException {

// if the application was not already enabled in Sam when we started, we do not undo it
if (applicationAbleSam) {
return StepResult.getStepResultSuccess();
}

if (applicationAbleEnum == AbleEnum.ENABLE) {
samService.removeWorkspaceRole(
workspaceId, authUserInfo, WsmIamRole.APPLICATION, wsmApplication.getServiceAccount());
} else {
samService.grantWorkspaceRole(
workspaceId, authUserInfo, WsmIamRole.APPLICATION, wsmApplication.getServiceAccount());
}

return StepResult.getStepResultSuccess();
}
}
Loading