Repository: brooklyn-server
Updated Branches:
  refs/heads/master 8930ab6dd -> 48beb5c2f


add ZIP or JAR via REST API, with tests


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/5df28249
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/5df28249
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/5df28249

Branch: refs/heads/master
Commit: 5df28249e54fd6bc3a52c257944d0101ee269576
Parents: 2fe2ba0
Author: Alex Heneveld <[email protected]>
Authored: Mon Dec 12 16:55:49 2016 +0000
Committer: Alex Heneveld <[email protected]>
Committed: Wed Mar 8 09:35:42 2017 +0000

----------------------------------------------------------------------
 .../brooklyn/rest/api/ApplicationApi.java       |   3 -
 .../apache/brooklyn/rest/api/CatalogApi.java    |  46 +++++++-
 .../rest/resources/CatalogResource.java         | 104 ++++++++++++++++++-
 .../rest/resources/CatalogResourceTest.java     |  90 +++++++++++++++-
 4 files changed, 229 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5df28249/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/ApplicationApi.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/ApplicationApi.java 
b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/ApplicationApi.java
index f1e167f..ba2fec2 100644
--- 
a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/ApplicationApi.java
+++ 
b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/ApplicationApi.java
@@ -111,9 +111,6 @@ public interface ApplicationApi {
                     required = true)
             String yaml);
 
-    // TODO archives
-//    @Consumes({"application/x-tar", "application/x-tgz", 
"application/x-zip"})
-
     @POST
     @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, 
