Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
group = fi.linuxbox.upcloud
version = 0.0.8-SNAPSHOT
version = 0.1.0-SNAPSHOT

sourceCompatibility = 1.8
targetCompatibility = 1.8
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package fi.linuxbox.upcloud.api

import fi.linuxbox.upcloud.core.HTTPFacade
import fi.linuxbox.upcloud.core.Resource

import static fi.linuxbox.upcloud.core.ResourceUtil.wrapped

/**
* Private cloud host related APIs.
* <p>
* This trait can be implemented by any class that has
* </p>
* <ul>
* <li>non-null HTTP property, which can be read-only</li>
* <li>non-null id property, which can be read-only</li>
* </ul>
*/
trait HostApi {
abstract HTTPFacade<?> getHTTP()
abstract Long getId()

/**
* Fetch detailed information about a specific {@link fi.linuxbox.upcloud.resource.Host}.
* <p>
* A {@code 200 OK} response will include an instance of {@link fi.linuxbox.upcloud.resource.Host}
* in the {@code host} property.
* </p>
* <pre><code class="groovy">
* import fi.linuxbox.upcloud.api.HostApi
* import fi.linuxbox.upcloud.resource.Host
*
* // Obtain HostApi somehow. For example:
* final hostApi = [HTTP: session, id: 123] as HostApi
*
* hostApi.load { resp, err ->
* assert resp?.host instanceof Host
* }
* </code></pre>
*
* @param args Request callbacks for the {@code GET /host/&#36;&#123;host.id&#125;} call.
* @return Whatever is returned by the {@link fi.linuxbox.upcloud.core.Session} for starting an asynchronous request.
* @see <a href="https://www.upcloud.com/api/14-hosts/#get-host-details" target="_top">UpCloud API docs for GET /host/&#36;{host.id}</a>
*/
def load(...args) {
HTTP.GET(hostPath(), *args)
}

/**
* Modifies the description of a private cloud host.
* <p>
* A {@code 200 OK} response will include an instance of {@link fi.linuxbox.upcloud.resource.Host}
* in the {@code host} property.
* </p>
* <pre><code class="groovy">
* import fi.linuxbox.upcloud.api.HostApi
* import fi.linuxbox.upcloud.resource.Host
* import static fi.linuxbox.upcloud.resource.Builder.host
*
* // Obtain HostApi somehow. For example:
* final hostApi = [HTTP: session, id: 123] as HostApi
*
* // Configure the new host (only description can be changed)
* final description = host {
* description = "My New Host"
* }
*
* // Update the host description
* hostApi.update description, { resp, err ->
* assert resp?.host instanceof Host
* }
* </code></pre>
*
* @param resource Specification of the update
* @param args Request callbacks for the {@code PATCH /host/&#36;&#123;host.id&#125;} call.
* @return Whatever is returned by the {@link fi.linuxbox.upcloud.core.Session} for starting an asynchronous request.
* @see <a href="https://www.upcloud.com/api/14-hosts/#modify-host-details" target="_top">UpCloud API docs for PATCH /host/&#36;{host.id}</a>
*/
def update(Resource resource, ...args) {
HTTP.PATCH(hostPath(), wrapped(resource), *args)
}


private String hostPath() { "host/$id" }
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import static fi.linuxbox.upcloud.core.ResourceUtil.wrapped
* </p>
* <ul>
* <li>getting account information</li>
* <li>listing prices, zones, timezones, plans, and server sizes</li>
* <li>listing prices, zones, timezones, plans, server sizes, and hosts on private cloud</li>
* <li>listing and creating servers, IP addresses, tags, and storages</li>
* </ul>
* <p>
Expand Down Expand Up @@ -282,6 +282,32 @@ trait UpCloudApi {
HTTP.GET("storage", *args)
}

/**
* Fetch a list of hosts on private cloud associated with this account.
* <p>
* A {@code 200 OK} response will include a list of {@link fi.linuxbox.upcloud.resource.Host} instances in the
* {@code hosts} property.
* </p>
* <pre><code class="groovy">
* upcloud.hosts { resp, err ->
* assert resp?.hosts instanceof List
* assert resp.hosts.every { it instanceof Host }
* }
* </code></pre>
* @param args.zone Optional filter for hosts.
* @param args Request callbacks for the {@code GET /host} call.
* @return Whatever is returned by the {@link Session} for starting an asynchronous request.
* @see <a href="https://www.upcloud.com/api/14-hosts/#list-available-hosts" target="_top">UpCloud API docs for GET /host</a>
*/
def hosts(...args) {
// FIXME: modifies the user supplied map
def zone = args.find { it instanceof Map } ?.remove('zone')
if (zone)
HTTP.GET("host?zone=$zone", *args)
else
HTTP.GET('host', *args)
}

/**
* Create a resource.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Groovy UpCloud library - API Module
* Copyright (C) 2018 Mikko Värri
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fi.linuxbox.upcloud.api


import fi.linuxbox.upcloud.api.spec.ApiSpecification

import static fi.linuxbox.upcloud.builder.ResourceBuilder.build

class HostCRUDSpec extends ApiSpecification {

def "api can be created from map"() {
when:
def api = [HTTP: session, id: 123] as HostApi

then:
noExceptionThrown()
api instanceof HostApi
}

// build minimal class that works for the HostApi trait: HTTP and id
def host = build 'Host', HTTP: session, { id = 123L } withTraits HostApi

def "load: GET /host/123"() {
when:
host.load {}

then:
requestIs 'GET', '/host/123'
}

def "update: PATCH /host/123"() {
given:
def changes = build 'Host', {
description = "My New Host"
}

when:
host.update changes, {}

then:
requestIs 'PATCH', '/host/123',
[ "host": [
"description": "My New Host"
]
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ class UpCloudSpec extends ApiSpecification {
requestIs 'GET', '/storage/favorite'
}

def "Private Cloud host list can be filtered by zone"() {
when:
upCloud.hosts zone: 'test-zone-fi-1', {}

then:
requestIs 'GET', '/host?zone=test-zone-fi-1'
}

@Unroll
def "#methodName does GET ...#resource"() {
when:
Expand All @@ -64,6 +72,7 @@ class UpCloudSpec extends ApiSpecification {
'ipAddresses' | '/ip_address'
'storages' | '/storage'
'tags' | '/tag'
'hosts' | '/host'
}

def "creating an IP address"() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,48 @@ abstract class HTTPFacade<T> {
request(cbs, "POST", path, resource, cb)
}

/**
* Performs a HTTP PATCH method.
* <p>
* This method calls the
* {@link #request(Map&lt;?, Closure&lt;Void&gt;&gt;, java.lang.String, java.lang.String, fi.linuxbox.upcloud.core.Resource, Closure&lt;Void&gt;) request method}
* with {@code PATCH} method.
* </p>
* <p>
* This overload is used when the application code calls this directly. For example:
* </p>
* <pre><code lang="groovy">
* import static fi.linuxbox.upcloud.builder.ResourceBuilder.configure
*
* Resource lb = // obtain a LoadBalancer...
*
* lb = configure lb {
* method = 'round-robin'
* }
*
* session.PATCH "/lb/${lb.uuid}", lb, error: { log.fatal("${it.META}") }, { res, err ->
* if (err)
* log.warn("Network error: ${err.message}")
* else
* log.info("Load balancer updated")
* }
* </code></pre>
* <p>
* In the example above, there is a REST API endpoint that this library does not know about: {@code /lb}.
* Application can bypass the outdated library API and PATCH the custom resource to the REST API directly.
* </p>
*
* @param cbs Additional request callbacks.
* @param path Resource path relative to the API context path, i.e. without leading slash.
* @param resource Resource to send or {@code null}.
* @param cb Default request callback.
* @return Whatever is returned by a concrete subclass from the
* {@link #request(Map&lt;?, Closure&lt;Void&gt;&gt;, java.lang.String, java.lang.String, fi.linuxbox.upcloud.core.Resource, Closure&lt;Void&gt;) request method}.
*/
T PATCH(Map<?, Closure<Void>> cbs, String path, Resource resource, Closure<Void> cb = null) {
request(cbs, "PATCH", path, resource, cb)
}

/**
* Perform a HTTP request.
*
Expand Down Expand Up @@ -335,4 +377,39 @@ abstract class HTTPFacade<T> {
T POST(String path, Resource resource, Map<?, Closure<Void>> cbs, Closure<Void> cb = null) {
POST(cbs, path, resource, cb)
}

/**
* Calls {@code PATCH} with an empty map as additional request callbacks.
* <p>
* This overload is typically used by the higher level API traits when the application does not pass any
* additional request callbacks.
* </p>
*
* @param path Resource path relative to the API context path, i.e. without leading slash.
* @param resource Resource to send or {@code null}.
* @param cb Default request callback.
* @return Whatever is returned by a concrete subclass from the
* {@link #request(Map&lt;?, Closure&lt;Void&gt;&gt;, java.lang.String, java.lang.String, fi.linuxbox.upcloud.core.Resource, Closure&lt;Void&gt;) request method}.
*/
T PATCH(String path, Resource resource, Closure<Void> cb = null) {
PATCH(EMPTY_CBS, path, resource, cb)
}

/**
* Calls {@code PATCH} with given arguments in correct order.
* <p>
* This overload is typically used by the higher level API traits when the application does provide some
* additional request callbacks.
* </p>
*
* @param path Resource path relative to the API context path, i.e. without leading slash.
* @param resource Resource to send or {@code null}.
* @param cbs Additional request callbacks.
* @param cb Default request callback.
* @return Whatever is returned by a concrete subclass from the
* {@link #request(Map&lt;?, Closure&lt;Void&gt;&gt;, java.lang.String, java.lang.String, fi.linuxbox.upcloud.core.Resource, Closure&lt;Void&gt;) request method}.
*/
T PATCH(String path, Resource resource, Map<?, Closure<Void>> cbs, Closure<Void> cb = null) {
PATCH(cbs, path, resource, cb)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class UpCloudContract {
/**
* UpCloud API context path.
*/
static final String API_VERSION = '/1.2/'
static final String API_VERSION = '/1.3/'

/**
* Construct request headers for a session.
Expand All @@ -50,7 +50,7 @@ class UpCloudContract {
'Authorization': 'Basic ' + "$username:$password".bytes.encodeBase64().toString(),
'Content-Type' : 'application/json',
'Host' : 'api.upcloud.com',
'User-Agent' : 'Groovy UpCloud/0.0.8-SNAPSHOT ' + userAgent,
'User-Agent' : 'Groovy UpCloud/0.1.0-SNAPSHOT ' + userAgent,
])
}
}
Loading