/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.enricher.standard;

import io.fabric8.ianaservicehelper.Helper;
import io.fabric8.kubernetes.api.builder.TypedVisitor;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.ServiceFluent;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.maven.core.config.ResourceConfig;
import io.fabric8.maven.core.config.ServiceConfig;
import io.fabric8.maven.core.handler.ServiceHandler;
import io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.util.Configs;
import io.fabric8.maven.core.util.MavenUtil;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.docker.config.BuildImageConfiguration;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.enricher.api.BaseEnricher;
import io.fabric8.maven.enricher.api.MavenEnricherContext;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.maven.shared.utils.StringUtils;

/**
 * An enricher for creating default services when not present.
 *
 * @author roland
 * @since 25/05/16
 */
public class DefaultServiceEnricher extends BaseEnricher {

    private static final Pattern PORT_PROTOCOL_PATTERN =
        Pattern.compile("^(\\d+)(?:/(tcp|udp))?$", Pattern.CASE_INSENSITIVE);
    private static final Pattern PORT_MAPPING_PATTERN =
        Pattern.compile("^\\s*(?<port>\\d+)(\\s*:\\s*(?<targetPort>\\d+))?(\\s*/\\s*(?<protocol>(tcp|udp)))?\\s*$",
                        Pattern.CASE_INSENSITIVE);
    private static final String PORT_IMAGE_LABEL_PREFIX = "fabric8.generator.service.port";
    private static final String PORTS_IMAGE_LABEL_PREFIX = "fabric8.generator.service.ports";

    // Available configuration keys
    private enum Config implements Configs.Key {
        // Default name to use instead of a calculated one
        name,

        // Whether allow headless services.
        headless {{ d = "false"; }},

        // Whether expose the service as ingress. Needs an 'exposeController'
        // running
        expose {{ d = "false"; }},

        // Type of the service (LoadBalancer, NodePort, ClusterIP, ...)
        type,

        // Service port to use (only for the first exposed pod port)
        port,

        // Whether to expose multiple ports or only the first one
        multiPort {{ d = "false"; }},

        // protocol to use
        protocol {{ d = "tcp"; }};

        public String def() { return d; } protected String d;
    }

    public DefaultServiceEnricher(MavenEnricherContext buildContext) {
        super(buildContext, "fmp-service");
    }

    @Override
    public void create(PlatformMode platformMode, KubernetesListBuilder builder) {
        final Service defaultService = getDefaultService();

        if (hasServices(builder)) {
            mergeInDefaultServiceParameters(builder, defaultService);
        } else {
            addDefaultService(builder, defaultService);
        }

        // Add Services configured via XML
        addServices(builder);
    }

    // =======================================================================================================

    private void addServices(KubernetesListBuilder builder) {
        ResourceConfig resources = new ResourceConfig();

        if (resources != null && resources.getServices() != null) {
            List<ServiceConfig> serviceConfig = resources.getServices();
            ServiceHandler serviceHandler = new ServiceHandler();
            builder.addToServiceItems(toArray(serviceHandler.getServices(serviceConfig)));
        }
    }

    // convert list to array, never returns null.
    private Service[] toArray(List<Service> services) {
        if (services == null) {
            return new Service[0];
        }
        if (services instanceof ArrayList) {
            return ((ArrayList<Service>) services).toArray(new Service[services.size()]);
        } else {
            Service[] ret = new Service[services.size()];
            for (int i = 0; i < services.size(); i++) {
                ret[i] = services.get(i);
            }
            return ret;
        }
    }

    private Service getDefaultService() {

        // No image config, no service
        if (!hasImageConfiguration()) {
            return null;
        }

        String serviceName = getConfig(Config.name, MavenUtil.createDefaultResourceName(getContext().getGav().getSanitizedArtifactId()));

        // Create service only for all images which are supposed to live in a single pod
        List<ServicePort> ports = extractPorts(getImages().get());

        ServiceBuilder builder = new ServiceBuilder()
            .withNewMetadata()
              .withName(serviceName)
              .withLabels(extractLabels())
            .endMetadata();
        ServiceFluent.SpecNested<ServiceBuilder> specBuilder = builder.withNewSpec();
        if (!ports.isEmpty()) {
            specBuilder.withPorts(ports);
        } else if (Configs.asBoolean(getConfig(Config.headless))) {
            specBuilder.withClusterIP("None");
        } else {
            // No ports, no headless --> no service
            return null;
        }
        if (hasConfig(Config.type)) {
            specBuilder.withType(getConfig(Config.type));
        }
        specBuilder.endSpec();

        return builder.build();
    }

    private boolean hasServices(KubernetesListBuilder builder) {
        final AtomicBoolean hasService = new AtomicBoolean(false);
        builder.accept(new TypedVisitor<ServiceBuilder>() {
            @Override
            public void visit(ServiceBuilder element) {
                hasService.set(true);
            }
        });
        return hasService.get();
    }

    private void mergeInDefaultServiceParameters(KubernetesListBuilder builder, final Service defaultService) {
        builder.accept(new TypedVisitor<ServiceBuilder>() {
            @Override
            public void visit(ServiceBuilder service) {
                // Only update single service matching the default service's name
                String defaultServiceName = getDefaultServiceName(defaultService);

                ObjectMeta serviceMetadata = ensureServiceMetadata(service, defaultService);
                String serviceName = ensureServiceName(serviceMetadata, service, defaultServiceName);

                if (defaultService != null && defaultServiceName.equals(serviceName)) {
                    addMissingServiceParts(service, defaultService);
                }
            }
        });
    }

    private void addDefaultService(KubernetesListBuilder builder, Service defaultService) {
        if (defaultService == null) {
            return;
        }
        ServiceSpec spec = defaultService.getSpec();
        List<ServicePort> ports = spec.getPorts();
        if (ports.size() > 0) {
            log.info("Adding a default service '%s' with ports [%s]",
                     defaultService.getMetadata().getName(), formatPortsAsList(ports));
        } else {
            log.info("Adding headless default service '%s'",
                     defaultService.getMetadata().getName());
        }
        builder.addToServiceItems(defaultService);
    }

    // ....................................................................................

    private Map<String, String> extractLabels() {
        Map<String, String> labels = new HashMap<>();
        if (Configs.asBoolean(getConfig(Config.expose))) {
            labels.put("expose", "true");
        }
        return labels;
    }

    // ========================================================================================================
    // Port handling

    private List<ServicePort> extractPorts(List<ImageConfiguration> images) {
        List<ServicePort> ret = new ArrayList<>();
        boolean isMultiPort = Boolean.parseBoolean(getConfig(Config.multiPort));

        List<ServicePort> configuredPorts = extractPortsFromConfig();

        for (ImageConfiguration image : images) {
            Map<String, String> labels = extractLabelsFromConfig(image);
            List<String> podPorts = getPortsFromBuildConfiguration(image);
            List<String> portsFromImageLabels = getLabelWithService(labels);
            if (podPorts.isEmpty()) {
                continue;
            }

            // Extract first port and remove first element
            if(portsFromImageLabels == null || portsFromImageLabels.isEmpty()) {
                addPortIfNotNull(ret, extractPortsFromImageSpec(image.getName(), podPorts.remove(0), shiftOrNull(configuredPorts), null));
            } else {
                for (String imageLabelPort : portsFromImageLabels) {
                    addPortIfNotNull(ret, extractPortsFromImageSpec(image.getName(), podPorts.remove(0), shiftOrNull(configuredPorts), imageLabelPort));
                }
            }

            // Remaining port specs if multi-port is selected
            if (isMultiPort) {
                for (String port : podPorts) {
                    addPortIfNotNull(ret, extractPortsFromImageSpec(image.getName(), port, shiftOrNull(configuredPorts), null));
                }
            }
        }

        // If there are still ports configured add them directly
        if (isMultiPort) {
            ret.addAll(mirrorMissingTargetPorts(configuredPorts));
        } else if (ret.isEmpty() && !configuredPorts.isEmpty()) {
            ret.addAll(mirrorMissingTargetPorts(Collections.singletonList(configuredPorts.get(0))));
        }

        return ret;
    }

    private List<ServicePort> mirrorMissingTargetPorts(List<ServicePort> ports) {
        List<ServicePort> ret = new ArrayList<>();
        for (ServicePort port : ports) {
            ret.add(updateMissingTargetPort(port, port.getPort()));
        }
        return ret;
    }

    // Examine images for build configuration and extract all ports
    private List<String> getPortsFromBuildConfiguration(ImageConfiguration image) {
        // No build, no default service (doesn't make much sense to have no build config, though)
        BuildImageConfiguration buildConfig = image.getBuildConfiguration();
        if (buildConfig == null) {
            return Collections.emptyList();
        }

        return buildConfig.getPorts();
    }

    // Config can override ports
    private List<ServicePort> extractPortsFromConfig() {
        List<ServicePort> ret = new LinkedList<>();
        String ports = getConfig(Config.port);
        if (ports != null) {
            for (String port : StringUtils.split(ports, ",")) {
                ret.add(parsePortMapping(port));
            }
        }
        return ret;
    }

    private Map<String, String> extractLabelsFromConfig(ImageConfiguration imageConfiguration) {
        Map<String, String> labels = new HashMap<>();
        if(imageConfiguration.getBuildConfiguration() != null && imageConfiguration.getBuildConfiguration().getLabels() != null) {
            labels.putAll(imageConfiguration.getBuildConfiguration().getLabels());
        }
        return labels;
    }

