io.fabric8.api.registry.ApiFinder.java Source code

Java tutorial

Introduction

Here is the source code for io.fabric8.api.registry.ApiFinder.java

Source

/**
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.api.registry;

import io.fabric8.api.registry.rules.CamelEndpointFinder;
import io.fabric8.api.registry.rules.CxfEndpointFinder;
import io.fabric8.kubernetes.api.KubernetesHelper;
import io.fabric8.kubernetes.api.model.ContainerPort;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodStatus;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.jolokia.JolokiaClients;
import io.fabric8.swagger.model.ApiDeclaration;
import io.fabric8.utils.Closeables;
import io.fabric8.utils.Filter;
import io.fabric8.utils.Strings;
import org.apache.cxf.jaxrs.ext.MessageContext;
import org.apache.deltaspike.core.api.config.ConfigProperty;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jolokia.client.J4pClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PreDestroy;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicReference;

import static io.fabric8.kubernetes.api.KubernetesHelper.getName;
import static io.fabric8.utils.URLUtils.urlPathJoin;

/**
 */
public class ApiFinder {
    private static final transient Logger LOG = LoggerFactory.getLogger(ApiFinder.class);

    public static final String SWAGGER_POSTFIX = "/api-docs";
    public static final String WADL_POSTFIX = "?_wadl";
    public static final String WSDL_POSTFIX = "?wsdl";

    public static final String swaggerProperty = "Swagger";
    public static final String wadlProperty = "WADL";
    public static final String wsdlProperty = "WSDL";

    private final long pollTimeMs;
    private MessageContext messageContext;
    private JolokiaClients clients = new JolokiaClients();
    private KubernetesClient kubernetes = clients.getKubernetes();
    private AtomicReference<ApiSnapshot> snapshotCache = new AtomicReference<>();
    private Timer timer = new Timer();
    private TimerTask task = new TimerTask() {
        @Override
        public void run() {
            refreshSnapshot();
        }
    };
    private List<EndpointFinder> finders = new ArrayList<>();
    private String urlPrefix;

    /**
     * @param pollTimeMs the polling time interval in milliseconds
     */
    @Inject
    public ApiFinder(@ConfigProperty(name = "API_REGISTRY_POLL_TIME", defaultValue = "5000") long pollTimeMs) {
        this.pollTimeMs = pollTimeMs;
        timer.schedule(task, 0, pollTimeMs);
        finders.addAll(createDefaultEndpointFinders());
    }

    @PreDestroy
    public void destroy() {
        timer.cancel();
    }

    public ApiSnapshot refreshSnapshot() {
        Map<String, Pod> podMap = KubernetesHelper.getPodMap(kubernetes);
        ApiSnapshot snapshot = new ApiSnapshot(podMap);
        snapshot.setUrlPrefix(urlPrefix);
        snapshot.setMessageContext(messageContext);
        pollPodApis(snapshot);
        pollServiceApis(snapshot);
        snapshotCache.set(snapshot);
        return snapshot;
    }

    protected void pollPodApis(ApiSnapshot snapshot) {
        Map<String, List<ApiDTO>> answer = new HashMap<>();
        Set<Map.Entry<String, Pod>> entries = snapshot.getPodMap().entrySet();
        for (Map.Entry<String, Pod> entry : entries) {
            String podId = entry.getKey();
            Pod pod = entry.getValue();
            List<ApiDTO> list = new ArrayList<ApiDTO>();
            addApisForPod(snapshot, pod, list);
            if (list != null && !list.isEmpty()) {
                snapshot.putApis(podId, list);
            }
        }
    }

    public List<ApiDTO> findApisOnPods(String selector) {
        List<ApiDTO> answer = new ArrayList<>();
        Filter<Pod> filter = KubernetesHelper.createPodFilter(selector);
        ApiSnapshot snapshot = getSnapshot();
        snapshot.setMessageContext(messageContext);

        Map<String, Pod> podMap = snapshot.getPodMap();
        Map<String, List<ApiDTO>> cache = snapshot.getApiMap();

        Set<Map.Entry<String, Pod>> entries = podMap.entrySet();
        for (Map.Entry<String, Pod> entry : entries) {
            String key = entry.getKey();
            Pod pod = entry.getValue();
            if (filter.matches(pod)) {
                List<ApiDTO> dtos = cache.get(key);
                if (dtos != null) {
                    answer.addAll(dtos);
                }
            }
        }
        completeMissingFullUrls(answer);
        return answer;
    }

    /**
     * When we created the APIs we may not have had the {@link #urlPrefix} so we may not have completed the full URLs
     * for things like swagger. So lets fix them up now
     *
     * @param apis
     */
    protected void completeMissingFullUrls(List<ApiDTO> apis) {
        for (ApiDTO api : apis) {
            String swaggerUrl = api.getSwaggerUrl();
            String swaggerPath = api.getSwaggerPath();
            if (Strings.isNotBlank(swaggerPath) && Strings.isNullOrBlank(swaggerUrl)
                    && Strings.isNotBlank(urlPrefix)) {
                swaggerUrl = urlPathJoin(urlPrefix, swaggerPath);
                api.setSwaggerUrl(swaggerUrl);
            }
        }
    }

    public ApiSnapshot getSnapshot() {
        ApiSnapshot snapshot = snapshotCache.get();
        if (snapshot == null) {
            snapshot = refreshSnapshot();
        }
        return snapshot;
    }

    public void addApisForPod(ApiSnapshot snapshot, Pod pod, List<ApiDTO> list) {
        String host = KubernetesHelper.getHost(pod);
        List<Container> containers = KubernetesHelper.getContainers(pod);
        for (Container container : containers) {
            J4pClient jolokia = clients.clientForContainer(host, container, pod);
            if (jolokia != null) {
                List<ApiDTO> apiDTOs = findServices(snapshot, pod, container, jolokia);
                list.addAll(apiDTOs);
            }
        }
    }

    public List<ApiDTO> findApisOnServices(String selector) {
        ApiSnapshot snapshot = getSnapshot();
        snapshot.setMessageContext(messageContext);
        List<ApiDTO> answer = snapshot.getServiceApis();
        completeMissingFullUrls(answer);
        return answer;
    }

    protected void pollServiceApis(ApiSnapshot snapshot) {
        List<ApiDTO> answer = new ArrayList<>();
        Map<String, Service> serviceMap = KubernetesHelper.getServiceMap(kubernetes);

        // TODO pick a pod for each service and add its APIs?
        addDiscoveredServiceApis(snapshot, serviceMap, messageContext);
    }

    protected void addDiscoveredServiceApis(ApiSnapshot snapshot, Map<String, Service> serviceMap,
            MessageContext messageContext) {
        CloseableHttpClient client = createHttpClient();
        try {
            Set<Map.Entry<String, Service>> entries = serviceMap.entrySet();
            for (Map.Entry<String, Service> entry : entries) {
                String key = entry.getKey();
                Service service = entry.getValue();
                String id = getName(service);
                String url = KubernetesHelper.getServiceURL(service);
                if (Strings.isNotBlank(url)) {
                    // lets check if we've not got this service already
                    if (!apiExistsForUrl(snapshot.getServiceApis(), url)) {
                        tryFindApis(client, snapshot, service, url, messageContext);
                    }
                }
            }
        } finally {
            Closeables.closeQuietly(client);
        }
    }

    /**
     * Returns the default endpoint finder rules
     */
    protected List<? extends EndpointFinder> createDefaultEndpointFinders() {
        return Arrays.asList(new CxfEndpointFinder(), new CamelEndpointFinder());
    }

    protected CloseableHttpClient createHttpClient() {
        return HttpClientBuilder.create().build();
    }

    protected void tryFindApis(CloseableHttpClient client, ApiSnapshot snapshot, Service service, String url,
            MessageContext messageContext) {
        String podId = null;
        String containerName = null;
        String objectName = null;
        String serviceId = getName(service);
        Map<String, String> labels = KubernetesHelper.getLabels(service.getMetadata());
        String state = "STARTED";

        int port = 0;
        String jolokiaUrl = null;
        String wadlUrl = null;
        String wsdlUrl = null;

        String path = toPath(url);
        String wadlPath = toPath(wadlUrl);
        String wsdlPath = toPath(wsdlUrl);

        List<String> swaggerPostfixes = new ArrayList<>();

        // TODO use env vars to figure out what postfix to use for what service id?
        swaggerPostfixes.add(SWAGGER_POSTFIX);
        swaggerPostfixes.add("swaggerapi");

        for (String swaggerPostfix : swaggerPostfixes) {
            String swaggerUrl = urlPathJoin(url, swaggerPostfix);
            String swaggerPath = toPath(swaggerUrl);
            try {
                boolean valid = isValidApiEndpoint(client, swaggerUrl);
                if (valid) {
                    snapshot.addServiceApi(
                            new ApiDTO(podId, serviceId, labels, containerName, objectName, path, url, port, state,
                                    jolokiaUrl, swaggerPath, swaggerUrl, wadlPath, wadlUrl, wsdlPath, wsdlUrl));
                    break;
                }
            } catch (Throwable e) {
                LOG.error("Failed to discover any APIs for " + url + ". " + e, e);
            }
        }
    }

