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 942e1e9d74..da335ae704 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 @@ -26,12 +26,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; 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; @@ -172,8 +174,14 @@ private static ManifestV3Application.Builder toApplicationManifest( variables, raw -> getSidecar((Map) raw, variables), builder::sidecar); - as(application, "labels", variables, Map.class::cast, builder::labels); - as(application, "annotations", variables, Map.class::cast, builder::annotations); + + as( + application, + "metadata", + variables, + raw -> getMetadata((Map) raw, variables), + builder::metadata); + asBoolean(application, "default-route", variables, builder::defaultRoute); return builder; @@ -253,6 +261,31 @@ private static ManifestV3Service getService(Object raw, Map vari return builder.build(); } + private static Metadata getMetadata(Map raw, Map variables) { + + if (raw == null) return null; + + Map labels = new HashMap<>(); + Map annotations = new HashMap<>(); + + asMap(raw, "labels", variables, String.class::cast, labels::put); + + asMap(raw, "annotations", variables, String.class::cast, annotations::put); + + if (labels.isEmpty() && annotations.isEmpty()) { + return null; + } + + Metadata.Builder builder = Metadata.builder(); + if (!labels.isEmpty()) { + builder.labels(labels); + } + if (!annotations.isEmpty()) { + builder.annotations(annotations); + } + return builder.build(); + } + private static Map toYaml(ManifestV3 manifest) { Map yaml = new TreeMap<>(); yaml.put("version", manifest.getVersion()); @@ -282,8 +315,8 @@ private static Map toApplicationYaml(ManifestV3Application appli "sidecars", convertCollection( application.getSidecars(), ApplicationManifestUtilsV3::toSidecarsYaml)); - putIfPresent(yaml, "labels", application.getLabels()); - putIfPresent(yaml, "annotations", application.getAnnotations()); + + putIfPresent(yaml, "metadata", toMetadataYaml(application.getMetadata())); return yaml; } @@ -337,4 +370,12 @@ private static Map toProcessYaml(ManifestV3Process process) { putIfPresent(yaml, "timeout", process.getTimeout()); return yaml; } + + private static Map toMetadataYaml(Metadata metadata) { + if (metadata == null) return null; + Map map = new HashMap<>(); + putIfPresent(map, "labels", metadata.getLabels()); + putIfPresent(map, "annotations", metadata.getAnnotations()); + return map.isEmpty() ? null : map; + } } 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 1da9f0fc43..b235a1eb5d 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,10 +16,9 @@ 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 java.util.List; import java.util.Map; @@ -31,13 +30,6 @@ @Value.Immutable abstract class _ManifestV3Application extends _ApplicationManifestCommon { - /** - * The annotations configured for this application - */ - @AllowNulls - @Nullable - abstract Map getAnnotations(); - /** * Generate a default route based on the application name */ @@ -45,11 +37,10 @@ abstract class _ManifestV3Application extends _ApplicationManifestCommon { abstract Boolean getDefaultRoute(); /** - * The labels configured for this application + * The metadata for this application */ - @AllowNulls @Nullable - abstract Map getLabels(); + abstract Metadata getMetadata(); /** * The collection of processes configured for this application 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 fd7826d444..cf54b8120b 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 @@ -5,6 +5,7 @@ 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; @@ -60,6 +61,62 @@ void testWithDockerApp() throws IOException { assertSerializeDeserialize(manifest); } + @Test + void testWithMetadata() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .metadata( + Metadata.builder() + .label("test-label", "test-label-value") + .annotation( + "test-annotation", + "test-annotation-value") + .build()) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithMetadataOnlyLabel() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .metadata( + Metadata.builder() + .label("test-label", "test-label-value") + .build()) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + + @Test + void testWithMetadataOnlyAnnotation() throws IOException { + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .name("test-app") + .metadata( + Metadata.builder() + .annotation( + "test-annotation", + "test-annotation-value") + .build()) + .build()) + .build(); + + assertSerializeDeserialize(manifest); + } + private void assertSerializeDeserialize(ManifestV3 manifest) throws IOException { Path file = Files.createTempFile("test-manifest-", ".yml"); ApplicationManifestUtilsV3.write(file, 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 2ffd49b285..36e1bd9456 100644 --- a/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java +++ b/integration-test/src/test/java/org/cloudfoundry/operations/ApplicationsTest.java @@ -29,6 +29,7 @@ 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; @@ -108,6 +109,7 @@ public final class ApplicationsTest extends AbstractIntegrationTest { @Autowired private String serviceName; @Autowired private LogCacheClient logCacheClient; + @Autowired private CloudFoundryClient cloudFoundryClient; // To create a service in #pushBindService, the Service Broker must be installed first. // We ensure it is by loading the serviceBrokerId @Lazy bean. @@ -789,6 +791,62 @@ public void pushManifestV3() throws IOException { .verify(Duration.ofMinutes(5)); } + @Test + @IfCloudFoundryVersion(greaterThanOrEqualTo = CloudFoundryVersion.PCF_4_v2) + public void pushManifestV3WithMetadata() throws IOException { + String applicationName = this.nameFactory.getApplicationName(); + Map labels = Collections.singletonMap("test-label", "test-label-value"); + Map annotations = + Collections.singletonMap("test-annotation", "test-annotation-value"); + + ManifestV3 manifest = + ManifestV3.builder() + .application( + ManifestV3Application.builder() + .buildpack("staticfile_buildpack") + .disk(512) + .healthCheckType(ApplicationHealthCheck.PORT) + .memory(64) + .name(applicationName) + .path( + new ClassPathResource("test-application.zip") + .getFile() + .toPath()) + .metadata( + org.cloudfoundry.client.v3.Metadata.builder() + .labels(labels) + .annotations(annotations) + .build()) + .build()) + .build(); + + this.cloudFoundryOperations + .applications() + .pushManifestV3(PushManifestV3Request.builder().manifest(manifest).build()) + .then( + this.cloudFoundryOperations + .applications() + .get(GetApplicationRequest.builder().name(applicationName).build())) + .map(ApplicationDetail::getId) + .flatMap( + id -> + this.cloudFoundryClient + .applicationsV3() + .get( + org.cloudfoundry.client.v3.applications + .GetApplicationRequest.builder() + .applicationId(id) + .build())) + .as(StepVerifier::create) + .expectNextMatches( + createdApp -> + labels.equals(createdApp.getMetadata().getLabels()) + && annotations.equals( + createdApp.getMetadata().getAnnotations())) + .expectComplete() + .verify(Duration.ofMinutes(5)); + } + @Test @IfCloudFoundryVersion( greaterThanOrEqualTo =