com.redhat.jenkins.nodesharingfrontend.Api.java Source code

Java tutorial

Introduction

Here is the source code for com.redhat.jenkins.nodesharingfrontend.Api.java

Source

/*
 * The MIT License
 *
 * Copyright (c) Red Hat, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.redhat.jenkins.nodesharingfrontend;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.redhat.jenkins.nodesharing.ActionFailed;
import com.redhat.jenkins.nodesharing.ConfigRepo;
import com.redhat.jenkins.nodesharing.NodeDefinition;
import com.redhat.jenkins.nodesharing.RestEndpoint;
import com.redhat.jenkins.nodesharing.transport.DiscoverRequest;
import com.redhat.jenkins.nodesharing.transport.DiscoverResponse;
import com.redhat.jenkins.nodesharing.transport.Entity;
import com.redhat.jenkins.nodesharing.transport.ExecutorEntity;
import com.redhat.jenkins.nodesharing.transport.NodeStatusRequest;
import com.redhat.jenkins.nodesharing.transport.NodeStatusResponse;
import com.redhat.jenkins.nodesharing.transport.ReportUsageRequest;
import com.redhat.jenkins.nodesharing.transport.ReportUsageResponse;
import com.redhat.jenkins.nodesharing.transport.ReportWorkloadRequest;
import com.redhat.jenkins.nodesharing.transport.ReportWorkloadResponse;
import com.redhat.jenkins.nodesharing.transport.ReturnNodeRequest;
import com.redhat.jenkins.nodesharing.transport.UtilizeNodeRequest;
import com.redhat.jenkins.nodesharing.transport.UtilizeNodeResponse;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.labels.LabelAtom;
import hudson.security.ACL;
import jenkins.model.Jenkins;
import jenkins.security.NotReallyRoleSensitiveCallable;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpPost;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;
import java.util.logging.Logger;

/**
 * Receive and send REST commands from/to Orchestrator Jenkins.
 */
@Restricted(NoExternalUse.class)
public class Api {

    private static final Logger LOGGER = Logger.getLogger(Api.class.getName());
    @Nonnull
    private final ExecutorEntity.Fingerprint fingerprint;

    private final SharedNodeCloud cloud;
    private final RestEndpoint rest;
    private final String version;

    public Api(@Nonnull ConfigRepo.Snapshot snapshot, @Nonnull String configRepoUrl, @Nonnull SharedNodeCloud cloud,
            @Nonnull String jenkinsUrl) throws IllegalStateException {
        this.cloud = cloud;

        try {
            // TODO getClass().getPackage().getImplementationVersion() might work equally well
            // PJ: Not working, during JUnit phase execution there aren't made packages...
            InputStream resource = this.getClass().getClassLoader()
                    .getResourceAsStream("nodesharingfrontend.properties");
            if (resource == null) {
                version = Jenkins.getActiveInstance().pluginManager.whichPlugin(getClass()).getVersion();
            } else {
                Properties properties = new Properties();
                properties.load(resource);
                version = properties.getProperty("version");
            }
            if (version == null)
                throw new AssertionError("No version in assembly properties");
        } catch (IOException e) {
            throw new AssertionError("Cannot load assembly properties", e);
        }

        this.fingerprint = new ExecutorEntity.Fingerprint(configRepoUrl, version, jenkinsUrl);
        rest = new RestEndpoint(snapshot.getOrchestratorUrl(), "node-sharing-orchestrator",
                getRestCredential(cloud));
    }

    @Nonnull
    private UsernamePasswordCredentials getRestCredential(@Nonnull SharedNodeCloud cloud)
            throws IllegalStateException {
        String cid = cloud.getOrchestratorCredentialsId();
        UsernamePasswordCredentials cred = CredentialsMatchers.firstOrNull(CredentialsProvider
                .lookupCredentials(UsernamePasswordCredentials.class, Jenkins.getInstance(), ACL.SYSTEM),
                CredentialsMatchers.withId(cid));
        if (cred == null)
            throw new IllegalStateException(
                    "No credential found for id = " + cid + " configured in cloud " + cloud.name);
        return cred;
    }

