All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.radanalytics.operator.SDKEntrypoint Maven / Gradle / Ivy

There is a newer version: 0.6.15
Show newest version
package io.radanalytics.operator;

import com.jcabi.manifests.Manifests;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.DefaultKubernetesClient;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.Watch;
import io.fabric8.kubernetes.client.utils.HttpClientUtils;
import io.prometheus.client.Gauge;
import io.prometheus.client.exporter.HTTPServer;
import io.prometheus.client.hotspot.DefaultExports;
import io.prometheus.client.log4j.InstrumentedAppender;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import io.radanalytics.operator.common.AbstractOperator;
import io.radanalytics.operator.common.AnsiColors;
import io.radanalytics.operator.common.EntityInfo;
import io.radanalytics.operator.common.OperatorConfig;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static io.radanalytics.operator.common.AnsiColors.*;
import static io.radanalytics.operator.common.OperatorConfig.ALL_NAMESPACES;
import static io.radanalytics.operator.common.OperatorConfig.SAME_NAMESPACE;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Entry point class that contains the main method and should bootstrap all the registered operators
 * that are present on the class path. It scans the class path for those classes that have the
 * {@link io.radanalytics.operator.common.Operator} annotations on them or extends the {@link AbstractOperator}.
 */
@ApplicationScoped
public class SDKEntrypoint {
    private static ExecutorService executors;

    private OperatorConfig config;
    private KubernetesClient client;
    @Inject
    private Logger log;


    @Inject @Any
    private Instance> operators;

    void onStop(@Observes ShutdownEvent event) {
        log.info("Stopped");
    }

    public void onStart(@Observes StartupEvent event) {
        log.info("Starting..");
        config = OperatorConfig.fromMap(System.getenv());
        client = new DefaultKubernetesClient();
        boolean isOpenshift = isOnOpenShift();
        CompletableFuture future = run(isOpenshift).exceptionally(ex -> {
            log.error("Unable to start operator for one or more namespaces", ex);
            System.exit(1);
            return null;
        });
        if (config.isMetrics()) {
            CompletableFuture> maybeMetricServer = future.thenCompose(s -> runMetrics(isOpenshift));
        }
//        future.join();
    }