    /**
     * Returns the path for the given URL
     */
    public static String toPath(String url) {
        if (Strings.isNullOrBlank(url)) {
            return null;
        }
        int idx = url.indexOf("://");
        if (idx <= 0) {
            idx = url.indexOf(":");
            if (idx >= 0) {
                idx++;
            } else {
                idx = 0;
            }
        } else {
            idx += 3;
        }
        idx = url.indexOf("/", idx);
        if (idx < 0) {
            return null;
        }
        return url.substring(idx);
    }

    protected boolean isValidApiEndpoint(CloseableHttpClient client, String url) {
        boolean valid = false;
        try {
            CloseableHttpResponse response = client.execute(new HttpGet(url));
            StatusLine statusLine = response.getStatusLine();
            if (statusLine != null) {
                int statusCode = statusLine.getStatusCode();
                if (statusCode >= 200 && statusCode < 300) {
                    valid = true;
                }
            }
        } catch (IOException e) {
            LOG.debug("Ignored failed request on " + url + ". " + e, e);
        }
        return valid;
    }

    protected boolean apiExistsForUrl(List<ApiDTO> apis, String url) {
        for (ApiDTO api : apis) {
            String apiUrl = api.getUrl();
            if (apiUrl != null && apiUrl.startsWith(url)) {
                return true;
            }
        }
        return false;
    }

    protected List<ApiDTO> findServices(ApiSnapshot snapshot, Pod pod, Container container, J4pClient jolokia) {
        List<ApiDTO> answer = new ArrayList<>();
        for (EndpointFinder finder : finders) {
            List<ApiDTO> apis = finder.findApis(snapshot, pod, container, jolokia);
            if (apis != null) {
                answer.addAll(apis);
            }
        }
        return answer;
    }

    public static String getHttpUrl(Pod pod, Container container, J4pClient jolokia) {
        // lets find the HTTP port
        if (container != null) {
            List<ContainerPort> ports = container.getPorts();
            for (ContainerPort port : ports) {
                Integer containerPort = port.getContainerPort();
                if (containerPort != null) {
                    String name = port.getName();
                    if (name != null) {
                        name = name.toLowerCase();
                    }

                    boolean httpPortNumber = containerPort == 80 || containerPort == 443 || containerPort == 8080
                            || containerPort == 8181;
                    boolean httpName = Objects.equals("http", name) || Objects.equals("https", name);
                    String protocolName = containerPort == 443 || Objects.equals("https", name) ? "https" : "http";

                    if (httpPortNumber || (httpName && containerPort.intValue() > 0)) {
                        PodStatus currentState = pod.getStatus();
                        if (currentState != null) {
                            String podIP = currentState.getPodIP();
                            if (Strings.isNotBlank(podIP)) {
                                return protocolName + "://" + podIP + ":" + containerPort;
                            }

                            // lets try use the host port and host name
                            String host = currentState.getHostIP();
                            Integer hostPort = port.getHostPort();
                            if (Strings.isNotBlank(host) && hostPort != null) {
                                return protocolName + "://" + host + ":" + hostPort;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    public static boolean booleanAttribute(Map map, String propertyName) {
        Object value = map.get(propertyName);
        if (value instanceof Boolean) {
            Boolean b = (Boolean) value;
            return b.booleanValue();
        } else {
            return false;
        }
    }

    public ApiDeclaration getSwaggerForPodAndContainer(PodAndContainerId key) {
        return getSnapshot().getSwaggerForPodAndContainer(key);
    }

    public MessageContext getMessageContext() {
        return messageContext;
    }

    public void setMessageContext(MessageContext messageContext) {
        this.messageContext = messageContext;
    }

    public void setUrlPrefix(String urlPrefix) {
        this.urlPrefix = urlPrefix;

    }

    public String getUrlPrefix() {
        return urlPrefix;
    }
}