diff --git a/orca-api/orca-api.gradle b/orca-api/orca-api.gradle new file mode 100644 index 0000000000..8be4734f3e --- /dev/null +++ b/orca-api/orca-api.gradle @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Armory, 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. + */ + +apply from: "$rootDir/gradle/kotlin.gradle" +apply from: "$rootDir/gradle/spock.gradle" + +test { + useJUnitPlatform { + includeEngines "junit-vintage", "junit-jupiter" + } +} + +dependencies { + implementation("com.google.guava:guava") +} diff --git a/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStage.java b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStage.java new file mode 100644 index 0000000000..d59506f0b4 --- /dev/null +++ b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStage.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Armory, 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.api; + +import com.google.common.annotations.Beta; + +@Beta +public interface SimpleStage { + SimpleStageOutput execute(SimpleStageInput simpleStageInput); + + String getName(); +} diff --git a/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageInput.java b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageInput.java new file mode 100644 index 0000000000..bb59424855 --- /dev/null +++ b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageInput.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Armory, 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.api; + +import com.google.common.annotations.Beta; + +@Beta +public class SimpleStageInput { + private T value; + + public SimpleStageInput(T value) { + this.value = value; + } + + public void setValue(T value) { + this.value = value; + } + + public T getValue() { + return this.value; + } +} diff --git a/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageOutput.java b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageOutput.java new file mode 100644 index 0000000000..138a29faf4 --- /dev/null +++ b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageOutput.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 Armory, 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.api; + +import com.google.common.annotations.Beta; +import java.util.Map; + +@Beta +public class SimpleStageOutput { + private SimpleStageStatus status; + + public void setStatus(SimpleStageStatus status) { + this.status = status; + } + + public SimpleStageStatus getStatus() { + return this.status; + } + + private Map outputs; + + public void setOutputs(Map outputs) { + this.outputs = outputs; + } + + public Map getOutputs() { + return this.outputs; + } +} diff --git a/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageStatus.java b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageStatus.java new file mode 100644 index 0000000000..e4299e9267 --- /dev/null +++ b/orca-api/src/main/java/com/netflix/spinnaker/orca/api/SimpleStageStatus.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Armory, 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.api; + +import com.google.common.annotations.Beta; + +@Beta +public enum SimpleStageStatus { + TERMINAL, + RUNNING, + COMPLETED, + NOT_STARTED +} diff --git a/orca-core/orca-core.gradle b/orca-core/orca-core.gradle index 0c9b3931a9..fe68437f31 100644 --- a/orca-core/orca-core.gradle +++ b/orca-core/orca-core.gradle @@ -32,6 +32,7 @@ dependencies { api("io.reactivex:rxjava") implementation(project(":orca-extensionpoint")) + implementation(project(":orca-api")) implementation("com.github.ben-manes.caffeine:guava") implementation("org.slf4j:slf4j-api") implementation("com.fasterxml.jackson.core:jackson-annotations") diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/StageResolver.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/StageResolver.java index 3f6fce064b..f3061a95c3 100644 --- a/orca-core/src/main/java/com/netflix/spinnaker/orca/StageResolver.java +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/StageResolver.java @@ -18,10 +18,13 @@ import static java.lang.String.format; +import com.netflix.spinnaker.orca.api.SimpleStage; +import com.netflix.spinnaker.orca.pipeline.SimpleStageDefinitionBuilder; import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import javax.annotation.Nonnull; /** @@ -32,7 +35,9 @@ public class StageResolver { private final Map stageDefinitionBuilderByAlias = new HashMap<>(); - public StageResolver(Collection stageDefinitionBuilders) { + public StageResolver( + Collection stageDefinitionBuilders, + Collection apiStages) { for (StageDefinitionBuilder stageDefinitionBuilder : stageDefinitionBuilders) { stageDefinitionBuilderByAlias.put(stageDefinitionBuilder.getType(), stageDefinitionBuilder); for (String alias : stageDefinitionBuilder.aliases()) { @@ -48,6 +53,13 @@ public StageResolver(Collection stageDefinitionBuilders) stageDefinitionBuilderByAlias.put(alias, stageDefinitionBuilder); } } + + if (!Objects.equals(apiStages, null)) { + for (SimpleStage stage : apiStages) { + SimpleStageDefinitionBuilder builder = new SimpleStageDefinitionBuilder(stage); + stageDefinitionBuilderByAlias.put(stage.getName(), builder); + } + } } /** diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/config/OrcaConfiguration.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/config/OrcaConfiguration.java index 0d2098f833..78e2b06c48 100644 --- a/orca-core/src/main/java/com/netflix/spinnaker/orca/config/OrcaConfiguration.java +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/config/OrcaConfiguration.java @@ -25,6 +25,7 @@ import com.netflix.spinnaker.orca.StageResolver; import com.netflix.spinnaker.orca.Task; import com.netflix.spinnaker.orca.TaskResolver; +import com.netflix.spinnaker.orca.api.SimpleStage; import com.netflix.spinnaker.orca.commands.ForceExecutionCancellationCommand; import com.netflix.spinnaker.orca.events.ExecutionEvent; import com.netflix.spinnaker.orca.events.ExecutionListenerAdapter; @@ -40,8 +41,7 @@ import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor; import java.time.Clock; import java.time.Duration; -import java.util.Collection; -import java.util.List; +import java.util.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -187,8 +187,12 @@ public TaskResolver taskResolver(Collection tasks) { } @Bean - public StageResolver stageResolver(Collection stageDefinitionBuilders) { - return new StageResolver(stageDefinitionBuilders); + public StageResolver stageResolver( + Collection stageDefinitionBuilders, + Optional> simpleStages) { + Collection stages = + simpleStages.isPresent() ? simpleStages.get() : new ArrayList(); + return new StageResolver(stageDefinitionBuilders, stages); } @Bean(name = EVENT_LISTENER_FACTORY_BEAN_NAME) diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleStageDefinitionBuilder.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleStageDefinitionBuilder.java new file mode 100644 index 0000000000..53019a6981 --- /dev/null +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleStageDefinitionBuilder.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019 Armory, 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.pipeline; + +import com.netflix.spinnaker.orca.api.SimpleStage; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import javax.annotation.Nonnull; + +public class SimpleStageDefinitionBuilder implements StageDefinitionBuilder { + private SimpleStage simpleStage; + + public SimpleStageDefinitionBuilder(SimpleStage simpleStage) { + this.simpleStage = simpleStage; + } + + public void taskGraph(@Nonnull Stage stage, @Nonnull TaskNode.Builder builder) { + SimpleTask task = new SimpleTask(simpleStage); + builder.withTask(simpleStage.getName(), task.getClass()); + } +} diff --git a/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleTask.java b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleTask.java new file mode 100644 index 0000000000..40f11952cc --- /dev/null +++ b/orca-core/src/main/java/com/netflix/spinnaker/orca/pipeline/SimpleTask.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 Armory, 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.pipeline; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.orca.ExecutionStatus; +import com.netflix.spinnaker.orca.Task; +import com.netflix.spinnaker.orca.TaskResult; +import com.netflix.spinnaker.orca.api.SimpleStage; +import com.netflix.spinnaker.orca.api.SimpleStageInput; +import com.netflix.spinnaker.orca.api.SimpleStageOutput; +import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.ResolvableType; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class SimpleTask implements Task { + private SimpleStage simpleStage; + + SimpleTask(@Nullable SimpleStage simpleStage) { + this.simpleStage = simpleStage; + } + + @Nonnull + public TaskResult execute(@Nonnull Stage stage) { + ObjectMapper objectMapper = OrcaObjectMapper.newInstance(); + + ExecutionStatus status; + SimpleStageOutput outputs = new SimpleStageOutput(); + + try { + List> cArg = Arrays.asList(SimpleStageInput.class); + Method method = simpleStage.getClass().getMethod("execute", cArg.toArray(new Class[0])); + Type inputType = ResolvableType.forMethodParameter(method, 0).getGeneric().getType(); + Map typeVariableMap = + GenericTypeResolver.getTypeVariableMap(simpleStage.getClass()); + + SimpleStageInput simpleStageInput = + new SimpleStageInput( + objectMapper.convertValue( + stage.getContext(), GenericTypeResolver.resolveType(inputType, typeVariableMap))); + outputs = simpleStage.execute(simpleStageInput); + switch (outputs.getStatus()) { + case TERMINAL: + status = ExecutionStatus.TERMINAL; + break; + case RUNNING: + status = ExecutionStatus.RUNNING; + break; + case COMPLETED: + status = ExecutionStatus.SUCCEEDED; + break; + case NOT_STARTED: + status = ExecutionStatus.NOT_STARTED; + break; + default: + status = ExecutionStatus.FAILED_CONTINUE; + break; + } + } catch (Exception e) { + log.error("Cannot execute stage " + simpleStage.getName()); + log.error(e.getMessage()); + status = ExecutionStatus.TERMINAL; + } + + return TaskResult.builder(status) + .context(outputs.getOutputs()) + .outputs(outputs.getOutputs()) + .build(); + } +} diff --git a/orca-core/src/test/groovy/com/netflix/spinnaker/orca/StageResolverSpec.groovy b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/StageResolverSpec.groovy index 47e61e8d8a..e86bda3187 100644 --- a/orca-core/src/test/groovy/com/netflix/spinnaker/orca/StageResolverSpec.groovy +++ b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/StageResolverSpec.groovy @@ -16,22 +16,24 @@ package com.netflix.spinnaker.orca +import com.netflix.spinnaker.orca.api.SimpleStage +import com.netflix.spinnaker.orca.api.SimpleStageInput +import com.netflix.spinnaker.orca.api.SimpleStageOutput import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder import com.netflix.spinnaker.orca.pipeline.WaitStage -import com.netflix.spinnaker.orca.pipeline.model.Stage -import com.netflix.spinnaker.orca.pipeline.tasks.WaitTask import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll -import javax.annotation.Nonnull; - class StageResolverSpec extends Specification { @Subject def stageResolver = new StageResolver([ - new WaitStage(), - new AliasedStageDefinitionBuilder() - ]) + new WaitStage(), + new AliasedStageDefinitionBuilder() + ], + [ + new SimpleStage() + ]) @Unroll def "should lookup stage by name or alias"() { @@ -48,9 +50,12 @@ class StageResolverSpec extends Specification { def "should raise exception on duplicate alias"() { when: new StageResolver([ - new AliasedStageDefinitionBuilder(), - new AliasedStageDefinitionBuilder() - ]) + new AliasedStageDefinitionBuilder(), + new AliasedStageDefinitionBuilder() + ], + [ + new SimpleStage() + ]) then: thrown(StageResolver.DuplicateStageAliasException) @@ -67,4 +72,16 @@ class StageResolverSpec extends Specification { @StageDefinitionBuilder.Aliases("notAliased") class AliasedStageDefinitionBuilder implements StageDefinitionBuilder { } + + class SimpleStage implements SimpleStage { + @Override + public SimpleStageOutput execute(SimpleStageInput input) { + return new SimpleStageOutput(); + } + + @Override + public String getName() { + return "simpleApiStage"; + } + } } diff --git a/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/SimpleTaskSpec.groovy b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/SimpleTaskSpec.groovy new file mode 100644 index 0000000000..f5ff4d10f0 --- /dev/null +++ b/orca-core/src/test/groovy/com/netflix/spinnaker/orca/pipeline/SimpleTaskSpec.groovy @@ -0,0 +1,61 @@ +/* + * Copyright 2019 Armory, 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.pipeline + +import com.netflix.spinnaker.orca.ExecutionStatus +import com.netflix.spinnaker.orca.api.SimpleStage +import com.netflix.spinnaker.orca.api.SimpleStageInput +import com.netflix.spinnaker.orca.api.SimpleStageOutput +import com.netflix.spinnaker.orca.api.SimpleStageStatus +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +class SimpleTaskSpec extends Specification { + private static class MyStage implements SimpleStage { + @Override + String getName() { + return "myStage" + } + + @Override + SimpleStageOutput execute(SimpleStageInput input) { + SimpleStageOutput output = new SimpleStageOutput() + + Map stageOutput = new HashMap<>() + stageOutput.put("hello", "world") + + output.setStatus(SimpleStageStatus.COMPLETED) + output.setOutputs(stageOutput) + return output + } + } + + @Subject + def myStage = new MyStage() + + @Unroll + def "should check dynamic config property"() { + when: + def task = new SimpleTask(myStage) + def results = task.execute(new com.netflix.spinnaker.orca.pipeline.model.Stage()) + + then: + results.getStatus() == ExecutionStatus.SUCCEEDED + results.context.hello == "world" + } +} diff --git a/orca-queue/orca-queue.gradle b/orca-queue/orca-queue.gradle index d98b55a924..66fb09679e 100644 --- a/orca-queue/orca-queue.gradle +++ b/orca-queue/orca-queue.gradle @@ -19,6 +19,7 @@ apply from: "$rootDir/gradle/spek.gradle" dependencies { api(project(":orca-core")) + api(project(":orca-api")) implementation(project(":orca-kotlin")) implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("com.netflix.spinnaker.keiko:keiko-spring:$keikoVersion") diff --git a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CancelStageHandlerTest.kt b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CancelStageHandlerTest.kt index bf1cc89e5f..a6318f4956 100644 --- a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CancelStageHandlerTest.kt +++ b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CancelStageHandlerTest.kt @@ -24,6 +24,7 @@ import com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED import com.netflix.spinnaker.orca.ExecutionStatus.TERMINAL import com.netflix.spinnaker.orca.StageResolver import com.netflix.spinnaker.orca.TaskResolver +import com.netflix.spinnaker.orca.api.SimpleStage import com.netflix.spinnaker.orca.fixture.pipeline import com.netflix.spinnaker.orca.fixture.stage import com.netflix.spinnaker.orca.pipeline.DefaultStageDefinitionBuilderFactory @@ -56,7 +57,7 @@ object CancelStageHandlerTest : SubjectSpek({ val stageNavigator: StageNavigator = mock() val cancellableStage: CancelableStageDefinitionBuilder = mock() - val stageResolver = StageResolver(listOf(singleTaskStage, cancellableStage)) + val stageResolver = StageResolver(listOf(singleTaskStage, cancellableStage), emptyList>()) subject(GROUP) { CancelStageHandler( diff --git a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CompleteStageHandlerTest.kt b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CompleteStageHandlerTest.kt index cbef96b1dd..8a99d418d4 100644 --- a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CompleteStageHandlerTest.kt +++ b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/CompleteStageHandlerTest.kt @@ -26,6 +26,10 @@ import com.netflix.spinnaker.orca.ExecutionStatus.STOPPED import com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED import com.netflix.spinnaker.orca.ExecutionStatus.TERMINAL import com.netflix.spinnaker.orca.StageResolver +import com.netflix.spinnaker.orca.api.SimpleStage +import com.netflix.spinnaker.orca.api.SimpleStageInput +import com.netflix.spinnaker.orca.api.SimpleStageOutput +import com.netflix.spinnaker.orca.api.SimpleStageStatus import com.netflix.spinnaker.orca.events.StageComplete import com.netflix.spinnaker.orca.exceptions.DefaultExceptionHandler import com.netflix.spinnaker.orca.exceptions.ExceptionHandler @@ -143,6 +147,24 @@ object CompleteStageHandlerTest : SubjectSpek({ } } + val emptyApiStage = object : SimpleStage { + override fun getName() = "emptyApiStage" + + override fun execute(simpleStageInput: SimpleStageInput): SimpleStageOutput { + return SimpleStageOutput() + } + } + + val successfulApiStage = object : SimpleStage { + override fun getName() = "successfulApiStage" + + override fun execute(simpleStageInput: SimpleStageInput): SimpleStageOutput { + val output = SimpleStageOutput() + output.status = SimpleStageStatus.COMPLETED + return output + } + } + subject(GROUP) { CompleteStageHandler( queue, @@ -167,6 +189,10 @@ object CompleteStageHandlerTest : SubjectSpek({ stageWithNothingButAfterStages, stageWithSyntheticOnFailure, emptyStage + ), + listOf( + emptyApiStage, + successfulApiStage ) ) ) @@ -1467,4 +1493,22 @@ object CompleteStageHandlerTest : SubjectSpek({ } } } + + given("api stage completed successfully") { + val pipeline = pipeline { + stage { + refId = "1" + type = successfulApiStage.name + } + } + + val message = CompleteStage(pipeline.stageByRef("1")) + pipeline.stageById(message.stageId).apply { + status = SUCCEEDED + } + + it("stage was successfully ran") { + assertThat(pipeline.stageById(message.stageId).status).isEqualTo(SUCCEEDED) + } + } }) diff --git a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/RestartStageHandlerTest.kt b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/RestartStageHandlerTest.kt index cb4736fe91..9f90897c3c 100644 --- a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/RestartStageHandlerTest.kt +++ b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/RestartStageHandlerTest.kt @@ -22,6 +22,9 @@ import com.netflix.spinnaker.orca.ExecutionStatus.RUNNING import com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED import com.netflix.spinnaker.orca.ExecutionStatus.TERMINAL import com.netflix.spinnaker.orca.StageResolver +import com.netflix.spinnaker.orca.api.SimpleStage +import com.netflix.spinnaker.orca.api.SimpleStageInput +import com.netflix.spinnaker.orca.api.SimpleStageOutput import com.netflix.spinnaker.orca.fixture.pipeline import com.netflix.spinnaker.orca.fixture.stage import com.netflix.spinnaker.orca.pipeline.DefaultStageDefinitionBuilderFactory @@ -77,6 +80,14 @@ object RestartStageHandlerTest : SubjectSpek({ val pendingExecutionService: PendingExecutionService = mock() val clock = fixedClock() + val emptyApiStage = object : SimpleStage { + override fun getName() = "emptyApiStage" + + override fun execute(simpleStageInput: SimpleStageInput): SimpleStageOutput { + return SimpleStageOutput() + } + } + subject(GROUP) { RestartStageHandler( queue, @@ -87,6 +98,9 @@ object RestartStageHandlerTest : SubjectSpek({ singleTaskStage, stageWithSyntheticBefore, stageWithNestedSynthetics + ), + listOf( + emptyApiStage ) ) ), diff --git a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/StartStageHandlerTest.kt b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/StartStageHandlerTest.kt index 6297b06cb9..bd8439efa0 100644 --- a/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/StartStageHandlerTest.kt +++ b/orca-queue/src/test/kotlin/com/netflix/spinnaker/orca/q/handler/StartStageHandlerTest.kt @@ -24,6 +24,10 @@ import com.netflix.spinnaker.orca.ExecutionStatus.RUNNING import com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED import com.netflix.spinnaker.orca.ExecutionStatus.TERMINAL import com.netflix.spinnaker.orca.StageResolver +import com.netflix.spinnaker.orca.api.SimpleStage +import com.netflix.spinnaker.orca.api.SimpleStageInput +import com.netflix.spinnaker.orca.api.SimpleStageOutput +import com.netflix.spinnaker.orca.api.SimpleStageStatus import com.netflix.spinnaker.orca.events.StageStarted import com.netflix.spinnaker.orca.exceptions.ExceptionHandler import com.netflix.spinnaker.orca.fixture.pipeline @@ -100,6 +104,16 @@ object StartStageHandlerTest : SubjectSpek({ val registry = NoopRegistry() val retryDelay = Duration.ofSeconds(5) + val runningApiStage = object : SimpleStage { + override fun getName() = "runningApiStage" + + override fun execute(simpleStageInput: SimpleStageInput): SimpleStageOutput { + val output = SimpleStageOutput() + output.status = SimpleStageStatus.RUNNING + return output + } + } + subject(GROUP) { StartStageHandler( queue, @@ -118,6 +132,9 @@ object StartStageHandlerTest : SubjectSpek({ stageWithSyntheticAfterAndNoTasks, webhookStage, failPlanningStage + ), + listOf( + runningApiStage ) ) ), @@ -134,6 +151,7 @@ object StartStageHandlerTest : SubjectSpek({ fun resetMocks() = reset(queue, repository, publisher, exceptionHandler) describe("starting a stage") { + given("there is a single initial task") { val pipeline = pipeline { application = "foo" @@ -1132,5 +1150,25 @@ object StartStageHandlerTest : SubjectSpek({ verify(queue).push(isA()) } } + + given("api stage") { + val pipeline = pipeline { + stage { + refId = "1" + type = runningApiStage.name + } + } + + val message = StartStage(pipeline.stageByRef("1")) + pipeline.stageById(message.stageId).apply { + status = RUNNING + } + + afterGroup(::resetMocks) + + it("stage was successfully started") { + assertThat(pipeline.stageById(message.stageId).status).isEqualTo(RUNNING) + } + } } }) diff --git a/orca-web/orca-web.gradle b/orca-web/orca-web.gradle index 28557d7a64..bfa9b437f1 100644 --- a/orca-web/orca-web.gradle +++ b/orca-web/orca-web.gradle @@ -33,6 +33,7 @@ dependencies { implementation("net.logstash.logback:logstash-logback-encoder") implementation(project(":orca-core")) + implementation(project(":orca-api")) implementation(project(":orca-redis")) implementation(project(":orca-bakery")) implementation(project(":orca-clouddriver")) diff --git a/settings.gradle b/settings.gradle index b331447c04..0b0b8cfa99 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,7 @@ include "orca-extensionpoint", "orca-core", + "orca-api", "orca-core-tck", "orca-redis", "orca-test-redis",