    //// Outgoing

    /**
     * Put the queue items to Orchestrator
     */
    // Response never used as there is likely nothing to report - async request candidate
    public void reportWorkload(@Nonnull final ReportWorkloadRequest.Workload workload) {
        final ReportWorkloadRequest request = new ReportWorkloadRequest(fingerprint, workload);
        rest.executeRequest(rest.post("reportWorkload"), request, ReportWorkloadResponse.class);
    }

    /**
     * Request to discover the state of the Orchestrator.
     *
     * Note is is expected this call is made when executor is no longer part of the grid in order to complete existing
     * reservations.
     *
     * @return Discovery result.
     */
    @Nonnull
    public DiscoverResponse discover() throws ActionFailed {
        return rest.executeRequest(rest.post("discover"), new DiscoverRequest(fingerprint), DiscoverResponse.class);
    }

    /**
     * Send request to return node. No response needed.
     *
     * Note is is expected this call is made when executor is no longer part of the grid in order to complete existing
     * reservations.
     */
    public void returnNode(@Nonnull SharedNode node) {
        Computer computer = node.toComputer();
        String offlineCause = null;
        if (computer != null && computer.getOfflineCause() != null) {
            offlineCause = computer.getOfflineCause().toString();
        }
        final ReturnNodeRequest.Status status = offlineCause == null ? ReturnNodeRequest.Status.OK
                : ReturnNodeRequest.Status.FAILED;
        ReturnNodeRequest request = new ReturnNodeRequest(fingerprint, node.getHostName(), status, offlineCause);

        final HttpPost method = rest.post("returnNode");
        rest.executeRequest(method, request, new RestEndpoint.AbstractResponseHandler<Void>(method) {
            @Override
            protected boolean shouldFail(@Nonnull StatusLine sl) {
                return sl.getStatusCode() != 200 && sl.getStatusCode() != 404;
            }
        });
    }

    //// Incoming

    /**
     * Request to utilize reserved computer.
     *
     * Response codes:
     * - "200 OK" is used when the node was accepted, the node is expected to be correctly added to Jenkins by the time
     *   the request completes with the code. The code is also returned when the node is already helt by this executor.
     * - "410 Gone" when there is no longer the need for such host and orchestrator can reuse it immediately. The node must not be created.
     */
    @RequirePOST
    public void doUtilizeNode(@Nonnull final StaplerRequest req, @Nonnull final StaplerResponse rsp)
            throws IOException {
        final Jenkins jenkins = Jenkins.getActiveInstance();
        jenkins.checkPermission(RestEndpoint.RESERVE);

        UtilizeNodeRequest request = Entity.fromInputStream(req.getInputStream(), UtilizeNodeRequest.class);
        final NodeDefinition definition = NodeDefinition.create(request.getFileName(), request.getDefinition());
        if (definition == null)
            throw new AssertionError("Unknown node definition: " + request.getFileName());

        final String name = definition.getName();

        // utilizeNode call received even though the node is already being utilized
        Node node = getCollidingNode(jenkins, name);
        if (node != null) {
            new UtilizeNodeResponse(fingerprint).toOutputStream(rsp.getOutputStream());
            rsp.setStatus(HttpServletResponse.SC_OK);
            LOGGER.warning("Skipping node addition as it already exists");
            return;
        }

        // Do not accept the node when there is no load for it
        if (!isThereAWorkloadFor(jenkins, definition)) {
            rsp.setStatus(HttpServletResponse.SC_GONE);
            LOGGER.info("Skipping node addition as there isn't a workload for it");
            return;
        }

        try {
            final SharedNode newNode = cloud.createNode(definition);
            // Prevent replacing existing node due to a race condition in repeated utilizeNode calls
            Queue.withLock(new NotReallyRoleSensitiveCallable<Void, IOException>() {
                @Override
                public Void call() throws IOException {
                    Node node = getCollidingNode(jenkins, name);
                    if (node == null) {
                        jenkins.addNode(newNode);
                    } else {
                        LOGGER.warning("Skipping node addition due to race condition");
                    }
                    return null;
                }
            });

            new UtilizeNodeResponse(fingerprint).toOutputStream(rsp.getOutputStream());
            rsp.setStatus(HttpServletResponse.SC_OK);
        } catch (IllegalArgumentException e) {
            e.printStackTrace(new PrintStream(rsp.getOutputStream()));
            rsp.setStatus(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE);
        }
    }

