From 51a9acfa607c42088596ec27dc2f5023865bb63b Mon Sep 17 00:00:00 2001 From: Kiran Godishala <53332225+kirangodishala@users.noreply.github.com> Date: Wed, 2 Oct 2024 03:30:44 +0530 Subject: [PATCH] feat(clouddriver): add a new task that checks if the application specified in the moniker or cluster keys exists in front50 and/or clouddriver (#4788) * feat(clouddriver): introduce a 'checkIfApplicationExists' task that checks if the application defined in the stage context is known to front50 and/or clouddriver * also refactor the task configuration properties to make it more manageable as we go ahead and add more task specific config properties. * also make it easy to reuse common properties like RetryConfig * feat(clouddriver): add a 'check if application exists' task for server group workflows * chore(clouddriver): simplify get application name logic in determineHealthProvidersTask * feat(clouddriver): update server group stages to include 'check if application exists' task * feat(clouddriver): add a 'check if application exists' task for cluster based workflows * feat(clouddriver): update cluster based stages to include 'check if application exists' task * feat(clouddriver): add a 'check if application exists' task for k8s manifest workflows * feat(clouddriver): update k8s manifest stages to include 'check if application exists' task * chore(clouddriver): use simpler presence check * feat(clouddriver): enable audit mode for checking if application exists in front50 --------- Co-authored-by: Apoorv Mahajan --- .../CheckIfApplicationExistsTaskConfig.java | 34 +++ .../clouddriver/config/tasks/RetryConfig.java | 31 +++ .../servergroup/CloneServerGroupStage.groovy | 10 + .../servergroup/CreateServerGroupStage.groovy | 29 ++- .../servergroup/ResizeServerGroupStage.groovy | 2 + .../AbstractDeployStrategyStage.groovy | 40 +++ .../AbstractCheckIfApplicationExistsTask.java | 196 ++++++++++++++ ...ctWaitForClusterWideClouddriverTask.groovy | 2 +- ...heckIfApplicationExistsForClusterTask.java | 51 ++++ ...IfApplicationExistsForServerGroupTask.java | 84 ++++++ .../config/TaskConfigurationProperties.java | 25 +- ...tClusterWideClouddriverOperationStage.java | 45 ++++ .../manifest/DeployManifestStage.java | 50 +++- .../BulkDestroyServerGroupStage.java | 22 ++ .../tasks/DetermineHealthProvidersTask.java | 22 +- ...eckIfApplicationExistsForManifestTask.java | 67 +++++ .../CreateServerGroupStageSpec.groovy | 18 +- ...sterWideClouddriverOperationStageTest.java | 103 ++++++++ .../manifest/DeployManifestStageTest.java | 33 ++- .../CreateServerGroupStageTest.java | 67 +++++ .../tasks/job/WaitOnJobCompletionTest.java | 4 +- ...plicationExistsForServerGroupTaskTest.java | 242 ++++++++++++++++++ .../DeployManifestExpressionEvaluationTest.kt | 2 +- .../servergroup/clouddriver-application.json | 30 +++ 24 files changed, 1160 insertions(+), 49 deletions(-) create mode 100644 orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/CheckIfApplicationExistsTaskConfig.java create mode 100644 orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/RetryConfig.java create mode 100644 orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/AbstractCheckIfApplicationExistsTask.java create mode 100644 orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/CheckIfApplicationExistsForClusterTask.java create mode 100644 orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTask.java create mode 100644 orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/CheckIfApplicationExistsForManifestTask.java create mode 100644 orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStageTest.java create mode 100644 orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageTest.java create mode 100644 orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTaskTest.java create mode 100644 orca-clouddriver/src/test/resources/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/clouddriver-application.json diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/CheckIfApplicationExistsTaskConfig.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/CheckIfApplicationExistsTaskConfig.java new file mode 100644 index 0000000000..98f67011f9 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/CheckIfApplicationExistsTaskConfig.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Salesforce.com, 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.config.tasks; + +import lombok.Data; + +@Data +public class CheckIfApplicationExistsTaskConfig { + // controls whether clouddriver should be queried for an application or not. Defaults to true + boolean checkClouddriver = true; + + // controls whether the task should fail or simply log a warning + boolean auditModeEnabled = true; + + // front50 specific retry config. This is only applicable when services.front50.enabled: true + private RetryConfig front50Retries = new RetryConfig(); + + // clouddriver specific retry config. This is only applicable when checkClouddriver: true + private RetryConfig clouddriverRetries = new RetryConfig(); +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/RetryConfig.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/RetryConfig.java new file mode 100644 index 0000000000..8c32294bfc --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/config/tasks/RetryConfig.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 Salesforce.com, 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.config.tasks; + +import lombok.Data; + +@Data +public class RetryConfig { + // total number of attempts + int maxAttempts = 6; + + // time in ms to wait before subsequent retry attempts + long backOffInMs = 5000; + + // flag to enable exponential backoff + boolean exponentialBackoffEnabled = false; +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CloneServerGroupStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CloneServerGroupStage.groovy index 3059216eeb..cd968324a4 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CloneServerGroupStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CloneServerGroupStage.groovy @@ -23,6 +23,7 @@ import com.netflix.spinnaker.orca.clouddriver.ForceCacheRefreshAware import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.strategies.AbstractDeployStrategyStage import com.netflix.spinnaker.orca.clouddriver.tasks.MonitorKatoTask import com.netflix.spinnaker.orca.clouddriver.tasks.instance.WaitForUpInstancesTask +import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.CheckIfApplicationExistsForServerGroupTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.CloneServerGroupTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ServerGroupCacheForceRefreshTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.AddServerGroupEntityTagsTask @@ -78,4 +79,13 @@ class CloneServerGroupStage extends AbstractDeployStrategyStage implements Force return tasks } + + @Override + protected Map getOptionalPreValidationTasks(){ + Map output = [:] + if (isCheckIfApplicationExistsEnabled(dynamicConfigService)) { + output[CheckIfApplicationExistsForServerGroupTask.getTaskName()] = CheckIfApplicationExistsForServerGroupTask + } + return output + } } diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStage.groovy index 480937f330..e847026211 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStage.groovy @@ -21,6 +21,7 @@ import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution import com.netflix.spinnaker.orca.api.pipeline.graph.StageGraphBuilder import com.netflix.spinnaker.orca.clouddriver.ForceCacheRefreshAware +import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.CheckIfApplicationExistsForServerGroupTask import com.netflix.spinnaker.orca.kato.pipeline.strategy.Strategy import javax.annotation.Nonnull @@ -37,7 +38,6 @@ import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ServerGroupCache import com.netflix.spinnaker.orca.clouddriver.utils.MonikerHelper import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component import static java.util.concurrent.TimeUnit.MINUTES @@ -47,20 +47,22 @@ import static java.util.concurrent.TimeUnit.MINUTES class CreateServerGroupStage extends AbstractDeployStrategyStage implements ForceCacheRefreshAware { public static final String PIPELINE_CONFIG_TYPE = "createServerGroup" - @Autowired private FeaturesService featuresService - - @Autowired private RollbackClusterStage rollbackClusterStage - - @Autowired private DestroyServerGroupStage destroyServerGroupStage - - @Autowired private DynamicConfigService dynamicConfigService - CreateServerGroupStage() { + CreateServerGroupStage( + FeaturesService featuresService, + RollbackClusterStage rollbackClusterStage, + DestroyServerGroupStage destroyServerGroupStage, + DynamicConfigService dynamicConfigService + ){ super(PIPELINE_CONFIG_TYPE) + this.featuresService = featuresService + this.rollbackClusterStage = rollbackClusterStage + this.destroyServerGroupStage = destroyServerGroupStage + this.dynamicConfigService = dynamicConfigService } @Override @@ -162,6 +164,15 @@ class CreateServerGroupStage extends AbstractDeployStrategyStage implements Forc super.onFailureStages(stage, graph) } + @Override + protected Map getOptionalPreValidationTasks(){ + Map output = [:] + if (isCheckIfApplicationExistsEnabled(dynamicConfigService)) { + output[CheckIfApplicationExistsForServerGroupTask.getTaskName()] = CheckIfApplicationExistsForServerGroupTask + } + return output + } + static class StageData { String application String account diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/ResizeServerGroupStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/ResizeServerGroupStage.groovy index d1e43b4460..9148cf6881 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/ResizeServerGroupStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/ResizeServerGroupStage.groovy @@ -22,6 +22,7 @@ import com.netflix.spinnaker.orca.clouddriver.pipeline.providers.aws.ModifyAwsSc import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.support.TargetServerGroupLinearStageSupport import com.netflix.spinnaker.orca.clouddriver.tasks.DetermineHealthProvidersTask import com.netflix.spinnaker.orca.clouddriver.tasks.MonitorKatoTask +import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.CheckIfApplicationExistsForServerGroupTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ResizeServerGroupTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ServerGroupCacheForceRefreshTask import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.WaitForCapacityMatchTask @@ -43,6 +44,7 @@ class ResizeServerGroupStage extends TargetServerGroupLinearStageSupport { @Override protected void taskGraphInternal(StageExecution stage, TaskNode.Builder builder) { builder + .withTask(CheckIfApplicationExistsForServerGroupTask.getTaskName(), CheckIfApplicationExistsForServerGroupTask) .withTask("determineHealthProviders", DetermineHealthProvidersTask) .withTask("resizeServerGroup", ResizeServerGroupTask) .withTask("monitorServerGroup", MonitorKatoTask) diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/strategies/AbstractDeployStrategyStage.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/strategies/AbstractDeployStrategyStage.groovy index c6093b9c56..958b7d0341 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/strategies/AbstractDeployStrategyStage.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/strategies/AbstractDeployStrategyStage.groovy @@ -16,6 +16,8 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.strategies +import com.google.common.base.CaseFormat +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService import com.netflix.spinnaker.moniker.Moniker import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution import com.netflix.spinnaker.orca.api.pipeline.graph.StageGraphBuilder @@ -67,8 +69,23 @@ abstract class AbstractDeployStrategyStage extends AbstractCloudProviderAwareSta protected abstract List basicTasks(StageExecution stage) + /** + * helper method that returns a map of task name to task class that are added to a stage. These + * tasks are added only if the correct configuration property is set. + * + * + * @return map of task name to task class + */ + protected Map getOptionalPreValidationTasks() { + return [:] + } + @Override void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder builder) { + // add any optional pre-validation tasks at the beginning of the stage + getOptionalPreValidationTasks().each {key, val -> + builder.withTask(key, val) + } String cloudProvider = getCloudProvider(stage); if ("cloudrun".equals(cloudProvider)) { @@ -193,6 +210,29 @@ abstract class AbstractDeployStrategyStage extends AbstractCloudProviderAwareSta } } + /** + * method that checks if the check if application exists task is enabled via + * configuration properties + * + * @param dynamicConfigService config properties + * @return true, if the config is set, false otherwise + */ + boolean isCheckIfApplicationExistsEnabled(DynamicConfigService dynamicConfigService) { + String className = getClass().getSimpleName(); + + try { + return dynamicConfigService.isEnabled( + String.format( + "stages.%s.check-if-application-exists", + CaseFormat.LOWER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, + Character.toLowerCase(className.charAt(0)).toString() + className.substring(1))), + false) + } catch (Exception ignored) { + return false + } + } + /** * This nasty method is here because of an unfortunate misstep in pipeline configuration that introduced a nested * "cluster" key, when in reality we want all of the parameters to be derived from the top level. To preserve diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/AbstractCheckIfApplicationExistsTask.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/AbstractCheckIfApplicationExistsTask.java new file mode 100644 index 0000000000..816549cd2d --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/AbstractCheckIfApplicationExistsTask.java @@ -0,0 +1,196 @@ +/* + * Copyright 2021 Salesforce.com, 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.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; +import com.netflix.spinnaker.orca.api.pipeline.Task; +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.OortService; +import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.config.tasks.CheckIfApplicationExistsTaskConfig; +import com.netflix.spinnaker.orca.front50.Front50Service; +import com.netflix.spinnaker.orca.front50.model.Application; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import retrofit.client.Response; + +/** + * Abstract class that is meant to test the presence of a spinnaker application. It will first check + * if the application exists in front50. Since front50 can be disabled, it falls back to checking + * for the application in clouddriver. + * + *

If the application doesn't exist, the task fails. + * + *

The motivation for adding such a task is to prevent creation of any ad-hoc applications in + * amazon and kubernetes deployment pipeline stages. + * + *

Depending on what is the application value set in the moniker and/or the cluster keys in such + * stages, any application that isn't known to front50 can be created by clouddriver on demand. This + * can have an adverse effect on the security of such applications since these applications aren't + * created via a controlled process. + */ +@Slf4j +@Component +public abstract class AbstractCheckIfApplicationExistsTask implements Task { + @Getter private static final String taskName = "checkIfApplicationExists"; + @Nullable private final Front50Service front50Service; + private final OortService oortService; + private final ObjectMapper objectMapper; + private final RetrySupport retrySupport; + private final CheckIfApplicationExistsTaskConfig config; + + public AbstractCheckIfApplicationExistsTask( + @Nullable Front50Service front50Service, + OortService oortService, + ObjectMapper objectMapper, + RetrySupport retrySupport, + TaskConfigurationProperties configurationProperties) { + this.front50Service = front50Service; + this.oortService = oortService; + this.objectMapper = objectMapper; + this.retrySupport = retrySupport; + this.config = configurationProperties.getCheckIfApplicationExistsTask(); + } + + @Nonnull + @Override + public TaskResult execute(@Nonnull StageExecution stage) { + Map outputs = new HashMap<>(); + // get the application name + String applicationName = getApplicationName(stage); + + // first check front50 to see if this application exists in it + log.info("Querying front50 to get information about the application: {}", applicationName); + Application fetchedApplication = getApplicationFromFront50(applicationName); + String errorMessage = "did not find application: " + applicationName + " in front50"; + if (fetchedApplication == null) { + if (this.config.isCheckClouddriver()) { + log.info("querying clouddriver for application: {}", applicationName); + fetchedApplication = getApplicationFromClouddriver(applicationName); + if (fetchedApplication == null) { + errorMessage += " and in clouddriver"; + } + } + } + if (fetchedApplication == null) { + if (this.config.isAuditModeEnabled()) { + String pipelineName = "unknown"; + if (stage.getParent() != null) { + pipelineName = stage.getParent().getName(); + } + log.warn( + "Warning: stage: {}, pipeline: {}, message: {}. " + + "This will be a terminal failure in the near future.", + errorMessage, + stage.getName(), + pipelineName); + outputs.put("checkIfApplicationExistsWarning", errorMessage); + } else { + log.error(errorMessage); + throw new NotFoundException(errorMessage); + } + } + return TaskResult.builder(ExecutionStatus.SUCCEEDED).outputs(outputs).build(); + } + + /** + * attempts to query front50 for the application name that is provided to it as an input. + * + *

If front50 is disabled, then it returns null. It also returns null if the application + * doesn't exist or if any exception arises on querying this data from front50. The expectation is + * that the caller method should handle the return value in a suitable manner + * + * @param applicationName the application to search for in front50 + * @return the application, if it exists in front50, or null otherwise + */ + protected Application getApplicationFromFront50(String applicationName) { + // this can happen if front50 is disabled + if (front50Service == null) { + log.info("Front50 is disabled, cannot query application: {}", applicationName); + return null; + } + return retrySupport.retry( + () -> { + try { + Application fetchedApplication = front50Service.get(applicationName); + if (fetchedApplication == null) { + log.warn("Application: " + applicationName + " does not exist in front50"); + } else { + log.info("Application: " + applicationName + " found in front50"); + } + return fetchedApplication; + } catch (Exception e) { + log.error( + "Application: " + applicationName + " could not be retrieved from front50. Error: ", + e); + return null; + } + }, + this.config.getFront50Retries().getMaxAttempts(), + Duration.ofMillis(this.config.getFront50Retries().getBackOffInMs()), + this.config.getFront50Retries().isExponentialBackoffEnabled()); + } + + /** + * attempts to query clouddriver for the application name that is provided to it as an input. + * + *

It returns null if the application doesn't exist in clouddriver or if any exception arises + * on querying this data from clouddriver. The expectation is that the caller method should handle + * the return value in a suitable manner + * + * @param applicationName the application to search for in clouddriver + * @return the application, if it exists in clouddriver, or null otherwise + */ + protected Application getApplicationFromClouddriver(String applicationName) { + return retrySupport.retry( + () -> { + try { + Response response = oortService.getApplication(applicationName); + Application fetchedApplication = + objectMapper.readValue(response.getBody().in(), Application.class); + if (fetchedApplication == null) { + log.warn("Application: " + applicationName + " does not exist in clouddriver"); + } else { + log.info("Application: " + applicationName + " found in clouddriver"); + } + return fetchedApplication; + } catch (Exception e) { + log.error( + "Application: " + + applicationName + + " could not be retrieved from clouddriver. Error: ", + e); + return null; + } + }, + this.config.getClouddriverRetries().getMaxAttempts(), + Duration.ofMillis(this.config.getClouddriverRetries().getBackOffInMs()), + this.config.getClouddriverRetries().isExponentialBackoffEnabled()); + } + + public abstract String getApplicationName(@Nonnull StageExecution stageExecution); +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/AbstractWaitForClusterWideClouddriverTask.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/AbstractWaitForClusterWideClouddriverTask.groovy index 7665e5f2dc..2138cc5840 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/AbstractWaitForClusterWideClouddriverTask.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/AbstractWaitForClusterWideClouddriverTask.groovy @@ -113,7 +113,7 @@ abstract class AbstractWaitForClusterWideClouddriverTask implements CloudProvide } Optional cluster = cloudDriverService.maybeCluster(clusterSelection.getApplication(), clusterSelection.credentials, clusterSelection.cluster, clusterSelection.cloudProvider) - if (!cluster.isPresent()) { + if (cluster.isEmpty()) { return missingClusterResult(stage, clusterSelection) } diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/CheckIfApplicationExistsForClusterTask.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/CheckIfApplicationExistsForClusterTask.java new file mode 100644 index 0000000000..8760d332ca --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/cluster/CheckIfApplicationExistsForClusterTask.java @@ -0,0 +1,51 @@ +/* + * Copyright 2021 Salesforce.com, 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.cluster; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution; +import com.netflix.spinnaker.orca.clouddriver.OortService; +import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.pipeline.cluster.AbstractClusterWideClouddriverOperationStage.ClusterSelection; +import com.netflix.spinnaker.orca.clouddriver.tasks.AbstractCheckIfApplicationExistsTask; +import com.netflix.spinnaker.orca.front50.Front50Service; +import javax.annotation.Nonnull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +/** + * This checks if the application name provided for any cluster related tasks actually exists in + * front50 and/or clouddriver + */ +@Component +public class CheckIfApplicationExistsForClusterTask extends AbstractCheckIfApplicationExistsTask { + public CheckIfApplicationExistsForClusterTask( + @Nullable Front50Service front50Service, + OortService oortService, + ObjectMapper objectMapper, + RetrySupport retrySupport, + TaskConfigurationProperties configurationProperties) { + super(front50Service, oortService, objectMapper, retrySupport, configurationProperties); + } + + @Override + public String getApplicationName(@Nonnull StageExecution stageExecution) { + ClusterSelection clusterSelection = stageExecution.mapTo(ClusterSelection.class); + return clusterSelection.getApplication(); + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTask.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTask.java new file mode 100644 index 0000000000..185816ef16 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTask.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021 Salesforce.com, 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.servergroup; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.frigga.Names; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.moniker.Moniker; +import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution; +import com.netflix.spinnaker.orca.clouddriver.OortService; +import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.CreateServerGroupStage; +import com.netflix.spinnaker.orca.clouddriver.tasks.AbstractCheckIfApplicationExistsTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.DetermineHealthProvidersTask; +import com.netflix.spinnaker.orca.clouddriver.utils.MonikerHelper; +import com.netflix.spinnaker.orca.front50.Front50Service; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +/** + * This checks if the application name provided for any server group related tasks actually exists + * in front50 and/or clouddriver + */ +@Slf4j +@Component +public class CheckIfApplicationExistsForServerGroupTask + extends AbstractCheckIfApplicationExistsTask { + + public CheckIfApplicationExistsForServerGroupTask( + @Nullable Front50Service front50Service, + OortService oortService, + ObjectMapper objectMapper, + RetrySupport retrySupport, + TaskConfigurationProperties config) { + super(front50Service, oortService, objectMapper, retrySupport, config); + } + + /** + * get the application name from the provided stage context. + * + *

This matches the logic to retrieve application name from {@link + * DetermineHealthProvidersTask#execute(StageExecution)} method. The rationale is that this task + * will appear before the above-mentioned task in stages like {@link CreateServerGroupStage}. + * Therefore, if an application is referenced in later tasks, we should use the same criteria in + * this task to determine if such an application exists or not. + * + * @param stage the stage execution context + * @return the application name + */ + @Override + public String getApplicationName(@Nonnull StageExecution stage) { + String applicationName = (String) stage.getContext().get("application"); + if (applicationName == null) { + Moniker moniker = MonikerHelper.monikerFromStage(stage); + if (moniker != null && moniker.getApp() != null) { + applicationName = moniker.getApp(); + } else if (stage.getContext().containsKey("serverGroupName")) { + applicationName = + Names.parseName((String) stage.getContext().get("serverGroupName")).getApp(); + } else if (stage.getContext().containsKey("asgName")) { + applicationName = Names.parseName((String) stage.getContext().get("asgName")).getApp(); + } else if (stage.getContext().containsKey("cluster")) { + applicationName = Names.parseName((String) stage.getContext().get("cluster")).getApp(); + } + } + return applicationName; + } +} diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/config/TaskConfigurationProperties.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/config/TaskConfigurationProperties.java index afaa4b4048..b8671d8dc4 100644 --- a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/config/TaskConfigurationProperties.java +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/config/TaskConfigurationProperties.java @@ -16,6 +16,9 @@ package com.netflix.spinnaker.orca.clouddriver.config; +import com.netflix.spinnaker.orca.clouddriver.config.tasks.CheckIfApplicationExistsTaskConfig; +import com.netflix.spinnaker.orca.clouddriver.config.tasks.RetryConfig; +import com.netflix.spinnaker.orca.clouddriver.tasks.AbstractCheckIfApplicationExistsTask; import com.netflix.spinnaker.orca.clouddriver.tasks.job.WaitOnJobCompletion; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.PromoteManifestKatoOutputsTask; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.ResolveDeploySourceManifestTask; @@ -23,9 +26,9 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +/** configuration properties for various Orca tasks that are in the orca-clouddriver module */ @Data @ConfigurationProperties("tasks.clouddriver") -/** configuration properties for various Orca tasks that are in the orca-clouddriver module */ public class TaskConfigurationProperties { /** properties that pertain to {@link WaitOnJobCompletion} task. */ @@ -40,6 +43,10 @@ public class TaskConfigurationProperties { private ResolveDeploySourceManifestTaskConfig resolveDeploySourceManifestTask = new ResolveDeploySourceManifestTaskConfig(); + /** properties that pertain to {@link AbstractCheckIfApplicationExistsTask} task */ + private CheckIfApplicationExistsTaskConfig checkIfApplicationExistsTask = + new CheckIfApplicationExistsTaskConfig(); + @Data public static class WaitOnJobCompletionTaskConfig { /** @@ -48,21 +55,9 @@ public static class WaitOnJobCompletionTaskConfig { */ private Set excludeKeysFromOutputs = Set.of(); - private Retries jobStatusRetry = new Retries(); - - private Retries fileContentRetry = new Retries(); - - @Data - public static class Retries { - // total number of attempts - int maxAttempts = 6; - - // time in ms to wait before subsequent retry attempts - long backOffInMs = 5000; + private RetryConfig jobStatusRetry = new RetryConfig(); - // flag to enable exponential backoff - boolean exponentialBackoffEnabled = false; - } + private RetryConfig fileContentRetry = new RetryConfig(); } @Data diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStage.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStage.java index e2b86ff256..3d1c671980 100644 --- a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStage.java +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStage.java @@ -18,9 +18,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.CaseFormat; import com.netflix.frigga.Names; import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; import com.netflix.spinnaker.moniker.Moniker; +import com.netflix.spinnaker.orca.api.pipeline.Task; 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; @@ -31,6 +33,7 @@ import com.netflix.spinnaker.orca.clouddriver.tasks.MonitorKatoTask; import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.AbstractClusterWideClouddriverTask; import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.AbstractWaitForClusterWideClouddriverTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.CheckIfApplicationExistsForClusterTask; import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ServerGroupCacheForceRefreshTask; import com.netflix.spinnaker.orca.clouddriver.utils.MonikerHelper; import java.beans.Introspector; @@ -170,6 +173,9 @@ public void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder b Class waitTask = getWaitForTask(); String waitName = Introspector.decapitalize(getStepName(waitTask.getSimpleName())); + // add any optional pre-validation tasks at the beginning of the stage + getOptionalPreValidationTasks().forEach(builder::withTask); + builder .withTask("determineHealthProviders", DetermineHealthProvidersTask.class) .withTask(opName, operationTask) @@ -185,4 +191,43 @@ public void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder b builder.withTask("forceCacheRefresh", ServerGroupCacheForceRefreshTask.class); } } + + /** + * helper method that returns a map of task name to task class that are added to a stage. These + * tasks are added only if the correct configuration property is set. + * + *

This is also used in unit tests. + * + * @return map of task name to task class + */ + protected Map> getOptionalPreValidationTasks() { + Map> output = new HashMap<>(); + if (isCheckIfApplicationExistsEnabled(dynamicConfigService)) { + output.put( + CheckIfApplicationExistsForClusterTask.getTaskName(), + CheckIfApplicationExistsForClusterTask.class); + } + return output; + } + + /** + * helper method that returns a map of task name to task class that are added to a stage. These + * tasks are added only if the correct configuration property is set. + * + * @return map of task name to task class + */ + private boolean isCheckIfApplicationExistsEnabled(DynamicConfigService dynamicConfigService) { + String className = getClass().getSimpleName(); + try { + return dynamicConfigService.isEnabled( + String.format( + "stages.%s.check-if-application-exists", + CaseFormat.LOWER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, + Character.toLowerCase(className.charAt(0)) + className.substring(1))), + false); + } catch (Exception e) { + return false; + } + } } diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java index e49fe39f47..4daa5cd2fa 100644 --- a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java @@ -20,8 +20,11 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Collections.emptyMap; +import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableList; +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; import com.netflix.spinnaker.kork.expressions.ExpressionEvaluationSummary; +import com.netflix.spinnaker.orca.api.pipeline.Task; 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; @@ -34,10 +37,7 @@ import com.netflix.spinnaker.orca.pipeline.ExpressionAwareStageDefinitionBuilder; import com.netflix.spinnaker.orca.pipeline.tasks.artifacts.BindProducedArtifactsTask; import com.netflix.spinnaker.orca.pipeline.util.ContextParameterProcessor; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.BiConsumer; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -53,6 +53,7 @@ public class DeployManifestStage extends ExpressionAwareStageDefinitionBuilder { public static final String PIPELINE_CONFIG_TYPE = "deployManifest"; private final OldManifestActionAppender oldManifestActionAppender; + private final DynamicConfigService dynamicConfigService; private static boolean shouldRemoveStageOutputs(@NotNull StageExecution stage) { return stage.getContext().getOrDefault("noOutput", "false").toString().equals("true"); @@ -60,6 +61,8 @@ private static boolean shouldRemoveStageOutputs(@NotNull StageExecution stage) { @Override public void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder builder) { + // add any optional pre-validation tasks at the beginning of the stage + getOptionalPreValidationTasks().forEach(builder::withTask); builder .withTask(ResolveDeploySourceManifestTask.TASK_NAME, ResolveDeploySourceManifestTask.class) .withTask(DeployManifestTask.TASK_NAME, DeployManifestTask.class) @@ -109,6 +112,45 @@ public void afterStages(@Nonnull StageExecution stage, @Nonnull StageGraphBuilde } } + /** + * helper method that returns a map of task name to task class that are added to a stage. These + * tasks are added only if the correct configuration property is set. + * + * @return map of task name to task class + */ + protected Map> getOptionalPreValidationTasks() { + Map> output = new HashMap<>(); + if (isCheckIfApplicationExistsEnabled(dynamicConfigService)) { + output.put( + CheckIfApplicationExistsForManifestTask.getTaskName(), + CheckIfApplicationExistsForManifestTask.class); + } + return output; + } + + /** + * method that checks if the {@link CheckIfApplicationExistsForManifestTask} is enabled via + * configuration properties + * + * @param dynamicConfigService config properties + * @return true, if the config is set, false otherwise + */ + private boolean isCheckIfApplicationExistsEnabled(DynamicConfigService dynamicConfigService) { + String className = getClass().getSimpleName(); + + try { + return dynamicConfigService.isEnabled( + String.format( + "stages.%s.check-if-application-exists", + CaseFormat.LOWER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, + Character.toLowerCase(className.charAt(0)) + className.substring(1))), + false); + } catch (Exception e) { + return false; + } + } + /** {@code OldManifestActionAppender} appends new stages to old manifests */ @Component @RequiredArgsConstructor diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/BulkDestroyServerGroupStage.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/BulkDestroyServerGroupStage.java index 7917d7d4f4..f54205484f 100644 --- a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/BulkDestroyServerGroupStage.java +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/BulkDestroyServerGroupStage.java @@ -16,6 +16,7 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup; +import com.google.common.base.CaseFormat; import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; import com.netflix.spinnaker.orca.api.pipeline.graph.StageDefinitionBuilder; import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode; @@ -47,6 +48,11 @@ public void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder b // break into several parallel bulk ops based on cluster and lock/unlock around those? // question: do traffic guard checks actually even work in the bulk disable/destroy tasks? + if (isCheckIfApplicationExistsEnabled(dynamicConfigService)) { + builder.withTask( + CheckIfApplicationExistsForServerGroupTask.getTaskName(), + CheckIfApplicationExistsForServerGroupTask.class); + } builder .withTask("bulkDisableServerGroup", BulkDisableServerGroupTask.class) .withTask("monitorServerGroups", MonitorKatoTask.class) @@ -66,4 +72,20 @@ public void taskGraph(@Nonnull StageExecution stage, @Nonnull TaskNode.Builder b public String getName() { return this.getType(); } + + private boolean isCheckIfApplicationExistsEnabled(DynamicConfigService dynamicConfigService) { + String className = getClass().getSimpleName(); + + try { + return dynamicConfigService.isEnabled( + String.format( + "stages.%s.check-if-application-exists", + CaseFormat.LOWER_CAMEL.to( + CaseFormat.LOWER_HYPHEN, + Character.toLowerCase(className.charAt(0)) + className.substring(1))), + true); + } catch (Exception e) { + return true; + } + } } diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/DetermineHealthProvidersTask.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/DetermineHealthProvidersTask.java index 6828514841..3365f6d2b4 100644 --- a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/DetermineHealthProvidersTask.java +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/DetermineHealthProvidersTask.java @@ -103,16 +103,18 @@ public TaskResult execute(StageExecution stage) { try { String applicationName = (String) stage.getContext().get("application"); - Moniker moniker = MonikerHelper.monikerFromStage(stage); - if (applicationName == null && moniker != null && moniker.getApp() != null) { - applicationName = moniker.getApp(); - } else if (applicationName == null && stage.getContext().containsKey("serverGroupName")) { - applicationName = - Names.parseName((String) stage.getContext().get("serverGroupName")).getApp(); - } else if (applicationName == null && stage.getContext().containsKey("asgName")) { - applicationName = Names.parseName((String) stage.getContext().get("asgName")).getApp(); - } else if (applicationName == null && stage.getContext().containsKey("cluster")) { - applicationName = Names.parseName((String) stage.getContext().get("cluster")).getApp(); + if (applicationName == null) { + Moniker moniker = MonikerHelper.monikerFromStage(stage); + if (moniker != null && moniker.getApp() != null) { + applicationName = moniker.getApp(); + } else if (stage.getContext().containsKey("serverGroupName")) { + applicationName = + Names.parseName((String) stage.getContext().get("serverGroupName")).getApp(); + } else if (stage.getContext().containsKey("asgName")) { + applicationName = Names.parseName((String) stage.getContext().get("asgName")).getApp(); + } else if (stage.getContext().containsKey("cluster")) { + applicationName = Names.parseName((String) stage.getContext().get("cluster")).getApp(); + } } if (front50Service == null) { diff --git a/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/CheckIfApplicationExistsForManifestTask.java b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/CheckIfApplicationExistsForManifestTask.java new file mode 100644 index 0000000000..43f643028d --- /dev/null +++ b/orca-clouddriver/src/main/java/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/CheckIfApplicationExistsForManifestTask.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 Salesforce.com, 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.manifest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.moniker.Moniker; +import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution; +import com.netflix.spinnaker.orca.clouddriver.OortService; +import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.tasks.AbstractCheckIfApplicationExistsTask; +import com.netflix.spinnaker.orca.clouddriver.utils.MonikerHelper; +import com.netflix.spinnaker.orca.front50.Front50Service; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +/** + * This checks if the application name provided for any server group related tasks actually exists + * in front50 and/or clouddriver + */ +@Slf4j +@Component +public class CheckIfApplicationExistsForManifestTask extends AbstractCheckIfApplicationExistsTask { + + public CheckIfApplicationExistsForManifestTask( + @Nullable Front50Service front50Service, + OortService oortService, + ObjectMapper objectMapper, + RetrySupport retrySupport, + TaskConfigurationProperties configurationProperties) { + super(front50Service, oortService, objectMapper, retrySupport, configurationProperties); + } + + /** + * get the application name from the provided stage context. + * + * @param stage the stage execution context + * @return the application name + */ + @Override + public String getApplicationName(@Nonnull StageExecution stage) { + String applicationName = (String) stage.getContext().get("application"); + if (applicationName == null) { + Moniker moniker = MonikerHelper.monikerFromStage(stage); + if (moniker != null && moniker.getApp() != null) { + applicationName = moniker.getApp(); + } + } + return applicationName; + } +} diff --git a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageSpec.groovy b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageSpec.groovy index 477783bb3b..050f5d9753 100644 --- a/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageSpec.groovy +++ b/orca-clouddriver/src/test/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageSpec.groovy @@ -16,8 +16,9 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup - +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus +import com.netflix.spinnaker.orca.clouddriver.FeaturesService import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.strategies.DeployStagePreProcessor import com.netflix.spinnaker.orca.clouddriver.utils.TrafficGuard import com.netflix.spinnaker.orca.api.pipeline.graph.StageDefinitionBuilder @@ -38,11 +39,16 @@ class CreateServerGroupStageSpec extends Specification { def env = new MockEnvironment() @Subject - def createServerGroupStage = new CreateServerGroupStage( - rollbackClusterStage: new RollbackClusterStage(), - destroyServerGroupStage: new DestroyServerGroupStage(), - deployStagePreProcessors: [ deployStagePreProcessor ] - ) + CreateServerGroupStage createServerGroupStage + + def setup() { + def dynamicConfigService = Mock(DynamicConfigService) + createServerGroupStage = new CreateServerGroupStage(Mock(FeaturesService), + new RollbackClusterStage(), + new DestroyServerGroupStage(dynamicConfigService), + dynamicConfigService) + createServerGroupStage.deployStagePreProcessors = [ deployStagePreProcessor ] + } @Unroll def "should build RollbackStage when 'rollbackOnFailure' is enabled"() { diff --git a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStageTest.java b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStageTest.java new file mode 100644 index 0000000000..de49cb4a82 --- /dev/null +++ b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/cluster/AbstractClusterWideClouddriverOperationStageTest.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Salesforce.com, 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.cluster; + +import static com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType.PIPELINE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; +import com.netflix.spinnaker.orca.api.pipeline.graph.TaskNode; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.AbstractClusterWideClouddriverTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.AbstractWaitForClusterWideClouddriverTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.CheckIfApplicationExistsForClusterTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.ShrinkClusterTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.cluster.WaitForClusterShrinkTask; +import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl; +import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl; +import java.util.HashMap; +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class AbstractClusterWideClouddriverOperationStageTest { + private DynamicConfigService dynamicConfigService; + TestStage testStage; + StageExecutionImpl stageExecution; + + @BeforeEach + public void setup() { + dynamicConfigService = mock(DynamicConfigService.class); + testStage = new TestStage(dynamicConfigService); + PipelineExecutionImpl pipeline = new PipelineExecutionImpl(PIPELINE, "1", "testapp"); + + // Test Stage + stageExecution = + new StageExecutionImpl(pipeline, TestStage.STAGE_TYPE, "Test Stage", new HashMap<>()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testPresenceOfCheckIfApplicationExistsForClusterTask(boolean isTaskEnabled) { + // setup: + when(dynamicConfigService.isEnabled("stages.test-stage.check-if-application-exists", false)) + .thenReturn(isTaskEnabled); + + // when: + TaskNode.TaskGraph taskGraph = testStage.buildTaskGraph(stageExecution); + + // then: + Iterator iterator = taskGraph.iterator(); + AtomicBoolean doesTaskExist = new AtomicBoolean(false); + iterator.forEachRemaining( + element -> { + if (element instanceof TaskNode.TaskDefinition) { + if (((TaskNode.TaskDefinition) element) + .getImplementingClass() + .equals(CheckIfApplicationExistsForClusterTask.class)) { + doesTaskExist.set(true); + } + } + }); + + assertThat(doesTaskExist.get()).isEqualTo(isTaskEnabled); + } + + /** + * helper class that is created only for test purposes. It mimics the ShrinkClusterStage class + * with the way some methods are overridden in it. But this class exists since I wanted to test + * the abstract class itself and not any one specific class that extends it. + */ + private static class TestStage extends AbstractClusterWideClouddriverOperationStage { + protected static final String STAGE_TYPE = "testStage"; + + protected TestStage(DynamicConfigService dynamicConfigService) { + super(dynamicConfigService); + } + + protected Class getClusterOperationTask() { + return ShrinkClusterTask.class; + } + + protected Class getWaitForTask() { + return WaitForClusterShrinkTask.class; + } + } +} diff --git a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStageTest.java b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStageTest.java index dc35e537b3..e2388b4ed0 100644 --- a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStageTest.java +++ b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStageTest.java @@ -17,6 +17,8 @@ package com.netflix.spinnaker.orca.clouddriver.pipeline.manifest; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -26,6 +28,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; +import com.netflix.spinnaker.orca.api.pipeline.Task; import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType; import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution; import com.netflix.spinnaker.orca.clouddriver.OortService; @@ -34,6 +38,7 @@ import com.netflix.spinnaker.orca.clouddriver.pipeline.manifest.DeployManifestStage.GetDeployedManifests; import com.netflix.spinnaker.orca.clouddriver.pipeline.manifest.DeployManifestStage.ManifestOperationsHelper; import com.netflix.spinnaker.orca.clouddriver.pipeline.manifest.DeployManifestStage.OldManifestActionAppender; +import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.CheckIfApplicationExistsForManifestTask; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.DeployManifestContext; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.DeployManifestContext.TrafficManagement.ManifestStrategyType; import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper; @@ -45,6 +50,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -60,6 +67,7 @@ final class DeployManifestStageTest { private GetDeployedManifests getDeployedManifests; private DeployManifestStage deployManifestStage; private OldManifestActionAppender oldManifestActionAppender; + private DynamicConfigService dynamicConfigService; private static Map getContext(DeployManifestContext deployManifestContext) { Map context = @@ -87,11 +95,12 @@ private static Map getContext(DeployManifestContext deployManife @BeforeEach void setUp() { oortService = mock(OortService.class); + dynamicConfigService = mock(DynamicConfigService.class); manifestOperationsHelper = new ManifestOperationsHelper(oortService); getDeployedManifests = new GetDeployedManifests(manifestOperationsHelper); oldManifestActionAppender = new OldManifestActionAppender(getDeployedManifests, manifestOperationsHelper); - deployManifestStage = new DeployManifestStage(oldManifestActionAppender); + deployManifestStage = new DeployManifestStage(oldManifestActionAppender, dynamicConfigService); } @Test @@ -496,4 +505,26 @@ private void givenManifestIs(Manifest manifest) { when(oortService.getManifest(anyString(), anyString(), anyString(), anyBoolean())) .thenReturn(manifest); } + + @DisplayName( + "parameterized test to verify if checkIfApplicationExistsForManifest Task is present in the" + + " deploy manifest stage") + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testPresenceOfCheckIfApplicationExistsForManifestTask(boolean isTaskEnabled) { + when(dynamicConfigService.isEnabled( + "stages.deploy-manifest-stage.check-if-application-exists", false)) + .thenReturn(isTaskEnabled); + Map> optionalTasks = + deployManifestStage.getOptionalPreValidationTasks(); + + if (isTaskEnabled) { + assertFalse(optionalTasks.isEmpty()); + assertThat(optionalTasks.size()).isEqualTo(1); + assertTrue(optionalTasks.containsKey(CheckIfApplicationExistsForManifestTask.getTaskName())); + assertTrue(optionalTasks.containsValue(CheckIfApplicationExistsForManifestTask.class)); + } else { + assertTrue(optionalTasks.isEmpty()); + } + } } diff --git a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageTest.java b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageTest.java new file mode 100644 index 0000000000..515d3d93b7 --- /dev/null +++ b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/pipeline/servergroup/CreateServerGroupStageTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Salesforce.com, 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.servergroup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.netflix.spinnaker.kork.dynamicconfig.DynamicConfigService; +import com.netflix.spinnaker.orca.clouddriver.FeaturesService; +import com.netflix.spinnaker.orca.clouddriver.pipeline.cluster.RollbackClusterStage; +import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.CheckIfApplicationExistsForServerGroupTask; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +public class CreateServerGroupStageTest { + private DynamicConfigService dynamicConfigService; + CreateServerGroupStage createServerGroupStage; + + @BeforeEach + public void setup() { + FeaturesService featuresService = mock(FeaturesService.class); + RollbackClusterStage rollbackClusterStage = mock(RollbackClusterStage.class); + DestroyServerGroupStage destroyServerGroupStage = mock(DestroyServerGroupStage.class); + dynamicConfigService = mock(DynamicConfigService.class); + createServerGroupStage = + new CreateServerGroupStage( + featuresService, rollbackClusterStage, destroyServerGroupStage, dynamicConfigService); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testPresenceOfCheckIfApplicationExistsForServerGroupTask(boolean isTaskEnabled) { + when(dynamicConfigService.isEnabled( + "stages.create-server-group-stage.check-if-application-exists", false)) + .thenReturn(isTaskEnabled); + Map optionalTasks = createServerGroupStage.getOptionalPreValidationTasks(); + + if (isTaskEnabled) { + assertFalse(optionalTasks.isEmpty()); + assertThat(optionalTasks.size()).isEqualTo(1); + assertTrue( + optionalTasks.containsKey(CheckIfApplicationExistsForServerGroupTask.getTaskName())); + assertTrue(optionalTasks.containsValue(CheckIfApplicationExistsForServerGroupTask.class)); + } else { + assertTrue(optionalTasks.isEmpty()); + } + } +} diff --git a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/job/WaitOnJobCompletionTest.java b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/job/WaitOnJobCompletionTest.java index a03fad6349..ba1051198a 100644 --- a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/job/WaitOnJobCompletionTest.java +++ b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/job/WaitOnJobCompletionTest.java @@ -36,6 +36,7 @@ import com.netflix.spinnaker.orca.api.pipeline.models.StageExecution; import com.netflix.spinnaker.orca.clouddriver.KatoRestService; import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.config.tasks.RetryConfig; import com.netflix.spinnaker.orca.clouddriver.exception.JobFailedException; import com.netflix.spinnaker.orca.front50.Front50Service; import com.netflix.spinnaker.orca.front50.model.Application; @@ -79,8 +80,7 @@ public void setup() { mockFront50Service = mock(Front50Service.class); configProperties = new TaskConfigurationProperties(); - TaskConfigurationProperties.WaitOnJobCompletionTaskConfig.Retries retries = - new TaskConfigurationProperties.WaitOnJobCompletionTaskConfig.Retries(); + RetryConfig retries = new RetryConfig(); retries.setMaxAttempts(3); retries.setBackOffInMs(1); configProperties.getWaitOnJobCompletionTask().setFileContentRetry(retries); diff --git a/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTaskTest.java b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTaskTest.java new file mode 100644 index 0000000000..aeb05311f7 --- /dev/null +++ b/orca-clouddriver/src/test/java/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/CheckIfApplicationExistsForServerGroupTaskTest.java @@ -0,0 +1,242 @@ +/* + * Copyright 2021 Salesforce.com, 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.servergroup; + +import static com.netflix.spinnaker.orca.TestUtils.getResourceAsStream; +import static com.netflix.spinnaker.orca.api.pipeline.models.ExecutionType.PIPELINE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.kork.core.RetrySupport; +import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; +import com.netflix.spinnaker.orca.api.pipeline.TaskResult; +import com.netflix.spinnaker.orca.api.pipeline.models.ExecutionStatus; +import com.netflix.spinnaker.orca.clouddriver.OortService; +import com.netflix.spinnaker.orca.clouddriver.config.TaskConfigurationProperties; +import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.CreateServerGroupStage; +import com.netflix.spinnaker.orca.front50.Front50Service; +import com.netflix.spinnaker.orca.front50.model.Application; +import com.netflix.spinnaker.orca.pipeline.model.PipelineExecutionImpl; +import com.netflix.spinnaker.orca.pipeline.model.StageExecutionImpl; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import retrofit.client.Response; +import retrofit.mime.TypedByteArray; + +public class CheckIfApplicationExistsForServerGroupTaskTest { + private @Nullable Front50Service front50Service; + private OortService oortService; + private ObjectMapper objectMapper; + private RetrySupport retrySupport; + private CheckIfApplicationExistsForServerGroupTask task; + private Application front50Application; + private TaskConfigurationProperties configurationProperties; + StageExecutionImpl stageExecution; + + @BeforeEach + public void setup() { + front50Service = mock(Front50Service.class); + oortService = mock(OortService.class); + objectMapper = new ObjectMapper(); + retrySupport = new RetrySupport(); + configurationProperties = new TaskConfigurationProperties(); + + front50Application = new Application(); + front50Application.setUser("test-user"); + PipelineExecutionImpl pipeline = new PipelineExecutionImpl(PIPELINE, "1", "testapp"); + + // Test Stage + stageExecution = + new StageExecutionImpl( + pipeline, CreateServerGroupStage.PIPELINE_CONFIG_TYPE, "Test Stage", new HashMap<>()); + } + + @DisplayName("parameterized test where front50 is queried for an application") + @ParameterizedTest( + name = "{index} ==> when application name is obtained from = {0} key in the context") + @ValueSource(strings = {"application", "moniker", "serverGroupName", "asgName", "cluster"}) + public void testSuccessfulRetrievalOfApplicationFromFront50(String applicationNameSource) { + // setup: + task = + new CheckIfApplicationExistsForServerGroupTask( + front50Service, oortService, objectMapper, retrySupport, configurationProperties); + + assert front50Service != null; + when(front50Service.get("testapp")).thenReturn(front50Application); + stageExecution.setContext(getStageContext(applicationNameSource)); + + // when: + TaskResult result = task.execute(stageExecution); + + // then: + assertThat(task.getApplicationName(stageExecution)).isEqualTo("testapp"); + assertThat(front50Application.getUser()).isEqualTo("test-user"); + verify(front50Service).get("testapp"); + assertEquals(result.getStatus(), ExecutionStatus.SUCCEEDED); + verifyNoInteractions(oortService); + } + + @DisplayName( + "parameterized test where clouddriver is queried for an application " + + "if the application is not found in front50") + @ParameterizedTest( + name = "{index} ==> when application name is obtained from = {0} key in the context") + @ValueSource(strings = {"application", "moniker", "serverGroupName", "asgName", "cluster"}) + public void testSuccessfulRetrievalOfApplicationFromClouddriverIfFront50IsDisabled( + String applicationNameSource) throws IOException { + // setup: + task = + new CheckIfApplicationExistsForServerGroupTask( + null, oortService, objectMapper, retrySupport, configurationProperties); + + when(oortService.getApplication("testapp")) + .thenReturn( + getApplicationResponse("clouddriver/tasks/servergroup/clouddriver-application.json")); + stageExecution.setContext(getStageContext(applicationNameSource)); + + // when: + TaskResult result = task.execute(stageExecution); + + // then: + assertThat(task.getApplicationName(stageExecution)).isEqualTo("testapp"); + verifyNoInteractions(front50Service); + verify(oortService).getApplication("testapp"); + assertEquals(result.getStatus(), ExecutionStatus.SUCCEEDED); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testIfApplicationCannotBeRetrievedFromFront50AndCheckClouddriverIsFalse( + boolean auditModeEnabled) { + TaskConfigurationProperties configurationProperties = new TaskConfigurationProperties(); + configurationProperties.getCheckIfApplicationExistsTask().setCheckClouddriver(false); + configurationProperties.getCheckIfApplicationExistsTask().setAuditModeEnabled(auditModeEnabled); + final String expectedErrorMessage = "did not find application: testapp in front50"; + // setup: + task = + new CheckIfApplicationExistsForServerGroupTask( + null, oortService, objectMapper, retrySupport, configurationProperties); + + stageExecution.setContext(getStageContext("application")); + + // then + if (auditModeEnabled) { + TaskResult result = task.execute(stageExecution); + assertEquals(result.getStatus(), ExecutionStatus.SUCCEEDED); + assertEquals( + expectedErrorMessage, result.getOutputs().get("checkIfApplicationExistsWarning")); + } else { + NotFoundException thrown = + assertThrows(NotFoundException.class, () -> task.execute(stageExecution)); + + assertThat(thrown.getMessage()).contains(expectedErrorMessage); + } + + verifyNoInteractions(front50Service); + verifyNoInteractions(oortService); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testAnApplicationWhichDoesNotExistInBothFront50AndClouddriver( + boolean auditModeEnabled) { + // setup: + task = + new CheckIfApplicationExistsForServerGroupTask( + null, oortService, objectMapper, retrySupport, configurationProperties); + configurationProperties.getCheckIfApplicationExistsTask().setAuditModeEnabled(auditModeEnabled); + + final String expectedErrorMessage = + "did not find application: invalid app in front50 and in clouddriver"; + when(oortService.getApplication("invalid app")) + .thenReturn( + new Response( + "test-url", + HttpStatus.NOT_FOUND.value(), + "application does not exist", + Collections.emptyList(), + new TypedByteArray("application/json", new byte[0]))); + + Map stageContext = new HashMap<>(); + stageContext.put("application", "invalid app"); + stageExecution.setContext(stageContext); + // then + if (auditModeEnabled) { + TaskResult result = task.execute(stageExecution); + assertEquals(result.getStatus(), ExecutionStatus.SUCCEEDED); + assertEquals( + expectedErrorMessage, result.getOutputs().get("checkIfApplicationExistsWarning")); + } else { + // then + NotFoundException thrown = + assertThrows(NotFoundException.class, () -> task.execute(stageExecution)); + + assertThat(thrown.getMessage()).contains(expectedErrorMessage); + } + verifyNoInteractions(front50Service); + verify(oortService).getApplication("invalid app"); + } + + private Map getStageContext(String applicationNameSource) { + Map stageContext = new HashMap<>(); + switch (applicationNameSource) { + case "application": + stageContext.put("application", "testapp"); + break; + case "moniker": + stageContext.put("moniker", Map.of("app", "testapp", "stack", "preprod", "detail", "test")); + break; + case "serverGroupName": + stageContext.put("serverGroupName", "testapp-preprod-test-v003"); + break; + case "asgName": + stageContext.put("asgName", "testapp-preprod-test-v003"); + break; + case "cluster": + stageContext.put("cluster", "testapp-preprod-test"); + break; + } + return stageContext; + } + + private Response getApplicationResponse(String resourceName) throws IOException { + InputStream jobStatusInputStream = getResourceAsStream(resourceName); + + return new Response( + "test-url", + 200, + "test-reason", + Collections.emptyList(), + new TypedByteArray("application/json", IOUtils.toByteArray(jobStatusInputStream))); + } +} diff --git a/orca-clouddriver/src/test/kotlin/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestExpressionEvaluationTest.kt b/orca-clouddriver/src/test/kotlin/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestExpressionEvaluationTest.kt index 1a0ddc45c6..615bb8467a 100644 --- a/orca-clouddriver/src/test/kotlin/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestExpressionEvaluationTest.kt +++ b/orca-clouddriver/src/test/kotlin/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestExpressionEvaluationTest.kt @@ -143,7 +143,7 @@ class DeployManifestExpressionEvaluationTest : JUnit5Minutests { override val contextParameterProcessor = ContextParameterProcessor() override val stageDefinitionBuilderFactory = StageDefinitionBuilderFactory { execution -> when (execution.type) { - "deployManifest" -> DeployManifestStage(mockk()) + "deployManifest" -> DeployManifestStage(mockk(), mockk()) else -> throw IllegalArgumentException("Test factory can't make \"${execution.type}\" stages.") } } diff --git a/orca-clouddriver/src/test/resources/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/clouddriver-application.json b/orca-clouddriver/src/test/resources/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/clouddriver-application.json new file mode 100644 index 0000000000..12482cd543 --- /dev/null +++ b/orca-clouddriver/src/test/resources/com/netflix/spinnaker/orca/clouddriver/tasks/servergroup/clouddriver-application.json @@ -0,0 +1,30 @@ +{ + "name": "testapp", + "attributes": { + "cloudProviders": "aws", + "name": "testapp", + "accounts": "test-account-1, test-account-2" + }, + "clusters": { + "test-account-1": [ + { + "loadBalancers": [], + "name": "testapp-preprod-test", + "provider": "aws", + "serverGroups": [ + "testapp-preprod-test-v003" + ] + } + ], + "test-account-2": [ + { + "loadBalancers": [], + "name": "testapp-preprod-apoorvtest", + "provider": "aws", + "serverGroups": [ + "testapp-preprod-test-v005" + ] + } + ] + } +}