    private List<String> getLabelWithService(Map<String, String> labels) {
        List<String> portsList = new ArrayList<>();
        for(Map.Entry<String, String> entry : labels.entrySet()) {
            if(entry.getKey().equals(PORT_IMAGE_LABEL_PREFIX)) {
                portsList.add(entry.getValue());
            } if(entry.getKey().equals(PORTS_IMAGE_LABEL_PREFIX)) {
                portsList.addAll(Arrays.asList(entry.getValue().split(",")));
            }
        }
        return portsList;
    }

    // parse config specified ports
    private ServicePort parsePortMapping(String port) {
        Matcher matcher = PORT_MAPPING_PATTERN.matcher(port);
        if (!matcher.matches()) {
            log.error("Invalid 'port' configuration '%s'. Must match <port>(:<targetPort>)?,<port2>?,...", port);
            throw new IllegalArgumentException("Invalid port mapping specification " + port);
        }

        int servicePort = Integer.parseInt(matcher.group("port"));
        String optionalTargetPort = matcher.group("targetPort");
        String protocol = getProtocol(matcher.group("protocol"));

        ServicePortBuilder builder = new ServicePortBuilder()
            .withPort(servicePort)
            .withProtocol(protocol)
            .withName(getDefaultPortName(servicePort, protocol));

        // leave empty if not set. will be filled up with the port from the image config
        if (optionalTargetPort != null) {
            builder.withNewTargetPort(Integer.parseInt(optionalTargetPort));
        }
        return builder.build();
    }

    // null ports can happen for ignored mappings
    private void addPortIfNotNull(List<ServicePort> ret, ServicePort port) {
        if (port != null) {
            ret.add(port);
        }
    }

    private ServicePort extractPortsFromImageSpec(String imageName, String portSpec, ServicePort portOverride, String targetPortFromImageLabel) {

        Matcher portMatcher = PORT_PROTOCOL_PATTERN.matcher(portSpec);
        if (!portMatcher.matches()) {
            log.warn("Invalid port specification '%s' for image %s. Must match \\d+(/(tcp|udp))?. Ignoring for now for service generation",
                     portSpec, imageName);
            return null;
        }

        Integer targetPort = Integer.parseInt(targetPortFromImageLabel != null ? targetPortFromImageLabel : portMatcher.group(1));
        String protocol = getProtocol(portMatcher.group(2));
        Integer port = checkForLegacyMapping(targetPort);

        // With a port override you can override the detected ports
        if (portOverride != null) {
            return updateMissingTargetPort(portOverride, targetPort);
        }

        return new ServicePortBuilder()
            .withPort(port)
            .withNewTargetPort(targetPort)
            .withProtocol(protocol)
            .withName(getDefaultPortName(port, protocol))
            .build();
    }

    private ServicePort updateMissingTargetPort(ServicePort port, Integer targetPort) {
        if (port.getTargetPort() == null) {
            return new ServicePortBuilder(port).withNewTargetPort(targetPort).build();
        }
        return port;
    }

    private int checkForLegacyMapping(int port) {
        return port;
    }

    private String getProtocol(String imageProtocol) {
        String protocol = imageProtocol != null ? imageProtocol : getConfig(Config.protocol);
        if ("tcp".equalsIgnoreCase(protocol) || "udp".equalsIgnoreCase(protocol)) {
            return protocol.toUpperCase();
        } else {
            throw new IllegalArgumentException(
                String.format("Invalid service protocol %s specified for enricher '%s'. Must be 'tcp' or 'udp'",
                              protocol, getName()));
        }
    }

    private String formatPortsAsList(List<ServicePort> ports)  {
        List<String> p = new ArrayList<>();
        for (ServicePort port : ports) {
            String targetPort = getPortValue(port.getTargetPort());
            String servicePort= port.getPort() != null ? Integer.toString(port.getPort()) : targetPort;
            p.add(targetPort.equals(servicePort) ? targetPort : servicePort + ":" + targetPort);
        }
        return StringUtils.join(p.iterator(), ",");
    }

    private String getPortValue(IntOrString port) {
        String val = port.getStrVal();
        if (val == null) {
            val = Integer.toString(port.getIntVal());
        }
        return val;
    }

    private String getDefaultPortName(int port, String serviceProtocol) {
        if ("TCP".equals(serviceProtocol)) {
            switch (port) {
                case 80:
                case 8080:
                case 9090:
                    return "http";
                case 443:
                case 8443:
                    return "https";
                case 8778:
                    return "jolokia";
                case 9779:
                    return "prometheus";
            }
        }

        try {
            Set<String> serviceNames = Helper.serviceNames(port, serviceProtocol.toLowerCase());
            if (serviceNames != null && !serviceNames.isEmpty()) {
                return serviceNames.iterator().next();
            } else {
                return null;
            }
        } catch (IOException e) {
            log.warn("Cannot lookup port %d/%s in IANA database: %s", port, serviceProtocol.toLowerCase(), e.getMessage());
            return null;
        }
    }

    // remove first element of list or null if list is empty
    private ServicePort shiftOrNull(List<ServicePort> ports) {
        if (!ports.isEmpty()) {
            return ports.remove(0);
        }
        return null;
    }

    // ==============================================================================================================
    // Enhance existing services
    // -------------------------

    private String getDefaultServiceName(Service defaultService) {
        String defaultServiceName = KubernetesHelper.getName(defaultService);
        if (StringUtils.isBlank(defaultServiceName)) {
            defaultServiceName = getContext().getGav().getSanitizedArtifactId();
        }
        return defaultServiceName;
    }

    // Merge services of same name with the default service
    private void addMissingServiceParts(ServiceBuilder service, Service defaultService) {

        // If service has no spec -> take over the complete spec from default service
        if (!service.hasSpec()) {
            service.withNewSpecLike(defaultService.getSpec()).endSpec();
            return;
        }

        // If service has no ports -> take over ports from default service
        List<ServicePort> ports = service.buildSpec().getPorts();
        if (ports == null || ports.isEmpty()) {
            service.editSpec().withPorts(defaultService.getSpec().getPorts()).endSpec();
            return;
        }

        // Complete missing parts:
        service.editSpec()
               .withPorts(addMissingDefaultPorts(ports, defaultService))
               .endSpec();
    }

    private String ensureServiceName(ObjectMeta serviceMetadata, ServiceBuilder service, String defaultServiceName) {
        String serviceName = KubernetesHelper.getName(serviceMetadata);
        if (StringUtils.isBlank(serviceName)) {
            service.buildMetadata().setName(defaultServiceName);
            serviceName = KubernetesHelper.getName(service.buildMetadata());
        }
        return serviceName;
    }

    private ObjectMeta ensureServiceMetadata(ServiceBuilder service, Service defaultService) {
        if (!service.hasMetadata() && defaultService != null) {
            service.withNewMetadataLike(defaultService.getMetadata()).endMetadata();
        }
        return service.buildMetadata();
    }

    private List<ServicePort> addMissingDefaultPorts(List<ServicePort> ports, Service defaultService) {

        // Ensure protocol and port names are set on the given ports
        ensurePortProtocolAndName(ports);

        // lets add at least one default port
        return tryToFindAtLeastOnePort(ports, defaultService);
    }

    private void ensurePortProtocolAndName(List<ServicePort> ports) {
        for (ServicePort port : ports) {
            String protocol = ensureProtocol(port);
            ensurePortName(port, protocol);
        }
    }

    private List<ServicePort> tryToFindAtLeastOnePort(List<ServicePort> ports, Service defaultService) {
        List<ServicePort> defaultPorts = defaultService.getSpec().getPorts();
        if (!ports.isEmpty() || defaultPorts == null || defaultPorts.isEmpty()) {
            return ports;
        }
        return Collections.singletonList(defaultPorts.get(0));
    }

    private void ensurePortName(ServicePort port, String protocol) {
        if (StringUtils.isBlank(port.getName())) {
            port.setName(getDefaultPortName(port.getPort(), getProtocol(protocol)));
        }
    }

    private String ensureProtocol(ServicePort port) {
        String protocol = port.getProtocol();
        if (StringUtils.isBlank(protocol)) {
            port.setProtocol("TCP");
            return "TCP";
        }
        return protocol;
    }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.service.openshift;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.maven.core.util.ResourceUtil;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.core.util.kubernetes.KubernetesResourceUtil;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.openshift.api.model.ImageStream;
import io.fabric8.openshift.api.model.ImageStreamBuilder;
import io.fabric8.openshift.api.model.ImageStreamSpec;
import io.fabric8.openshift.api.model.ImageStreamStatus;
import io.fabric8.openshift.api.model.NamedTagEventList;
import io.fabric8.openshift.api.model.TagEvent;
import io.fabric8.openshift.api.model.TagReference;
import io.fabric8.openshift.client.OpenShiftClient;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;

/**
 * Handler object for managing image streams
 *
 * @author roland
 * @since 16/01/17
 */
public class ImageStreamService {

    public static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private final OpenShiftClient client;
    private final Logger log;

    /**
     * Retry parameter
     */
    private final int IMAGE_STREAM_TAG_RETRIES = 15;
    private final long IMAGE_STREAM_TAG_RETRY_TIMEOUT_IN_MILLIS = 1000;

    public ImageStreamService(OpenShiftClient client, Logger log) {
        this.client = client;
        this.log = log;
    }

    /**
     * Save the images stream to a file
     * @param imageName name of the image for which the stream should be extracted
     * @param target file to store the image stream
     */
    public void appendImageStreamResource(ImageName imageName, File target) throws MojoExecutionException {
        String tag = StringUtils.isBlank(imageName.getTag()) ? "latest" : imageName.getTag();
        try {
            ImageStream is = new ImageStreamBuilder()
                    .withNewMetadata()
                    .withName(imageName.getSimpleName())
                    .endMetadata()

                    .withNewSpec()
                    .addNewTag()
                      .withName(tag)
                      .withNewFrom().withKind("ImageStreamImage").endFrom()
                    .endTag()
                    .endSpec()

                    .build();
            createOrUpdateImageStreamTag(client, imageName, is);
            appendImageStreamToFile(is, target);
            log.info("ImageStream %s written to %s", imageName.getSimpleName(), target);
        } catch (KubernetesClientException e) {
            KubernetesResourceUtil.handleKubernetesClientException(e, this.log);
        } catch (IOException e) {
            throw new MojoExecutionException(String.format("Cannot write ImageStream descriptor for %s to %s : %s",
                                                           imageName.getFullName(), target.getAbsoluteFile(), e.getMessage()),e);
        }
    }

    private void appendImageStreamToFile(ImageStream is, File target) throws MojoExecutionException, IOException {

        Map<String, ImageStream> imageStreams = readAlreadyExtractedImageStreams(target);
        // Override with given image stream
        imageStreams.put(is.getMetadata().getName(),is);
        KubernetesList isList =
            new KubernetesListBuilder().withItems(new ArrayList<HasMetadata>(imageStreams.values())).build();
        ResourceUtil.save(target, isList);
    }

    private Map<String, ImageStream> readAlreadyExtractedImageStreams(File target) throws IOException {
        // If it already exists, read in the file and use it for update
        Map<String, ImageStream> imageStreams = new HashMap<>();
        if (target.length() > 0) {
            for (HasMetadata entity : KubernetesResourceUtil.loadResources(target)) {
                if ("ImageStream".equals(KubernetesHelper.getKind(entity))) {
                    imageStreams.put(entity.getMetadata().getName(), (ImageStream) entity);
                }
                // Ignore all other kind of entities. There shouldn't be any included anyway
            }
        }
        return imageStreams;
    }

    private void createOrUpdateImageStreamTag(OpenShiftClient client, ImageName image, ImageStream is) throws MojoExecutionException {
        String namespace = client.getNamespace();
        String tagSha = findTagSha(client, image.getSimpleName(), client.getNamespace());
        String name = image.getSimpleName() + "@" + tagSha;

        TagReference tag = extractTag(is);
        ObjectReference from = extractFrom(tag);

        if (!Objects.equals(image.getTag(), tag.getName())) {
            tag.setName(image.getTag());
        }
        if (!Objects.equals("ImageStreamImage", from.getKind())) {
            from.setKind("ImageStreamImage");
        }
        if (!Objects.equals(namespace, from.getNamespace())) {
            from.setNamespace(namespace);
        }
        if (!Objects.equals(name, from.getName())) {
            from.setName(name);
        }
    }

    private ObjectReference extractFrom(TagReference tag) {
        ObjectReference from = tag.getFrom();
        if (from == null) {
            from = new ObjectReference();
            tag.setFrom(from);
        }
        return from;
    }

    private TagReference extractTag(ImageStream is) {
        ImageStreamSpec spec = is.getSpec();
        if (spec == null) {
            spec = new ImageStreamSpec();
            is.setSpec(spec);
        }
        List<TagReference> tags = spec.getTags();
        if (tags == null) {
            tags = new ArrayList<>();
            spec.setTags(tags);
        }
        TagReference tag = null;
        if (tags.isEmpty()) {
            tag = new TagReference();
            tags.add(tag);
        } else {
            tag = tags.get(tags.size() - 1);
        }
        return tag;
    }

    private String findTagSha(OpenShiftClient client, String imageStreamName, String namespace) throws MojoExecutionException {
        ImageStream currentImageStream = null;

        for (int i = 0; i < IMAGE_STREAM_TAG_RETRIES; i++) {
            if (i > 0) {
                log.info("Retrying to find tag on ImageStream %s", imageStreamName);
                try {
                    Thread.sleep(IMAGE_STREAM_TAG_RETRY_TIMEOUT_IN_MILLIS);
                } catch (InterruptedException e) {
                    log.debug("interrupted", e);
                }
            }
            currentImageStream = client.imageStreams().withName(imageStreamName).get();
            if (currentImageStream == null) {
                continue;
            }
            ImageStreamStatus status = currentImageStream.getStatus();
            if (status == null) {
                continue;
            }
            List<NamedTagEventList> tags = status.getTags();
            if (tags == null || tags.isEmpty()) {
                continue;
            }

            // Iterate all imagestream tags and get the latest one by 'created' attribute
            TagEvent latestTag = null;

            TAG_EVENT_LIST:
            for (NamedTagEventList list : tags) {
                List<TagEvent> items = list.getItems();
                if (items == null || items.isEmpty()) {
                    continue TAG_EVENT_LIST;
                }

                for (TagEvent tag : items) {
                    latestTag = latestTag == null ? tag : newerTag(tag, latestTag);
                }
            }

            if (latestTag != null && StringUtils.isNotBlank(latestTag.getImage())) {
                String image = latestTag.getImage();
                log.info("Found tag on ImageStream " + imageStreamName + " tag: " + image);
                return image;
            }
        }

        // No image found, even after several retries:
        if (currentImageStream == null) {
            throw new MojoExecutionException("Could not find a current ImageStream with name " + imageStreamName + " in namespace " + namespace);
        } else {
            throw new MojoExecutionException("Could not find a tag in the ImageStream " + imageStreamName);
        }
    }

    public TagEvent newerTag(TagEvent tag1, TagEvent tag2) {
        Date tag1Date = extractDate(tag1);
        Date tag2Date = extractDate(tag2);

        if(tag1Date == null) {
            return tag2;
        }

        if(tag2Date == null) {
            return tag1;
        }

        return tag1Date.compareTo(tag2Date) > 0 ? tag1 : tag2;
    }

    private Date extractDate(TagEvent tag) {
        try {
            return new SimpleDateFormat(DATE_FORMAT).parse(tag.getCreated());
        } catch (ParseException e) {
            log.error("parsing date error : " + e.getMessage(), e);
            return null;
        } catch (NullPointerException e) {
            log.error("tag date is null : " + e.getMessage(), e);
            return null;
        }

    }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.util.kubernetes;

import java.util.List;

import com.google.common.base.Objects;
import io.fabric8.kubernetes.api.model.IntOrString;
import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
import io.fabric8.kubernetes.api.model.LoadBalancerStatus;
import io.fabric8.kubernetes.api.model.Node;
import io.fabric8.kubernetes.api.model.NodeAddress;
import io.fabric8.kubernetes.api.model.NodeList;
import io.fabric8.kubernetes.api.model.NodeSpec;
import io.fabric8.kubernetes.api.model.NodeStatus;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServiceStatus;
import io.fabric8.kubernetes.api.model.extensions.HTTPIngressPath;
import io.fabric8.kubernetes.api.model.extensions.HTTPIngressRuleValue;
import io.fabric8.kubernetes.api.model.extensions.Ingress;
import io.fabric8.kubernetes.api.model.extensions.IngressBackend;
import io.fabric8.kubernetes.api.model.extensions.IngressList;
import io.fabric8.kubernetes.api.model.extensions.IngressRule;
import io.fabric8.kubernetes.api.model.extensions.IngressSpec;
import io.fabric8.kubernetes.api.model.extensions.IngressTLS;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.openshift.api.model.Route;
import io.fabric8.openshift.client.OpenShiftClient;
import org.apache.commons.lang3.StringUtils;

/**
 * @author roland
 * @since 23.05.17
 */
public class ServiceUrlUtil {

    private static final String DEFAULT_PROTO = "tcp";
    private static final String HOST_SUFFIX = "_SERVICE_HOST";
    private static final String PORT_SUFFIX = "_SERVICE_PORT";
    private static final String PROTO_SUFFIX = "_TCP_PROTO";

