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

org.jgroups.protocols.kubernetes.Client Maven / Gradle / Ivy

The newest version!
package org.jgroups.protocols.kubernetes;

import mjson.Json;
import org.jgroups.logging.Log;
import org.jgroups.protocols.kubernetes.stream.StreamProvider;
import org.jgroups.util.Util;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

import static org.jgroups.protocols.kubernetes.Utils.openStream;
import static org.jgroups.protocols.kubernetes.Utils.urlencode;

/**
 * @author Ales Justin
 */
public class Client {
    protected final String              masterUrl;
    protected final Map headers;
    protected final int                 connectTimeout;
    protected final int                 readTimeout;
    protected final int                 operationAttempts;
    protected final long                operationSleep;
    protected final StreamProvider      streamProvider;
    protected final String              info;
    protected final Log                 log;

    public Client(String masterUrl, Map headers, int connectTimeout, int readTimeout, int operationAttempts,
                  long operationSleep, StreamProvider streamProvider, Log log) {
        this.masterUrl = masterUrl;
        this.headers = headers;
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
        this.operationAttempts = operationAttempts;
        this.operationSleep = operationSleep;
        this.streamProvider = streamProvider;
        this.log=log;
        Map maskedHeaders=new TreeMap<>();
        if (headers != null) {
            for (Map.Entry header : headers.entrySet()) {
                String key = header.getKey();
                String value = header.getValue();
                if ("Authorization".equalsIgnoreCase(key) && value != null)
                    value = "#MASKED:" + value.length() + "#";
                maskedHeaders.put(key, value);
            }
        }
        info=String.format("%s[masterUrl=%s, headers=%s, connectTimeout=%s, readTimeout=%s, operationAttempts=%s, " +
                             "operationSleep=%s, streamProvider=%s]",
                           getClass().getSimpleName(), masterUrl, maskedHeaders, connectTimeout, readTimeout,
                           operationAttempts, operationSleep, streamProvider);
    }

    public String info() {
        return info;
    }

    protected String fetchFromKubernetes(String op, String namespace, String labels, boolean dump_requests) throws Exception {
        String url = masterUrl;
        if(namespace != null && !namespace.isEmpty())
            url = url + "/namespaces/" + urlencode(namespace);
        url = url + "/" + op;
        if(labels != null && !labels.isEmpty())
            url = url + "?labelSelector=" + urlencode(labels);

        InputStream stream=null;
        String retval=null;
        try {
            stream=openStream(url, headers, connectTimeout, readTimeout, operationAttempts, operationSleep, streamProvider);
            retval=Util.readContents(stream);
            if(dump_requests)
                System.out.printf("--> %s\n<-- %s\n", url, retval);
            return retval;
        }
        catch(Throwable t) {
            retval=t.getMessage();
            if(dump_requests)
                System.out.printf("--> %s\n<-- ERROR: %s\n", url, t.getMessage());
            throw t;
        }
        finally {
            Util.close(stream);
        }
    }



    public List getPods(String namespace, String labels, boolean dump_requests) throws Exception {
        String result = fetchFromKubernetes("pods", namespace, labels, dump_requests);
        if(result == null)
            return Collections.emptyList();
        return parseJsonResult(result, namespace, labels);
    }

    /**
     * get pod group during Rolling Update
     * @param pod - json returned by k8s
     * @return
     */
    String getPodGroup(Json pod) {
        Json meta = Optional.ofNullable(pod.at("metadata")).orElse(null);
        Json labels = Optional.ofNullable(meta)
                .map(podMetadata -> podMetadata.at("labels"))
                .orElse(null);

        // This works for Deployment Config
        String group = Optional.ofNullable(labels)
                .map(l -> l.at("pod-template-hash"))
                .map(Json::asString)
                .orElse(null);

        if (group == null) {
            // Ok, maybe, it's a Deployment and has a valid deployment flag?
            group = Optional.ofNullable(labels)
                    .map(l -> l.at("deployment"))
                    .map(Json::asString)
                    .orElse(null);
        }

        if (group == null) {
            // Final check, maybe it's a StatefulSet?
            group = Optional.ofNullable(labels)
                    .map(l -> l.at("controller-revision-hash"))
                    .map(Json::asString)
                    .orElse(null);
        }

        log.debug("pod %s, group %s", Optional.ofNullable(meta)
                .map(m -> m.at("name"))
                .map(Json::asString)
                .orElse(null), group);
        return group;
    }

    protected List parseJsonResult(String input, String namespace, String labels) {
        if(input == null)
            return Collections.emptyList();
        Json json=Json.read(input);

        if(json == null || !json.isObject()) {
            log.error("JSON is not a map: %s", json);
            return Collections.emptyList();
        }

        if(!json.has("items")) {
            log.error("JSON object is missing property \"items\": %s", json);
            return Collections.emptyList();
        }
        List items=json.at("items").asJsonList();
        List pods=new ArrayList<>();
        for(Json obj: items) {
            String parentDeployment = getPodGroup(obj);
            String name = Optional.ofNullable(obj.at("metadata"))
                  .map(podMetadata -> podMetadata.at("name"))
                  .map(Json::asString)
                  .orElse(null);
            Json podStatus = Optional.ofNullable(obj.at("status")).orElse(null);
            String podIP = null;
            if(podStatus != null) {
                podIP = Optional.ofNullable(podStatus.at("podIP"))
                  .map(Json::asString)
                  .orElse(null);
            }
            boolean running = podRunning(podStatus);
            if(podIP == null) {
                log.trace("Skipping pod %s since it's IP is %s", name, podIP);
            } else {
                pods.add(new Pod(name, podIP, parentDeployment, running));
            }
        }
        log.trace("getPods(%s, %s) = %s", namespace, labels, pods);
        return pods;
    }
    
    /**
     * Helper method to determine if a pod is considered running or not.
     * 
     * @param podStatus a Json object expected to contain the "status" object of a pod
     * @return true if the pod is considered available, false otherwise
     */
    protected boolean podRunning(Json podStatus) {
        if(podStatus == null) {
            return false;
        }
        
        /*
         * A pod can only be considered 'running' if the following conditions are all true:
         * 1. status.phase == "Running",
         * 2. status.message is Undefined (does not exist)
         * 3. status.reason is Undefined (does not exist)
         * 4. all of status.containerStatuses[*].ready == true
         * 5. for conditions[*].type == "Ready" conditions[*].status must be "True" 
         */
        // walk through each condition step by step
        // 1 status.phase
        log.trace("Determining pod status");
        String phase = Optional.ofNullable(podStatus.at("phase"))
                .map(Json::asString)
                .orElse("not running");
        log.trace("  status.phase=%s", phase);
        if(!phase.equalsIgnoreCase("Running")) {
            return false;
        }
        // 2. and 3. status.message and status.reason
        String statusMessage = Optional.ofNullable(podStatus.at("message"))
                .map(Json::asString)
                .orElse(null);
        String statusReason = Optional.ofNullable(podStatus.at("reason"))
                .map(Json::asString)
                .orElse(null);
        log.trace("  status.message=%s and status.reason=%s", statusMessage, statusReason);
        if(statusMessage != null || statusReason != null) {
            return false;
        }
        // 4. status.containerStatuses.ready
        List containerStatuses = Optional.ofNullable(podStatus.at("containerStatuses"))
                .map(Json::asJsonList)
                .orElse(Collections.emptyList());
        boolean ready = true;
        // if we have no containerStatuses, we don't check for it and consider this condition as passed
        for(Json containerStatus: containerStatuses) {
            ready = ready && containerStatus.at("ready").asBoolean();
        }
        log.trace("  containerStatuses[].status of all container is %s", Boolean.toString(ready));
        if(!ready) {
            return false;
        }
        // 5. ready condition must be "True"
        Boolean readyCondition = Boolean.FALSE;
        List conditions = podStatus.at("conditions").asJsonList();
        // walk through all the conditions and find type=="Ready" and get the value of the status property
        for(Json condition: conditions) {
            String type = condition.at("type").asString();
            if(type.equalsIgnoreCase("Ready")) {
                readyCondition = new Boolean(condition.at("status").asString());
            }
        }
        log.trace(  "conditions with type==\"Ready\" has status property value = %s", readyCondition.toString());
        if(!readyCondition.booleanValue()) {
            return false;
        }
        return true;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy