org.wso2.carbon.apimgt.core.impl.ServiceDiscovererKubernetes.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.carbon.apimgt.core.impl.ServiceDiscovererKubernetes.java

Source

/*
 * Copyright (c) 2017, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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 org.wso2.carbon.apimgt.core.impl;

import io.fabric8.kubernetes.api.model.LoadBalancerIngress;
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServiceSpec;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.openshift.client.DefaultOpenShiftClient;
import io.fabric8.openshift.client.OpenShiftClient;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.apimgt.core.exception.APIManagementException;
import org.wso2.carbon.apimgt.core.exception.APIMgtDAOException;
import org.wso2.carbon.apimgt.core.exception.ExceptionCodes;
import org.wso2.carbon.apimgt.core.exception.ServiceDiscoveryException;
import org.wso2.carbon.apimgt.core.models.Endpoint;
import org.wso2.carbon.apimgt.core.util.APIFileUtils;
import org.wso2.carbon.apimgt.core.util.APIMgtConstants;
import org.wso2.carbon.apimgt.core.util.APIMgtConstants.ServiceDiscoveryConstants;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Kubernetes and OpenShift implementation of Service Discoverer
 */
public class ServiceDiscovererKubernetes extends ServiceDiscoverer {
    private final Logger log = LoggerFactory.getLogger(ServiceDiscovererKubernetes.class);

    //Constants that are also used in the configuration as keys of key-value pairs
    public static final String MASTER_URL = "masterUrl";
    public static final String CA_CERT_PATH = "CACertPath";
    public static final String INCLUDE_CLUSTER_IPS = "includeClusterIPs";
    public static final String INCLUDE_EXTERNAL_NAME_SERVICES = "includeExternalNameServices";
    public static final String EXTERNAL_SA_TOKEN_FILE_NAME = "externalSATokenFileName";
    public static final String POD_MOUNTED_SA_TOKEN_FILE_PATH = "podMountedSATokenFilePath";

    private static final String CLUSTER_IP = "ClusterIP";
    private static final String NODE_PORT = "NodePort";
    private static final String EXTERNAL_NAME = "ExternalName";
    private static final String LOAD_BALANCER = "LoadBalancer";
    private static final String EXTERNAL_IP = "ExternalIP";
    private static final String TRY_KUBE_CONFIG = "kubernetes.auth.tryKubeConfig";
    private static final String TRY_SERVICE_ACCOUNT = "kubernetes.auth.tryServiceAccount";

    private OpenShiftClient client;
    private Boolean includeClusterIP;
    private Boolean includeExternalNameTypeServices;

    /**
     * Initializes OpenShiftClient (extended KubernetesClient) and sets the necessary parameters
     *
     * @param implParameters implementation parameters added by the super class #initImpl(java.util.Map) method
     * @throws ServiceDiscoveryException if an error occurs while initializing the client
     */
    @Override
    public void initImpl(Map<String, String> implParameters) throws ServiceDiscoveryException {
        try {
            setClient(new DefaultOpenShiftClient(buildConfig(implParameters)));
        } catch (KubernetesClientException | APIMgtDAOException e) {
            String msg = "Error occurred while creating Kubernetes client";
            throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_INITIALIZING_SERVICE_DISCOVERY);
        } catch (ArrayIndexOutOfBoundsException e) {
            String msg = "Error occurred while reading filtering criteria from the configuration";
            throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_INITIALIZING_SERVICE_DISCOVERY);
        }
        includeClusterIP = Boolean.parseBoolean(implParameters.get(INCLUDE_CLUSTER_IPS));
        includeExternalNameTypeServices = Boolean.parseBoolean(implParameters.get(INCLUDE_EXTERNAL_NAME_SERVICES));
    }

    /**
     * Builds the Config required by DefaultOpenShiftClient
     * Also sets the system properties
     *  (1) to not refer .kube/config file and
     *  (2) the client to use service account procedure to get authenticated and authorised
     *
     * @return {@link io.fabric8.kubernetes.client.Config} object to build the client
     * @throws ServiceDiscoveryException if an error occurs while building the config using externally stored token
     */
    private Config buildConfig(Map<String, String> implParameters)
            throws ServiceDiscoveryException, APIMgtDAOException {

        System.setProperty(TRY_KUBE_CONFIG, "false");
        System.setProperty(TRY_SERVICE_ACCOUNT, "true");

        /*
         *  Common to both situations,
         *      - Token found inside APIM pod
         *      - Token stored in APIM resources/security folder }
         */
        ConfigBuilder configBuilder = new ConfigBuilder().withMasterUrl(implParameters.get(MASTER_URL))
                .withCaCertFile(implParameters.get(CA_CERT_PATH));

        /*
         *  Check if a service account token File Name is given in the configuration
         *      - if not : assume APIM is running inside a pod and look for the pod's token
         */
        String externalSATokenFileName = implParameters.get(EXTERNAL_SA_TOKEN_FILE_NAME);
        if (StringUtils.isEmpty(externalSATokenFileName)) {
            log.debug("Looking for service account token in " + POD_MOUNTED_SA_TOKEN_FILE_PATH);
            String podMountedSAToken = APIFileUtils
                    .readFileContentAsText(implParameters.get(POD_MOUNTED_SA_TOKEN_FILE_PATH));
            return configBuilder.withOauthToken(podMountedSAToken).build();
        } else {
            log.info("Using externally stored service account token");
            return configBuilder.withOauthToken(resolveToken("encrypted" + externalSATokenFileName)).build();
        }
    }

    /**
     * Get the token after decrypting using {@link FileEncryptionUtility#readFromEncryptedFile(java.lang.String)}
     *
     * @return service account token
     * @throws ServiceDiscoveryException if an error occurs while resolving the token
     */
    private String resolveToken(String encryptedTokenFileName) throws ServiceDiscoveryException {
        String token;
        try {
            String externalSATokenFilePath = System.getProperty(FileEncryptionUtility.CARBON_HOME)
                    + FileEncryptionUtility.SECURITY_DIR + File.separator + encryptedTokenFileName;
            token = FileEncryptionUtility.getInstance().readFromEncryptedFile(externalSATokenFilePath);
        } catch (APIManagementException e) {
            String msg = "Error occurred while resolving externally stored token";
            throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_INITIALIZING_SERVICE_DISCOVERY);
        }
        return token;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Endpoint> listServices() throws ServiceDiscoveryException {
        List<Endpoint> endpointList = new ArrayList<>();
        if (client != null) {
            log.debug("Looking for services in all namespaces");
            try {
                List<Service> serviceList = client.services().inNamespace(null).list().getItems();
                addServicesToEndpointList(serviceList, endpointList);
            } catch (KubernetesClientException | MalformedURLException e) {
                String msg = "Error occurred while trying to list services using Kubernetes client";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            }
        }
        return endpointList;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Endpoint> listServices(String namespace) throws ServiceDiscoveryException {
        List<Endpoint> endpointList = new ArrayList<>();
        if (client != null) {
            log.debug("Looking for services in namespace {}", namespace);
            try {
                List<Service> serviceList = client.services().inNamespace(namespace).list().getItems();
                addServicesToEndpointList(serviceList, endpointList);
            } catch (KubernetesClientException | MalformedURLException e) {
                String msg = "Error occurred while trying to list services using Kubernetes client";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            }
        }
        return endpointList;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Endpoint> listServices(String namespace, Map<String, String> criteria)
            throws ServiceDiscoveryException {
        List<Endpoint> endpointList = new ArrayList<>();
        if (client != null) {
            log.debug("Looking for services, with the specified labels, in namespace {}", namespace);
            try {
                List<Service> serviceList = client.services().inNamespace(namespace).withLabels(criteria).list()
                        .getItems();
                addServicesToEndpointList(serviceList, endpointList);
            } catch (KubernetesClientException | MalformedURLException e) {
                String msg = "Error occurred while trying to list services using Kubernetes client";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            } catch (NoSuchMethodError e) {
                String msg = "Filtering criteria in the deployment yaml includes unwanted characters";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            }
        }
        return endpointList;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Endpoint> listServices(Map<String, String> criteria) throws ServiceDiscoveryException {
        List<Endpoint> endpointList = new ArrayList<>();
        if (client != null) {
            log.debug("Looking for services, with the specified labels, in all namespaces");
            try {
                //namespace has to be set to null to check all allowed namespaces
                List<Service> serviceList = client.services().inNamespace(null).withLabels(criteria).list()
                        .getItems();
                addServicesToEndpointList(serviceList, endpointList);
            } catch (KubernetesClientException | MalformedURLException e) {
                String msg = "Error occurred while trying to list services using Kubernetes client";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            } catch (NoSuchMethodError e) {
                String msg = "Filtering criteria in the deployment yaml includes unwanted characters";
                throw new ServiceDiscoveryException(msg, e, ExceptionCodes.ERROR_WHILE_TRYING_TO_DISCOVER_SERVICES);
            }
        }
        return endpointList;
    }

    /**
     * For each service in {@code serviceList} list, methods are called to add endpoints of different types,
     * for each of service's ports
     *
     * @param serviceList   filtered list of services
     */
    private void addServicesToEndpointList(List<Service> serviceList, List<Endpoint> endpointList)
            throws MalformedURLException {
        for (Service service : serviceList) {
            //Set the parameters that does not change with the service port
            String serviceName = service.getMetadata().getName();
            String namespace = service.getMetadata().getNamespace();
            Map<String, String> labelsMap = service.getMetadata().getLabels();
            String labels = (labelsMap != null) ? labelsMap.toString() : "";
            ServiceSpec serviceSpec = service.getSpec();
            String serviceType = serviceSpec.getType();

            if (includeExternalNameTypeServices && EXTERNAL_NAME.equals(serviceType)) {
                // Since only a "ExternalName" type service can have an "externalName" (the alias in kube-dns)
                addExternalNameEndpoint(serviceName, serviceSpec.getExternalName(), namespace, labels,
                        endpointList);
            }

            for (ServicePort servicePort : serviceSpec.getPorts()) {
                String protocol = servicePort.getName();
                if (APIMgtConstants.HTTP.equals(protocol) || APIMgtConstants.HTTPS.equals(protocol)) {
                    int port = servicePort.getPort();

                    if (includeClusterIP && !EXTERNAL_NAME.equals(serviceType)) {
                        // Since almost every service has a cluster IP, except for ExternalName type
                        addClusterIPEndpoint(serviceName, serviceSpec.getClusterIP(), port, protocol, namespace,
                                labels, endpointList);
                    }
                    if (NODE_PORT.equals(serviceType) || LOAD_BALANCER.equals(serviceType)) {
                        // Because both "NodePort" and "LoadBalancer" types of services have "NodePort" type URLs
                        addNodePortEndpoint(serviceName, servicePort.getNodePort(), protocol, namespace, labels,
                                endpointList);
                    }
                    if (LOAD_BALANCER.equals(serviceType)) {
                        // Since only "LoadBalancer" type services have "LoadBalancer" type URLs
                        addLoadBalancerEndpoint(serviceName, service, port, protocol, namespace, labels,
                                endpointList);
                    }
                    // A Special case (can be any of the service types above)
                    addExternalIPEndpoint(serviceName, serviceSpec.getExternalIPs(), port, protocol, namespace,
                            labels, endpointList);
                } else if (log.isDebugEnabled()) {
                    log.debug("Service:{} Namespace:{} Port:{}/{}  Application level protocol not defined.",
                            serviceName, namespace, servicePort.getPort(), protocol);
                }
            }
        }
    }

    /**
     * Adds the ExternalName Endpoint to the endpointList
     *
     * @param serviceName    name of the service the endpoint belongs to
     * @param externalName   externalName found in service's spec
     * @param namespace      namespace of the service
     * @param labels         labels of the service
     * @param endpointList   endpointList which the endpoint has to be added to
     * @throws MalformedURLException if protocol unknown or URL spec is null, therefore will not get thrown
     */
    private void addExternalNameEndpoint(String serviceName, String externalName, String namespace, String labels,
            List<Endpoint> endpointList) throws MalformedURLException {
        URL url = new URL("http://" + externalName);
        endpointList.add(constructEndpoint(serviceName, namespace, "http", EXTERNAL_NAME, url, labels));
    }

    /**
     * Adds the ClusterIP Endpoint to the endpointList
     *
     * @param serviceName   name of the service the endpoint belongs to
     * @param clusterIP     ClusterIP of the service
     * @param port          port number
     * @param protocol      whether http or https
     * @param namespace     namespace of the service
     * @param labels        labels of the service
     * @param endpointList  endpointList which the endpoint has to be added to
     * @throws MalformedURLException  if protocol unknown, therefore will not get thrown
     */
    private void addClusterIPEndpoint(String serviceName, String clusterIP, int port, String protocol,
            String namespace, String labels, List<Endpoint> endpointList) throws MalformedURLException {
        URL url = new URL(protocol, clusterIP, port, "");
        endpointList.add(constructEndpoint(serviceName, namespace, protocol, CLUSTER_IP, url, labels));
    }

    /**
     * Adds the NodePort Endpoint to the endpointList
     *
     * @param serviceName   name of the service the endpoint belongs to
     * @param nodePort      nodePort of the service
     * @param protocol      whether http or https
     * @param namespace     namespace of the service
     * @param labels        labels of the service
     * @param endpointList  endpointList which the endpoint has to be added to
     * @throws MalformedURLException  if protocol unknown, therefore will not get thrown
     */
    private void addNodePortEndpoint(String serviceName, int nodePort, String protocol, String namespace,
            String labels, List<Endpoint> endpointList) throws MalformedURLException {
        String nodeIP = client.getMasterUrl().getHost();
        URL url = new URL(protocol, nodeIP, nodePort, "");
        endpointList.add(constructEndpoint(serviceName, namespace, protocol, NODE_PORT, url, labels));
    }

    /**
     * Adds the LoadBalancer Endpoint to the endpointList
     *
     * @param serviceName   name of the service the endpoint belongs to
     * @param service       service object instance
     * @param port          port number
     * @param protocol      whether http or https
     * @param namespace     namespace of the service
     * @param labels        labels of the service
     * @param endpointList  endpointList which the endpoint has to be added to
     * @throws MalformedURLException  if protocol unknown, therefore will not get thrown
     */
    private void addLoadBalancerEndpoint(String serviceName, Service service, int port, String protocol,
            String namespace, String labels, List<Endpoint> endpointList) throws MalformedURLException {
        List<LoadBalancerIngress> loadBalancerIngresses = service.getStatus().getLoadBalancer().getIngress();
        if (!loadBalancerIngresses.isEmpty()) {
            for (LoadBalancerIngress loadBalancerIngress : loadBalancerIngresses) {
                String hostname = loadBalancerIngress.getHostname();
                String host = (hostname == null || "".equals(hostname)) ? loadBalancerIngress.getIp() : hostname;
                URL url = new URL(protocol, host, port, "");
                endpointList.add(constructEndpoint(serviceName, namespace, protocol, LOAD_BALANCER, url, labels));
            }
        } else {
            log.debug("Service:{}  Namespace:{}  Port:{}/{} has no loadBalancer ingresses available.", serviceName,
                    namespace, port, protocol);
        }
    }

    /**
     * Adds the ExternalIP Endpoint to the endpointList
     *
     * @param serviceName   name of the service the endpoint belongs to
     * @param externalIPs   service object instance
     * @param port          port number
     * @param protocol      whether http or https
     * @param namespace     namespace of the service
     * @param labels        labels of the service
     * @param endpointList  endpointList which the endpoint has to be added to
     * @throws MalformedURLException  if protocol unknown, therefore will not get thrown
     */
    private void addExternalIPEndpoint(String serviceName, List<String> externalIPs, int port, String protocol,
            String namespace, String labels, List<Endpoint> endpointList) throws MalformedURLException {
        if (!externalIPs.isEmpty()) {
            for (String externalIP : externalIPs) {
                URL url = new URL(protocol, externalIP, port, "");
                endpointList.add(constructEndpoint(serviceName, namespace, protocol, EXTERNAL_IP, url, labels));
            }
        }
    }

    /**
     * Populates the necessary parameters required by the Endpoint object,
     * and call buildEndpoint method in the super class.
     *
     * @param serviceName  service name as defined in the cluster
     * @param namespace    service's namespace as defined in the cluster
     * @param portType     whether http or https
     * @param urlType      type of the service URL (eg. NodePort)
     * @param url          endpoint URL
     * @param labels       service's labels as defined in the cluster
     * @return {@link org.wso2.carbon.apimgt.core.models.Endpoint} object
     */
    private Endpoint constructEndpoint(String serviceName, String namespace, String portType, String urlType,
            URL url, String labels) {
        JSONObject endpointConfig = new JSONObject();
        endpointConfig.put("serviceUrl", url.toString());
        endpointConfig.put("urlType", urlType);
        endpointConfig.put(ServiceDiscoveryConstants.NAMESPACE, namespace);
        endpointConfig.put(ServiceDiscoveryConstants.CRITERIA, labels);

        String endpointIndex = String.format("kubernetes-%d", this.serviceEndpointIndex);

        return buildEndpoint(endpointIndex, serviceName, endpointConfig.toString(), 1000L, portType,
                "{\"enabled\": false}", APIMgtConstants.GLOBAL_ENDPOINT);
    }

    void setClient(OpenShiftClient openShiftClient) {
        this.client = openShiftClient;
    }

    void setIncludeClusterIP(Boolean includeClusterIP) {
        this.includeClusterIP = includeClusterIP;
    }

    void setIncludeExternalNameTypeServices(Boolean includeExternalNameTypeServices) {
        this.includeExternalNameTypeServices = includeExternalNameTypeServices;
    }
}