MediaType.TEXT_PLAIN})
     @ApiOperation(

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5df28249/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java 
b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java
index f561759..93a812e 100644
--- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java
+++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/api/CatalogApi.java
@@ -38,11 +38,13 @@ import org.apache.brooklyn.rest.domain.CatalogItemSummary;
 import org.apache.brooklyn.rest.domain.CatalogLocationSummary;
 import org.apache.brooklyn.rest.domain.CatalogPolicySummary;
 
+import com.google.common.annotations.Beta;
+
 import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiResponse;
-import io.swagger.annotations.ApiResponses;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
 
 @Path("/catalog")
 @Api("Catalog")
@@ -50,15 +52,49 @@ import io.swagger.annotations.ApiParam;
 @Produces(MediaType.APPLICATION_JSON)
 public interface CatalogApi {
 
-    @Consumes
+    @Deprecated /** @deprecated since 0.11.0 use {@link 
#createFromYaml(String)} instead */
+    public Response create(String yaml);
+
+    @Consumes({MediaType.APPLICATION_JSON, "application/x-yaml",
+        // see http://stackoverflow.com/questions/332129/yaml-mime-type
+        "text/yaml", "text/x-yaml", "application/yaml"})
     @POST
-    @ApiOperation(value = "Add a catalog item (e.g. new type of entity, policy 
or location) by uploading YAML descriptor "
+    @ApiOperation(value = "Add a catalog item (e.g. new type of entity, policy 
or location) by uploading YAML descriptor. "
         + "Return value is map of ID to CatalogItemSummary, with code 201 
CREATED.", response = String.class)
-    public Response create(
+    public Response createFromYaml(
             @ApiParam(name = "yaml", value = "YAML descriptor of catalog 
item", required = true)
             @Valid String yaml);
 
     @POST
+    @Beta
+    @Consumes  // anything (if doesn't match other methods with specific 
content types
+    @ApiOperation(value = "Add items to the catalog, either YAML or JAR/ZIP, 
format autodetected. "
+            + "Specify a content-type header to skip auto-detection and invoke 
one of the more specific methods. "
+            + "Return value is 201 CREATED if bundle could be added.", 
response = String.class)
+    public Response createPoly(
+            @ApiParam(
+                    name = "item",
+                    value = "Item to install, as JAR/ZIP or Catalog YAML 
(autodetected)",
+                    required = true)
+            byte[] item,
+            @ApiParam(name="name", value="Symbolic name to use for bundle", 
required=false, defaultValue="") String bundleName, 
+            @ApiParam(name="version", value="Version to set for bundle", 
required=false, defaultValue="") String bundleVersion);
+    
+    @POST
+    @Beta
+    @Consumes({"application/x-zip", "application/x-jar"})
+    @ApiOperation(value = "Add a catalog item (e.g. new type of entity, policy 
or location) by uploading OSGi bundle JAR, or ZIP if bundle name and optionally 
version are supplied. "
+            + "Return value is 201 CREATED if bundle could be added.", 
response = String.class)
+    public Response createFromArchive(
+            @ApiParam(
+                    name = "archive",
+                    value = "Bundle to install, in JAR format, or ZIP if 
bundle name and optionally version are supplied, optionally with catalog.bom 
contained within",
+                    required = true)
+            byte[] archive,
+            @ApiParam(name="name", value="Symbolic name to use for bundle", 
required=false, defaultValue="") String bundleName, 
+            @ApiParam(name="version", value="Version to set for bundle", 
required=false, defaultValue="") String bundleVersion);
+    
+    @POST
     @Consumes(MediaType.APPLICATION_XML)
     @Path("/reset")
     @ApiOperation(value = "Resets the catalog to the given (XML) format")

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5df28249/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
 
b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
index 2ac5b35..edc8b4d 100644
--- 
a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
+++ 
b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/resources/CatalogResource.java
@@ -18,6 +18,10 @@
  */
 package org.apache.brooklyn.rest.resources;
 
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -25,6 +29,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
 
 import javax.annotation.Nullable;
 import javax.ws.rs.core.MediaType;
@@ -41,13 +48,16 @@ import org.apache.brooklyn.api.location.LocationSpec;
 import org.apache.brooklyn.api.policy.Policy;
 import org.apache.brooklyn.api.policy.PolicySpec;
 import org.apache.brooklyn.api.typereg.RegisteredType;
+import org.apache.brooklyn.core.BrooklynFeatureEnablement;
 import org.apache.brooklyn.core.catalog.CatalogPredicates;
 import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog;
+import org.apache.brooklyn.core.catalog.internal.CatalogBomScanner;
 import org.apache.brooklyn.core.catalog.internal.CatalogDto;
 import org.apache.brooklyn.core.catalog.internal.CatalogItemComparator;
 import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
 import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
 import 
org.apache.brooklyn.core.mgmt.entitlement.Entitlements.StringAndArgument;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
 import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts;
 import org.apache.brooklyn.core.typereg.RegisteredTypePredicates;
 import org.apache.brooklyn.core.typereg.RegisteredTypes;
@@ -60,13 +70,19 @@ import org.apache.brooklyn.rest.domain.CatalogPolicySummary;
 import org.apache.brooklyn.rest.filter.HaHotStateRequired;
 import org.apache.brooklyn.rest.transform.CatalogTransformer;
 import org.apache.brooklyn.rest.util.WebResourceUtils;
+import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.collections.MutableSet;
 import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.core.osgi.BundleMaker;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
+import org.apache.brooklyn.util.os.Os;
 import org.apache.brooklyn.util.text.StringPredicates;
 import org.apache.brooklyn.util.text.Strings;
+import org.apache.brooklyn.util.yaml.Yamls;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -94,9 +110,42 @@ public class CatalogResource extends 
AbstractBrooklynRestResource implements Cat
     };
 
     static Set<String> missingIcons = MutableSet.of();
-    
+
     @Override
+    public Response createPoly(
+            byte[] item,
+            String bundleName, 
+            String bundleVersion) {
+        Throwable yamlException = null;
+        try {
+            MutableList.copyOf( Yamls.parseAll(new InputStreamReader(new 
ByteArrayInputStream(item))) );
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            yamlException = e;
+        }
+        
+        if (yamlException==null) {
+            // treat as yaml
+            if (Strings.isNonBlank(bundleName) || 
Strings.isNonBlank(bundleVersion)) {
+                throw new IllegalArgumentException("Bundle name/version not 
permitted when installing catalog YAML");
+            }
+            return createFromYaml(new String(item));
+        }
+        
+        try {
+            return createFromArchive(item, bundleName, bundleVersion);
+        } catch (Exception e) {
+            throw Exceptions.propagate("Unable to handle input: not YAML or 
compatible ZIP. Specify Content-Type to clarify.", e);
+        }
+    }
+    
+    @Override @Deprecated
     public Response create(String yaml) {
+        return createFromYaml(yaml);
+    }
+    
+    @Override
+    public Response createFromYaml(String yaml) {
         if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), 
Entitlements.ADD_CATALOG_ITEM, yaml)) {
             throw WebResourceUtils.forbidden("User '%s' is not authorized to 
add catalog item",
                 Entitlements.getEntitlementContext().user());
@@ -106,6 +155,7 @@ public class CatalogResource extends 
AbstractBrooklynRestResource implements Cat
         try {
             items = brooklyn().getCatalog().addItems(yaml);
         } catch (Exception e) {
+            e.printStackTrace();
             Exceptions.propagateIfFatal(e);
             return ApiError.of(e).asBadRequestResponseJson();
         }
@@ -129,6 +179,57 @@ public class CatalogResource extends 
AbstractBrooklynRestResource implements Cat
         return Response.status(Status.CREATED).entity(result).build();
     }
 
+    @Override
+    public Response createFromArchive(byte[] zipInput, String bundleName, 
String bundleVersion) {
+        if (!Entitlements.isEntitled(mgmt().getEntitlementManager(), 
Entitlements.ROOT, null)) {
+            throw WebResourceUtils.forbidden("User '%s' is not authorized to 
add catalog item",
+                Entitlements.getEntitlementContext().user());
+        }
+
+        BundleMaker bm = new BundleMaker(mgmt());
+        File f = Os.newTempFile("brooklyn-posted-archive", "zip");
+        try {
+            Files.write(zipInput, f);
+        } catch (IOException e) {
+            Exceptions.propagate(e);
+        }
+        Manifest mf = bm.getManifest(f);
+        if (mf==null) {
+            mf = new Manifest();
+        }
+        String bundleNameInMF = 
mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+        if (Strings.isBlank(bundleName) && Strings.isBlank(bundleNameInMF)) {
+            throw new IllegalStateException("Require either 
"+Constants.BUNDLE_SYMBOLICNAME+" in "+JarFile.MANIFEST_NAME+" or name supplied 
in REST API");
+        }
+        if (!Strings.isBlank(bundleName)) {
+            mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, 
bundleName);
+        }
+        if (!Strings.isBlank(bundleVersion) || 
Strings.isBlank(mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION))) {
+            if (Strings.isBlank(bundleVersion)) {
+                bundleVersion = "0.0.0.SNAPSHOT";
+            }
+            mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, 
bundleVersion);
+        }
+        if 
(mf.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION)==null) {
+            
mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), 
"1.0");
+        }
+        
+        File f2 = bm.copyAddingManifest(f, mf);
+        f.delete();
+        
+        Bundle bundle = bm.installBundle(f2, true);
+        f2.delete();
+        
+        if 
(!BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_LOAD_BUNDLE_CATALOG_BOM))
 {
+            // if the above feature is not enabled, let's do it manually (as a 
contract of this method)
+            new CatalogBomScanner().new CatalogPopulator(
+                    ((LocalManagementContext) 
mgmt()).getOsgiManager().get().getFramework().getBundleContext(),
+                    mgmt()).addingBundle(bundle, null);
+        }
+        
+        return Response.status(Status.CREATED).build();
+    }
+    
     @SuppressWarnings("deprecation")
     @Override
     public Response resetXml(String xml, boolean ignoreErrors) {
@@ -514,3 +615,4 @@ public class CatalogResource extends 
AbstractBrooklynRestResource implements Cat
         return result;
     }
 }
+ 
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/5df28249/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
index af5d705..d1a74ae 100644
--- 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/CatalogResourceTest.java
@@ -22,14 +22,21 @@ import static 
com.google.common.base.Preconditions.checkNotNull;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertTrue;
 
-import java.awt.*;
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.zip.ZipEntry;
 
+import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
@@ -45,7 +52,12 @@ import 
org.apache.brooklyn.rest.domain.CatalogLocationSummary;
 import org.apache.brooklyn.rest.domain.CatalogPolicySummary;
 import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
 import org.apache.brooklyn.test.support.TestResourceUnavailableException;
+import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.ResourceUtils;
+import org.apache.brooklyn.util.core.osgi.BundleMaker;
 import org.apache.brooklyn.util.javalang.Reflections;
+import org.apache.brooklyn.util.os.Os;
+import org.apache.brooklyn.util.stream.Streams;
 import org.apache.http.HttpHeaders;
 import org.apache.http.entity.ContentType;
 import org.eclipse.jetty.http.HttpStatus;
@@ -58,10 +70,6 @@ import org.testng.reporters.Files;
 import com.google.common.base.Joiner;
 import com.google.common.collect.Iterables;
 
-import java.io.InputStream;
-
-import javax.ws.rs.core.GenericType;
-
 @Test( // by using a different suite name we disallow interleaving other tests 
between the methods of this test class, which wrecks the test fixtures
         suiteName = "CatalogResourceTest")
 public class CatalogResourceTest extends BrooklynRestResourceTest {
@@ -487,6 +495,78 @@ public class CatalogResourceTest extends 
BrooklynRestResourceTest {
         //equivalent to HTTP response 404 text/html
         
addAddCatalogItemWithInvalidBundleUrl("classpath://missing-jar-file.txt");
     }
+    
+    @Test
+    public void testOsgiBundleWithBom() throws Exception {
+        
TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), 
OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH);
+        String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL;
+        BundleMaker bm = new BundleMaker(manager);
+        File f = Os.newTempFile("osgi", "jar");
+        
Files.copyFile(ResourceUtils.create(this).getResourceFromUrl(bundleUrl), f);
+        
+        String symbolicName = "my.catalog.entity.id.testOsgiBundleWithBom";
+        String bom = Joiner.on("\n").join(
+                "brooklyn.catalog:",
+                "  id: " + symbolicName,
+                "  version: " + TEST_VERSION,
+                "  itemType: entity",
+                "  name: My Catalog App",
+                "  description: My description",
+                "  icon_url: 
classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif",
+                "  item:",
+                "    type: org.apache.brooklyn.core.test.entity.TestEntity");
+        
+        f = bm.copyAdding(f, MutableMap.of(new ZipEntry("catalog.bom"), 
(InputStream) new ByteArrayInputStream(bom.getBytes())));
+
+        Response response = client().path("/catalog")
+                .header(HttpHeaders.CONTENT_TYPE, "application/x-zip")
+                .post(Streams.readFully(new FileInputStream(f)));
+
+        
+        assertEquals(response.getStatus(), 
Response.Status.CREATED.getStatusCode());
+
+        CatalogEntitySummary entityItem = 
client().path("/catalog/entities/"+symbolicName + "/" + TEST_VERSION)
+                .get(CatalogEntitySummary.class);
+
+        Assert.assertNotNull(entityItem.getPlanYaml());
+        
Assert.assertTrue(entityItem.getPlanYaml().contains("org.apache.brooklyn.core.test.entity.TestEntity"));
+
+        assertEquals(entityItem.getId(), ver(symbolicName));
+        assertEquals(entityItem.getSymbolicName(), symbolicName);
+        assertEquals(entityItem.getVersion(), TEST_VERSION);
+
+        // and internally let's check we have libraries
+        RegisteredType item = 
getManagementContext().getTypeRegistry().get(symbolicName, TEST_VERSION);
+        Assert.assertNotNull(item);
+        Collection<OsgiBundleWithUrl> libs = item.getLibraries();
+        assertEquals(libs.size(), 1);
+        OsgiBundleWithUrl lib = Iterables.getOnlyElement(libs);
+        Assert.assertNull(lib.getUrl());
+
+        assertEquals(lib.getSymbolicName(), 
"org.apache.brooklyn.test.resources.osgi.brooklyn-test-osgi-entities");
+        assertEquals(lib.getVersion(), "0.1.0");
+
+        // now let's check other things on the item
+        URI expectedIconUrl = URI.create(getEndpointAddress() + 
"/catalog/icon/" + symbolicName + "/" + entityItem.getVersion()).normalize();
+        assertEquals(entityItem.getName(), "My Catalog App");
+        assertEquals(entityItem.getDescription(), "My description");
+        assertEquals(entityItem.getIconUrl(), expectedIconUrl.getPath());
+        assertEquals(item.getIconUrl(), 
"classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif");
+
+        // an InterfacesTag should be created for every catalog item
+        assertEquals(entityItem.getTags().size(), 1);
+        Object tag = entityItem.getTags().iterator().next();
+        @SuppressWarnings("unchecked")
+        List<String> actualInterfaces = ((Map<String, List<String>>) 
tag).get("traits");
+        List<Class<?>> expectedInterfaces = 
Reflections.getAllInterfaces(TestEntity.class);
+        assertEquals(actualInterfaces.size(), expectedInterfaces.size());
+        for (Class<?> expectedInterface : expectedInterfaces) {
+            assertTrue(actualInterfaces.contains(expectedInterface.getName()));
+        }
+
+        byte[] iconData = client().path("/catalog/icon/" + symbolicName + "/" 
+ TEST_VERSION).get(byte[].class);
+        assertEquals(iconData.length, 43);
+    }
 
     private void addAddCatalogItemWithInvalidBundleUrl(String bundleUrl) {
         String symbolicName = "my.catalog.entity.id";

Reply via email to