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

dev.galasa.kubernetes.internal.KubernetesClusterImpl Maven / Gradle / Ivy

There is a newer version: 0.34.0
Show newest version
/*
 * Licensed Materials - Property of IBM
 * 
 * (c) Copyright IBM Corp. 2020.
 */
package dev.galasa.kubernetes.internal;

import java.net.URL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;

import javax.validation.constraints.NotNull;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import dev.galasa.ICredentials;
import dev.galasa.ICredentialsToken;
import dev.galasa.framework.spi.IDynamicStatusStoreService;
import dev.galasa.framework.spi.IFramework;
import dev.galasa.framework.spi.IResourcePoolingService;
import dev.galasa.framework.spi.InsufficientResourcesAvailableException;
import dev.galasa.framework.spi.creds.CredentialsException;
import dev.galasa.framework.spi.creds.ICredentialsService;
import dev.galasa.kubernetes.KubernetesManagerException;
import dev.galasa.kubernetes.internal.properties.KubernetesCredentials;
import dev.galasa.kubernetes.internal.properties.KubernetesMaxSlots;
import dev.galasa.kubernetes.internal.properties.KubernetesNamespaces;
import dev.galasa.kubernetes.internal.properties.KubernetesNodePortProxy;
import dev.galasa.kubernetes.internal.properties.KubernetesUrl;
import dev.galasa.kubernetes.internal.properties.KubernetesValidateCertificate;
import io.kubernetes.client.custom.IntOrString;
import io.kubernetes.client.custom.Quantity;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.util.Config;

/**
 * Represents a Kubernetes Cluster
 * 
 * @author Michael Baylis
 *
 */
public class KubernetesClusterImpl {

    private final Log                        logger = LogFactory.getLog(getClass());

    private final String                     clusterId;
    private final IDynamicStatusStoreService dss;
    private final IFramework                 framework;
    
    private ApiClient                        apiClient;

    public KubernetesClusterImpl(String clusterId, IDynamicStatusStoreService dss, IFramework framework) {
        this.clusterId = clusterId;     
        this.dss       = dss;
        this.framework = framework;
    }

    public String getId() {
        return this.clusterId;
    }

    /**
     * Return the current availability of namespaces in the cluster
     * 
     * @return a percentage of available namespaces from 0.0-1.0, or null if there is no availability
     * @throws KubernetesManagerException there is problem accessing the CPS or DSS 
     */
    public Float getAvailability() throws KubernetesManagerException {

        try {
            int maxSlots = KubernetesMaxSlots.get(this);
            int currentSlots = 0;
            String sCurrentSlots = dss.get("cluster." + this.clusterId + ".current.slots");
            if (sCurrentSlots != null) {
                currentSlots = Integer.parseInt(sCurrentSlots);
            }

            if (currentSlots >= maxSlots) { 
                return null; // so fuzzy floats don't get involved
            }

            return 1.0f - (((float)currentSlots) / ((float)maxSlots));
        } catch (Exception e) {
            throw new KubernetesManagerException("Unable to determine current slot count for cluster " + this.clusterId, e);
        }
    }

    /**
     * Allocate a Namespace Object and set the fields in the DSS
     * 
     * @return A Namespace object or null if there is no room
     */
    public KubernetesNamespaceImpl allocateNamespace(String namespaceTag) {
        try {
            IResourcePoolingService pooling = this.framework.getResourcePoolingService();
            String runName = this.framework.getTestRunName();

            ArrayList rejectedNamespaces = new ArrayList<>();

            String dssKeyPrefix = "cluster." + this.clusterId + ".namespace.";

            List definedNamespaces = KubernetesNamespaces.get(this);

            String selectedNamespace = null;
            while(selectedNamespace == null) {
                // TODO ask pooling to return what it can.   can then set to 10, 1
                List possibleNamespaces = pooling.obtainResources(definedNamespaces, rejectedNamespaces, 1, 1, dss, dssKeyPrefix);

                if (possibleNamespaces.isEmpty()) { // there are no available namespaces
                    return null;
                }

                for(String possibleNamespace : possibleNamespaces) {
                    String namespacePrefix = dssKeyPrefix + possibleNamespace;
                    //*** First reserve the name
                    HashMap otherValues = new HashMap<>();
                    otherValues.put(namespacePrefix + ".run", runName);
                    otherValues.put(namespacePrefix + ".allocated", Instant.now().toString());
                    if (!dss.putSwap(namespacePrefix, null, "allocating", otherValues)) {
                        rejectedNamespaces.add(possibleNamespace);
                        continue; //*** Unable to reserve this name,  add to rejected and try next
                    }

                    //*** Now we have a namespace,  increase the slot count
                    while(true) { //*** have to loop around incase another test changed the current slot count
                        int maxSlots = KubernetesMaxSlots.get(this);
                        int currentSlots = 0;
                        String sCurrentSlots = dss.get("cluster." + this.clusterId + ".current.slots");
                        if (sCurrentSlots != null) {
                            currentSlots = Integer.parseInt(sCurrentSlots);
                        }

                        if (currentSlots >= maxSlots) {
                            dss.deletePrefix(namespacePrefix); // Clear the reserved namespace
                            return null; // no availability
                        }

                        currentSlots++;
                        HashMap slotOtherValues = new HashMap<>();
                        slotOtherValues.put(namespacePrefix, "active");
                        slotOtherValues.put("slot.run." + runName + ".cluster." + this.clusterId + ".namespace." + possibleNamespace, "active");
                        slotOtherValues.put("slot.run." + runName + ".cluster." + this.clusterId + ".namespace." + possibleNamespace + ".tag", namespaceTag);
                        if (dss.putSwap("cluster." + this.clusterId + ".current.slots", sCurrentSlots, Integer.toString(currentSlots), slotOtherValues)) {
                            selectedNamespace = possibleNamespace;
                            break;
                        }
                    }
                    
                    if (selectedNamespace != null) {
                        break;
                    }
                }
            }
            
            KubernetesNamespaceImpl newNamespace = new KubernetesNamespaceImpl(this, selectedNamespace, namespaceTag, this.framework, this.dss);
            newNamespace.initialiseNamespace();
            return newNamespace;
        } catch(InsufficientResourcesAvailableException e) {
            return null;
        } catch(Exception e) {
            logger.warn("Problem allocating namespace",e);
            return null;
        }
    }
    
