com.vmware.admiral.compute.container.volume.VolumeUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.admiral.compute.container.volume.VolumeUtil.java

Source

/*
 * Copyright (c) 2016 VMware, Inc. All Rights Reserved.
 *
 * This product is licensed to you under the Apache License, Version 2.0 (the "License").
 * You may not use this product except in compliance with the License.
 *
 * This product may include a number of subcomponents with separate copyright notices
 * and license terms. Your use of these subcomponents is subject to the terms and
 * conditions of the subcomponent's license, as noted in the LICENSE file.
 */

package com.vmware.admiral.compute.container.volume;

import static java.util.Collections.disjoint;

import static com.vmware.admiral.compute.container.volume.ContainerVolumeDescriptionService.DEFAULT_VOLUME_DRIVER;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;

import com.vmware.admiral.common.util.QueryUtil;
import com.vmware.admiral.compute.ComponentDescription;
import com.vmware.admiral.compute.container.ContainerDescriptionService.ContainerDescription;
import com.vmware.admiral.compute.container.volume.ContainerVolumeDescriptionService.ContainerVolumeDescription;
import com.vmware.admiral.compute.container.volume.ContainerVolumeService.ContainerVolumeState;
import com.vmware.admiral.compute.container.volume.ContainerVolumeService.ContainerVolumeState.PowerState;
import com.vmware.photon.controller.model.resources.ResourceState;
import com.vmware.xenon.common.LocalizableValidationException;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.services.common.QueryTask;
import com.vmware.xenon.services.common.QueryTask.Query;
import com.vmware.xenon.services.common.QueryTask.Query.Occurance;
import com.vmware.xenon.services.common.QueryTask.QueryTerm.MatchType;

/**
 * Utility class for docker volume related operations.
 */
public class VolumeUtil {

    private static final String HOST_CONTAINER_DIR_DELIMITER = ":/";

    public static final String ERROR_VOLUME_NAME_IS_REQUIRED = "Volume name is required.";
    public static final String ERROR_VOLUME_NAME_INVALID = "\"%s\" includes invalid characters for"
            + " a local volume name, only \"[a-zA-Z0-9][a-zA-Z0-9@_.-]+\" are allowed.";

    private static final Pattern RX_VOLUME_NAME = Pattern.compile("^[a-zA-Z0-9][a-zA-Z0-9_.-]+$");

    public static void validateLocalVolumeName(String name) {
        if (name == null || name.trim().isEmpty()) {
            throw new LocalizableValidationException(ERROR_VOLUME_NAME_IS_REQUIRED,
                    "compute.volumes.name.required");
        }

        Matcher matcher = RX_VOLUME_NAME.matcher(name);
        if (!matcher.matches()) {
            String errMsg = String.format(ERROR_VOLUME_NAME_INVALID, name);
            throw new LocalizableValidationException(errMsg, "compute.volumes.name.invalid");
        }
    }

    /**
     * Parses volume host directory only.
     *
     * @param volume
     *            - volume name. It might be named volume [volume-name] or path:
     *            [/host-directory:/container-directory, named-volume1:/container-directory]
     * @return host directory or named volume itself.
     */
    public static String parseVolumeHostDirectory(String volume) {

        if (StringUtils.isEmpty(volume)) {
            return volume;
        }

        if (!volume.contains(HOST_CONTAINER_DIR_DELIMITER)) {
            return volume;
        }

        String[] hostContainerDir = volume.split(HOST_CONTAINER_DIR_DELIMITER);

        if (hostContainerDir.length != 2) {
            throw new LocalizableValidationException("Invalid volume directory.",
                    "compute.volumes.invalid.directory");
        }

        String hostDir = hostContainerDir[0];

        return hostDir;
    }

    /**
     * Creates additional affinity rules between container descriptions which share
     * local volumes. Each container group should be deployed on a single host.
     */
    public static void applyLocalNamedVolumeConstraints(Collection<ComponentDescription> componentDescriptions) {

        Map<String, ContainerVolumeDescription> volumes = filterDescriptions(ContainerVolumeDescription.class,
                componentDescriptions);

        List<String> localVolumes = volumes.values().stream().filter(v -> DEFAULT_VOLUME_DRIVER.equals(v.driver))
                .map(v -> v.name).collect(Collectors.toList());

        if (localVolumes.isEmpty()) {
            return;
        }

        Map<String, ContainerDescription> containers = filterDescriptions(ContainerDescription.class,
                componentDescriptions);

        // sort containers by local volume: each set is a group of container names
        // that share a particular local volume
        List<Set<String>> localVolumeContainers = localVolumes.stream()
                .map(v -> filterByVolume(v, containers.values())).filter(s -> !s.isEmpty())
                .collect(Collectors.toList());

        if (localVolumeContainers.isEmpty()) {
            return;
        }

        /** Merge sets of containers sharing local volumes
         *
         *  C1  C2  C3  C4  C5  C6
         *   \  /\  /   |    \  /
         *    L1  L2    L3    L4
         *
         *    Input: [C1, C2], [C2, C3], [C4], [C5, C6]
         *    Output: [C1, C2, C3], [C4], [C5, C6]
         */
        localVolumeContainers = mergeSets(localVolumeContainers);

        Map<String, List<ContainerVolumeDescription>> containerToVolumes = containers.values().stream()
                .collect(Collectors.toMap(cd -> cd.name, cd -> filterVolumes(cd, volumes.values())));

        Map<String, Integer> containerToDriverCount = containerToVolumes.entrySet().stream()
                .collect(Collectors.toMap(e -> e.getKey(),
                        e -> e.getValue().stream().map(vd -> vd.driver).collect(Collectors.toSet()).size()));

        for (Set<String> s : localVolumeContainers) {
            if (s.size() > 1) {
                // find the container with highest number of required drivers
                int max = s.stream().map(cn -> containerToDriverCount.get(cn))
                        .max((vc1, vc2) -> Integer.compare(vc1, vc2)).get();
                Set<String> maxDrivers = s.stream().filter(cn -> containerToDriverCount.get(cn) == max)
                        .collect(Collectors.toSet());

                String maxCont = maxDrivers.iterator().next();
                s.remove(maxCont);
                s.stream().forEach(cn -> addAffinity(maxCont, containers.get(cn)));
            }
        }
    }

