/* * Copyright 2013-2017 the original author or authors. * * 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 org.cloudfoundry.operations.applications; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; import org.cloudfoundry.util.tuple.Consumer2; import reactor.core.Exceptions; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Spliterator; import java.util.Spliterators; import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; /** * Utilities for dealing with {@link ApplicationManifest}s. Includes the functionality to transform to and from standard CLI YAML files. */ public final class ApplicationManifestUtils { private static final int GIBI = 1_024; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory() .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)) .setSerializationInclusion(JsonInclude.Include.NON_NULL); private ApplicationManifestUtils() { } /** * Reads a YAML manifest file (defined by the <a href="https://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html">CLI</a>) from a {@link Path} and converts it into a collection of {@link * ApplicationManifest}s. Note that all resolution (both inheritance and common) is performed during read. * * @param path the path to read from * @return the resolved manifests */ public static List<ApplicationManifest> read(Path path) { return doRead(path) .values().stream() .map(ApplicationManifest.Builder::build) .collect(Collectors.toList()); } /** * Write {@link ApplicationManifest}s to a {@link Path} * * @param path the path to write to * @param applicationManifests the manifests to write */ public static void write(Path path, ApplicationManifest... applicationManifests) { write(path, Arrays.asList(applicationManifests)); } /** * Write {@link ApplicationManifest}s to a {@link Path} * * @param path the path to write to * @param applicationManifests the manifests to write */ public static void write(Path path, List<ApplicationManifest> applicationManifests) { try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { OBJECT_MAPPER.writeValue(out, Collections.singletonMap("applications", applicationManifests)); } catch (IOException e) { throw Exceptions.propagate(e); } } private static <T> void as(JsonNode payload, String key, Function<JsonNode, T> mapper, Consumer<T> consumer) { Optional.ofNullable(payload.get(key)) .map(mapper) .ifPresent(consumer); } private static void asBoolean(JsonNode payload, String key, Consumer<Boolean> consumer) { as(payload, key, JsonNode::asBoolean, consumer); } private static void asInteger(JsonNode payload, String key, Consumer<Integer> consumer) { as(payload, key, JsonNode::asInt, consumer); } private static <T> void asList(JsonNode payload, String key, Function<JsonNode, T> mapper, Consumer<T> consumer) { as(payload, key, ApplicationManifestUtils::streamOf, domains -> domains .map(mapper) .forEach(consumer)); } private static void asListOfString(JsonNode payload, String key, Consumer<String> consumer) { asList(payload, key, JsonNode::asText, consumer); } private static <T> void asMap(JsonNode payload, String key, Function<JsonNode, T> valueMapper, Consumer2<String, T> consumer) { as(payload, key, environmentVariables -> streamOf(environmentVariables.fields()), environmentVariables -> environmentVariables .forEach(entry -> consumer.accept(entry.getKey(), valueMapper.apply(entry.getValue())))); } private static void asMapOfStringString(JsonNode payload, String key, Consumer2<String, String> consumer) { asMap(payload, key, JsonNode::asText, consumer); } private static void asMemoryInteger(JsonNode payload, String key, Consumer<Integer> consumer) { as(payload, key, raw -> { if (raw.isNumber()) { return raw.asInt(); } else if (raw.isTextual()) { String text = raw.asText(); if (text.endsWith("G")) { return Integer.parseInt(text.substring(0, text.length() - 1)) * GIBI; } else if (text.endsWith("M")) { return Integer.parseInt(text.substring(0, text.length() - 1)); } else { return 0; } } else { return 0; } }, consumer); } private static void asString(JsonNode payload, String key, Consumer<String> consumer) { as(payload, key, JsonNode::asText, consumer); } private static JsonNode deserialize(Path path) { try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) { return OBJECT_MAPPER.readTree(in); } catch (IOException e) { throw Exceptions.propagate(e); } } private static Map<String, ApplicationManifest.Builder> doRead(Path path) { Map<String, ApplicationManifest.Builder> applicationManifests = new TreeMap<>(); JsonNode root = deserialize(path); asString(root, "inherit", inherit -> applicationManifests.putAll(doRead(path.getParent().resolve(inherit)))); applicationManifests .forEach((name, builder) -> applicationManifests.put(name, toApplicationManifest(root, builder, path))); ApplicationManifest template = getTemplate(path, root); Optional.ofNullable(root.get("applications")) .map(ApplicationManifestUtils::streamOf) .ifPresent(applications -> applications .forEach(application -> { String name = application.get("name").asText(); ApplicationManifest.Builder builder = getBuilder(applicationManifests, template, name); applicationManifests.put(name, toApplicationManifest(application, builder, path)); })); return applicationManifests; } private static ApplicationManifest.Builder getBuilder(Map<String, ApplicationManifest.Builder> applicationManifests, ApplicationManifest template, String name) { ApplicationManifest.Builder builder = applicationManifests.get(name); if (builder == null) { builder = ApplicationManifest.builder().from(template); } return builder; } private static ApplicationManifest getTemplate(Path path, JsonNode root) { return toApplicationManifest(root, ApplicationManifest.builder(), path) .name("template") .build(); } private static <T> Stream<T> streamOf(Iterator<T> iterator) { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); } private static <T> Stream<T> streamOf(Iterable<T> iterable) { return StreamSupport.stream(iterable.spliterator(), false); } private static ApplicationManifest.Builder toApplicationManifest(JsonNode application, ApplicationManifest.Builder builder, Path root) { asString(application, "buildpack", builder::buildpack); asString(application, "command", builder::command); asInteger(application, "disk_quota", builder::disk); asString(application, "domain", builder::domain); asListOfString(application, "domains", builder::domain); asMapOfStringString(application, "env", builder::environmentVariable); asString(application, "health-check-http-endpoint", builder::healthCheckHttpEndpoint); asString(application, "health-check-type", healthCheckType -> builder.healthCheckType(ApplicationHealthCheck.from(healthCheckType))); asString(application, "host", builder::host); asListOfString(application, "hosts", builder::host); asInteger(application, "instances", builder::instances); asMemoryInteger(application, "memory", builder::memory); asString(application, "name", builder::name); asBoolean(application, "no-hostname", builder::noHostname); asBoolean(application, "no-route", builder::noRoute); asString(application, "path", path -> builder.path(root.getParent().resolve(path))); asBoolean(application, "random-route", builder::randomRoute); asList(application, "routes", route -> Route.builder().route(route.get("route").asText()).build(), builder::route); asListOfString(application, "services", builder::service); asString(application, "stack", builder::stack); asInteger(application, "timeout", builder::timeout); return builder; } }