    /**
     * Create an APIClient for the Cluster.  Can't use the default way of doing this as we 
     * could be talking to two or clusters at the same time.
     * 
     * @return An APIClient.  never null
     * @throws KubernetesManagerException - If there is a problem with authentication or communication
     */
    @NotNull
    public synchronized ApiClient getApi() throws KubernetesManagerException {
        if (this.apiClient != null) {
            return this.apiClient;
        }
        
        URL url = KubernetesUrl.get(this);
        boolean validateCertificate = KubernetesValidateCertificate.get(this);
        String credentialsId = KubernetesCredentials.get(this);
        
        ICredentials credentials = null;
        try {
            ICredentialsService creds = this.framework.getCredentialsService();
            credentials = creds.getCredentials(credentialsId);
        } catch (CredentialsException e) {
            throw new KubernetesManagerException("Problem accessing credentials " + credentialsId, e);
        }
        
        if (credentials == null) {
            throw new KubernetesManagerException("Credentials " + credentialsId + " are missing");
        }
        
        if (!(credentials instanceof ICredentialsToken)) {
            throw new KubernetesManagerException("Credentials " + credentialsId + " is not a token credentials");
        }
        
        
        try {
            this.apiClient = Config.fromToken(url.toString(), new String(((ICredentialsToken)credentials).getToken()), validateCertificate);
            //TODO do, raise issue because Quantity is not being serialized properly
            applyNewGson(this.apiClient);
            this.apiClient.setDebugging(false);
            
            return this.apiClient;
        } catch(Exception e) {
            throw new KubernetesManagerException("Unable the initialise the Kubernetes API Client", e);
        }
        
    }

    /**
     * For some reason, v7 of the client does not serialize Quantity or IntOrString.  Should raise an issue
     * but in the meantime...
     * 
     * @param apiClient The APClient to rework
     */
    private static void applyNewGson(ApiClient apiClient) {
        
        JSON json = apiClient.getJSON();
        Gson gson = json.getGson();
        
        GsonBuilder newGsonBuilder = JSON.createGson();
        newGsonBuilder.registerTypeAdapter(Date.class, gson.getAdapter(Date.class));
        newGsonBuilder.registerTypeAdapter(java.sql.Date.class, gson.getAdapter(java.sql.Date.class));
        newGsonBuilder.registerTypeAdapter(DateTime.class, gson.getAdapter(DateTime.class));
        newGsonBuilder.registerTypeAdapter(LocalDate.class, gson.getAdapter(LocalDate.class));
        newGsonBuilder.registerTypeAdapter(byte[].class, gson.getAdapter(byte[].class));
        newGsonBuilder.registerTypeAdapter(Quantity.class, new Quantity.QuantityAdapter());
        newGsonBuilder.registerTypeAdapter(IntOrString.class, new IntOrString.IntOrStringAdapter());
        Gson newGson = newGsonBuilder.create();
        
        json.setGson(newGson);   
    }

    /**
     * Retrieve the hostname that should be used to access nodeports.
     * 
     * @return The hostname, will default to the API hostname
     * @throws KubernetesManagerException If there is a problem with the CPS
     */
    @NotNull
    public String getNodePortProxyHostname() throws KubernetesManagerException {
        return KubernetesNodePortProxy.get(this);
    }
    
    
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy