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

com.github.dekobon.CloudApi Maven / Gradle / Ivy

package com.github.dekobon;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dekobon.config.ConfigContext;
import com.github.dekobon.domain.Instance;
import com.github.dekobon.domain.InstanceFilter;
import com.github.dekobon.exceptions.InstanceGoneMissingException;
import com.github.dekobon.http.CloudApiConnectionFactory;
import com.github.dekobon.http.CloudApiResponseHandler;
import com.github.dekobon.http.HttpCollectionResponse;
import com.github.dekobon.http.JsonEntity;
import com.github.dekobon.json.CloudApiObjectMapper;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.*;

import static com.github.dekobon.CloudApiHttpHeaders.X_QUERY_LIMIT;
import static com.github.dekobon.CloudApiHttpHeaders.X_RESOURCE_COUNT;
import static com.github.dekobon.CloudApiUtils.firstNonNull;
import static org.apache.http.HttpStatus.*;

/**
 * @author Elijah Zupancic
 * @since 1.0.0
 */
public class CloudApi {
    private final ConfigContext config;
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private final CloudApiConnectionFactory connectionFactory;
    private final ObjectMapper mapper = new CloudApiObjectMapper();

    private final CloudApiResponseHandler> headerInstanceHandler;
    private final CloudApiResponseHandler> listInstanceHandler;
    private final CloudApiResponseHandler createInstanceHandler;
    private final CloudApiResponseHandler deleteInstanceHandler;
    private final CloudApiResponseHandler findInstanceHandler;
    private final CloudApiResponseHandler> tagsHandler;

    public CloudApi(ConfigContext config) {
        this.config = config;
        this.connectionFactory = new CloudApiConnectionFactory(config);

        this.headerInstanceHandler = new CloudApiResponseHandler<>(
                "get headers", mapper, new TypeReference>() { }, SC_OK, true
        );
        this.listInstanceHandler = new CloudApiResponseHandler<>(
                "list instances", mapper, new TypeReference>() {}, SC_OK, false
        );
        this.createInstanceHandler = new CloudApiResponseHandler<>(
                "create instance", mapper, new TypeReference() {}, SC_CREATED, false
        );
        this.deleteInstanceHandler = new CloudApiResponseHandler<>(
                "delete instance", mapper, new TypeReference() {}, SC_NO_CONTENT, false
        );
        this.findInstanceHandler = new CloudApiResponseHandler<>(
                "find instance", mapper, new TypeReference() {},
                new int[] { SC_OK, SC_GONE }, true
        );
        this.tagsHandler = new CloudApiResponseHandler<>(
                "tag instance", mapper, new TypeReference>() {}, SC_OK, false
        );
    }

    public Iterator listInstances() throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return listInstances(context);
        }
    }

    public Iterator listInstances(final CloudApiConnectionContext context) throws IOException {
        return listInstances(context, new InstanceFilter());
    }

    public Iterator listInstances(final CloudApiConnectionContext context,
                                            final InstanceFilter filter) throws IOException {
        Objects.requireNonNull(context, "Context object must be present");
        Objects.requireNonNull(context, "Filter object must be present");

        final List filterParams = FilterEncoders.urlParamsFromFilter(filter);
        final String path = String.format("/%s/machines", config.getUser());

        final HttpClient client = context.getHttpClient();

        final HttpHead head = connectionFactory.head(path, filterParams);

        /* We first perform a head request because we can use it to determine if any results are going
         * to be returned. If there are no results, we can just return an empty collection and give up. */
        final Map headHeaders = client.execute(head, headerInstanceHandler, context.getHttpContext());
        final int headResourceCount = resourceCount(headHeaders);

        // -1 indicates error, 1+ indicates values present
        if (headResourceCount == 0) {
            return Collections.emptyIterator();
        }

        final HttpGet get = connectionFactory.get(path, filterParams);

        @SuppressWarnings("unchecked")
        final HttpCollectionResponse result =
                (HttpCollectionResponse)client.execute(
                        get, listInstanceHandler, context.getHttpContext());

        final HttpResponse response = result.getResponse();

        final String resourceCountVal = response.getFirstHeader(X_RESOURCE_COUNT).getValue();
        @SuppressWarnings("ConstantConditions")
        final int resourceCount = Integer.parseInt(firstNonNull(resourceCountVal, "0"));
        final String queryLimitVal = response.getFirstHeader(X_QUERY_LIMIT).getValue();
        @SuppressWarnings("ConstantConditions")
        final int queryLimit = Integer.parseInt(firstNonNull(queryLimitVal, "1000"));

        if (resourceCount < queryLimit) {
            logger.info("Total instances: {}", result.size());
            return result.getWrapped().iterator();
        }

        // TODO: Add code for handling more instances than the query limit

        return result.iterator();
    }

    private int resourceCount(final Map headers) {
        final int UNAVAILABLE = -1;

        if (headers == null) {
            return UNAVAILABLE;
        }

        final Header header = headers.get(X_RESOURCE_COUNT);

        if (header != null) {
            final String headResourceCountVal = header.getValue();
            @SuppressWarnings("ConstantConditions")
            // We default to -1 in order to indicate an error
            final int headResourceCount = Integer.parseInt(
                    firstNonNull(headResourceCountVal, String.valueOf(UNAVAILABLE)));
            return headResourceCount;
        }

        return UNAVAILABLE;
    }

    public CloudApiConnectionContext createConnectionContext() {
        return new CloudApiConnectionContext(connectionFactory);
    }

    public Instance create(final Instance instance) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return create(context, instance);
        }
    }

    public Instance create(final CloudApiConnectionContext context,
                           final Instance instance) throws IOException {
        Objects.requireNonNull(context, "Context object must be present");
        Objects.requireNonNull(instance, "Instance must be present");
        Objects.requireNonNull(instance.getPackageId(), "Package id must be present");
        Objects.requireNonNull(instance.getImage(), "Image id must be present");

        final String path = String.format("/%s/machines", config.getUser());
        final HttpPost post = connectionFactory.post(path);

        HttpEntity entity = new JsonEntity(mapper, instance);
        post.setEntity(entity);

        final HttpClient client = context.getHttpClient();
        final Instance result = client.execute(post,
                createInstanceHandler, context.getHttpContext());

        logger.info("Created new instance: {}", result.getId());

        return result;
    }

    public void delete(final Instance instance) throws IOException {
        Objects.requireNonNull(instance, "Instance must be present");
        delete(instance.getId());
    }

    public void delete(final UUID instanceId) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            delete(context, instanceId);
        }
    }

    public void delete(final CloudApiConnectionContext context,
                       final Instance instance) throws IOException {
        Objects.requireNonNull(instance, "Instance must be present");
        delete(context, instance.getId());
    }

    public void delete(final CloudApiConnectionContext context,
                       final UUID instanceId) throws IOException {
        Objects.requireNonNull(context, "Context object must be present");
        Objects.requireNonNull(instanceId, "Instance id to be deleted must be present");

        final String path = String.format("/%s/machines/%s",
                config.getUser(), instanceId);
        final HttpDelete delete = connectionFactory.delete(path);

        final HttpClient client = context.getHttpClient();
        client.execute(delete, deleteInstanceHandler, context.getHttpContext());

        logger.info("Deleted instance: {}", instanceId);
    }

    public Instance waitForStateChange(final UUID instanceId,
                                       final String initialState,
                                       final long maxWaitTimeMs,
                                       final long waitIntervalMs) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return waitForStateChange(context, instanceId, initialState, maxWaitTimeMs, waitIntervalMs);
        }
    }

    public Instance waitForStateChange(final CloudApiConnectionContext context,
                                       final UUID instanceId,
                                       final String initialState,
                                       final long maxWaitTimeMs,
                                       final long waitIntervalMs) throws IOException {
        Objects.requireNonNull(instanceId, "Instance id must be present");
        Objects.requireNonNull(initialState, "Initial state value must be present");

        if (maxWaitTimeMs < 1) {
            throw new IllegalArgumentException("Maximum wait time must be 1 millisecond "
                    + "or greater");
        }

        Instance lastPoll = findMachineById(context, instanceId);

        if (lastPoll == null) {
            return null;
        }

        // Don't even bother sleeping if the state was never set
        if (!lastPoll.getState().equals(initialState)) {
            logger.debug("State changed from [{}] to [{}]- no longer waiting",
                    initialState,  lastPoll.getState());
            return lastPoll;
        }

        long waited = 0;

        while (true) {
            try {
                Thread.sleep(waitIntervalMs);
                waited += waitIntervalMs;
            } catch (InterruptedException e) {
                return null;
            }

            lastPoll = findMachineById(context, instanceId);

            if (lastPoll == null) {
                final String msg = String.format("The instance [%s] was successfully polled "
                                + "previously, but is no longer available. Maybe it was deleted?",
                        instanceId);
                throw new InstanceGoneMissingException(msg);
            }

            if (!lastPoll.getState().equals(initialState)) {
                logger.debug("State changed from [{}] to [{}] - no longer waiting",
                        initialState,  lastPoll.getState());
                break;
            }

            if (waited > maxWaitTimeMs) {
                logger.debug("Exceeded maximum wait time [{} ms] for state change - "
                        + "no longer waiting", maxWaitTimeMs);
                break;
            }
        }

        return lastPoll;
    }

    public Instance findMachineById(final UUID instanceId) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return findMachineById(context, instanceId);
        }
    }

    public Instance findMachineById(final CloudApiConnectionContext context,
                                    final UUID instanceId) throws IOException {
        final HttpClient client = context.getHttpClient();
        final String path = String.format("/%s/machines/%s",
                config.getUser(), instanceId);
        final HttpGet get = connectionFactory.get(path);
        return client.execute(get, findInstanceHandler, context.getHttpContext());
    }

    public Map addTags(final UUID instanceId,
                                       final Map tags) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return addTags(context, instanceId, tags);
        }
    }

    public Map addTags(final CloudApiConnectionContext context,
                                       final UUID instanceId,
                                       final Map tags) throws IOException {
        Objects.requireNonNull(context, "Context object must be present");
        Objects.requireNonNull(instanceId, "Instance id must be present");
        Objects.requireNonNull(tags, "Tags to add must be present");

        if (tags.isEmpty()) {
            return Collections.emptyMap();
        }

        final String path = String.format("/%s/machines/%s/tags", config.getUser(), instanceId);
        final HttpPost post = connectionFactory.post(path);
        final HttpEntity entity = new JsonEntity(mapper, tags);
        post.setEntity(entity);

        final HttpClient client = context.getHttpClient();
        final Map result = client.execute(post,
                tagsHandler, context.getHttpContext());

        if (logger.isInfoEnabled()) {
            logger.info("Add/updated [%d] tags to instance [{}]", result.size());
        }

        return result;
    }

    public Map replaceTags(final UUID instanceId,
                                       final Map tags) throws IOException {
        try (CloudApiConnectionContext context = createConnectionContext()) {
            return replaceTags(context, instanceId, tags);
        }
    }

    public Map replaceTags(final CloudApiConnectionContext context,
                                           final UUID instanceId,
                                            final Map tags) throws IOException {
        Objects.requireNonNull(context, "Context object must be present");
        Objects.requireNonNull(instanceId, "Instance id must be present");
        Objects.requireNonNull(tags, "Tags to replace must be present");

        final String path = String.format("/%s/machines/%s/tags", config.getUser(), instanceId);
        final HttpPut put = connectionFactory.put(path);
        final HttpEntity entity = new JsonEntity(mapper, tags);
        put.setEntity(entity);

        final HttpClient client = context.getHttpClient();
        final Map result = client.execute(put,
                tagsHandler, context.getHttpContext());

        if (logger.isInfoEnabled()) {
            logger.info("Replaced all tags on instance [{}] with [%d] tags", result.size());
        }

        return result;
    }

    public CloudApiConnectionFactory getConnectionFactory() {
        return connectionFactory;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy