KubernetesService.java

package org.entando.kubernetes.service;

import static org.entando.kubernetes.model.EntandoDeploymentPhase.FAILED;
import static org.entando.kubernetes.model.EntandoDeploymentPhase.SUCCESSFUL;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.awaitility.core.ConditionFactory;
import org.awaitility.core.ConditionTimeoutException;
import org.entando.kubernetes.client.k8ssvc.K8SServiceClient;
import org.entando.kubernetes.exception.k8ssvc.EntandoAppPluginLinkingProcessException;
import org.entando.kubernetes.exception.k8ssvc.PluginNotFoundException;
import org.entando.kubernetes.exception.k8ssvc.PluginNotReadyException;
import org.entando.kubernetes.model.debundle.EntandoDeBundle;
import org.entando.kubernetes.model.link.EntandoAppPluginLink;
import org.entando.kubernetes.model.plugin.EntandoPlugin;
import org.entando.kubernetes.model.plugin.EntandoPluginBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

@Slf4j
@Component
public class KubernetesService {

    public static final String ENTANDOPLUGIN_CRD_NAME = "entandoplugins.entando.org";
    public static final String KUBERNETES_NAMESPACE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace";

    private final ConditionFactory waitingConditionFactory;
    private final K8SServiceClient k8sServiceClient;
    private final String entandoAppName;
    private final String entandoAppNamespace;
    private Set<String> digitalExchangesNames;

    public KubernetesService(@Value("${entando.app.name}") String entandoAppName,
            @Value("${entando.app.namespace}") String entandoAppNamespace,
            @Value("${entando.component.repository.namespaces}") Set<String> digitalExchangesNames,
            K8SServiceClient k8SServiceClient,
            ConditionFactory waitingConditionFactory) {
        this.waitingConditionFactory = waitingConditionFactory;
        this.k8sServiceClient = k8SServiceClient;
        this.entandoAppName = entandoAppName;
        this.digitalExchangesNames = digitalExchangesNames;
        this.entandoAppNamespace = getCurrentKubernetesNamespace().orElse(entandoAppNamespace);
    }

    public List<EntandoPlugin> getLinkedPlugins() {
        return getCurrentAppLinks()
                .stream().map(k8sServiceClient::getPluginForLink)
                .collect(Collectors.toList());
    }

    public EntandoPlugin getLinkedPlugin(String pluginId) {
        return getCurrentAppLinkToPlugin(pluginId)
                .map(k8sServiceClient::getPluginForLink)
                .orElseThrow(PluginNotFoundException::new);
    }

    public boolean isLinkedPlugin(String pluginId) {
        return getCurrentAppLinkToPlugin(pluginId).isPresent();
    }

    public boolean isPluginReady(EntandoPlugin plugin) {
        return k8sServiceClient.isPluginReadyToServeApp(plugin, entandoAppName);
    }

    private List<EntandoAppPluginLink> getCurrentAppLinks() {
        return k8sServiceClient.getAppLinks(entandoAppName);
    }

    private Optional<EntandoAppPluginLink> getCurrentAppLinkToPlugin(String pluginId) {
        return getCurrentAppLinks()
                .stream()
                .filter(el -> el.getSpec().getEntandoPluginName().equals(pluginId))
                .findFirst();
    }

    public void unlinkPlugin(String pluginId) {
        getCurrentAppLinkToPlugin(pluginId).ifPresent(k8sServiceClient::unlink);
    }


    public EntandoAppPluginLink linkPlugin(EntandoPlugin plugin) {
        EntandoPlugin newPlugin = createNewPlugin(plugin);
        return k8sServiceClient.linkAppWithPlugin(entandoAppName, entandoAppNamespace, newPlugin);
    }

    public void linkPluginAndWaitForSuccess(EntandoPlugin plugin) {
        EntandoAppPluginLink createdLink = this.linkPlugin(plugin);
        try {
            this.waitingConditionFactory.until(() -> this.hasLinkingProcessCompletedSuccessfully(createdLink, plugin));
        } catch (ConditionTimeoutException e) {
            throw new PluginNotReadyException(plugin.getMetadata().getName());
        }

    }

    public EntandoPlugin updatePlugin(EntandoPlugin plugin) {
        EntandoPlugin updatedPlugin = createNewPlugin(plugin);
        return k8sServiceClient.updatePlugin(updatedPlugin);
    }

    private EntandoPlugin createNewPlugin(EntandoPlugin plugin) {
        EntandoPlugin newPlugin = new EntandoPluginBuilder()
                .withMetadata(plugin.getMetadata())
                .withSpec(plugin.getSpec())
                .build();

        newPlugin.getMetadata().setNamespace(this.entandoAppNamespace);

        return newPlugin;
    }

    public boolean hasLinkingProcessCompletedSuccessfully(EntandoAppPluginLink link, EntandoPlugin plugin) {
        boolean result = false;
        Optional<EntandoAppPluginLink> linkByName = k8sServiceClient.getLinkByName(link.getMetadata().getName());
        if (linkByName.isPresent()) {
            if (linkByName.get().getStatus().getEntandoDeploymentPhase().equals(FAILED)) {
                String msg = String.format("Linking procedure between app %s and plugin %s failed", entandoAppName,
                        plugin.getMetadata().getName());
                throw new EntandoAppPluginLinkingProcessException(msg);
            }
            result = linkByName.get().getStatus().getEntandoDeploymentPhase().equals(SUCCESSFUL)
                    && isPluginReady(plugin);
        }
        return result;
    }

    public List<EntandoDeBundle> getBundlesInDefaultNamespace() {
        return k8sServiceClient.getBundlesInObservedNamespaces();
    }

    public Optional<EntandoDeBundle> fetchBundleByName(String name) {
        if (CollectionUtils.isEmpty(this.digitalExchangesNames)) {
            log.info("Fetching bundle by name {}", name);
            return this.getBundleByName(name);
        }

        return this.digitalExchangesNames.stream()
                .map(namespace -> {
                    log.info("Fetching bundle by name {} in namespace {}", name, namespace);
                    return this.getBundleByNameAndNamespace(name, namespace).orElse(null);
                })
                .filter(Objects::nonNull)
                .findFirst();
    }

    protected Optional<EntandoDeBundle> getBundleByName(String name) {
        return k8sServiceClient.getBundleWithName(name);
    }

    protected Optional<EntandoDeBundle> getBundleByNameAndNamespace(String name, String namespace) {
        return k8sServiceClient.getBundleWithNameAndNamespace(name, namespace);
    }

    private Optional<String> getCurrentKubernetesNamespace() {
        Path namespacePath = Paths.get(KUBERNETES_NAMESPACE_PATH);
        String namespace = null;
        if (namespacePath.toFile().exists()) {
            try {
                namespace = new String(Files.readAllBytes(namespacePath));
            } catch (IOException e) {
                log.error(String.format("An error occurred while reading the namespace from file %s",
                        namespacePath.toString()), e);
            }
        }
        return Optional.ofNullable(namespace);
    }

}