    private CompletableFuture run(boolean isOpenShift) {
        printInfo();
        if (isOpenShift) {
            log.info("{}OpenShift{} environment detected.", AnsiColors.ye(), AnsiColors.xx());
        } else {
            log.info("{}Kubernetes{} environment detected.", AnsiColors.ye(), AnsiColors.xx());
        }

        List futures = new ArrayList<>();
        if (SAME_NAMESPACE.equals(config.getNamespaces().iterator().next())) { // current namespace
            String namespace = client.getNamespace();
            CompletableFuture future = runForNamespace(isOpenShift, namespace, config.getReconciliationIntervalS(), 0);
            futures.add(future);
        } else {
            if (ALL_NAMESPACES.equals(config.getNamespaces().iterator().next())) {
                CompletableFuture future = runForNamespace(isOpenShift, ALL_NAMESPACES, config.getReconciliationIntervalS(), 0);
                futures.add(future);
            } else {
                Iterator ns;
                int i;
                for (ns = config.getNamespaces().iterator(), i = 0; i < config.getNamespaces().size(); i++) {
                    CompletableFuture future = runForNamespace(isOpenShift, ns.next(), config.getReconciliationIntervalS(), i);
                    futures.add(future);
                }
            }
        }
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{}));
    }

    private CompletableFuture> runMetrics(boolean isOpenShift) {
        HTTPServer httpServer = null;
        try {
            log.info("Starting a simple HTTP server for exposing internal metrics..");
            httpServer = new HTTPServer(config.getMetricsPort());
            log.info("{}metrics server{} listens on port {}", AnsiColors.ye(), AnsiColors.xx(), config.getMetricsPort());
            // todo: create also the service and for openshift also expose the service (?)
        } catch (IOException e) {
            log.error("Can't start metrics server because of: {} ", e.getMessage());
            e.printStackTrace();
        }
        if (config.isMetricsJvm()) {
            DefaultExports.initialize();
        }
        final Optional maybeServer = Optional.of(httpServer);
        return CompletableFuture.supplyAsync(() -> maybeServer);
    }

    private CompletableFuture runForNamespace(boolean isOpenShift, String namespace, long reconInterval, int delay) {
        List> operatorList = operators.stream().collect(Collectors.toList());

        if (operatorList.isEmpty()) {
            log.warn("No suitable operators were found, make sure your class extends AbstractOperator and have @Singleton on it.");
        }

        List futures = new ArrayList<>();
        final int operatorNumber = operatorList.size();
        IntStream.range(0, operatorNumber).forEach(operatorIndex -> {
            AbstractOperator operator = operatorList.get(operatorIndex);
            if (!AbstractOperator.class.isAssignableFrom(operator.getClass())) {
                log.error("Class {} annotated with @Operator doesn't extend the AbstractOperator", operator.getClass());
                return; // do not fail
            }

            if (!operator.isEnabled()) {
                log.info("Skipping initialization of {} operator", operator.getClass());
                return;
            }

            operator.setClient(client);
            operator.setNamespace(namespace);
            operator.setOpenshift(isOpenShift);

            CompletableFuture future = operator.start().thenApply(res -> {
                log.info("{} started in namespace {}", operator.getName(), namespace);
                return res;
            }).exceptionally(ex -> {
                log.error("{} in namespace {} failed to start", operator.getName(), namespace, ((Throwable) ex).getCause());
                System.exit(1);
                return null;
            });

            ScheduledExecutorService s = Executors.newScheduledThreadPool(1);
            int realDelay = (delay * operatorNumber) + operatorIndex + 2;
            ScheduledFuture scheduledFuture =
                    s.scheduleAtFixedRate(() -> {
                        try {
                            operator.fullReconciliation();
                            operator.setFullReconciliationRun(true);
                        } catch (Throwable t) {
                            log.warn("error during full reconciliation: {}", t.getMessage());
                            t.printStackTrace();
                        }
                    }, realDelay, reconInterval, SECONDS);
            log.info("full reconciliation for {} scheduled (periodically each {} seconds)", operator.getName(), reconInterval);
            log.info("the first full reconciliation for {} is happening in {} seconds", operator.getName(), realDelay);

            futures.add(future);
        });
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[]{}));
    }

    private boolean isOnOpenShift() {
        URL kubernetesApi = client.getMasterUrl();

        HttpUrl.Builder urlBuilder = new HttpUrl.Builder();
        urlBuilder.host(kubernetesApi.getHost());

        if (kubernetesApi.getPort() == -1) {
            urlBuilder.port(kubernetesApi.getDefaultPort());
        } else {
            urlBuilder.port(kubernetesApi.getPort());
        }
        if (kubernetesApi.getProtocol().equals("https")) {
            urlBuilder.scheme("https");
        }
        urlBuilder.addPathSegment("apis/route.openshift.io/v1");

        OkHttpClient httpClient = HttpClientUtils.createHttpClient(new ConfigBuilder().build());
        HttpUrl url = urlBuilder.build();
        Response response;
        try {
            response = httpClient.newCall(new Request.Builder().url(url).build()).execute();
        } catch (IOException e) {
            e.printStackTrace();
            log.error("Failed to distinguish between Kubernetes and OpenShift");
            log.warn("Let's assume we are on K8s");
            return false;
        }
        boolean success = response.isSuccessful();
        if (success) {
            log.info("{} returned {}. We are on OpenShift.", url, response.code());
        } else {
            log.info("{} returned {}. We are not on OpenShift. Assuming, we are on Kubernetes.", url, response.code());
        }
        return success;
    }

    private void printInfo() {
        String gitSha = "unknown";
        String version = "unknown";
        try {
            version = Optional.ofNullable(SDKEntrypoint.class.getPackage().getImplementationVersion()).orElse(version);
            gitSha = Optional.ofNullable(Manifests.read("Implementation-Build")).orElse(gitSha);
        } catch (Exception e) {
            // ignore, not critical
        }

        if(config.isMetrics()) {
            registerMetrics(gitSha, version);
        }

        log.info("\n{}Operator{} has started in version {}{}{}.\n", re(), xx(), gr(),
                version, xx());
        if (!gitSha.isEmpty()) {
            log.info("Git sha: {}{}{}", ye(), gitSha, xx());
        }
        log.info("==================\n");
    }

    private void registerMetrics(String gitSha, String version) {
        List labels = new ArrayList<>();
        List values = new ArrayList<>();

        labels.addAll(Arrays.asList("gitSha", "version",
                "CRD",
                "COLORS",
                OperatorConfig.WATCH_NAMESPACE,
                OperatorConfig.METRICS,
                OperatorConfig.METRICS_JVM,
                OperatorConfig.METRICS_PORT,
                OperatorConfig.FULL_RECONCILIATION_INTERVAL_S,
                OperatorConfig.OPERATOR_OPERATION_TIMEOUT_MS
        ));
        values.addAll(Arrays.asList(gitSha, version,
                Optional.ofNullable(System.getenv().get("CRD")).orElse("true"),
                Optional.ofNullable(System.getenv().get("COLORS")).orElse("true"),
                SAME_NAMESPACE.equals(config.getNamespaces().iterator().next()) ? client.getNamespace() : config.getNamespaces().toString(),
                String.valueOf(config.isMetrics()),
                String.valueOf(config.isMetricsJvm()),
                String.valueOf(config.getMetricsPort()),
                String.valueOf(config.getReconciliationIntervalS()),
                String.valueOf(config.getOperationTimeoutMs())
        ));

        Gauge.build()
                .name("operator_info")
                .help("Basic information about the abstract operator library.")
                .labelNames(labels.toArray(new String[]{}))
                .register()
                .labels(values.toArray(new String[]{}))
                .set(1);

        // add log appender for metrics
        final org.apache.log4j.Logger rootLogger = org.apache.log4j.Logger.getRootLogger();
        InstrumentedAppender metricsLogAppender = new InstrumentedAppender();
        metricsLogAppender.setName("metrics");
        rootLogger.addAppender(metricsLogAppender);
    }

    public static ExecutorService getExecutors() {
        if (null == executors) {
            executors = Executors.newFixedThreadPool(10);
        }
        return executors;
    }

    private static OkHttpClient getOkHttpClient() {
        try {
            // Create a trust manager that does not validate certificate chains
            final X509TrustManager trustAllCerts = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[]{};
                }
            };
            final SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null, new X509TrustManager[]{trustAllCerts}, new SecureRandom());
            final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            builder.sslSocketFactory(sslSocketFactory, trustAllCerts);
            builder.hostnameVerifier((hostname, session) -> true);
            OkHttpClient okHttpClient = builder.build();
            return okHttpClient;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy