EntandoCompositeAppController.java

/*
 *
 * Copyright 2015-Present Entando Inc. (http://www.entando.com) All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 *  This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 */

package org.entando.kubernetes.compositeapp.controller;

import static java.lang.String.format;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import io.fabric8.kubernetes.api.builder.Builder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Watcher.Action;
import io.quarkus.runtime.StartupEvent;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import javax.enterprise.event.Observes;
import javax.inject.Inject;
import org.entando.kubernetes.controller.spi.common.PodResult;
import org.entando.kubernetes.controller.spi.common.ResourceUtils;
import org.entando.kubernetes.controller.support.client.EntandoResourceClient;
import org.entando.kubernetes.controller.support.client.SimpleK8SClient;
import org.entando.kubernetes.controller.support.common.EntandoOperatorConfig;
import org.entando.kubernetes.controller.support.common.KubeUtils;
import org.entando.kubernetes.controller.support.common.OperatorProcessingInstruction;
import org.entando.kubernetes.controller.support.controller.AbstractDbAwareController;
import org.entando.kubernetes.controller.support.controller.ControllerExecutor;
import org.entando.kubernetes.controller.support.controller.DefaultControllerImageResolver;
import org.entando.kubernetes.controller.support.controller.EntandoControllerException;
import org.entando.kubernetes.model.EntandoBaseCustomResource;
import org.entando.kubernetes.model.EntandoBaseFluent;
import org.entando.kubernetes.model.EntandoIngressingDeploymentBaseFluent;
import org.entando.kubernetes.model.NestedIngressingDeploymentSpecFluent;
import org.entando.kubernetes.model.WebServerStatus;
import org.entando.kubernetes.model.compositeapp.EntandoCompositeApp;
import org.entando.kubernetes.model.compositeapp.EntandoCompositeAppSpec;
import org.entando.kubernetes.model.compositeapp.EntandoCompositeAppSpecFluent;
import org.entando.kubernetes.model.compositeapp.EntandoCustomResourceReference;
import org.jetbrains.annotations.NotNull;

public class EntandoCompositeAppController extends AbstractDbAwareController<EntandoCompositeAppSpec, EntandoCompositeApp> {

    private final String namespace;

    @Inject
    public EntandoCompositeAppController(KubernetesClient kubernetesClient) {
        this(kubernetesClient, true);
    }

    /**
     * This constructor is intended for in-process tests where we do not want the controller to exit automatically.
     */
    public EntandoCompositeAppController(KubernetesClient kubernetesClient, boolean exitAutomatically) {
        super(kubernetesClient, exitAutomatically);
        this.namespace = kubernetesClient.getNamespace();
    }

    //We know this won't ever break.
    @SuppressWarnings("unchecked")
    public SimpleK8SClient<EntandoResourceClient> getClient() {
        return (SimpleK8SClient<EntandoResourceClient>) super.k8sClient;
    }

    public void onStartup(@Observes StartupEvent event) {
        processCommand();
    }

    @Override
    protected void synchronizeDeploymentState(EntandoCompositeApp newCompositeApp) {
        ControllerExecutor executor = new ControllerExecutor(namespace, k8sClient, new DefaultControllerImageResolver());
        for (EntandoBaseCustomResource<?> resource : newCompositeApp.getSpec().getComponents()) {
            if (resource instanceof EntandoCustomResourceReference) {
                resource = prepareReference(newCompositeApp, resource);
            } else {
                resource = prepareComponent(newCompositeApp, resource);
            }
            Pod pod = processResource(newCompositeApp, executor, resource);
            if (PodResult.of(pod).hasFailed()) {
                String message = logFailure(resource);
                throw new EntandoControllerException(message);
            } else {
                if (KubeUtils.resolveProcessingInstruction(resource) == OperatorProcessingInstruction.DEFER) {
                    removeDeferInstruction(resource);
                }
                if (EntandoOperatorConfig.garbageCollectSuccessfullyCompletedPods()) {
                    getClient().pods()
                            .removeSuccessfullyCompletedPods(namespace,
                                    Map.of(KubeUtils.ENTANDO_RESOURCE_KIND_LABEL_NAME, resource.getKind(),
                                            KubeUtils.ENTANDO_RESOURCE_NAMESPACE_LABEL_NAME, resource.getMetadata().getNamespace(),
                                            resource.getKind(), resource.getMetadata().getName()));
                }
            }
        }
    }