    private boolean isThereAWorkloadFor(Jenkins jenkins, NodeDefinition definition) {
        // Nothing will be executed
        if (jenkins.isQuietingDown() || jenkins.isTerminating()) {
            return false;
        }

        Collection<LabelAtom> nodeLabels = definition.getLabelAtoms();
        for (Queue.Item item : jenkins.getQueue().getItems()) {
            // Do not schedule unrestricted items here
            if (item.getAssignedLabel() != null && item.getAssignedLabel().matches(nodeLabels)) {
                return true;
            }
        }
        return false;
    }

    private @CheckForNull Node getCollidingNode(Jenkins jenkins, String name) {
        Node node = jenkins.getNode(name);
        if (node == null)
            return null;

        Computer computer = node.toComputer();
        if (node instanceof SharedNode) {
            if (computer != null && computer.getTerminatedBy() == null) {
                return node;
            } else {
                return null; // being terminated
            }
        } else if ("com.redhat.jenkins.nodesharingbackend.ShareableNode".equals(node.getClass().getName())) {
            /* jth-test hack */
            return null;
        } else {
            throw new IllegalStateException(
                    "Node " + name + " already exists but it is a " + node.getClass().getName());
        }
    }

    /**
     * Query Executor Jenkins to report the status of shared node.
     */
    @RequirePOST
    public void doNodeStatus(@Nonnull final StaplerRequest req, @Nonnull final StaplerResponse rsp)
            throws IOException {
        Jenkins.getActiveInstance().checkPermission(RestEndpoint.RESERVE);

        NodeStatusRequest request = Entity.fromInputStream(req.getInputStream(), NodeStatusRequest.class);
        String nodeName = request.getNodeName();
        NodeStatusResponse.Status status = NodeStatusResponse.Status.NOT_FOUND;
        if (nodeName != null) // TODO Why would it be null?
            status = cloud.getNodeStatus(request.getNodeName());
        NodeStatusResponse response = new NodeStatusResponse(fingerprint, request.getNodeName(), status);
        response.toOutputStream(rsp.getOutputStream());
    }

    @RequirePOST
    public void doReportUsage(@Nonnull final StaplerRequest req, @Nonnull final StaplerResponse rsp)
            throws IOException {
        Jenkins.getActiveInstance().checkPermission(RestEndpoint.RESERVE);

        ReportUsageRequest request = Entity.fromInputStream(req.getInputStream(), ReportUsageRequest.class);
        ArrayList<String> usedNodes = new ArrayList<>();
        for (Node node : Jenkins.getActiveInstance().getNodes()) {
            if (node instanceof SharedNode) {
                SharedNode sharedNode = (SharedNode) node;
                SharedNodeCloud cloud = SharedNodeCloud.getByName(sharedNode.getId().getCloudName());
                if (cloud != null) {
                    if (request.getConfigRepoUrl().equals(cloud.getConfigRepoUrl())) {
                        usedNodes.add(sharedNode.getHostName());
                    }
                }
            }
        }

        new ReportUsageResponse(fingerprint, usedNodes).toOutputStream(rsp.getOutputStream());
    }

    /**
     * Immediately return node to orchestrator. (Nice to have feature)
     */
    @RequirePOST
    public void doImmediatelyReturnNode() {
        Jenkins.getActiveInstance().checkPermission(RestEndpoint.RESERVE);
        throw new UnsupportedOperationException("TODO");
    }
}