diff --git a/orca-clouddriver/orca-clouddriver.gradle b/orca-clouddriver/orca-clouddriver.gradle index c755a2250c..bedfb9f23b 100644 --- a/orca-clouddriver/orca-clouddriver.gradle +++ b/orca-clouddriver/orca-clouddriver.gradle @@ -16,6 +16,7 @@ dependencies { compile spinnaker.dependency('frigga') + compileOnly spinnaker.dependency('lombok') compile project(":orca-retrofit") compile project(":orca-front50") compile project(":orca-bakery") diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/DelegatingOortService.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/DelegatingOortService.java index 0c8d4d0db1..8e188327e0 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/DelegatingOortService.java +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/DelegatingOortService.java @@ -35,6 +35,11 @@ public Response getCluster(String app, String account, String cluster, String cl return getService().getCluster(app, account, cluster, cloudProvider); } + @Override + public Response getManifest(String account, String location, String name) { + return getService().getManifest(account, location, name); + } + @Override public Response getServerGroupFromCluster(String app, String account, String cluster, String serverGroup, String region, String cloudProvider) { return getService().getServerGroupFromCluster(app, account, cluster, serverGroup, region, cloudProvider); diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/OortService.groovy b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/OortService.groovy index 2585d0920c..f4df383309 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/OortService.groovy +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/OortService.groovy @@ -41,6 +41,11 @@ interface OortService { @Query("region") String region, @Path("cloudProvider") String cloudProvider) + @GET("/manifests/{account}/{location}/{manifest}") + Response getManifest(@Path("account") String account, + @Path("location") String location, + @Path("manifest") String manifest) + @Deprecated @GET("/applications/{app}/serverGroups/{account}/{region}/{serverGroup}") Response getServerGroup(@Path("app") String app, diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/model/Manifest.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/model/Manifest.java new file mode 100644 index 0000000000..3f95fa05a2 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/model/Manifest.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.spinnaker.orca.clouddriver.model; + +import lombok.Data; + +@Data +public class Manifest { + private Status status; + + @Data + public static class Status { + boolean stable; + String message; + } +} diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java index 72f18c5de1..8eb4892ae6 100644 --- a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/pipeline/manifest/DeployManifestStage.java @@ -20,6 +20,7 @@ import com.netflix.spinnaker.orca.clouddriver.tasks.MonitorKatoTask; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.DeployManifestTask; import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.ManifestForceCacheRefreshTask; +import com.netflix.spinnaker.orca.clouddriver.tasks.manifest.WaitForManifestStableTask; import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder; import com.netflix.spinnaker.orca.pipeline.TaskNode; import com.netflix.spinnaker.orca.pipeline.model.Execution; @@ -34,6 +35,7 @@ public class DeployManifestStage implements StageDefinitionBuilder { public > void taskGraph(Stage stage, TaskNode.Builder builder) { builder.withTask(DeployManifestTask.TASK_NAME, DeployManifestTask.class) .withTask("monitorDeploy", MonitorKatoTask.class) - .withTask(ManifestForceCacheRefreshTask.TASK_NAME, ManifestForceCacheRefreshTask.class); + .withTask(ManifestForceCacheRefreshTask.TASK_NAME, ManifestForceCacheRefreshTask.class) + .withTask(WaitForManifestStableTask.TASK_NAME, WaitForManifestStableTask.class); } } diff --git a/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/WaitForManifestStableTask.java b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/WaitForManifestStableTask.java new file mode 100644 index 0000000000..17f63fc956 --- /dev/null +++ b/orca-clouddriver/src/main/groovy/com/netflix/spinnaker/orca/clouddriver/tasks/manifest/WaitForManifestStableTask.java @@ -0,0 +1,122 @@ +/* + * Copyright 2017 Google, 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.google.common.collect.ImmutableMap; +import com.netflix.spinnaker.orca.ExecutionStatus; +import com.netflix.spinnaker.orca.OverridableTimeoutRetryableTask; +import com.netflix.spinnaker.orca.TaskResult; +import com.netflix.spinnaker.orca.clouddriver.OortService; +import com.netflix.spinnaker.orca.clouddriver.model.Manifest; +import com.netflix.spinnaker.orca.clouddriver.model.Manifest.Status; +import com.netflix.spinnaker.orca.clouddriver.utils.CloudProviderAware; +import com.netflix.spinnaker.orca.pipeline.model.Stage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import retrofit.RetrofitError; +import retrofit.client.Response; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class WaitForManifestStableTask implements OverridableTimeoutRetryableTask, CloudProviderAware { + public final static String TASK_NAME = "waitForManifestToStabilize"; + + @Autowired + OortService oortService; + + @Autowired + ObjectMapper objectMapper; + + @Override + public long getBackoffPeriod() { + return TimeUnit.SECONDS.toMillis(1); + } + + @Override + public long getTimeout() { + return TimeUnit.MINUTES.toMillis(30); + } + + @Nonnull + @Override + public TaskResult execute(@Nonnull Stage stage) { + String account = getCredentials(stage); + Map> deployedManifests = (Map>) stage.getContext().get("deploy.outputs"); + List messages = new ArrayList<>(); + boolean allStable = true; + + for (Map.Entry> entry : deployedManifests.entrySet()) { + String location = entry.getKey(); + for (String name : entry.getValue()) { + String identifier = readableIdentifier(account, location, name); + Response response; + try { + response = oortService.getManifest(account, location, name); + } catch (RetrofitError e) { + allStable = false; + log.warn("Unable to read manifest " + identifier, e); + continue; + } + + if (response.getStatus() != 200) { + allStable = false; + messages.add(identifier + ": could not retrieve status"); + continue; + } + + Manifest manifest; + try { + manifest = objectMapper.readValue(response.getBody().in(), Manifest.class); + } catch (IOException e) { + log.error("Failed to read " + identifier, e); + throw new RuntimeException(e); + } + + Status status = manifest.getStatus(); + if (!status.isStable()) { + allStable = false; + messages.add(identifier + ": " + status.getMessage()); + } + } + } + + if (allStable) { + return TaskResult.SUCCEEDED; + } else { + Map context = new ImmutableMap.Builder() + .put("stableMessages", messages) + .build(); + + return new TaskResult(ExecutionStatus.RUNNING, context, new HashMap<>()); + } + } + + private String readableIdentifier(String account, String location, String name) { + return String.format("'%s' in '%s' for account %s", name, location, account); + } +}