    private void removeDeferInstruction(EntandoBaseCustomResource<?> resource) {
        final EntandoBaseCustomResource<?> reloaded = k8sClient.entandoResources().reload(resource);
        reloaded.getMetadata().getAnnotations().remove(KubeUtils.PROCESSING_INSTRUCTION_ANNOTATION_NAME);
        k8sClient.entandoResources().createOrPatchEntandoResource(reloaded);
    }

    private String logFailure(EntandoBaseCustomResource<?> resource) {
        String message = format("Unexpected exception occurred while adding %s %s/%s", resource.getKind(),
                resource.getMetadata().getNamespace(),
                resource.getMetadata().getName());
        this.logger.log(Level.SEVERE, message);
        return message;
    }

    @NotNull
    private Pod processResource(EntandoCompositeApp newCompositeApp, ControllerExecutor executor, EntandoBaseCustomResource<?> resource) {
        Pod pod = executor.runControllerFor(Action.ADDED, resource, null);
        WebServerStatus webServerStatus = new WebServerStatus(resource.getMetadata().getName());
        webServerStatus.setPodStatus(pod.getStatus());
        getClient().entandoResources().updateStatus(newCompositeApp, webServerStatus);
        return pod;
    }

    @SuppressWarnings("unchecked")
    private <S extends Serializable, C extends EntandoBaseCustomResource<S>> C prepareComponent(EntandoCompositeApp newCompositeApp,
            C component) {
        EntandoBaseFluent<?> componentBuilder = EntandoCompositeAppSpecFluent.newBuilderFrom(component);
        if (component.getMetadata().getNamespace() == null) {
            componentBuilder = componentBuilder.editMetadata().withNamespace(newCompositeApp.getMetadata().getNamespace()).endMetadata();
        }
        if (componentBuilder instanceof EntandoIngressingDeploymentBaseFluent) {
            NestedIngressingDeploymentSpecFluent<?, ?> eidsbf = ((EntandoIngressingDeploymentBaseFluent<?, ?>) componentBuilder).editSpec();
            newCompositeApp.getSpec().getDbmsOverride().ifPresent(eidsbf::withDbms);
            newCompositeApp.getSpec().getIngressHostNameOverride().ifPresent(eidsbf::withIngressHostName);
            newCompositeApp.getSpec().getTlsSecretNameOverride().ifPresent(eidsbf::withTlsSecretName);
            componentBuilder = eidsbf.endSpec();
        }
        componentBuilder = componentBuilder.editMetadata()
                .withOwnerReferences(Collections.singletonList(ResourceUtils.buildOwnerReference(newCompositeApp)))
                .endMetadata();
        component = ((Builder<C>) componentBuilder).build();
        return k8sClient.entandoResources().createOrPatchEntandoResource(component);
    }

    private <S extends Serializable, T extends EntandoBaseCustomResource<S>> T prepareReference(EntandoCompositeApp newCompositeApp,
            T component) {
        EntandoCustomResourceReference ref = (EntandoCustomResourceReference) component;
        return k8sClient.entandoResources().load(this.<S, T>resolveType(ref.getSpec().getTargetKind()),
                ref.getSpec().getTargetNamespace().orElse(newCompositeApp.getMetadata().getNamespace()),
                ref.getSpec().getTargetName());
    }

    @SuppressWarnings("unchecked")
    protected <S extends Serializable, T extends EntandoBaseCustomResource<S>> Class<T> resolveType(String kind) {
        return (Class<T>) Arrays
                .stream(EntandoBaseCustomResource.class.getAnnotation(JsonSubTypes.class).value())
                .filter(type -> type.name().equals(kind))
                .findAny()
                .orElseThrow(IllegalArgumentException::new)
                .value();
    }
}