    /**
     * Returns the URL to access the service; using the environment variables, routes
     * or service clusterIP address
     *
     * @throws IllegalArgumentException if the URL cannot be found for the serviceName and namespace
     */
    public static String getServiceURL(KubernetesClient client, String serviceName, String serviceNamespace, String serviceProtocol, boolean serviceExternal) {
        Service srv = null;
        String serviceHost = serviceToHostOrBlank(serviceName);
        String servicePort = serviceToPortOrBlank(serviceName);
        String serviceProto = serviceProtocol != null ? serviceProtocol : serviceToProtocol(serviceName, servicePort);

        //Use specified or fallback namespace.
        String actualNamespace = StringUtils.isNotBlank(serviceNamespace) ? serviceNamespace : client.getNamespace();

        //1. Inside Kubernetes: Services as ENV vars
        if (!serviceExternal && StringUtils.isNotBlank(serviceHost) && StringUtils.isNotBlank(servicePort) && StringUtils.isNotBlank(serviceProtocol)) {
            return serviceProtocol + "://" + serviceHost + ":" + servicePort;
            //2. Anywhere: When namespace is passed System / Env var. Mostly needed for integration tests.
        } else if (StringUtils.isNotBlank(actualNamespace)) {
            srv = client.services().inNamespace(actualNamespace).withName(serviceName).get();
        }

        if (srv == null) {
            // lets try use environment variables
            String hostAndPort = getServiceHostAndPort(serviceName, "", "");
            if (!hostAndPort.startsWith(":")) {
                return serviceProto + "://" + hostAndPort;
            }
        }
        if (srv == null) {
            throw new IllegalArgumentException("No kubernetes service could be found for name: " + serviceName + " in namespace: " + actualNamespace);
        }

        String answer = KubernetesHelper.getOrCreateAnnotations(srv).get(Fabric8Annotations.SERVICE_EXPOSE_URL.toString());
        if (StringUtils.isNotBlank(answer)) {
            return answer;
        }

        if (OpenshiftHelper.isOpenShift(client)) {
            OpenShiftClient openShiftClient = client.adapt(OpenShiftClient.class);
            Route route = openShiftClient.routes().inNamespace(actualNamespace).withName(serviceName).get();
            if (route != null) {
                return (serviceProto + "://" + route.getSpec().getHost()).toLowerCase();
            }
        }

        ServicePort port = findServicePortByName(srv, null);
        if (port == null) {
            throw new RuntimeException("Couldn't find port: " + null + " for service:" + serviceName);
        }

        String clusterIP = srv.getSpec().getClusterIP();
        if ("None".equals(clusterIP)) {
            throw new IllegalStateException("Service: " + serviceName + " in namespace:" + serviceNamespace + "is head-less. Search for endpoints instead.");
        }

        Integer portNumber = port.getPort();
        if (StringUtils.isBlank(clusterIP)) {
            IngressList ingresses = client.extensions().ingresses().inNamespace(serviceNamespace).list();
            if (ingresses != null) {
                List<Ingress> items = ingresses.getItems();
                if (items != null) {
                    for (Ingress item : items) {
                        String ns = KubernetesHelper.getNamespace(item);
                        if (Objects.equal(serviceNamespace, ns)) {
                            IngressSpec spec = item.getSpec();
                            if (spec != null) {
                                List<IngressRule> rules = spec.getRules();
                                List<IngressTLS> tls = spec.getTls();
                                if (rules != null) {
                                    for (IngressRule rule : rules) {
                                        HTTPIngressRuleValue http = rule.getHttp();
                                        if (http != null) {
                                            List<HTTPIngressPath> paths = http.getPaths();
                                            if (paths != null) {
                                                for (HTTPIngressPath path : paths) {
                                                    IngressBackend backend = path.getBackend();
                                                    if (backend != null) {
                                                        String backendServiceName = backend.getServiceName();
                                                        if (serviceName.equals(backendServiceName) && portsMatch(port, backend.getServicePort())) {
                                                            String pathPostfix = path.getPath();
                                                            if (tls != null) {
                                                                for (IngressTLS tlsHost : tls) {
                                                                    List<String> hosts = tlsHost.getHosts();
                                                                    if (hosts != null) {
                                                                        for (String host : hosts) {
                                                                            if (StringUtils.isNotBlank(host)) {
                                                                                return String.format("https://%s/%s", host, preparePath(pathPostfix));
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                            answer = rule.getHost();
                                                            if (StringUtils.isNotBlank(answer)) {
                                                                return String.format("http://%s/%s",answer, preparePath(pathPostfix));
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // lets try use the status on GKE
            ServiceStatus status = srv.getStatus();
            if (status != null) {
                LoadBalancerStatus loadBalancerStatus = status.getLoadBalancer();
                if (loadBalancerStatus != null) {
                    List<LoadBalancerIngress> loadBalancerIngresses = loadBalancerStatus.getIngress();
                    if (loadBalancerIngresses != null) {
                        for (LoadBalancerIngress loadBalancerIngress : loadBalancerIngresses) {
                            String ip = loadBalancerIngress.getIp();
                            if (StringUtils.isNotBlank(ip)) {
                                clusterIP = ip;
                                break;
                            }
                        }
                    }
                }
            }
        }

        if (StringUtils.isBlank(clusterIP)) {
            // on vanilla kubernetes we can use nodePort to access things externally
            boolean found = false;
            Integer nodePort = port.getNodePort();
            if (nodePort != null) {
                NodeList nodeList = client.nodes().list();
                if (nodeList != null) {
                    List<Node> items = nodeList.getItems();
                    if (items != null) {
                        for (Node item : items) {
                            NodeStatus status = item.getStatus();
                            if (!found && status != null) {
                                List<NodeAddress> addresses = status.getAddresses();
                                if (addresses != null) {
                                    for (NodeAddress address : addresses) {
                                        String ip = address.getAddress();
                                        if (StringUtils.isNotBlank(ip)) {
                                            clusterIP = ip;
                                            portNumber = nodePort;
                                            found = true;
                                            break;
                                        }

                                    }

                                }
                            }
                            if (!found) {
                                NodeSpec spec = item.getSpec();
                                if (spec != null) {
                                    clusterIP = spec.getExternalID();
                                    if (StringUtils.isNotBlank(clusterIP)) {
                                        portNumber = nodePort;
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return (serviceProto + "://" + clusterIP + ":" + portNumber).toLowerCase();
    }

    /**
     * Returns true if the given servicePort matches the intOrString value
     */
    private static boolean portsMatch(ServicePort servicePort, IntOrString intOrString) {
        if (intOrString != null) {
            Integer port = servicePort.getPort();
            Integer intVal = intOrString.getIntVal();
            String strVal = intOrString.getStrVal();
            if (intVal != null) {
                if (port != null) {
                    return port.intValue() == intVal.intValue();
                } else {
                    /// should we find the port by name now?
                }
            } else if (strVal != null ){
                return Objects.equal(strVal, servicePort.getName());
            }
        }
        return false;
    }

    private static String preparePath(String path) {
        if (StringUtils.isBlank(path)) {
            return "";
        }
        String ret = path;
        while (ret.startsWith("/")) {
            ret = ret.substring(1);
        }
        return ret;
    }

    private static ServicePort findServicePortByName(Service service, String portName) {
        if (StringUtils.isBlank(portName)) {
            return service.getSpec().getPorts().iterator().next();
        }

        for (ServicePort servicePort : service.getSpec().getPorts()) {
            if (Objects.equal(servicePort.getName(), portName)) {
                return servicePort;
            }
        }
        return null;
    }

    /**
     * Returns the service host name or a blank string if it could not be resolved
     */
    private static String serviceToHostOrBlank(String serviceName) {
        return getEnvVarOrSystemProperty(toServiceHostEnvironmentVariable(serviceName), "");
    }

    private static String serviceToProtocol(String serviceName, String servicePort) {
        return getEnvVarOrSystemProperty(toEnvVariable(serviceName + PORT_SUFFIX + "_" + servicePort + PROTO_SUFFIX), DEFAULT_PROTO);
    }

    /**
     * Returns the named port for the given service name or blank
     */
    private static String serviceToPortOrBlank(String serviceName) {
        String envVarName = toServicePortEnvironmentVariable(serviceName);
        return getEnvVarOrSystemProperty(envVarName, "");
    }

        /**
     * Returns the kubernetes environment variable name for the service host for the given service name
     */
    private static String toServiceHostEnvironmentVariable(String serviceName) {
        return toEnvVariable(serviceName + HOST_SUFFIX);
    }

    private static String toServicePortEnvironmentVariable(String serviceName) {
        String name = serviceName + PORT_SUFFIX;
        return toEnvVariable(name);
    }

    private static String toEnvVariable(String serviceName) {
        return serviceName.toUpperCase().replaceAll("-", "_");
    }

        /**
     * Returns the service host and port for the given environment variable name.
     *
     * @param serviceName the name of the service which is used as a prefix to access the ${serviceName}_SERVICE_HOST and ${serviceName}_SERVICE_PORT environment variables to find the hos and port
     * @param defaultHost the default host to use if not injected via an environment variable (e.g. localhost)
     * @parma defaultPort the default port to use to connect to the service if there is not an environment variable defined
     */
    private static String getServiceHostAndPort(String serviceName, String defaultHost, String defaultPort) {
        String serviceEnvVarPrefix = getServiceEnvVarPrefix(serviceName);
        String hostEnvVar = serviceEnvVarPrefix + "_HOST";
        String portEnvVar = serviceEnvVarPrefix + "_PORT";

        String host = getEnvVarOrSystemProperty(hostEnvVar, hostEnvVar, defaultHost);
        String port = getEnvVarOrSystemProperty(portEnvVar, portEnvVar, defaultPort);

        String answer = host + ":" + port;
        return answer;
    }

    private static String getServiceEnvVarPrefix(String serviceName) {
        return serviceName.toUpperCase().replace('-', '_') + "_SERVICE";
    }

    private static String getEnvVarOrSystemProperty(String envVarName, String systemProperty, String defaultValue) {
        String answer = null;
        answer = System.getenv(envVarName);
        if (StringUtils.isBlank(answer)) {
            answer = System.getProperty(systemProperty, defaultValue);
        }
        if (StringUtils.isNotBlank(answer)) {
            return answer;
        } else {
            return defaultValue;
        }
    }

    private static String getEnvVarOrSystemProperty(String envVarName, String defaultValue) {
        return getEnvVarOrSystemProperty(envVarName,envVarName,defaultValue);
    }
}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.util;

import java.util.HashMap;
import java.util.Map;

/**
 */
public class MapUtil {

    private MapUtil() {}

    /**
     * Adds the given key and value pair into the map if the map does not already contain a value for that key
     */
    public static void putIfAbsent(Map<String, String> map, String name, String value) {
        if (!map.containsKey(name)) {
            map.put(name, value);
        }
    }

    /**
     * Add all values of a map to another map, but onlfy if not already existing.
     * @param map target map
     * @param toMerge the values to ad
     */
    public static void mergeIfAbsent(Map<String, String> map, Map<String, String> toMerge) {
        for (Map.Entry<String, String> entry : toMerge.entrySet()) {
            putIfAbsent(map, entry.getKey(), entry.getValue());;
        }
    }

    /**
     * Returns a new map with all the entries of map1 and any from map2 which don't override map1.
     *
     * Can handle either maps being null. Always returns a new mutable map
     */
    public static <K,V> Map<K,V> mergeMaps(Map<K, V> map1, Map<K, V> map2) {
        Map<K, V> answer = new HashMap<>();
        if (map2 != null) {
            answer.putAll(map2);
        }
        if (map1 != null) {
            answer.putAll(map1);
        }
        return answer;

    }

    /**
     * Copies all of the elements i.e., the mappings, from toPut map into ret, if toPut isn't null.
     * @param ret
     * @param toPut
     */
    public static void putAllIfNotNull(Map<String, String> ret, Map<String, String> toPut) {
        if (toPut != null) {
            ret.putAll(toPut);
        }
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.enricher.standard;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import io.fabric8.kubernetes.api.builder.TypedVisitor;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecFluent;
import io.fabric8.kubernetes.api.model.ReplicationControllerBuilder;
import io.fabric8.kubernetes.api.model.ReplicationControllerFluent;
import io.fabric8.kubernetes.api.model.ReplicationControllerSpecFluent;
import io.fabric8.kubernetes.api.model.apps.DaemonSetBuilder;
import io.fabric8.kubernetes.api.model.apps.DaemonSetFluent;
import io.fabric8.kubernetes.api.model.apps.DaemonSetSpecFluent;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentFluent;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecFluent;
import io.fabric8.kubernetes.api.model.apps.ReplicaSetBuilder;
import io.fabric8.kubernetes.api.model.apps.ReplicaSetFluent;
import io.fabric8.kubernetes.api.model.apps.ReplicaSetSpecFluent;
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.fabric8.kubernetes.api.model.apps.StatefulSetFluent;
import io.fabric8.kubernetes.api.model.apps.StatefulSetSpecFluent;
import io.fabric8.maven.core.config.PlatformMode;
import io.fabric8.maven.core.config.ResourceConfig;
import io.fabric8.maven.core.util.Configs;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.enricher.api.BaseEnricher;
import io.fabric8.maven.enricher.api.MavenEnricherContext;
import io.fabric8.openshift.api.model.DeploymentConfigBuilder;
import io.fabric8.openshift.api.model.DeploymentConfigFluent;
import io.fabric8.openshift.api.model.DeploymentConfigSpecFluent;
import org.apache.commons.lang3.StringUtils;

import static io.fabric8.maven.core.util.kubernetes.KubernetesResourceUtil.extractContainerName;

/**
 * Merge in image configuration like the image name into ReplicaSet and ReplicationController's
 * Pod specification.
 *
 *

 *
The full image name is set as image

 *
An image alias is set as name

 *
The pull policy imagePullPolicy is set according to the given configuration. If no
 *         configuration is set, the default is "IfNotPresent" for release versions, and "Always" for snapshot versions

 *

 *
 * Any already configured container in the pod spec is updated if the property is not set.
 *
 * @author roland
 * @since 25/05/16
 */
public class ImageEnricher extends BaseEnricher {

    public ImageEnricher(MavenEnricherContext buildContext) {
        super(buildContext, "fmp-image");
    }

    // Available configuration keys
    private enum Config implements Configs.Key {
        // What pull policy to use when fetching images
        pullPolicy;

        public String def() { return d; } protected String d;
    }

    @Override
    public void create(PlatformMode platformMode, KubernetesListBuilder builder) {
        if (!hasImageConfiguration()) {
            log.verbose("No images resolved. Skipping ...");
            return;
        }

        // Ensure that all contoller have template specs
        ensureTemplateSpecs(builder);

        // Update containers in template specs
        updateContainers(builder);
    }

    // ============================================================================================================

    private void ensureTemplateSpecs(KubernetesListBuilder builder) {
        ensureTemplateSpecsInReplicationControllers(builder);
        ensureTemplateSpecsInRelicaSet(builder);
        ensureTemplateSpecsInDeployments(builder);
        ensureTemplateSpecsInDaemonSet(builder);
        ensureTemplateSpecsInStatefulSet(builder);
        ensureTemplateSpecsInDeploymentConfig(builder);
    }

    private void ensureTemplateSpecsInReplicationControllers(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<ReplicationControllerBuilder>() {
            @Override
            public void visit(ReplicationControllerBuilder item) {
                ReplicationControllerFluent.SpecNested<ReplicationControllerBuilder> spec =
                    item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                ReplicationControllerSpecFluent.TemplateNested<ReplicationControllerFluent.SpecNested<ReplicationControllerBuilder>>
                    template =
                    spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    private void ensureTemplateSpecsInRelicaSet(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<ReplicaSetBuilder>() {
            @Override
            public void visit(ReplicaSetBuilder item) {
                ReplicaSetFluent.SpecNested<ReplicaSetBuilder> spec =
                    item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                ReplicaSetSpecFluent.TemplateNested<ReplicaSetFluent.SpecNested<ReplicaSetBuilder>> template =
                    spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    private void ensureTemplateSpecsInDeployments(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<DeploymentBuilder>() {
            @Override
            public void visit(DeploymentBuilder item) {
                DeploymentFluent.SpecNested<DeploymentBuilder> spec =
                    item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                DeploymentSpecFluent.TemplateNested<DeploymentFluent.SpecNested<DeploymentBuilder>> template =
                    spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    private void ensureTemplateSpecsInDaemonSet(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<DaemonSetBuilder>() {
            @Override
            public void visit(DaemonSetBuilder item) {
                DaemonSetFluent.SpecNested<DaemonSetBuilder> spec =
                        item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                DaemonSetSpecFluent.TemplateNested<DaemonSetFluent.SpecNested<DaemonSetBuilder>> template =
                        spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    private void ensureTemplateSpecsInStatefulSet(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<StatefulSetBuilder>() {
            @Override
            public void visit(StatefulSetBuilder item) {
                StatefulSetFluent.SpecNested<StatefulSetBuilder> spec =
                        item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                StatefulSetSpecFluent.TemplateNested<StatefulSetFluent.SpecNested<StatefulSetBuilder>> template =
                        spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    private void ensureTemplateSpecsInDeploymentConfig(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<DeploymentConfigBuilder>() {
            @Override
            public void visit(DeploymentConfigBuilder item) {
                DeploymentConfigFluent.SpecNested<DeploymentConfigBuilder> spec =
                        item.getSpec() == null ? item.withNewSpec() : item.editSpec();
                DeploymentConfigSpecFluent.TemplateNested<DeploymentConfigFluent.SpecNested<DeploymentConfigBuilder>> template =
                        spec.getTemplate() == null ? spec.withNewTemplate() : spec.editTemplate();
                template.endTemplate().endSpec();
            }
        });
    }

    // ============================================================================================================

    private void updateContainers(KubernetesListBuilder builder) {
        builder.accept(new TypedVisitor<PodTemplateSpecBuilder>() {
            @Override
            public void visit(PodTemplateSpecBuilder templateBuilder) {
                PodTemplateSpecFluent.SpecNested<PodTemplateSpecBuilder> podSpec =
                    templateBuilder.getSpec() == null ? templateBuilder.withNewSpec() : templateBuilder.editSpec();

                List<Container> containers = podSpec.getContainers();
                if (containers == null) {
                    containers = new ArrayList<Container>();
                }
                mergeImageConfigurationWithContainerSpec(containers);
                podSpec.withContainers(containers).endSpec();
            }
        });
    }

    // Add missing information to the given containers as found
    // configured
    private void mergeImageConfigurationWithContainerSpec(List<Container> containers) {
        getImages().ifPresent(images -> {
            int idx = 0;
            for (ImageConfiguration image : images) {
                Container container = getContainer(idx, containers);
                mergeImagePullPolicy(image, container);
                mergeImage(image, container);
                mergeContainerName(image, container);
                mergeEnvVariables(container);
                idx++;
            }
        });
    }

    private Container getContainer(int idx, List<Container> containers) {
        Container container;
        if (idx < containers.size()) {
            container = containers.get(idx);
        } else {
            // Pad with new containers if missing
            container = new Container();
            containers.add(container);
        }
        return container;
    }

    private void mergeContainerName(ImageConfiguration imageConfiguration, Container container) {
        if (StringUtils.isBlank(container.getName())) {
            String containerName = extractContainerName(getContext().getGav(), imageConfiguration);
            log.verbose("Setting container name %s",containerName);
            container.setName(containerName);
        }
    }

    private void mergeImage(ImageConfiguration imageConfiguration, Container container) {
        if (StringUtils.isBlank(container.getImage())) {
            String prefix = "";
            if (StringUtils.isNotBlank(imageConfiguration.getRegistry())) {
                log.verbose("Using registry %s for the image", imageConfiguration.getRegistry());
                prefix = imageConfiguration.getRegistry() + "/";
            }
            String imageFullName = prefix + imageConfiguration.getName();
            log.verbose("Setting image %s", imageFullName);
            container.setImage(imageFullName);
        }
    }

    private void mergeImagePullPolicy(ImageConfiguration imageConfiguration, Container container) {
        if (StringUtils.isBlank(container.getImagePullPolicy())) {
            String policy = getConfig(Config.pullPolicy);
            if (policy == null) {
                policy = "IfNotPresent";
                String imageName = imageConfiguration.getName();
                if (StringUtils.isNotBlank(imageName) && imageName.endsWith(":latest")) {
                    policy = "Always";
                }
            }
            container.setImagePullPolicy(policy);
        }
    }

    private void mergeEnvVariables(Container container) {
        getConfiguration().getResource().flatMap(ResourceConfig::getEnv).ifPresent(resourceEnv -> {
            List<EnvVar> containerEnvVars = container.getEnv();
            if (containerEnvVars == null) {
                containerEnvVars = new LinkedList<>();
                container.setEnv(containerEnvVars);
            }

            for (Map.Entry<String, String> resourceEnvEntry : resourceEnv.entrySet()) {
                EnvVar newEnvVar =
                    new EnvVarBuilder()
                        .withName(resourceEnvEntry.getKey())
                        .withValue(resourceEnvEntry.getValue())
                        .build();
                if (!hasEnvWithName(containerEnvVars, newEnvVar.getName())) {
                    containerEnvVars.add(newEnvVar);
                } else {
                    log.warn(
                        "Environment variable %s will not be overridden: trying to set the value %s, but its actual value is %s",
                        newEnvVar.getName(), newEnvVar.getValue(), getEnvValue(containerEnvVars, newEnvVar.getName()));
                }
            }
        });
    }

    private String getEnvValue(List<EnvVar> envVars, String name) {
        for (EnvVar var : envVars) {
            if (var.getName().equals(name)) {
                return var.getValue();
            }
        }
        return "(not found)";
    }

    private boolean hasEnvWithName(List<EnvVar> envVars, String name) {
        return envVars.stream().anyMatch(e -> e.getName().equals(name));
    }

}


/**
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you 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 io.fabric8.maven.core.service.openshift;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
import io.fabric8.kubernetes.api.model.LocalObjectReferenceBuilder;
import io.fabric8.kubernetes.api.model.ObjectReference;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.Status;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.Watcher;
import io.fabric8.kubernetes.client.dsl.LogWatch;
import io.fabric8.maven.core.config.OpenShiftBuildStrategy;
import io.fabric8.maven.core.service.BuildService;
import io.fabric8.maven.core.service.Fabric8ServiceException;
import io.fabric8.maven.core.util.IoUtil;
import io.fabric8.maven.core.util.ResourceFileType;
import io.fabric8.maven.core.util.kubernetes.KubernetesClientUtil;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.core.util.kubernetes.KubernetesResourceUtil;
import io.fabric8.maven.core.util.kubernetes.OpenshiftHelper;
import io.fabric8.maven.docker.access.AuthConfig;
import io.fabric8.maven.docker.assembly.ArchiverCustomizer;
import io.fabric8.maven.docker.assembly.DockerAssemblyManager;
import io.fabric8.maven.docker.config.AssemblyConfiguration;
import io.fabric8.maven.docker.config.BuildImageConfiguration;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.service.RegistryService;
import io.fabric8.maven.docker.service.ServiceHub;
import io.fabric8.maven.docker.util.AuthConfigFactory;
import io.fabric8.maven.docker.util.DockerFileUtil;
import io.fabric8.maven.docker.util.EnvUtil;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.docker.util.Logger;
import io.fabric8.openshift.api.model.Build;
import io.fabric8.openshift.api.model.BuildConfig;
import io.fabric8.openshift.api.model.BuildConfigSpec;
import io.fabric8.openshift.api.model.BuildOutput;
import io.fabric8.openshift.api.model.BuildOutputBuilder;
import io.fabric8.openshift.api.model.BuildSource;
import io.fabric8.openshift.api.model.BuildStrategy;
import io.fabric8.openshift.api.model.BuildStrategyBuilder;
import io.fabric8.openshift.client.OpenShiftClient;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.MojoExecutionException;

/**
 * @author nicola
 * @since 21/02/17
 */
public class OpenshiftBuildService implements BuildService {

    private final OpenShiftClient client;
    private final Logger log;
    private ServiceHub dockerServiceHub;
    private BuildServiceConfig config;
    private RegistryService.RegistryConfig registryConfig;
    private AuthConfigFactory authConfigFactory;

    public OpenshiftBuildService(OpenShiftClient client, Logger log, ServiceHub dockerServiceHub, BuildServiceConfig config) {
        Objects.requireNonNull(client, "client");
        Objects.requireNonNull(log, "log");
        Objects.requireNonNull(dockerServiceHub, "dockerServiceHub");
        Objects.requireNonNull(config, "config");

        this.client = client;
        this.log = log;
        this.dockerServiceHub = dockerServiceHub;
        this.config = config;
    }

    @Override
    public void build(ImageConfiguration imageConfig) throws Fabric8ServiceException {
        String buildName = null;
        try {
            ImageName imageName = new ImageName(imageConfig.getName());

            File dockerTar = createBuildArchive(imageConfig);

            KubernetesListBuilder builder = new KubernetesListBuilder();

            // Check for buildconfig / imagestream / pullSecret and create them if necessary
            String openshiftPullSecret = config.getOpenshiftPullSecret();
            Boolean usePullSecret = checkOrCreatePullSecret(config, client, builder, openshiftPullSecret, imageConfig);
            if (usePullSecret) {
                buildName = updateOrCreateBuildConfig(config, client, builder, imageConfig, openshiftPullSecret);
            } else {
                buildName = updateOrCreateBuildConfig(config, client, builder, imageConfig, null);
            }

            checkOrCreateImageStream(config, client, builder, getImageStreamName(imageName));
            applyResourceObjects(config, client, builder);

            // Start the actual build
            Build build = startBuild(client, dockerTar, buildName);

            // Wait until the build finishes
            waitForOpenShiftBuildToComplete(client, build);

            // Create a file with generated image streams
            addImageStreamToFile(getImageStreamFile(config), imageName, client);
        } catch (Fabric8ServiceException e) {
            throw e;
        } catch (Exception ex) {
            // Log additional details in case of any IOException
            if (ex != null && ex.getCause() instanceof IOException) {
                log.error("Build for %s failed: %s", buildName, ex.getCause().getMessage());
                logBuildFailure(client, buildName);
            } else {
                throw new Fabric8ServiceException("Unable to build the image using the OpenShift build service", ex);
            }
        }
    }

    protected File createBuildArchive(ImageConfiguration imageConfig) throws Fabric8ServiceException {
        // Adding S2I artifacts such as environment variables in S2I mode
        ArchiverCustomizer customizer = getS2ICustomizer(imageConfig);

        try {
            // Create tar file with Docker archive
            File dockerTar;
            if (customizer != null) {
                dockerTar = dockerServiceHub.getArchiveService().createDockerBuildArchive(imageConfig, config.getDockerMojoParameters(), customizer);
            } else {
                dockerTar = dockerServiceHub.getArchiveService().createDockerBuildArchive(imageConfig, config.getDockerMojoParameters());
            }
            return dockerTar;
        } catch (MojoExecutionException e) {
            throw new Fabric8ServiceException("Unable to create the build archive", e);
        }
    }

    private ArchiverCustomizer getS2ICustomizer(ImageConfiguration imageConfiguration) throws Fabric8ServiceException {
        try {
            if (imageConfiguration.getBuildConfiguration() != null && imageConfiguration.getBuildConfiguration().getEnv() != null) {
                String fileName = IoUtil.sanitizeFileName("s2i-env-" + imageConfiguration.getName());
                final File environmentFile = new File(config.getBuildDirectory(), fileName);

                try (PrintWriter out = new PrintWriter(new FileWriter(environmentFile))) {
                    for (Map.Entry<String, String> e : imageConfiguration.getBuildConfiguration().getEnv().entrySet()) {
                        out.println(e.getKey() + "=" + e.getValue());
                    }
                }

                return tarArchiver -> {
                    tarArchiver.addFile(environmentFile, ".s2i/environment");
                    return tarArchiver;
                };
            } else {
                return null;
            }
        } catch (IOException e) {
            throw new Fabric8ServiceException("Unable to add environment variables to the S2I build archive", e);
        }
    }

    private File getImageStreamFile(BuildServiceConfig config) {
        return ResourceFileType.yaml.addExtensionIfMissing(new File(config.getBuildDirectory(), String.format("%s-is", config.getArtifactId())));
    }

    @Override
    public void postProcess(BuildServiceConfig config) {
        config.attachArtifact("is", getImageStreamFile(config));
    }

    private String updateOrCreateBuildConfig(BuildServiceConfig config, OpenShiftClient client, KubernetesListBuilder builder, ImageConfiguration imageConfig, String openshiftPullSecret) {
        ImageName imageName = new ImageName(imageConfig.getName());
        String buildName = getS2IBuildName(config, imageName);
        String imageStreamName = getImageStreamName(imageName);
        String outputImageStreamTag = imageStreamName + ":" + (imageName.getTag() != null ? imageName.getTag() : "latest");

        BuildStrategy buildStrategyResource = createBuildStrategy(imageConfig, config.getOpenshiftBuildStrategy(), openshiftPullSecret);
        BuildOutput buildOutput = new BuildOutputBuilder().withNewTo()
                .withKind("ImageStreamTag")
                .withName(outputImageStreamTag)
                .endTo().build();

        // Fetch existing build config
        BuildConfig buildConfig = client.buildConfigs().withName(buildName).get();
        if (buildConfig != null) {
            // lets verify the BC
            BuildConfigSpec spec = getBuildConfigSpec(buildConfig);
            validateSourceType(buildName, spec);

            if (config.getBuildRecreateMode().isBuildConfig()) {
                // Delete and recreate afresh
                client.buildConfigs().withName(buildName).delete();
                return createBuildConfig(builder, buildName, buildStrategyResource, buildOutput);
            } else {
                // Update & return
                return updateBuildConfig(client, buildName, buildStrategyResource, buildOutput, spec);
            }
        } else {
            // Create afresh
            return createBuildConfig(builder, buildName, buildStrategyResource, buildOutput);
        }
    }

    private void validateSourceType(String buildName, BuildConfigSpec spec) {
        BuildSource source = spec.getSource();
        if (source != null) {
            String sourceType = source.getType();
            if (!Objects.equals("Binary", sourceType)) {
                log.warn("BuildServiceConfig %s is not of type: 'Binary' but is '%s' !", buildName, sourceType);
            }
        }
    }

    private BuildConfigSpec getBuildConfigSpec(BuildConfig buildConfig) {
        BuildConfigSpec spec = buildConfig.getSpec();
        if (spec == null) {
            spec = new BuildConfigSpec();
            buildConfig.setSpec(spec);
        }
        return spec;
    }

    private String createBuildConfig(KubernetesListBuilder builder, String buildName, BuildStrategy buildStrategyResource, BuildOutput buildOutput) {
        log.info("Creating BuildServiceConfig %s for %s build", buildName, buildStrategyResource.getType());
        builder.addNewBuildConfigItem()
                .withNewMetadata()
                .withName(buildName)
                .endMetadata()
                .withNewSpec()
                .withNewSource()
                .withType("Binary")
                .endSource()
                .withStrategy(buildStrategyResource)
                .withOutput(buildOutput)
                .endSpec()
                .endBuildConfigItem();
        return buildName;
    }

    private String updateBuildConfig(OpenShiftClient client, String buildName, BuildStrategy buildStrategy,
                                     BuildOutput buildOutput, BuildConfigSpec spec) {
        // lets check if the strategy or output has changed and if so lets update the BC
        // e.g. the S2I builder image or the output tag and
        if (!Objects.equals(buildStrategy, spec.getStrategy()) || !Objects.equals(buildOutput, spec.getOutput())) {
            client.buildConfigs().withName(buildName).edit()
                    .editSpec()
                    .withStrategy(buildStrategy)
                    .withOutput(buildOutput)
                    .endSpec()
                    .done();
            log.info("Updating BuildServiceConfig %s for %s strategy", buildName, buildStrategy.getType());
        } else {
            log.info("Using BuildServiceConfig %s for %s strategy", buildName, buildStrategy.getType());
        }
        return buildName;
    }

    private BuildStrategy createBuildStrategy(ImageConfiguration imageConfig, OpenShiftBuildStrategy osBuildStrategy, String openshiftPullSecret) {
        if (osBuildStrategy == OpenShiftBuildStrategy.docker) {
            BuildImageConfiguration buildConfig = imageConfig.getBuildConfiguration();
            Map<String, String> fromExt = buildConfig.getFromExt();
            String fromName, fromKind, fromNamespace;

            if(buildConfig.isDockerFileMode()) {
                fromName = extractBaseFromDockerfile(buildConfig, config.getDockerBuildContext());
            } else {
                fromName = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.name, buildConfig.getFrom());
            }
            fromKind = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.kind, "DockerImage");
            fromNamespace = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.namespace, "ImageStreamTag".equals(fromKind) ? "openshift" : null);

            BuildStrategy buildStrategy = new BuildStrategyBuilder()
                    .withType("Docker")
                    .withNewDockerStrategy()
                    .withNewFrom()
                    .withKind(fromKind)
                    .withName(fromName)
                    .withNamespace(StringUtils.isEmpty(fromNamespace) ? null : fromNamespace)
                    .endFrom()
                    .withNoCache(checkForNocache(imageConfig))
                    .endDockerStrategy().build();

            if (openshiftPullSecret != null) {
                buildStrategy.getDockerStrategy().setPullSecret(new LocalObjectReferenceBuilder()
                .withName(openshiftPullSecret)
                .build());
            }

            return buildStrategy;
        } else if (osBuildStrategy == OpenShiftBuildStrategy.s2i) {
            BuildImageConfiguration buildConfig = imageConfig.getBuildConfiguration();
            Map<String, String> fromExt = buildConfig.getFromExt();
            String fromName, fromKind, fromNamespace;

            if(buildConfig.isDockerFileMode()) {
                fromName = extractBaseFromDockerfile(buildConfig, config.getDockerBuildContext());
            } else {
                fromName = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.name, buildConfig.getFrom());
            }
            fromKind = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.kind, "DockerImage");
            fromNamespace = getMapValueWithDefault(fromExt, OpenShiftBuildStrategy.SourceStrategy.namespace, "ImageStreamTag".equals(fromKind) ? "openshift" : null);

            BuildStrategy buildStrategy = new BuildStrategyBuilder()
                    .withType("Source")
                    .withNewSourceStrategy()
                    .withNewFrom()
                    .withKind(fromKind)
                    .withName(fromName)
                    .withNamespace(StringUtils.isEmpty(fromNamespace) ? null : fromNamespace)
                    .endFrom()
                    .withForcePull(config.isForcePullEnabled())
                    .endSourceStrategy()
                    .build();
            if (openshiftPullSecret != null) {
                buildStrategy.getSourceStrategy().setPullSecret(new LocalObjectReferenceBuilder()
                .withName(openshiftPullSecret)
                .build());
            }

            return buildStrategy;

        } else {
            throw new IllegalArgumentException("Unsupported BuildStrategy " + osBuildStrategy);
        }
    }

    private Boolean checkForNocache(ImageConfiguration imageConfig) {
        String nocache = System.getProperty("docker.nocache");
        if (nocache != null) {
            return nocache.length() == 0 || Boolean.valueOf(nocache);
        } else {
            BuildImageConfiguration buildConfig = imageConfig.getBuildConfiguration();
            return buildConfig.getNoCache();
        }
    }

    private Boolean checkOrCreatePullSecret(BuildServiceConfig config, OpenShiftClient client, KubernetesListBuilder builder, String pullSecretName, ImageConfiguration imageConfig)
            throws MojoExecutionException, UnsupportedEncodingException {
        io.fabric8.maven.docker.service.BuildService.BuildContext dockerBuildContext = config.getDockerBuildContext();
        BuildImageConfiguration buildConfig = imageConfig.getBuildConfiguration();

        String fromImage;
        if (buildConfig.isDockerFileMode()) {
            fromImage = extractBaseFromDockerfile(buildConfig, dockerBuildContext);
        } else {
            fromImage = extractBaseFromConfiguration(buildConfig);
        }

        String pullRegistry = EnvUtil.firstRegistryOf(new ImageName(fromImage).getRegistry(), dockerBuildContext.getRegistryConfig().getRegistry(), dockerBuildContext.getRegistryConfig().getRegistry());

        if (pullRegistry != null) {
            RegistryService.RegistryConfig registryConfig = dockerBuildContext.getRegistryConfig();
            AuthConfig authConfig = registryConfig.getAuthConfigFactory().createAuthConfig(false, registryConfig.isSkipExtendedAuth(), registryConfig.getAuthConfig(),
                    registryConfig.getSettings(), null, pullRegistry);

            if (authConfig != null) {

                JsonObject auths = new JsonObject();
                JsonObject auth = new JsonObject();
                JsonObject item = new JsonObject();

                String authString = authConfig.getUsername() + ":" + authConfig.getPassword();
                item.add("auth", new JsonPrimitive(Base64.encodeBase64String(authString.getBytes("UTF-8"))));
                auth.add(pullRegistry, item);
                auths.add("auths", auth);

                String credentials = Base64.encodeBase64String(auths.toString().getBytes("UTF-8"));

                Map<String, String> data = new HashMap<>();
                data.put(".dockerconfigjson", credentials);

                boolean hasPullSecret = client.secrets().withName(pullSecretName).get() != null;

                if (!hasPullSecret) {
                    log.info("Creating Secret %s", hasPullSecret);
                    builder.addNewSecretItem()
                            .withNewMetadata()
                            .withName(pullSecretName)
                            .endMetadata()
                            .withData(data)
                            .withType("kubernetes.io/dockerconfigjson")
                            .endSecretItem();
                } else {
                    log.info("Adding to Secret %s", pullSecretName);
                    return updateSecret(client, pullSecretName, data);
                }

                return true;
            } else {
                return false;
            }
        }
        return false;
    }

    private boolean updateSecret(OpenShiftClient client, String pullSecretName, Map<String, String> data) {
        if (!Objects.equals(data, client.secrets().withName(pullSecretName).get().getData())) {
            client.secrets().withName(pullSecretName).edit()
                    .editMetadata()
                    .withName(pullSecretName)
                    .endMetadata()
                    .withData(data)
                    .withType("kubernetes.io/dockerconfigjson")
                    .done();
            log.info("Updating Secret %s", pullSecretName);
        } else {
            log.info("Using Secret %s", pullSecretName);
        }
        return true;
    }

    private String extractBaseFromConfiguration(BuildImageConfiguration buildConfig) {
        String fromImage;
        fromImage = buildConfig.getFrom();
        if (fromImage == null) {
            AssemblyConfiguration assemblyConfig = buildConfig.getAssemblyConfiguration();
            if (assemblyConfig == null) {
                fromImage = DockerAssemblyManager.DEFAULT_DATA_BASE_IMAGE;
            }
        }
        return fromImage;
    }

    private String extractBaseFromDockerfile(BuildImageConfiguration buildConfig, io.fabric8.maven.docker.service.BuildService.BuildContext buildContext) {
        String fromImage;
        try {
            File fullDockerFilePath = buildConfig.getAbsoluteDockerFilePath(buildContext.getMojoParameters());
            fromImage = DockerFileUtil.extractBaseImages(
                    fullDockerFilePath,
                    DockerFileUtil.createInterpolator(buildContext.getMojoParameters(), buildConfig.getFilter())).stream().findFirst().orElse(null);
        } catch (IOException e) {
            // Cant extract base image, so we wont try an auto pull. An error will occur later anyway when
            // building the image, so we are passive here.
            fromImage = null;
        }
        return fromImage;
    }

    private void checkOrCreateImageStream(BuildServiceConfig config, OpenShiftClient client, KubernetesListBuilder builder, String imageStreamName) {
        boolean hasImageStream = client.imageStreams().withName(imageStreamName).get() != null;
        if (hasImageStream && config.getBuildRecreateMode().isImageStream()) {
            client.imageStreams().withName(imageStreamName).delete();
            hasImageStream = false;
        }
        if (!hasImageStream) {
            log.info("Creating ImageStream %s", imageStreamName);
            builder.addNewImageStreamItem()
                    .withNewMetadata()
                        .withName(imageStreamName)
                    .endMetadata()
                    .withNewSpec()
                        .withNewLookupPolicy()
                            .withLocal(config.isS2iImageStreamLookupPolicyLocal())
                        .endLookupPolicy()
                    .endSpec()
                    .endImageStreamItem();
        } else {
            log.info("Adding to ImageStream %s", imageStreamName);
        }
    }

    private void applyResourceObjects(BuildServiceConfig config, OpenShiftClient client, KubernetesListBuilder builder) throws Exception {
        if (config.getEnricherTask() != null) {
            config.getEnricherTask().execute(builder);
        }

        if (builder.hasItems()) {
            KubernetesList k8sList = builder.build();
            client.lists().create(k8sList);
        }
    }

    private Build startBuild(OpenShiftClient client, File dockerTar, String buildName) {
        log.info("Starting Build %s", buildName);
        try {
            return client.buildConfigs().withName(buildName)
                    .instantiateBinary()
                    .fromFile(dockerTar);
        } catch (KubernetesClientException exp) {
            Status status = exp.getStatus();
            if (status != null) {
                log.error("OpenShift Error: [%d %s] [%s] %s", status.getCode(), status.getStatus(), status.getReason(), status.getMessage());
            }
            if (exp.getCause() instanceof IOException && exp.getCause().getMessage().contains("Stream Closed")) {
                log.error("Build for %s failed: %s", buildName, exp.getCause().getMessage());
                logBuildFailedDetails(client, buildName);
            }
            throw exp;
        }
    }

    private void waitForOpenShiftBuildToComplete(OpenShiftClient client, Build build) throws MojoExecutionException, InterruptedException {
        final CountDownLatch latch = new CountDownLatch(1);
        final CountDownLatch logTerminateLatch = new CountDownLatch(1);
        final String buildName = KubernetesHelper.getName(build);

        final AtomicReference<Build> buildHolder = new AtomicReference<>();

        // Don't query for logs directly, Watch over the build pod:
        waitUntilPodIsReady(buildName + "-build", 120, log);
        log.info("Waiting for build " + buildName + " to complete...");
        try (LogWatch logWatch = client.pods().withName(buildName + "-build").watchLog()) {
            KubernetesClientUtil.printLogsAsync(logWatch,
                    "Failed to tail build log", logTerminateLatch, log);
            Watcher<Build> buildWatcher = getBuildWatcher(latch, buildName, buildHolder);
            try (Watch watcher = client.builds().withName(buildName).watch(buildWatcher)) {
                // Check if the build is already finished to avoid waiting indefinitely
                Build lastBuild = client.builds().withName(buildName).get();
                if (OpenshiftHelper.isFinished(KubernetesResourceUtil.getBuildStatusPhase(lastBuild))) {
                    log.debug("Build %s is already finished", buildName);
                    buildHolder.set(lastBuild);
                    latch.countDown();
                }

                waitUntilBuildFinished(latch);
                logTerminateLatch.countDown();

                build = buildHolder.get();
                if (build == null) {
                    log.debug("Build watcher on %s was closed prematurely", buildName);
                    build = client.builds().withName(buildName).get();
                }
                String status = KubernetesResourceUtil.getBuildStatusPhase(build);
                if (OpenshiftHelper.isFailed(status) || OpenshiftHelper.isCancelled(status)) {
                    throw new MojoExecutionException("OpenShift Build " + buildName + " failed: " + KubernetesResourceUtil.getBuildStatusReason(build));
                }

                if (!OpenshiftHelper.isFinished(status)) {
                    log.warn("Could not wait for the completion of build %s. It may be  may be still running (status=%s)", buildName, status);
                } else {
                    log.info("Build %s in status %s", buildName, status);
                }
            }
        }
    }

    /**
     * A Simple utility function to watch over pod until it gets ready
     *
     * @param podName Name of the pod
     * @param nAwaitTimeout Time in seconds upto which pod must be watched
     * @param log Logger object
     * @throws InterruptedException
     */
    private void waitUntilPodIsReady(String podName, int nAwaitTimeout, final Logger log) throws InterruptedException {
        final CountDownLatch readyLatch = new CountDownLatch(1);
        try (Watch watch = client.pods().withName(podName).watch(new Watcher<Pod>() {
            @Override
            public void eventReceived(Action action, Pod aPod) {
                if(KubernetesHelper.isPodReady(aPod)) {
                    readyLatch.countDown();
                }
            }

            @Override
            public void onClose(KubernetesClientException e) {
                // Ignore
            }
        })) {
            readyLatch.await(nAwaitTimeout, TimeUnit.SECONDS);
        } catch (KubernetesClientException | InterruptedException e) {
            log.error("Could not watch pod", e);
        }
    }

    private void waitUntilBuildFinished(CountDownLatch latch) {
        while (latch.getCount() > 0L) {
            try {
                latch.await();
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    private Watcher<Build> getBuildWatcher(final CountDownLatch latch, final String buildName, final AtomicReference<Build> buildHolder) {
        return new Watcher<Build>() {

            String lastStatus = "";

            @Override
            public void eventReceived(Action action, Build build) {
                buildHolder.set(build);
                String status = KubernetesResourceUtil.getBuildStatusPhase(build);
                log.verbose("BuildWatch: Received event %s , build status: %s", action, build.getStatus());
                if (!lastStatus.equals(status)) {
                    lastStatus = status;
                    log.verbose("Build %s status: %s", buildName, status);
                }
                if (OpenshiftHelper.isFinished(status)) {
                    latch.countDown();
                }
            }

            @Override
            public void onClose(KubernetesClientException cause) {
                if (cause != null) {
                    log.error("Error while watching for build to finish: %s [%d]",
                            cause.getMessage(), cause.getCode());
                    Status status = cause.getStatus();
                    if (status != null) {
                        log.error("%s [%s]", status.getReason(), status.getStatus());
                    }
                }
                latch.countDown();
            }
        };
    }

    private void logBuildFailedDetails(OpenShiftClient client, String buildName) {
        try {
            BuildConfig build = client.buildConfigs().withName(buildName).get();
            ObjectReference ref = build.getSpec().getStrategy().getSourceStrategy().getFrom();
            String kind = ref.getKind();
            String name = ref.getName();

            if ("DockerImage".equals(kind)) {
                log.error("Please, ensure that the Docker image '%s' exists and is accessible by OpenShift", name);
            } else if ("ImageStreamTag".equals(kind)) {
                String namespace = ref.getNamespace();
                String namespaceInfo = "current";
                String namespaceParams = "";
                if (namespace != null && !namespace.isEmpty()) {
                    namespaceInfo = "'" + namespace + "'";
                    namespaceParams = " -n " + namespace;
                }

                log.error("Please, ensure that the ImageStream Tag '%s' exists in the %s namespace (with 'oc get is%s')", name, namespaceInfo, namespaceParams);
            }
        } catch (Exception ex) {
            log.error("Unable to get detailed information from the BuildServiceConfig: " + ex.getMessage());
        }
    }

    private void logBuildFailure(OpenShiftClient client, String buildName) throws Fabric8ServiceException {
        try {
            List<Build> builds = client.builds().inNamespace(client.getNamespace()).list().getItems();
            for(Build build : builds) {
                if(build.getMetadata().getName().contains(buildName)) {
                    log.error(build.getMetadata().getName() + "\t" + "\t" + build.getStatus().getReason() + "\t" + build.getStatus().getMessage());
                    throw new Fabric8ServiceException("Unable to build the image using the OpenShift build service", new KubernetesClientException(build.getStatus().getReason() + " " + build.getStatus().getMessage()));
                }
            }

            log.error("Also, check cluster events via `oc get events` to see what could have possibly gone wrong");
        } catch (KubernetesClientException clientException) {
            Status status = clientException.getStatus();
            if (status != null)
                log.error("OpenShift Error: [%d] %s", status.getCode(), status.getMessage());
        }
    }

    private void addImageStreamToFile(File imageStreamFile, ImageName imageName, OpenShiftClient client) throws MojoExecutionException {
        ImageStreamService imageStreamHandler = new ImageStreamService(client, log);
        imageStreamHandler.appendImageStreamResource(imageName, imageStreamFile);
    }

    // == Utility methods ==========================

    private String getS2IBuildName(BuildServiceConfig config, ImageName imageName) {
        return imageName.getSimpleName() + config.getS2iBuildNameSuffix();
    }

    private String getImageStreamName(ImageName name) {
        return name.getSimpleName();
    }

    private String getMapValueWithDefault(Map<String, String> map, OpenShiftBuildStrategy.SourceStrategy strategy, String defaultValue) {
        return getMapValueWithDefault(map, strategy.key(), defaultValue);
    }

    private String getMapValueWithDefault(Map<String, String> map, String field, String defaultValue) {
        if (map == null) {
            return defaultValue;
        }
        String value = map.get(field);
        return value != null ? value : defaultValue;
    }

}