package io.radanalytics.operator; import com.jcabi.manifests.Manifests; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.api.model.ServicePortBuilder; 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.annotation.PostConstruct; 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 watches on StartupEvent 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; protected OperatorConfig config; protected KubernetesClient client; protected boolean isOpenShift; @Inject private Logger log; @Inject @Any private Instance<AbstractOperator<? extends EntityInfo>> operators; public SDKEntrypoint() { } /* this entrypoint can be called from an environment w/o CDI */ public SDKEntrypoint(Logger log) { this.log = log; init(); } @PostConstruct void init(){ config = OperatorConfig.fromMap(System.getenv()); client = new DefaultKubernetesClient(); checkIfOnOpenshift(); } void onStop(@Observes ShutdownEvent event) { log.info("Stopped"); } public void onStart(@Observes StartupEvent event) { log.info("Starting.."); CompletableFuture<Void> future = run().exceptionally(ex -> { log.error("Unable to start operator for one or more namespaces", ex); System.exit(1); return null; }); if (config.isMetrics()) { CompletableFuture<Optional<HTTPServer>> maybeMetricServer = future.thenCompose(s -> runMetrics()); } } private CompletableFuture<Void> run() { printInfo(); if (isOpenShift) { log.info("{}OpenShift{} environment detected.", AnsiColors.ye(), AnsiColors.xx()); } else { log.info("{}Kubernetes{} environment detected.", AnsiColors.ye(), AnsiColors.xx()); } List<CompletableFuture> futures = new ArrayList<>(); if (operators != null) { 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<String> 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<Optional<HTTPServer>> runMetrics() { 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()); } catch (IOException e) { log.error("Can't start metrics server because of: {} ", e.getMessage()); e.printStackTrace(); } if (config.isMetricsJvm()) { DefaultExports.initialize(); } final Optional<HTTPServer> maybeServer = Optional.of(httpServer); return CompletableFuture.supplyAsync(() -> maybeServer); } private CompletableFuture<Void> runForNamespace(boolean isOpenShift, String namespace, long reconInterval, int delay) { List<AbstractOperator<? extends EntityInfo>> 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<Future> 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<Watch> 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 void checkIfOnOpenshift() { try { 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 = httpClient.newCall(new Request.Builder().url(url).build()).execute(); 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()); } isOpenShift = success; } catch (Exception e) { e.printStackTrace(); log.error("Failed to distinguish between Kubernetes and OpenShift"); log.warn("Let's assume we are on K8s"); isOpenShift = false; } } 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<String> labels = new ArrayList<>(); List<String> 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; } public boolean isOpenShift() { return isOpenShift; } public OperatorConfig getConfig() { return config; } public KubernetesClient getClient() { return client; } }