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";
