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..25a4bd1507 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,59 @@ 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 { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .features(Map.of("file-based-vcap-services", true)) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + @Test void testGenericApplication() throws IOException { ManifestV3 manifest = @@ -47,20 +91,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 36e1bd9456..fefed31ecc 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -16,76 +16,17 @@ 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.logcache.v1.Envelope; -import org.cloudfoundry.logcache.v1.EnvelopeBatch; -import org.cloudfoundry.logcache.v1.EnvelopeType; -import org.cloudfoundry.logcache.v1.Log; -import org.cloudfoundry.logcache.v1.LogCacheClient; -import org.cloudfoundry.logcache.v1.LogType; -import org.cloudfoundry.logcache.v1.ReadRequest; -import org.cloudfoundry.logcache.v1.ReadResponse; -import org.cloudfoundry.operations.applications.ApplicationDetail; -import org.cloudfoundry.operations.applications.ApplicationEnvironments; -import org.cloudfoundry.operations.applications.ApplicationEvent; -import org.cloudfoundry.operations.applications.ApplicationHealthCheck; -import org.cloudfoundry.operations.applications.ApplicationLog; -import org.cloudfoundry.operations.applications.ApplicationLogType; -import org.cloudfoundry.operations.applications.ApplicationLogsRequest; -import org.cloudfoundry.operations.applications.ApplicationManifest; -import org.cloudfoundry.operations.applications.ApplicationSshEnabledRequest; -import org.cloudfoundry.operations.applications.ApplicationSummary; -import org.cloudfoundry.operations.applications.CopySourceApplicationRequest; -import org.cloudfoundry.operations.applications.DeleteApplicationRequest; -import org.cloudfoundry.operations.applications.DisableApplicationSshRequest; -import org.cloudfoundry.operations.applications.EnableApplicationSshRequest; -import org.cloudfoundry.operations.applications.GetApplicationEnvironmentsRequest; -import org.cloudfoundry.operations.applications.GetApplicationEventsRequest; -import org.cloudfoundry.operations.applications.GetApplicationHealthCheckRequest; -import org.cloudfoundry.operations.applications.GetApplicationManifestRequest; -import org.cloudfoundry.operations.applications.GetApplicationRequest; -import org.cloudfoundry.operations.applications.ListApplicationTasksRequest; -import org.cloudfoundry.operations.applications.ManifestV3; -import org.cloudfoundry.operations.applications.ManifestV3Application; -import org.cloudfoundry.operations.applications.PushApplicationManifestRequest; -import org.cloudfoundry.operations.applications.PushApplicationRequest; -import org.cloudfoundry.operations.applications.PushManifestV3Request; -import org.cloudfoundry.operations.applications.RenameApplicationRequest; -import org.cloudfoundry.operations.applications.RestageApplicationRequest; -import org.cloudfoundry.operations.applications.RestartApplicationInstanceRequest; -import org.cloudfoundry.operations.applications.RestartApplicationRequest; -import org.cloudfoundry.operations.applications.Route; -import org.cloudfoundry.operations.applications.RunApplicationTaskRequest; -import org.cloudfoundry.operations.applications.ScaleApplicationRequest; -import org.cloudfoundry.operations.applications.SetApplicationHealthCheckRequest; -import org.cloudfoundry.operations.applications.SetEnvironmentVariableApplicationRequest; -import org.cloudfoundry.operations.applications.StartApplicationRequest; -import org.cloudfoundry.operations.applications.StopApplicationRequest; -import org.cloudfoundry.operations.applications.Task; -import org.cloudfoundry.operations.applications.TaskState; -import org.cloudfoundry.operations.applications.TerminateApplicationTaskRequest; -import org.cloudfoundry.operations.applications.UnsetEnvironmentVariableApplicationRequest; +import org.cloudfoundry.logcache.v1.*; +import org.cloudfoundry.operations.applications.*; import org.cloudfoundry.operations.domains.CreateDomainRequest; import org.cloudfoundry.operations.domains.CreateSharedDomainRequest; import org.cloudfoundry.operations.routes.ListRoutesRequest; -import org.cloudfoundry.operations.services.BindServiceInstanceRequest; -import org.cloudfoundry.operations.services.CreateServiceInstanceRequest; -import org.cloudfoundry.operations.services.CreateUserProvidedServiceInstanceRequest; -import org.cloudfoundry.operations.services.GetServiceInstanceRequest; -import org.cloudfoundry.operations.services.ServiceInstance; +import org.cloudfoundry.operations.services.*; import org.cloudfoundry.util.FluentMap; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -95,6 +36,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 { @@ -619,6 +570,54 @@ 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() + .getApplicationManifest( + GetApplicationManifestRequest.builder() + .name(applicationName) + .build())) + .map(ApplicationManifest::getFeatures) + .as(StepVerifier::create) + .expectNextMatches(actual -> actual.containsKey(featureKey)) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test public void pushDirectory() throws IOException { String applicationName = this.nameFactory.getApplicationName();