From e0457fc14820290a80fce927c0097f28e3246cee Mon Sep 17 00:00:00 2001 From: zach smith Date: Tue, 26 Jan 2021 14:50:42 -0800 Subject: [PATCH] feat(cloudfoundry): support docker deployments --- .../docker/DockerArtifactConfiguration.java | 4 +- .../clouddriver-cloudfoundry.gradle | 1 + .../cloudfoundry/client/Applications.java | 13 ++- .../client/model/v3/CreateApplication.java | 35 +------- .../client/model/v3/CreatePackage.java | 19 ++++- .../cloudfoundry/client/model/v3/Docker.java | 29 +++++++ .../client/model/v3/Lifecycle.java | 59 +++++++++++++ ...ryServerGroupAtomicOperationConverter.java | 7 +- ...ryServerGroupAtomicOperationConverter.java | 31 ++++++- ...loyCloudFoundryServerGroupDescription.java | 3 + ...loudFoundryServerGroupAtomicOperation.java | 44 +++++++--- .../model/v3/CreateApplicationTest.java | 23 ++--- ...rverGroupAtomicOperationConverterTest.java | 3 +- ...FoundryServerGroupAtomicOperationTest.java | 85 ++++++++++++++++--- 14 files changed, 267 insertions(+), 89 deletions(-) create mode 100644 clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Docker.java create mode 100644 clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Lifecycle.java diff --git a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/docker/DockerArtifactConfiguration.java b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/docker/DockerArtifactConfiguration.java index a295057106a..6af82fe1b07 100644 --- a/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/docker/DockerArtifactConfiguration.java +++ b/clouddriver-artifacts/src/main/java/com/netflix/spinnaker/clouddriver/artifacts/docker/DockerArtifactConfiguration.java @@ -21,12 +21,12 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -@ConditionalOnProperty("kubernetes.enabled") +@ConditionalOnExpression("${kubernetes.enabled:false} || ${dockerRegistry.enabled:false}") @RequiredArgsConstructor @Slf4j class DockerArtifactConfiguration { diff --git a/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle b/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle index a458e4361d6..1bccff12f02 100644 --- a/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle +++ b/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle @@ -30,6 +30,7 @@ dependencies { implementation project(":clouddriver-core") implementation project(":clouddriver-security") implementation project(":cats:cats-core") + implementation project(":clouddriver-docker") compileOnly "org.projectlombok:lombok" annotationProcessor "org.projectlombok:lombok" diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java index 97f1c74c715..fe24d1ee0ad 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/Applications.java @@ -34,7 +34,6 @@ import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.*; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Package; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Process; -import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; import com.netflix.spinnaker.clouddriver.cloudfoundry.model.*; import com.netflix.spinnaker.clouddriver.helpers.AbstractServerGroupNameResolver; import com.netflix.spinnaker.clouddriver.model.HealthState; @@ -536,17 +535,15 @@ public void deleteAppInstance(String guid, String index) throws CloudFoundryApiE public CloudFoundryServerGroup createApplication( String appName, CloudFoundrySpace space, - DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes, - @Nullable Map environmentVariables) + @Nullable Map environmentVariables, + Lifecycle lifecycle) throws CloudFoundryApiException { Map relationships = new HashMap<>(); relationships.put("space", new ToOneRelationship(new Relationship(space.getId()))); - return safelyCall( () -> api.createApplication( - new CreateApplication( - appName, relationships, environmentVariables, applicationAttributes))) + new CreateApplication(appName, relationships, environmentVariables, lifecycle))) .map(this::map) .orElseThrow( () -> @@ -589,8 +586,8 @@ public void updateProcess( safelyCall(() -> api.updateProcess(guid, new UpdateProcess(command, healthCheck))); } - public String createPackage(String appGuid) throws CloudFoundryApiException { - return safelyCall(() -> api.createPackage(new CreatePackage(appGuid))) + public String createPackage(CreatePackage createPackageRequest) throws CloudFoundryApiException { + return safelyCall(() -> api.createPackage(createPackageRequest)) .map(Package::getGuid) .orElseThrow( () -> diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplication.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplication.java index 7b06a2094d9..f62ce53387c 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplication.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplication.java @@ -17,11 +17,8 @@ package com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3; import com.fasterxml.jackson.annotation.JsonInclude; -import com.google.common.collect.ImmutableMap; -import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; import java.util.Map; import javax.annotation.Nullable; -import lombok.AllArgsConstructor; import lombok.Getter; @JsonInclude(JsonInclude.Include.NON_NULL) @@ -32,42 +29,16 @@ public class CreateApplication { @Nullable private final Map environmentVariables; - @Nullable private final BuildpackLifecycle lifecycle; + @Nullable private final Lifecycle lifecycle; public CreateApplication( String name, Map relationships, @Nullable Map environmentVariables, - DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes) { + Lifecycle lifecycle) { this.name = name; this.relationships = relationships; this.environmentVariables = environmentVariables; - this.lifecycle = - applicationAttributes.getBuildpacks() != null || applicationAttributes.getStack() != null - ? new BuildpackLifecycle(applicationAttributes) - : null; - } - - @AllArgsConstructor - @Getter - public static class BuildpackLifecycle { - private String type = "buildpack"; - private Map data; - - BuildpackLifecycle( - DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes) { - this.data = - new BuildpackLifecycleBuilder() - .putIfValueNotNull("buildpacks", applicationAttributes.getBuildpacks()) - .putIfValueNotNull("stack", applicationAttributes.getStack()) - .build(); - } - } - - static class BuildpackLifecycleBuilder extends ImmutableMap.Builder { - public BuildpackLifecycleBuilder putIfValueNotNull(K key, V value) { - if (value != null) super.put(key, value); - return this; - } + this.lifecycle = lifecycle; } } diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreatePackage.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreatePackage.java index 4331375b208..6bf09ee9300 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreatePackage.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreatePackage.java @@ -18,14 +18,27 @@ import java.util.HashMap; import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.Getter; -@Getter +@Data public class CreatePackage { - private final String type = "bits"; + private final String type; private final Map relationships = new HashMap<>(); + private Docker data; - public CreatePackage(String appId) { + public CreatePackage(String appId, Type type, Docker data) { + this.type = type.getValue(); relationships.put("app", new ToOneRelationship(new Relationship(appId))); + this.data = data; + } + + @Getter + @AllArgsConstructor + public enum Type { + BITS("bits"), + DOCKER("docker"); + private String value; } } diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Docker.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Docker.java new file mode 100644 index 00000000000..cba8cf8671f --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Docker.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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.clouddriver.cloudfoundry.client.model.v3; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class Docker { + private final String image; + private final String username; + private final String password; +} diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Lifecycle.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Lifecycle.java new file mode 100644 index 00000000000..fe81b4d4786 --- /dev/null +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/Lifecycle.java @@ -0,0 +1,59 @@ +/* + * Copyright 2021 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.clouddriver.cloudfoundry.client.model.v3; + +import com.google.common.collect.ImmutableMap; +import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; +import java.util.Collections; +import java.util.Map; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +public class Lifecycle { + private final String type; + private final Map data; + + public Lifecycle( + Type type, + DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes) { + this.type = type.getValue(); + this.data = + type.equals(Type.BUILDPACK) + ? new BuildpackLifecycleBuilder() + .putIfValueNotNull("buildpacks", applicationAttributes.getBuildpacks()) + .putIfValueNotNull("stack", applicationAttributes.getStack()) + .build() + : Collections.emptyMap(); + } + + @Getter + @AllArgsConstructor + public enum Type { + BUILDPACK("buildpack"), + DOCKER("docker"); + private String value; + } + + static class BuildpackLifecycleBuilder extends ImmutableMap.Builder { + public BuildpackLifecycleBuilder putIfValueNotNull(K key, V value) { + if (value != null) super.put(key, value); + return this; + } + } +} diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CloneCloudFoundryServerGroupAtomicOperationConverter.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CloneCloudFoundryServerGroupAtomicOperationConverter.java index 87f473b9339..2776139debe 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CloneCloudFoundryServerGroupAtomicOperationConverter.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/CloneCloudFoundryServerGroupAtomicOperationConverter.java @@ -18,8 +18,10 @@ import com.netflix.spinnaker.clouddriver.artifacts.ArtifactCredentialsRepository; import com.netflix.spinnaker.clouddriver.cloudfoundry.CloudFoundryOperation; +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials; import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; +import java.util.List; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @@ -29,7 +31,8 @@ public class CloneCloudFoundryServerGroupAtomicOperationConverter extends DeployCloudFoundryServerGroupAtomicOperationConverter { public CloneCloudFoundryServerGroupAtomicOperationConverter( @Qualifier("cloudFoundryOperationPoller") OperationPoller operationPoller, - ArtifactCredentialsRepository credentialsRepository) { - super(operationPoller, credentialsRepository); + ArtifactCredentialsRepository credentialsRepository, + List dockerRegistryNamedAccountCredentials) { + super(operationPoller, credentialsRepository, dockerRegistryNamedAccountCredentials); } } diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverter.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverter.java index b6df2b38fd1..5055007828d 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverter.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverter.java @@ -28,9 +28,11 @@ import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactCredentials; import com.netflix.spinnaker.clouddriver.cloudfoundry.CloudFoundryOperation; import com.netflix.spinnaker.clouddriver.cloudfoundry.artifacts.CloudFoundryArtifactCredentials; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Docker; import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops.DeployCloudFoundryServerGroupAtomicOperation; import com.netflix.spinnaker.clouddriver.cloudfoundry.security.CloudFoundryCredentials; +import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials; import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperation; import com.netflix.spinnaker.clouddriver.orchestration.AtomicOperations; @@ -49,12 +51,16 @@ public class DeployCloudFoundryServerGroupAtomicOperationConverter extends AbstractCloudFoundryServerGroupAtomicOperationConverter { private final OperationPoller operationPoller; private final ArtifactCredentialsRepository credentialsRepository; + private final List + dockerRegistryNamedAccountCredentials; public DeployCloudFoundryServerGroupAtomicOperationConverter( @Qualifier("cloudFoundryOperationPoller") OperationPoller operationPoller, - ArtifactCredentialsRepository credentialsRepository) { + ArtifactCredentialsRepository credentialsRepository, + List dockerRegistryNamedAccountCredentials) { this.operationPoller = operationPoller; this.credentialsRepository = credentialsRepository; + this.dockerRegistryNamedAccountCredentials = dockerRegistryNamedAccountCredentials; } @Override @@ -86,9 +92,32 @@ public DeployCloudFoundryServerGroupDescription convertDescription(Map input) { converted.setApplicationAttributes( convertManifest( converted.getManifest().stream().findFirst().orElse(Collections.emptyMap()))); + converted.setDocker( + converted.getArtifactCredentials().getTypes().contains("docker/image") + ? resolveDockerAccount(converted.getApplicationArtifact()) + : null); return converted; } + private Docker resolveDockerAccount(Artifact artifact) { + DockerRegistryNamedAccountCredentials dockerCreds = + dockerRegistryNamedAccountCredentials.stream() + .filter(reg -> reg.getRegistry().equals(artifact.getReference().split("/")[0])) + .filter(reg -> reg.getRepositories().contains(artifact.getName().split("/", 2)[1])) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Could not find a docker registry for the docker image: " + + artifact.getName())); + + return Docker.builder() + .image(artifact.getReference()) + .username(dockerCreds.getUsername()) + .password(dockerCreds.getPassword()) + .build(); + } + private ArtifactCredentials getArtifactCredentials( DeployCloudFoundryServerGroupDescription converted) { Artifact artifact = converted.getApplicationArtifact(); diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/DeployCloudFoundryServerGroupDescription.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/DeployCloudFoundryServerGroupDescription.java index 9c49eece61b..72b466da367 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/DeployCloudFoundryServerGroupDescription.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/description/DeployCloudFoundryServerGroupDescription.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactCredentials; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Docker; import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundrySpace; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; @@ -45,6 +46,8 @@ public class DeployCloudFoundryServerGroupDescription @JsonIgnore private ApplicationAttributes applicationAttributes; + @JsonIgnore private Docker docker; + @Data public static class ApplicationAttributes { private int instances; diff --git a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperation.java b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperation.java index 4ee90fe45bd..746e932612d 100644 --- a/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperation.java +++ b/clouddriver-cloudfoundry/src/main/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperation.java @@ -17,8 +17,8 @@ package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops; import static com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.ops.CloudFoundryOperationUtils.describeProcessState; -import static com.netflix.spinnaker.clouddriver.deploy.DeploymentResult.*; -import static com.netflix.spinnaker.clouddriver.deploy.DeploymentResult.Deployment.*; +import static com.netflix.spinnaker.clouddriver.deploy.DeploymentResult.Deployment; +import static com.netflix.spinnaker.clouddriver.deploy.DeploymentResult.Deployment.Capacity; import static java.util.stream.Collectors.toList; import com.netflix.spinnaker.clouddriver.artifacts.config.ArtifactCredentials; @@ -28,6 +28,8 @@ import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryApiException; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.CloudFoundryClient; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.CreateServiceBinding; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.CreatePackage; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Lifecycle; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.ProcessStats; import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.CloudFoundryServerGroupNameResolver; import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; @@ -66,7 +68,6 @@ protected String getPhase() { @Override public DeploymentResult operate(List priorOutputs) { getTask().updateStatus(PHASE, "Deploying '" + description.getApplication() + "'"); - CloudFoundryClient client = description.getClient(); CloudFoundryServerGroupNameResolver serverGroupNameResolver = @@ -79,13 +80,12 @@ public DeploymentResult operate(List priorOutputs) { description.getFreeFormDetails(), false)); - CloudFoundryServerGroup serverGroup; + CloudFoundryServerGroup serverGroup = createApplication(description); String packageId; - // we download the package artifact first, because if this fails, we don't want to create an - // empty CF app - File packageArtifact = downloadPackageArtifact(description); + + File packageArtifact = + description.getDocker() == null ? downloadPackageArtifact(description) : null; try { - serverGroup = createApplication(description); packageId = buildPackage(serverGroup.getId(), description, packageArtifact); } finally { if (packageArtifact != null) { @@ -230,14 +230,19 @@ private static CloudFoundryServerGroup createApplication( .updateStatus( PHASE, "Creating Cloud Foundry application '" + description.getServerGroupName() + "'"); + Lifecycle lifecycle = + description.getDocker() == null + ? new Lifecycle(Lifecycle.Type.BUILDPACK, description.getApplicationAttributes()) + : new Lifecycle(Lifecycle.Type.DOCKER, description.getApplicationAttributes()); + CloudFoundryServerGroup serverGroup = client .getApplications() .createApplication( description.getServerGroupName(), description.getSpace(), - description.getApplicationAttributes(), - getEnvironmentVars(description)); + getEnvironmentVars(description), + lifecycle); getTask() .updateStatus( PHASE, "Created Cloud Foundry application '" + description.getServerGroupName() + "'"); @@ -399,8 +404,23 @@ private String buildPackage( .updateStatus( PHASE, "Creating package for application '" + description.getServerGroupName() + "'"); - String packageId = client.getApplications().createPackage(serverGroupId); - client.getApplications().uploadPackageBits(packageId, packageArtifact); + String packageId; + if (packageArtifact != null) { + // Bits Package + packageId = + client + .getApplications() + .createPackage(new CreatePackage(serverGroupId, CreatePackage.Type.BITS, null)); + client.getApplications().uploadPackageBits(packageId, packageArtifact); + } else { + // Docker Package + packageId = + client + .getApplications() + .createPackage( + new CreatePackage( + serverGroupId, CreatePackage.Type.DOCKER, description.getDocker())); + } operationPoller.waitForOperation( () -> client.getApplications().packageUploadComplete(packageId), diff --git a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplicationTest.java b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplicationTest.java index 1211150db46..53f3e7f948b 100644 --- a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplicationTest.java +++ b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/client/model/v3/CreateApplicationTest.java @@ -35,8 +35,9 @@ void getLifecycleShouldReturnMultipleBuildpacks() { DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes = new DeployCloudFoundryServerGroupDescription.ApplicationAttributes(); applicationAttributes.setBuildpacks(ImmutableList.of("buildpackOne", "buildpackTwo")); + Lifecycle lifecycle = new Lifecycle(Lifecycle.Type.BUILDPACK, applicationAttributes); CreateApplication createApplication = - new CreateApplication("some-application", relationships, null, applicationAttributes); + new CreateApplication("some-application", relationships, null, lifecycle); assertThat(createApplication.getLifecycle().getData().get("buildpacks")) .isEqualTo(applicationAttributes.getBuildpacks()); @@ -51,8 +52,9 @@ void getLifecycleShouldReturnWithBuildpackAndWithStack() { new DeployCloudFoundryServerGroupDescription.ApplicationAttributes(); applicationAttributes.setBuildpacks(ImmutableList.of("buildpackOne")); applicationAttributes.setStack("cflinuxfs3"); + Lifecycle lifecycle = new Lifecycle(Lifecycle.Type.BUILDPACK, applicationAttributes); CreateApplication createApplication = - new CreateApplication("some-application", relationships, null, applicationAttributes); + new CreateApplication("some-application", relationships, null, lifecycle); Map data = ImmutableMap.of( @@ -70,25 +72,12 @@ void getLifecycleShouldReturnWithoutBuildpackAndWithStack() { DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes = new DeployCloudFoundryServerGroupDescription.ApplicationAttributes(); applicationAttributes.setStack("cflinuxfs3"); + Lifecycle lifecycle = new Lifecycle(Lifecycle.Type.BUILDPACK, applicationAttributes); CreateApplication createApplication = - new CreateApplication("some-application", relationships, null, applicationAttributes); + new CreateApplication("some-application", relationships, null, lifecycle); Map data = ImmutableMap.of("stack", applicationAttributes.getStack()); assertThat(createApplication.getLifecycle().getData()).isEqualTo(data); } - - @Test - void getLifecycleShouldReturnNull() { - ToOneRelationship toOneRelationship = - new ToOneRelationship(new Relationship("relationship-guid")); - Map relationships = - Collections.singletonMap("relationship", toOneRelationship); - DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes = - new DeployCloudFoundryServerGroupDescription.ApplicationAttributes(); - CreateApplication createApplication = - new CreateApplication("some-application", relationships, null, applicationAttributes); - - assertThat(createApplication.getLifecycle()).isNull(); - } } diff --git a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverterTest.java b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverterTest.java index 6ad1f9879e1..889804fbef7 100644 --- a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverterTest.java +++ b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/converters/DeployCloudFoundryServerGroupAtomicOperationConverterTest.java @@ -16,6 +16,7 @@ package com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.converters; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; @@ -120,7 +121,7 @@ public CloudFoundryClient getClient() { private final DeployCloudFoundryServerGroupAtomicOperationConverter converter = new DeployCloudFoundryServerGroupAtomicOperationConverter( - null, artifactCredentialsRepository); + null, artifactCredentialsRepository, emptyList()); @BeforeEach void initializeClassUnderTest() { diff --git a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperationTest.java b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperationTest.java index 1190358d8b3..d46c7dcdd6b 100644 --- a/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperationTest.java +++ b/clouddriver-cloudfoundry/src/test/java/com/netflix/spinnaker/clouddriver/cloudfoundry/deploy/ops/DeployCloudFoundryServerGroupAtomicOperationTest.java @@ -28,6 +28,8 @@ import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.AbstractServiceInstance; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.Resource; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v2.ServiceInstance; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.CreatePackage; +import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.Docker; import com.netflix.spinnaker.clouddriver.cloudfoundry.client.model.v3.ProcessStats; import com.netflix.spinnaker.clouddriver.cloudfoundry.deploy.description.DeployCloudFoundryServerGroupDescription; import com.netflix.spinnaker.clouddriver.cloudfoundry.model.CloudFoundryServerGroup; @@ -94,6 +96,29 @@ void executeOperationAndDeploySucceeds() { .isEqualTo(Collections.singletonList("region1:app1-stack1-detail1-v000")); } + @Test + void executeOperationAndDeployDockerSucceeds() { + // Given + final DeployCloudFoundryServerGroupDescription description = + getDockerDeployCloudFoundryServerGroupDescription(true); + final CloudFoundryClusterProvider clusterProvider = mock(CloudFoundryClusterProvider.class); + final DeployCloudFoundryServerGroupAtomicOperation operation = + new DeployCloudFoundryServerGroupAtomicOperation( + new PassThroughOperationPoller(), description); + final Applications apps = getApplications(clusterProvider, ProcessStats.State.RUNNING); + final ServiceInstances serviceInstances = getServiceInstances(); + + // When + final DeploymentResult result = operation.operate(Lists.emptyList()); + + // Then + verifyInOrderDockerDeploy(apps, serviceInstances, () -> atLeastOnce()); + + assertThat(testTask.getStatus().isFailed()).isFalse(); + assertThat(result.getServerGroupNames()) + .isEqualTo(Collections.singletonList("region1:app1-stack1-detail1-v000")); + } + @Test void executeOperationAndDeployFails() { // Given @@ -153,20 +178,27 @@ private void verifyInOrder( ServiceInstances serviceInstances, Supplier calls) { InOrder inOrder = Mockito.inOrder(apps, serviceInstances); - DeployCloudFoundryServerGroupDescription.ApplicationAttributes applicationAttributes = - new DeployCloudFoundryServerGroupDescription.ApplicationAttributes(); - applicationAttributes.setBuildpacks( - io.vavr.collection.List.of("buildpack1", "buildpack2").asJava()); + inOrder.verify(apps).createApplication(any(), any(), any(), any()); inOrder .verify(apps) - .createApplication( - "app1-stack1-detail1-v000", - CloudFoundrySpace.builder().id("space1Id").name("space1").build(), - getDeployCloudFoundryServerGroupDescription(true).getApplicationAttributes(), - HashMap.of("token", "ASDF").toJavaMap()); - inOrder.verify(apps).uploadPackageBits(eq("serverGroupId_package"), any()); + .createPackage(eq(new CreatePackage("serverGroupId", CreatePackage.Type.BITS, null))); + inOrder.verify(apps).uploadPackageBits(any(), any()); + inOrder.verify(cloudFoundryClient.getServiceInstances()).createServiceBinding(any()); + inOrder.verify(apps).createBuild(any()); + inOrder.verify(apps).scaleApplication("serverGroupId", 7, 1024, 2048); + inOrder.verify(apps).updateProcess("serverGroupId", null, "http", "/health"); + inOrder.verify(apps, calls.get()).startApplication("serverGroupId"); + } + + private void verifyInOrderDockerDeploy( + final Applications apps, + ServiceInstances serviceInstances, + Supplier calls) { + InOrder inOrder = Mockito.inOrder(apps, serviceInstances); + inOrder.verify(apps).createApplication(any(), any(), any(), any()); + inOrder.verify(apps).createPackage(any()); inOrder.verify(cloudFoundryClient.getServiceInstances()).createServiceBinding(any()); - inOrder.verify(apps).createBuild("serverGroupId_package"); + inOrder.verify(apps).createBuild(any()); inOrder.verify(apps).scaleApplication("serverGroupId", 7, 1024, 2048); inOrder.verify(apps).updateProcess("serverGroupId", null, "http", "/health"); inOrder.verify(apps, calls.get()).startApplication("serverGroupId"); @@ -222,6 +254,7 @@ private DeployCloudFoundryServerGroupDescription getDeployCloudFoundryServerGrou "test", io.vavr.collection.List.of("a").asJava(), "")) .setSpace(CloudFoundrySpace.builder().id("space1Id").name("space1").build()) .setApplicationArtifact(Artifact.builder().reference("ref1").build()) + .setDocker(null) .setApplicationAttributes( new DeployCloudFoundryServerGroupDescription.ApplicationAttributes() .setInstances(7) @@ -237,4 +270,34 @@ private DeployCloudFoundryServerGroupDescription getDeployCloudFoundryServerGrou description.setStartApplication(b); return description; } + + private DeployCloudFoundryServerGroupDescription + getDockerDeployCloudFoundryServerGroupDescription(boolean b) { + final DeployCloudFoundryServerGroupDescription description = + new DeployCloudFoundryServerGroupDescription() + .setAccountName("account1") + .setApplication("app1") + .setStack("stack1") + .setFreeFormDetails("detail1") + .setSpace(CloudFoundrySpace.builder().id("space1Id").name("space1").build()) + .setArtifactCredentials( + new ArtifactCredentialsFromString( + "test", io.vavr.collection.List.of("a").asJava(), "")) + .setApplicationArtifact(Artifact.builder().reference("ref1").build()) + .setDocker(Docker.builder().image("some/image").build()) + .setApplicationAttributes( + new DeployCloudFoundryServerGroupDescription.ApplicationAttributes() + .setInstances(7) + .setMemory("1G") + .setDiskQuota("2048M") + .setHealthCheckType("http") + .setHealthCheckHttpEndpoint("/health") + .setBuildpacks(Collections.emptyList()) + .setServices(List.of("service1")) + .setEnv(HashMap.of("token", "ASDF").toJavaMap())); + description.setClient(cloudFoundryClient); + description.setRegion("region1"); + description.setStartApplication(b); + return description; + } }