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..d2444d09b7 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,30 +16,25 @@ 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; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Optional; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; 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 @@ -71,6 +66,7 @@ static T toApplicationManifestCom asString(application, "domain", variables, builder::domain); asListOfString(application, "domains", variables, builder::domain); asMapOfStringString(application, "env", variables, builder::environmentVariable); + asMap(application, "features", variables, String::valueOf, (k,v) -> builder.feature(k, Boolean.valueOf(v))); asString( application, "health-check-http-endpoint", @@ -430,6 +426,7 @@ static Map toApplicationYaml(_ApplicationManifestCommon applicat ApplicationManifestUtilsCommon::toDockerYaml); putIfPresent(yaml, "domains", applicationManifest.getDomains()); putIfPresent(yaml, "env", applicationManifest.getEnvironmentVariables()); + putIfPresent(yaml, "features", applicationManifest.getFeatures()); putIfPresent( yaml, "health-check-http-endpoint", diff --git a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationManifestCommon.java b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationManifestCommon.java index 95d53c6930..14a79607b7 100644 --- a/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationManifestCommon.java +++ b/cloudfoundry-operations/src/main/java/org/cloudfoundry/operations/applications/_ApplicationManifestCommon.java @@ -46,6 +46,10 @@ interface Builder { Builder environmentVariable(Map.Entry entry); Builder environmentVariables(@Nullable Map entries); Builder putAllEnvironmentVariables(Map entries); + Builder feature(String key, Object value); + Builder feature(Map.Entry entry); + Builder features(@Nullable Map entries); + Builder putAllFeatures(Map entries); Builder healthCheckHttpEndpoint(@Nullable String healthCheckHttpEndpoint); Builder healthCheckType(@Nullable ApplicationHealthCheck healthCheckType); Builder host(String element); @@ -142,6 +146,13 @@ void check() { @Nullable abstract Map getEnvironmentVariables(); + /** + * Manage whether optional capabilities are enabled + */ + @AllowNulls + @Nullable + abstract Map getFeatures(); + /** * The HTTP health check endpoint */ 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..84bf8fa680 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -30,6 +30,8 @@ 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 +81,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; @@ -573,6 +576,65 @@ public void pushBindServices() throws IOException { .verify(Duration.ofMinutes(5)); } + @Test + public void pushAppFeatures() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + String serviceInstanceName = this.nameFactory.getServiceInstanceName(); + + final String featureKey = "file-based-vcap-services"; + createServiceInstance( + this.cloudFoundryOperations, + this.planName, + serviceInstanceName, + this.serviceName) + .then( + this.cloudFoundryOperations + .applications() + .pushManifest( + PushApplicationManifestRequest.builder() + .manifest( + ApplicationManifest.builder() + .path( + new ClassPathResource( + "test-application.zip") + .getFile() + .toPath()) + .buildpack("staticfile_buildpack") + .disk(512) + .healthCheckType( + ApplicationHealthCheck.PORT) + .memory(64) + .name(applicationName) + .service(serviceInstanceName) + .feature(featureKey, true) + .build()) + .noStart(false) + .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(true) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test public void pushDirectory() throws IOException { String applicationName = this.nameFactory.getApplicationName();