    public static ContainerVolumeDescription createContainerVolumeDescription(ContainerVolumeState state) {

        ContainerVolumeDescription volumeDescription = new ContainerVolumeDescription();

        volumeDescription.documentSelfLink = state.descriptionLink;
        volumeDescription.documentDescription = state.documentDescription;
        volumeDescription.tenantLinks = state.tenantLinks;
        volumeDescription.instanceAdapterReference = state.adapterManagementReference;
        volumeDescription.name = state.name;
        volumeDescription.customProperties = state.customProperties;

        // TODO - fill in other volume settings

        return volumeDescription;
    }

    public static String buildVolumeLink(String name) {
        return UriUtils.buildUriPath(ContainerVolumeService.FACTORY_LINK, buildVolumeId(name));
    }

    public static String buildVolumeId(String resourceName) {
        return resourceName.replaceAll(" ", "-");
    }

    public static QueryTask getVolumeByHostAndNameQueryTask(String hostLink, String volumeName) {

        QueryTask queryTask = QueryUtil.buildQuery(ContainerVolumeState.class, true);

        String parentLinksItemField = QueryTask.QuerySpecification
                .buildCollectionItemName(ContainerVolumeState.FIELD_NAME_PARENT_LINKS);
        QueryTask.Query parentsClause = new QueryTask.Query().setTermPropertyName(parentLinksItemField)
                .setTermMatchValue(hostLink).setTermMatchType(MatchType.TERM).setOccurance(Occurance.MUST_OCCUR);

        QueryTask.Query nameClause = new QueryTask.Query().setTermPropertyName(ContainerVolumeState.FIELD_NAME_NAME)
                .setTermMatchValue(volumeName).setTermMatchType(MatchType.TERM).setOccurance(Occurance.MUST_OCCUR);

        QueryTask.Query stateClause = new QueryTask.Query()
                .setTermPropertyName(ContainerVolumeState.FIELD_NAME_POWER_STATE)
                .setTermMatchValue(PowerState.CONNECTED.toString()).setTermMatchType(MatchType.TERM)
                .setOccurance(Occurance.MUST_OCCUR);

        Query intermediate = new QueryTask.Query().setOccurance(Occurance.MUST_OCCUR);
        intermediate.addBooleanClause(parentsClause);
        intermediate.addBooleanClause(nameClause);
        intermediate.addBooleanClause(stateClause);

        queryTask.querySpec.query.addBooleanClause(intermediate);

        QueryUtil.addExpandOption(queryTask);
        QueryUtil.addBroadcastOption(queryTask);

        return queryTask;
    }

    private static void addAffinity(String affinityTo, ContainerDescription cd) {
        if (cd.affinity != null) {
            boolean alreadyIn = Arrays.stream(cd.affinity).anyMatch(af -> affinityTo.equals(af));
            if (!alreadyIn) {
                int newSize = cd.affinity.length + 1;
                cd.affinity = Arrays.copyOf(cd.affinity, newSize);
                cd.affinity[newSize - 1] = affinityTo;
            }
        } else {
            cd.affinity = new String[] { affinityTo };
        }
    }

    private static Set<String> filterByVolume(String volumeName, Collection<ContainerDescription> descs) {

        Predicate<ContainerDescription> hasVolume = cd -> {
            if (cd.volumes != null) {
                return Arrays.stream(cd.volumes).anyMatch(v -> v.startsWith(volumeName));
            }
            return false;
        };

        return descs.stream().filter(hasVolume).map(cd -> cd.name).collect(Collectors.toSet());
    }

    private static List<ContainerVolumeDescription> filterVolumes(ContainerDescription cd,
            Collection<ContainerVolumeDescription> volumes) {

        if (cd.volumes == null) {
            return Collections.emptyList();
        }

        Predicate<ContainerVolumeDescription> hasVolume = vd -> Arrays.stream(cd.volumes)
                .anyMatch(v -> v.startsWith(vd.name));

        return volumes.stream().filter(hasVolume).collect(Collectors.toList());
    }

    private static <T extends ResourceState> Map<String, T> filterDescriptions(Class<T> clazz,
            Collection<ComponentDescription> componentDescriptions) {

        return componentDescriptions.stream().filter(cd -> clazz.isInstance(cd.getServiceDocument()))
                .map(cd -> clazz.cast(cd.getServiceDocument())).collect(Collectors.toMap(c -> c.name, c -> c));
    }

    private static List<Set<String>> mergeSets(List<Set<String>> list) {
        if (list.size() < 2) {
            return list;
        }

        for (int i = 0; i < list.size() - 1; i++) {
            Set<String> current = list.get(i);
            Set<String> next = list.get(i + 1);
            if (!disjoint(current, next)) {
                list.remove(current);
                list.remove(next);
                current.addAll(next);
                list.add(current);
                list = mergeSets(list);
                break;
            }
        }

        return list;
    }
}