diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsCommon.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsCommon.java index 79b50ab809..3baca9f415 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsCommon.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsCommon.java @@ -16,7 +16,10 @@ package org.cloudfoundry.operations.applications; -import static java.util.Collections.emptyMap; +import org.cloudfoundry.util.tuple.Consumer2; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import reactor.core.Exceptions; import java.io.IOException; import java.io.InputStream; @@ -36,10 +39,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.cloudfoundry.util.tuple.Consumer2; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; -import reactor.core.Exceptions; + +import static java.util.Collections.emptyMap; /** * Common base class for dealing with manifests @@ -321,7 +322,7 @@ static Map getNamedObject(List array, String name) { value -> value instanceof Map && name.equals( - ((Map) value).get("name"))) + ((Map) value).get("name"))) .findFirst() .orElseGet(() -> getEmptyNamedObject(array, name)); } diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java index da335ae704..b085234c0d 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3.java @@ -16,8 +16,12 @@ package org.cloudfoundry.operations.applications; -import static java.util.Collections.emptyMap; -import static java.util.stream.Collectors.toMap; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.processes.HealthCheckType; +import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import reactor.core.Exceptions; import java.io.IOException; import java.io.OutputStream; @@ -33,12 +37,9 @@ import java.util.TreeMap; import java.util.regex.Pattern; import java.util.stream.Stream; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.processes.HealthCheckType; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; -import org.yaml.snakeyaml.DumperOptions; -import org.yaml.snakeyaml.Yaml; -import reactor.core.Exceptions; + +import static java.util.Collections.emptyMap; +import static java.util.stream.Collectors.toMap; /** * Utilities for dealing with {@link ManifestV3}s. Includes the functionality to transform to and from standard CLI YAML files. @@ -156,6 +157,13 @@ private static ManifestV3Application.Builder toApplicationManifest( Path root) { toApplicationManifestCommon(application, variables, builder, root); + asMap( + application, + "features", + variables, + String::valueOf, + (k,v) -> builder.feature(k, Boolean.valueOf(v)) + ); asList( application, "processes", @@ -299,6 +307,11 @@ private static Map toYaml(ManifestV3 manifest) { private static Map toApplicationYaml(ManifestV3Application application) { Map yaml = ApplicationManifestUtilsCommon.toApplicationYaml(application); + putIfPresent( + yaml, + "features", + application.getFeatures() + ); putIfPresent( yaml, "processes", diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java index b235a1eb5d..fe0ac3412e 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ManifestV3Application.java @@ -16,9 +16,10 @@ package org.cloudfoundry.operations.applications; +import org.cloudfoundry.AllowNulls; import org.cloudfoundry.Nullable; -import org.immutables.value.Value; import org.cloudfoundry.client.v3.Metadata; +import org.immutables.value.Value; import java.util.List; import java.util.Map; @@ -36,6 +37,13 @@ abstract class _ManifestV3Application extends _ApplicationManifestCommon { @Nullable abstract Boolean getDefaultRoute(); + /** + * Manage whether optional capabilities are enabled + */ + @AllowNulls + @Nullable + abstract Map getFeatures(); + /** * The metadata for this application */ @@ -61,6 +69,9 @@ abstract class _ManifestV3Application extends _ApplicationManifestCommon { abstract List getSidecars(); public abstract static class Builder implements _ApplicationManifestCommon.Builder { - + abstract Builder feature(String key, Object value); + abstract Builder feature(Map.Entry entry); + abstract Builder features(@Nullable Map entries); + abstract Builder putAllFeatures(Map entries); } } diff --git a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java index cf54b8120b..fac85f0377 100644 --- a/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java +++ b/cloudfoundry-operations/src/test/java/org/cloudfoundry/operations/applications/ApplicationManifestUtilsV3Test.java @@ -1,15 +1,61 @@ package org.cloudfoundry.operations.applications; -import static org.junit.jupiter.api.Assertions.*; +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import org.cloudfoundry.client.v3.Metadata; -import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; -import org.junit.jupiter.api.Test; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; class ApplicationManifestUtilsV3Test { + @Test + void testWithDockerApp() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .docker(Docker.builder().image("test-image").build()) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithFeature() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .feature("file-based-vcap-services", true) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithFeatureAsMap() throws IOException { + Map features = new java.util.HashMap<>(); + features.put("file-based-vcap-services", true); + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .features(features) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + @Test void testGenericApplication() throws IOException { ManifestV3 manifest = @@ -47,20 +93,6 @@ void testGenericApplication() throws IOException { assertSerializeDeserialize(manifest); } - @Test - void testWithDockerApp() throws IOException { - ManifestV3 manifest = - ManifestV3.builder() - .application( - ManifestV3Application.builder() - .name("test-app") - .docker(Docker.builder().image("test-image").build()) - .build()) - .build(); - - assertSerializeDeserialize(manifest); - } - @Test void testWithMetadata() throws IOException { ManifestV3 manifest = diff --git a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java index 37c701dbbe..9b836ebf1b 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -16,20 +16,13 @@ package org.cloudfoundry.operations; -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; import org.cloudfoundry.AbstractIntegrationTest; import org.cloudfoundry.CleanupCloudFoundryAfterClass; import org.cloudfoundry.CloudFoundryVersion; import org.cloudfoundry.IfCloudFoundryVersion; import org.cloudfoundry.client.CloudFoundryClient; +import org.cloudfoundry.client.v3.applications.ApplicationFeatureResource; +import org.cloudfoundry.client.v3.applications.ListApplicationFeaturesRequest; import org.cloudfoundry.operations.applications.ApplicationDetail; import org.cloudfoundry.operations.applications.ApplicationEnvironments; import org.cloudfoundry.operations.applications.ApplicationEvent; @@ -79,6 +72,7 @@ import org.cloudfoundry.operations.services.GetServiceInstanceRequest; import org.cloudfoundry.operations.services.ServiceInstance; import org.cloudfoundry.util.FluentMap; +import org.cloudfoundry.util.PaginationUtils; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -87,6 +81,16 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + @CleanupCloudFoundryAfterClass public final class ApplicationsTest extends AbstractIntegrationTest { @@ -745,6 +749,61 @@ public void pushManifestV3() throws IOException { .verify(Duration.ofMinutes(5)); } + @Test + @IfCloudFoundryVersion( + greaterThanOrEqualTo = + CloudFoundryVersion.PCF_4_v3) + public void pushManifestV3WithFeature() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + + final String featureKey = "ssh"; + final boolean featureValue = false; + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .buildpack("staticfile_buildpack") + .disk(512) + .healthCheckType(ApplicationHealthCheck.PORT) + .memory(64) + .name(applicationName) + .feature(featureKey, false) + .path( + new ClassPathResource("test-application.zip") + .getFile() + .toPath()) + .build()) + .build(); + + this.cloudFoundryOperations + .applications() + .pushManifestV3(PushManifestV3Request.builder().manifest(manifest).build()) + .then( + this.cloudFoundryOperations + .applications() + .get(GetApplicationRequest.builder().name(applicationName).build())) + + .map(ApplicationDetail::getId) + .flatMapMany( + applicationId -> + PaginationUtils.requestClientV3Resources( + page -> + this.cloudFoundryClient + .applicationsV3() + .listFeatures( + ListApplicationFeaturesRequest + .builder() + .applicationId(applicationId) + .page(page) + .build()))) + .filter(feature -> featureKey.equals(feature.getName())) + .map(ApplicationFeatureResource::getEnabled) + .as(StepVerifier::create) + .expectNext(featureValue) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v2) public void pushManifestV3WithMetadata() throws IOException {