diff --git a/src/main/java/com/amihaiemil/docker/Plugin.java b/src/main/java/com/amihaiemil/docker/Plugin.java index 7adddfd4..151f085a 100644 --- a/src/main/java/com/amihaiemil/docker/Plugin.java +++ b/src/main/java/com/amihaiemil/docker/Plugin.java @@ -34,9 +34,10 @@ * A docker plugin. * @author Boris Kuzmic (boris.kuzmic@gmail.com) * @see Docker Plugin API - * @todo #266:30min Continue implementing rest of the Plugin methods besides - * enable and disable. More information about API methods can be found at: - * https://docs.docker.com/engine/api/v1.35/#tag/Plugin + * @todo #266:30min Implement Plugin#configure method. The tests are already + * coded, so after the implementation just remove the ignore annotation from + * these tests. More information about Configure API method can be found at: + * https://docs.docker.com/engine/api/v1.35/#operation/PluginSet * @since 0.0.7 */ public interface Plugin extends JsonObject { diff --git a/src/main/java/com/amihaiemil/docker/RtPlugin.java b/src/main/java/com/amihaiemil/docker/RtPlugin.java index 5ba9aa42..78f83132 100644 --- a/src/main/java/com/amihaiemil/docker/RtPlugin.java +++ b/src/main/java/com/amihaiemil/docker/RtPlugin.java @@ -33,6 +33,8 @@ import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; /** * Runtime {@link Plugin}. @@ -122,24 +124,47 @@ public void disable() throws IOException, UnexpectedResponseException { @Override public void upgrade(final String remote, final JsonArray properties) throws IOException, UnexpectedResponseException { - throw new UnsupportedOperationException( - String.join(" ", - "RtPlugin.upgrade() is not yet implemented.", - "If you can contribute please", - "do it here: https://www.github.com/amihaiemil/docker-java-api" - ) - ); + final HttpPost upgrade = + new HttpPost( + new UncheckedUriBuilder(this.uri.toString().concat("/upgrade")) + .addParameter("remote", remote) + .build() + ); + try { + upgrade.setEntity( + new StringEntity( + properties.toString(), ContentType.APPLICATION_JSON + ) + ); + this.client.execute( + upgrade, + new MatchStatus( + upgrade.getURI(), + HttpStatus.SC_NO_CONTENT + ) + ); + } finally { + upgrade.releaseConnection(); + } } @Override public void push() throws IOException, UnexpectedResponseException { - throw new UnsupportedOperationException( - String.join(" ", - "RtPlugin.push() is not yet implemented.", - "If you can contribute please", - "do it here: https://www.github.com/amihaiemil/docker-java-api" - ) - ); + final HttpPost push = + new HttpPost( + String.format("%s/%s", this.uri.toString(), "push") + ); + try { + this.client.execute( + push, + new MatchStatus( + push.getURI(), + HttpStatus.SC_OK + ) + ); + } finally { + push.releaseConnection(); + } } @Override diff --git a/src/test/java/com/amihaiemil/docker/RtPluginTestCase.java b/src/test/java/com/amihaiemil/docker/RtPluginTestCase.java index edb859a3..da2e09aa 100644 --- a/src/test/java/com/amihaiemil/docker/RtPluginTestCase.java +++ b/src/test/java/com/amihaiemil/docker/RtPluginTestCase.java @@ -25,16 +25,25 @@ */ package com.amihaiemil.docker; +import com.amihaiemil.docker.mock.ArrayPayloadOf; import com.amihaiemil.docker.mock.AssertRequest; import com.amihaiemil.docker.mock.Condition; import com.amihaiemil.docker.mock.Response; +import java.io.IOException; import java.net.URI; +import java.util.HashMap; +import java.util.Map; import javax.json.Json; +import javax.json.JsonArray; import javax.json.JsonObject; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpRequest; import org.apache.http.HttpStatus; +import org.apache.http.util.EntityUtils; import org.hamcrest.MatcherAssert; import org.hamcrest.collection.IsCollectionWithSize; import org.hamcrest.core.IsEqual; +import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; @@ -43,6 +52,8 @@ * * @author Boris Kuzmic (boris.kuzmic@gmail.com) * @since 0.0.8 + * @todo #266:30min Extract method stringPayloadOf to class to reuse in + * other test cases (like in RtPluginsTestCase.createOk() method) * @checkstyle MethodName (500 lines) */ public final class RtPluginTestCase { @@ -153,7 +164,7 @@ public void enablesItself() throws Exception { } /** - * RtPluginenable() must throw UnexpectedResponseException + * RtPlugin enable() must throw UnexpectedResponseException * if service responds with 404. * @throws Exception If something goes wrong. */ @@ -255,4 +266,281 @@ public void failsToDisableItselfWhenNotInstalled() throws Exception { ); plugin.disable(); } + + /** + * RtPlugin upgrade with properties ok. + * @throws Exception If something goes wrong. + */ + @Test + public void upgradeOk() throws Exception { + new ListedPlugins( + new AssertRequest( + new Response( + HttpStatus.SC_NO_CONTENT + ) + ), + URI.create("http://localhost/plugins"), + DOCKER + ).pullAndInstall( + "vieus/sshfs", + "sshfs", + Json.createArrayBuilder().add( + Json.createObjectBuilder() + .add("Name", "network") + .add("Description", "") + .add("Value", "host") + ).build() + ); + final JsonArray properties = Json.createArrayBuilder().add( + Json.createObjectBuilder() + .add("Name", "mount") + .add("Description", "") + .add("Value", "/data") + ).build(); + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_NO_CONTENT + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/upgrade?remote=test", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/upgrade?remote=test") + ), + new Condition( + "upgrade() must send JsonArray request body", + req -> { + JsonObject payload = + new ArrayPayloadOf(req).next(); + return "mount".equals(payload.getString("Name")) + && "/data".equals(payload.getString("Value")); + } + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + plugin.upgrade("test", properties); + } + + /** + * RtPlugin upgrade throws UnexpectedResponseException if service + * responds with 404. + * @throws Exception If something goes wrong. + */ + @Test(expected = UnexpectedResponseException.class) + public void upgradeFailsPluginNotInstalled() throws Exception { + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_NOT_FOUND + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/upgrade?remote=test", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/upgrade?remote=test") + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + final JsonArray properties = Json.createArrayBuilder().add( + Json.createObjectBuilder() + .add("Name", "mount") + .add("Description", "") + .add("Value", "/data") + ).build(); + plugin.upgrade("test", properties); + } + + /** + * RtPlugin push to repository ok. + * @throws Exception If something goes wrong. + */ + @Test + public void pushOk() throws Exception { + new ListedPlugins( + new AssertRequest( + new Response( + HttpStatus.SC_NO_CONTENT + ) + ), + URI.create("http://localhost/plugins"), + DOCKER + ).pullAndInstall( + "vieus/sshfs", + "sshfs", + Json.createArrayBuilder().add( + Json.createObjectBuilder() + .add("Name", "network") + .add("Description", "") + .add("Value", "host") + ).build() + ); + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_OK + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/push", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/push") + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + plugin.push(); + } + + /** + * RtPlugin push throws UnexpectedResponseException if service + * responds with 404. + * @throws Exception If something goes wrong. + */ + @Test(expected = UnexpectedResponseException.class) + public void pushFailsPluginNotInstalled() throws Exception { + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_NOT_FOUND + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/push", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/push") + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + plugin.push(); + } + + /** + * RtPlugin configure plugin. + * @throws Exception If something goes wrong. + */ + @Ignore + @Test + public void configureOk() throws Exception { + new ListedPlugins( + new AssertRequest( + new Response( + HttpStatus.SC_NO_CONTENT + ) + ), + URI.create("http://localhost/plugins"), + DOCKER + ).pullAndInstall( + "vieus/sshfs", + "sshfs", + Json.createArrayBuilder().add( + Json.createObjectBuilder() + .add("Name", "network") + .add("Description", "") + .add("Value", "host") + ).build() + ); + final Map options = new HashMap<>(); + options.put("DEBUG", "1"); + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_NO_CONTENT + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/set", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/set") + ), + new Condition( + "configure() must send String Array as request body", + req -> "[\"DEBUG=1\"]".equals(this.stringPayloadOf(req)) + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + plugin.configure(options); + } + + /** + * RtPlugin configure throws UnexpectedResponseException if service + * responds with 404. + * @throws Exception If something goes wrong. + */ + @Ignore + @Test(expected = UnexpectedResponseException.class) + public void configureFailsPluginNotInstalled() throws Exception { + final Plugin plugin = new RtPlugin( + Json.createObjectBuilder().build(), + new AssertRequest( + new Response( + HttpStatus.SC_NOT_FOUND + ), + new Condition( + "Method should be a POST", + req -> "POST".equals(req.getRequestLine().getMethod()) + ), + new Condition( + "Resource path must be /{name}/set", + req -> req.getRequestLine().getUri() + .endsWith("/sshfs/set") + ) + ), + URI.create("http://localhost/plugins/sshfs"), + DOCKER + ); + plugin.configure(new HashMap<>()); + } + + /** + * Extracts request payload as String. + * @param request Http Request. + * @return Payload as String. + */ + private String stringPayloadOf(final HttpRequest request) { + try { + final String payload; + if (request instanceof HttpEntityEnclosingRequest) { + payload = EntityUtils.toString( + ((HttpEntityEnclosingRequest) request).getEntity() + ); + } else { + payload = ""; + } + return payload; + } catch (final IOException ex) { + throw new IllegalStateException( + "Cannot read request payload", ex + ); + } + } } diff --git a/src/test/java/com/amihaiemil/docker/RtPluginsTestCase.java b/src/test/java/com/amihaiemil/docker/RtPluginsTestCase.java index a8109a9d..9614f140 100644 --- a/src/test/java/com/amihaiemil/docker/RtPluginsTestCase.java +++ b/src/test/java/com/amihaiemil/docker/RtPluginsTestCase.java @@ -25,6 +25,7 @@ */ package com.amihaiemil.docker; +import com.amihaiemil.docker.mock.ArrayPayloadOf; import com.amihaiemil.docker.mock.AssertRequest; import com.amihaiemil.docker.mock.Condition; import com.amihaiemil.docker.mock.Response; @@ -137,7 +138,7 @@ public void pullAndInstallOk() throws Exception { "pullAndInstall() must send Json body request", req -> { JsonObject payload = - this.arrayPayloadOf(req).getJsonObject(0); + new ArrayPayloadOf(req).next(); return "network".equals(payload.getString("Name")) && "host".equals(payload.getString("Value")); } @@ -232,28 +233,4 @@ private String stringPayloadOf(final HttpRequest request) { } } - /** - * Extracts request payload as JsonArray. - * @param request Http Request. - * @return Payload as Json array. - */ - private JsonArray arrayPayloadOf(final HttpRequest request) { - try { - final JsonArray body; - if (request instanceof HttpEntityEnclosingRequest) { - body = Json.createReader( - ((HttpEntityEnclosingRequest) request).getEntity() - .getContent() - ).readArray(); - } else { - body = Json.createArrayBuilder().build(); - } - return body; - } catch (final IOException ex) { - throw new IllegalStateException( - "Cannot read request payload", ex - ); - } - } - } diff --git a/src/test/java/com/amihaiemil/docker/mock/ArrayPayloadOf.java b/src/test/java/com/amihaiemil/docker/mock/ArrayPayloadOf.java new file mode 100644 index 00000000..a93ce09d --- /dev/null +++ b/src/test/java/com/amihaiemil/docker/mock/ArrayPayloadOf.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2018-2019, Mihai Emil Andronache + * All rights reserved. + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1)Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2)Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3)Neither the name of docker-java-api nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.amihaiemil.docker.mock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpRequest; + +/** + * Json Array payload of an HttpRequest. + * + * @author Boris Kuzmic (boris.kuzmic@gmail.com) + * @since 0.0.8 + */ +public final class ArrayPayloadOf implements Iterator { + + /** + * List of JsonObjects. + */ + private final Iterator resources; + + /** + * Ctor. + * + * @param request The http request + * @throws IllegalStateException if the request's payload cannot be read + */ + public ArrayPayloadOf(final HttpRequest request) { + try (JsonReader reader = Json.createReader( + ((HttpEntityEnclosingRequest) request).getEntity().getContent())) { + if (request instanceof HttpEntityEnclosingRequest) { + this.resources = + reader.readArray().getValuesAs(JsonObject.class).iterator(); + } else { + this.resources = new ArrayList().iterator(); + } + } catch (final IOException ex) { + throw new IllegalStateException( + "Cannot read request payload", ex + ); + } + } + + @Override + public boolean hasNext() { + return this.resources.hasNext(); + } + + @Override + public JsonObject next() { + return this.resources.next(); + } +}