diff --git a/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java b/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java index 3bda040991..75fe634376 100644 --- a/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java +++ b/api/src/main/java/org/apache/brooklyn/api/catalog/BrooklynCatalog.java @@ -21,7 +21,11 @@ import java.util.Collection; import java.util.NoSuchElementException; +import javax.annotation.Nullable; + import org.apache.brooklyn.api.internal.AbstractBrooklynObjectSpec; +import org.apache.brooklyn.api.typereg.ManagedBundle; + import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; @@ -111,12 +115,17 @@ public interface BrooklynCatalog { CatalogItem addItem(String yaml, boolean forceUpdate); /** - * Adds items (represented in yaml) to the catalog. - * Fails if the same version exists in catalog. + * As {@link #addItemsFromBundle(String, ManagedBundle)} with a null bundle. + */ + Iterable> addItems(String yaml); + + /** + * Adds items (represented in yaml) to the catalog coming from the indicated managed bundle. + * Fails if the same version exists in catalog (unless snapshot). * * @throws IllegalArgumentException if the yaml was invalid */ - Iterable> addItems(String yaml); + Iterable> addItems(String yaml, @Nullable ManagedBundle definingBundle); /** * Adds items (represented in yaml) to the catalog. diff --git a/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java b/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java index c8f97314de..f3440d9cd5 100644 --- a/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java +++ b/api/src/main/java/org/apache/brooklyn/api/catalog/CatalogItem.java @@ -128,6 +128,8 @@ public static interface CatalogItemLibraries { @Nullable public String getIconUrl(); public String getSymbolicName(); + + public String getContainingBundle(); public String getVersion(); diff --git a/api/src/main/java/org/apache/brooklyn/api/typereg/ManagedBundle.java b/api/src/main/java/org/apache/brooklyn/api/typereg/ManagedBundle.java index 4f975b7fd6..66ca59237e 100644 --- a/api/src/main/java/org/apache/brooklyn/api/typereg/ManagedBundle.java +++ b/api/src/main/java/org/apache/brooklyn/api/typereg/ManagedBundle.java @@ -20,6 +20,7 @@ import org.apache.brooklyn.api.mgmt.rebind.Rebindable; import org.apache.brooklyn.api.objs.BrooklynObject; +import org.apache.brooklyn.util.osgi.VersionedName; /** Describes an OSGi bundle which Brooklyn manages, including persisting */ public interface ManagedBundle extends BrooklynObject, Rebindable, OsgiBundleWithUrl { @@ -29,4 +30,6 @@ public interface ManagedBundle extends BrooklynObject, Rebindable, OsgiBundleWit * This typically includes the unique {@link #getId()} of this item. */ String getOsgiUniqueUrl(); + VersionedName getVersionedName(); + } diff --git a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredType.java b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredType.java index 1eb7391f55..0696dfc57a 100644 --- a/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredType.java +++ b/api/src/main/java/org/apache/brooklyn/api/typereg/RegisteredType.java @@ -37,6 +37,11 @@ public interface RegisteredType extends Identifiable { String getSymbolicName(); String getVersion(); + /** Bundle in symbolicname:id format where this type is defined */ + // TODO would prefer this to be VersionedName if/when everything comes from OSGi bundles + // unrevert 7260bf9cf3f3ebaaa790693e1b7217a81bef78a7 to start that, and adjust serialization + // as described in that commit message (supporting String in xstream serialization for VN) + String getContainingBundle(); Collection getLibraries(); diff --git a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynComponentTemplateResolver.java b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynComponentTemplateResolver.java index 34f67c38ad..7e9b56279b 100644 --- a/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynComponentTemplateResolver.java +++ b/camp/camp-brooklyn/src/main/java/org/apache/brooklyn/camp/brooklyn/spi/creation/BrooklynComponentTemplateResolver.java @@ -174,6 +174,8 @@ public EntitySpec resolveSpec(Set encounteredRegis // TODO propagate exception so we can provide better error messages msgDetails = "The reference " + type + " looks like a URL (running the CAMP Brooklyn assembly-template instantiator) but couldn't load it (missing or invalid syntax?). " + "It's also neither a catalog item nor a java type."; + } else if ("brooklyn".equals(proto)){ + msgDetails = "The reference " + type + " is not a registered catalog item nor a java type."; } else { msgDetails = "The reference " + type + " looks like a URL (running the CAMP Brooklyn assembly-template instantiator) but the protocol " + proto + " isn't white listed (" + BrooklynCampConstants.YAML_URL_PROTOCOL_WHITELIST + "). " + diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java index 6eca121460..397bcf70fd 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogMakeOsgiBundleTest.java @@ -37,10 +37,11 @@ import org.apache.brooklyn.core.catalog.internal.CatalogBomScanner; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityAsserts; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; -import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; @@ -48,6 +49,7 @@ 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.osgi.VersionedName; import org.apache.brooklyn.util.text.Identifiers; import org.apache.brooklyn.util.text.Strings; import org.osgi.framework.Bundle; @@ -89,7 +91,8 @@ public void cleanUpButKeepMgmt() throws Exception { Entities.destroy(app); } for (Bundle b: bundlesToRemove) { - b.uninstall(); + ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle( + ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundle(new VersionedName(b))); } bundlesToRemove.clear(); } @@ -156,7 +159,8 @@ public void testCatalogBomFromBundleWithManualManifest() throws Exception { jf = bm.copyAddingManifest(jf, MutableMap.of( "Manifest-Version", "1.0", - "Bundle-SymbolicName", customName)); + "Bundle-SymbolicName", customName, + "Bundle-Version", "0.0.0.SNAPSHOT")); Assert.assertTrue(bm.hasOsgiManifest(jf)); @@ -172,10 +176,8 @@ public void testCatalogBomFromBundleWithManualManifest() throws Exception { private void installBundle(File jf) { try (FileInputStream fin = new FileInputStream(jf)) { - BasicManagedBundle bundleMetadata = new BasicManagedBundle(); - Bundle bundle = - ((LocalManagementContext)mgmt()).getOsgiManager().get().installUploadedBundle(bundleMetadata, fin, true); - bundlesToRemove.add(bundle); + OsgiBundleInstallationResult br = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(fin).get(); + bundlesToRemove.add(br.getBundle()); } catch (Exception e) { throw Exceptions.propagate(e); } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiLibraryTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiLibraryTest.java index 111ec4b647..aebd80d86c 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiLibraryTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiLibraryTest.java @@ -178,9 +178,8 @@ public void testLibraryUrlDoesNotExist() throws Exception { " - type: " + BasicApplication.class.getName()); Asserts.shouldHaveFailedPreviously(); } catch (Exception e) { - if (!e.toString().contains("Bundle from " + wrongUrl + " failed to install")) { - throw e; - } + Asserts.expectedFailureContains(e, wrongUrl); + Asserts.expectedFailureContainsIgnoreCase(e, "not found"); } } @@ -199,9 +198,7 @@ public void testLibraryMalformed() throws Exception { " - type: " + BasicApplication.class.getName()); Asserts.shouldHaveFailedPreviously(); } catch (Exception e) { - if (!e.toString().contains("not a jar file")) { - throw e; - } + Asserts.expectedFailureContainsIgnoreCase(e, "opening zip"); } } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java index a3ca51bd55..d71142cb50 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityRebindTest.java @@ -30,19 +30,23 @@ import org.apache.brooklyn.api.typereg.ManagedBundle; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.camp.brooklyn.AbstractYamlRebindTest; +import org.apache.brooklyn.core.effector.Effectors; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.entity.StartableApplication; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest; import org.apache.brooklyn.core.test.entity.TestEntity; -import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.entity.stock.BasicApplication; import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.test.support.TestResourceUnavailableException; +import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ClassLoaderUtils; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.brooklyn.util.osgi.OsgiTestResources; +import org.apache.brooklyn.util.text.Strings; +import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; @@ -66,8 +70,8 @@ protected boolean useOsgi() { @Test public void testRebindAppIncludingBundle() throws Exception { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_COM_EXAMPLE_PATH); - ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(new BasicManagedBundle(), - new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL), true); + ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL) ); createAndStartApplication("services: [ { type: "+BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY+" } ]"); @@ -198,4 +202,113 @@ public void testEffectorInBundleReferencedByStockCatalogItem() throws Exception Effector newEffector = newEntity.getEntityType().getEffectorByName("myEffector").get(); newEntity.invoke(newEffector, ImmutableMap.of()).get(); } + + @Test + public void testClassAccessAfterUninstall() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH); + + // install dependency + ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ); + + // now the v2 bundle + OsgiBundleInstallationResult b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get(); + + Assert.assertEquals(b.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0"); + + String yaml = Strings.lines("name: simple-app-yaml", + "services:", + "- type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY); + Entity app = createAndStartApplication(yaml); + Entity more = Iterables.getOnlyElement( app.getChildren() ); + + Assert.assertEquals( + more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(), + "HI BOB FROM V2"); + + ((ManagementContextInternal)mgmt()).getOsgiManager().get().uninstallUploadedBundle(b.getMetadata()); + Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED); + + // can still call things + Assert.assertEquals( + more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Claudia")).get(), + "HI CLAUDIA FROM V2"); + + // but still uninstalled, and attempt to create makes error + Assert.assertEquals(b.getBundle().getState(), Bundle.UNINSTALLED); + try { + Entity app2 = createAndStartApplication(yaml); + Asserts.shouldHaveFailedPreviously("Expected deployment to fail after uninstall; instead got "+app2); + } catch (Exception e) { + Asserts.expectedFailureContainsIgnoreCase(e, "unable to match", BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY); + } + + try { + StartableApplication app2 = rebind(); + Asserts.shouldHaveFailedPreviously("Expected deployment to fail rebind; instead got "+app2); + } catch (Exception e) { + // should fail to rebind this entity + Asserts.expectedFailureContainsIgnoreCase(e, more.getId(), "unable to load", BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY); + } + } + + @Test + public void testClassAccessAfterUpgrade() throws Exception { + TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), BROOKLYN_TEST_OSGI_MORE_ENTITIES_0_1_0_PATH); + + // install dependency + ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_OSGI_ENTITIES_URL) ).checkNoError(); + + // now the v2 bundle + OsgiBundleInstallationResult b2a = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_URL) ).get(); + + Assert.assertEquals(b2a.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.2.0"); + Assert.assertEquals(b2a.getCode(), OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE); + + String yaml = Strings.lines("name: simple-app-yaml", + "services:", + "- type: " + BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY); + Entity app = createAndStartApplication(yaml); + Entity more = Iterables.getOnlyElement( app.getChildren() ); + + Assert.assertEquals( + more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Bob")).get(), + "HI BOB FROM V2"); + + // unforced upgrade should report already installed + Assert.assertEquals( ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL) ).get().getCode(), + OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_AREADY_INSTALLED); + + // force upgrade + OsgiBundleInstallationResult b2b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install(b2a.getMetadata(), + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V2_EVIL_TWIN_URL), true, true, true).get(); + Assert.assertEquals(b2a.getBundle(), b2b.getBundle()); + Assert.assertEquals(b2b.getCode(), OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE); + + // calls to things previously instantiated get the old behaviour + Assert.assertEquals( + more.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Claudia")).get(), + "HI CLAUDIA FROM V2"); + + // but new deployment gets the new behaviour + StartableApplication app2 = (StartableApplication) createAndStartApplication(yaml); + Entity more2 = Iterables.getOnlyElement( app2.getChildren() ); + Assert.assertEquals( + more2.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Daphne")).get(), + "HO DAPHNE FROM V2 EVIL TWIN"); + app2.stop(); + + // and after rebind on the old we get new behaviour + StartableApplication app1 = rebind(); + Entity more1 = Iterables.getOnlyElement( app1.getChildren() ); + Assert.assertEquals( + more1.invoke(Effectors.effector(String.class, "sayHI").buildAbstract(), MutableMap.of("name", "Eric")).get(), + "HO ERIC FROM V2 EVIL TWIN"); + } + + } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java index 118c51830a..8961c6f7d1 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiVersionMoreEntityTest.java @@ -33,10 +33,11 @@ import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest; import org.apache.brooklyn.camp.brooklyn.spi.creation.BrooklynEntityMatcher; +import org.apache.brooklyn.core.entity.Entities; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.osgi.OsgiVersionMoreEntityTest; import org.apache.brooklyn.core.objs.BrooklynTypes; -import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; import org.apache.brooklyn.core.typereg.RegisteredTypes; import org.apache.brooklyn.test.Asserts; @@ -44,7 +45,6 @@ import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.osgi.OsgiTestResources; import org.apache.brooklyn.util.text.Strings; -import org.osgi.framework.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; @@ -69,19 +69,19 @@ private static String getLocalResource(String filename) { @Test public void testBrooklynManagedBundleInstall() throws Exception { - BasicManagedBundle mb = new BasicManagedBundle(); - Bundle b = ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(mb, - new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL), true); - Assert.assertEquals(mb.getSymbolicName(), b.getSymbolicName()); + OsgiBundleInstallationResult br = ((ManagementContextInternal)mgmt()).getOsgiManager().get().install( + new ResourceUtils(getClass()).getResourceFromUrl(BROOKLYN_TEST_MORE_ENTITIES_V1_URL) ).get(); + Assert.assertEquals(br.getVersionedName().toString(), BROOKLYN_TEST_MORE_ENTITIES_SYMBOLIC_NAME_FULL+":"+"0.1.0"); // bundle installed Map bundles = ((ManagementContextInternal)mgmt()).getOsgiManager().get().getManagedBundles(); Asserts.assertSize(bundles.keySet(), 1); - Assert.assertEquals(mb.getId(), Iterables.getOnlyElement( bundles.keySet() )); + Assert.assertEquals(br.getMetadata().getId(), Iterables.getOnlyElement( bundles.keySet() )); // types installed RegisteredType t = mgmt().getTypeRegistry().get(BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY); Assert.assertNotNull(t); + Assert.assertEquals(t.getContainingBundle(), br.getVersionedName().toString()); // can deploy createAndStartApplication("services: [ { type: "+BROOKLYN_TEST_MORE_ENTITIES_MORE_ENTITY+" } ]"); @@ -181,7 +181,8 @@ public void testMoreEntityV1ThenV2GivesV2() throws Exception { OsgiVersionMoreEntityTest.assertV2MethodCall(moreEntity); } - @Test + @Test(groups="Broken") // won't work until search path is based on bundles instead of registered types + // (though it would work if we set versions properly in the OSGi bundles, but brooklyn types there all declare brooklyn version) public void testMoreEntityBothV1AndV2() throws Exception { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar"); TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), "/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar"); diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java index 7b726bc6e7..fe86ed0648 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogOsgiYamlEntityTest.java @@ -39,6 +39,7 @@ import org.apache.brooklyn.core.test.entity.TestEntityImpl; import org.apache.brooklyn.entity.stock.BasicApplication; import org.apache.brooklyn.entity.stock.BasicEntity; +import org.apache.brooklyn.test.Asserts; import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.core.ResourceUtils; import org.apache.brooklyn.util.osgi.OsgiTestResources; @@ -224,9 +225,9 @@ public void testReferenceNonInstalledBundledByNameFails() { " version: " + nonExistentVersion, " item:", " type: " + SIMPLE_ENTITY_TYPE); - fail(); + Asserts.shouldHaveFailedPreviously(); } catch (IllegalStateException e) { - Assert.assertEquals(e.getMessage(), "Bundle from null failed to install: Bundle CatalogBundleDto{symbolicName=" + nonExistentId + ", version=" + nonExistentVersion + ", url=null} not previously registered, but URL is empty."); + Asserts.expectedFailureContainsIgnoreCase(e, nonExistentId, nonExistentVersion, "no input stream", "no URL"); } } @@ -305,13 +306,11 @@ public void testFullBundleReferenceUrlMetaOverridesLocalNameVersion() { "", " item:", " type: " + SIMPLE_ENTITY_TYPE); - fail(); + Asserts.shouldHaveFailedPreviously(); } catch (IllegalStateException e) { - assertEquals(e.getMessage(), "Bundle from " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL + " failed to install: " + - "Bundle already installed as " + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_NAME + ":" + - OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_VERSION + " but user explicitly requested " + - "CatalogBundleDto{symbolicName=" + nonExistentId + ", version=" + nonExistentVersion + ", url=" + - OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL + "}"); + Asserts.expectedFailureContainsIgnoreCase(e, nonExistentId, nonExistentVersion, + "symbolic name mismatch", OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_NAME, + OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL); } } diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlRebindTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlRebindTest.java index c4af503476..127a110fdd 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlRebindTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/catalog/CatalogYamlRebindTest.java @@ -175,6 +175,10 @@ private void applyCompoundStateTransformer(RebindOptions options, final Compound for (BrooklynObjectType type : BrooklynObjectType.values()) { final List contents = objectStore.listContentsWithSubPath(type.getSubPathName()); for (String path : contents) { + if (path.endsWith(".jar")) { + // don't apply transformers to JARs + continue; + } StoreObjectAccessor accessor = objectStore.newAccessor(path); String memento = checkNotNull(accessor.get(), path); String replacement = transformed.getObjectsOfType(type).get(idFromPath(type, path)); diff --git a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java index 68c7feded5..506e8387e1 100644 --- a/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java +++ b/camp/camp-brooklyn/src/test/java/org/apache/brooklyn/camp/brooklyn/test/lite/CampYamlLiteTest.java @@ -40,8 +40,6 @@ import org.apache.brooklyn.camp.spi.pdp.PdpYamlTest; import org.apache.brooklyn.camp.test.mock.web.MockWebPlatform; import org.apache.brooklyn.core.catalog.CatalogPredicates; -import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; -import org.apache.brooklyn.core.catalog.internal.CatalogDto; import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.effector.AddChildrenEffector; @@ -52,6 +50,7 @@ import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; import org.apache.brooklyn.core.test.entity.TestApplication; import org.apache.brooklyn.core.test.entity.TestEntity; +import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.core.typereg.RegisteredTypes; import org.apache.brooklyn.test.support.TestResourceUnavailableException; @@ -156,6 +155,7 @@ public void testAddChildrenEffector() throws Exception { @Test public void testYamlServiceForCatalog() { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + installWithoutCatalogBom(mgmt, OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL); CatalogItem realItem = Iterables.getOnlyElement(mgmt.getCatalog().addItems(Streams.readFullyStringAndClose(getClass().getResourceAsStream("test-app-service-blueprint.yaml")))); Iterable> retrievedItems = mgmt.getCatalog() @@ -184,14 +184,16 @@ public void testRegisterCustomEntityWithBundleWhereEntityIsFromCoreAndIconFromBu String symbolicName = "my.catalog.app.id"; String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; - String yaml = getSampleMyCatalogAppYaml(symbolicName, bundleUrl); + String yaml = prepAndGetSampleMyCatalogAppYaml(symbolicName, bundleUrl); mgmt.getCatalog().addItems(yaml); assertMgmtHasSampleMyCatalogApp(symbolicName, bundleUrl); } - private String getSampleMyCatalogAppYaml(String symbolicName, String bundleUrl) { + private String prepAndGetSampleMyCatalogAppYaml(String symbolicName, String bundleUrl) { + installWithoutCatalogBom(mgmt, bundleUrl); + return Joiner.on("\n").join( "brooklyn.catalog:", " id: " + symbolicName, @@ -203,7 +205,14 @@ private String getSampleMyCatalogAppYaml(String symbolicName, String bundleUrl) " libraries:", " - url: " + bundleUrl, " item:", - " type: io.camp.mock:AppServer"); + " type: " + MockWebPlatform.APPSERVER.getName()); + } + + protected void installWithoutCatalogBom(LocalManagementContext mgmt, String bundleUrl) { + // install bundle for class access but without loading its catalog.bom, + // since we only have mock matchers here + // (if we don't do this, the default routines install it and try to process the catalog.bom, failing) + mgmt.getOsgiManager().get().installDeferredStart(new BasicManagedBundle(null, null, bundleUrl), null).get(); } private void assertMgmtHasSampleMyCatalogApp(String symbolicName, String bundleUrl) { diff --git a/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java b/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java index fc9556dc57..161eff0903 100644 --- a/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java +++ b/core/src/main/java/org/apache/brooklyn/core/BrooklynVersion.java @@ -46,6 +46,7 @@ import org.apache.brooklyn.util.osgi.OsgiUtil; import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.VersionComparator; import org.osgi.framework.Bundle; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkUtil; @@ -155,7 +156,7 @@ public String getVersion() { @Override public boolean isSnapshot() { - return (getVersion().indexOf("-SNAPSHOT") >= 0); + return VersionComparator.isSnapshot(getVersion()); } private void readPropertiesFromMavenResource(ClassLoader resourceLoader) { diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java index 161e1ca93a..a3a4e7f1dd 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/BasicBrooklynCatalog.java @@ -39,6 +39,7 @@ import org.apache.brooklyn.api.location.LocationSpec; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext; +import org.apache.brooklyn.api.typereg.ManagedBundle; import org.apache.brooklyn.core.catalog.CatalogPredicates; import org.apache.brooklyn.core.catalog.internal.CatalogClasspathDo.CatalogScanningModes; import org.apache.brooklyn.core.location.BasicLocationRegistry; @@ -425,17 +426,25 @@ public static Map getCatalogMetadata(String yaml) { return getFirstAsMap(itemDef, "brooklyn.catalog").orNull(); } + /** @deprecated since 0.12.0 - use {@link #getVersionedName(Map, boolean)} */ + @Deprecated public static VersionedName getVersionedName(Map catalogMetadata) { + return getVersionedName(catalogMetadata, true); + } + + public static VersionedName getVersionedName(Map catalogMetadata, boolean required) { String version = getFirstAs(catalogMetadata, String.class, "version").orNull(); String bundle = getFirstAs(catalogMetadata, String.class, "bundle").orNull(); if (Strings.isBlank(bundle) && Strings.isBlank(version)) { + if (!required) return null; throw new IllegalStateException("Catalog BOM must define bundle and version"); } if (Strings.isBlank(bundle)) { + if (!required) return null; throw new IllegalStateException("Catalog BOM must define bundle"); } if (Strings.isBlank(version)) { - throw new IllegalStateException("Catalog BOM must define version"); + throw new IllegalStateException("Catalog BOM must define version if bundle is defined"); } return new VersionedName(bundle, Version.valueOf(version)); } @@ -574,7 +583,7 @@ private void collectCatalogItems(String sourceYaml, Map itemMetadata, List< PlanInterpreterGuessingType planInterpreter = new PlanInterpreterGuessingType(null, item, sourceYaml, itemType, libraryBundles, result).reconstruct(); if (!planInterpreter.isResolved()) { - throw Exceptions.create("Could not resolve item" + throw Exceptions.create("Could not resolve definition of item" + (Strings.isNonBlank(id) ? " '"+id+"'" : Strings.isNonBlank(symbolicName) ? " '"+symbolicName+"'" : Strings.isNonBlank(name) ? " '"+name+"'" : "") // better not to show yaml, takes up lots of space, and with multiple plan transformers there might be multiple errors; // some of the errors themselves may reproduce it @@ -982,7 +991,12 @@ public CatalogItem addItem(String yaml) { @Override public List> addItems(String yaml) { - return addItems(yaml, false); + return addItems(yaml, null); + } + + @Override + public List> addItems(String yaml, ManagedBundle bundle) { + return addItems(yaml, bundle, false); } @Override @@ -992,12 +1006,19 @@ public CatalogItem addItem(String yaml, boolean forceUpdate) { @Override public List> addItems(String yaml, boolean forceUpdate) { + return addItems(yaml, null, forceUpdate); + } + + private List> addItems(String yaml, ManagedBundle bundle, boolean forceUpdate) { log.debug("Adding manual catalog item to "+mgmt+": "+yaml); checkNotNull(yaml, "yaml"); List> result = collectCatalogItems(yaml); // do this at the end for atomic updates; if there are intra-yaml references, we handle them specially for (CatalogItemDtoAbstract item: result) { + if (bundle!=null && bundle.getVersionedName()!=null) { + item.setContainingBundle(bundle.getVersionedName()); + } addItemDto(item, forceUpdate); } return result; diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogBundleLoader.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogBundleLoader.java index b1f8fd3b0f..4bf88240c5 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogBundleLoader.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogBundleLoader.java @@ -29,8 +29,11 @@ import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.osgi.VersionedName; import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.yaml.Yamls; @@ -69,6 +72,8 @@ public CatalogBundleLoader(Predicate applicationsPermitted, ManagementCo * @throws RuntimeException if the catalog items failed to be added to the catalog */ public Iterable> scanForCatalog(Bundle bundle) { + ManagedBundle mb = ((ManagementContextInternal)managementContext).getOsgiManager().get().getManagedBundle( + new VersionedName(bundle)); Iterable> catalogItems = MutableList.of(); @@ -77,7 +82,7 @@ public CatalogBundleLoader(Predicate applicationsPermitted, ManagementCo LOG.debug("Found catalog BOM in {} {} {}", CatalogUtils.bundleIds(bundle)); String bomText = readBom(bom); String bomWithLibraryPath = addLibraryDetails(bundle, bomText); - catalogItems = this.managementContext.getCatalog().addItems(bomWithLibraryPath); + catalogItems = this.managementContext.getCatalog().addItems(bomWithLibraryPath, mb); for (CatalogItem item : catalogItems) { LOG.debug("Added to catalog: {}, {}", item.getSymbolicName(), item.getVersion()); } diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java index 7e8a9567c6..2c1bec64bc 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDo.java @@ -197,6 +197,11 @@ public String getIconUrl() { public String getSymbolicName() { return itemDto.getSymbolicName(); } + + @Override + public String getContainingBundle() { + return itemDto.getContainingBundle(); + } @Override public String getVersion() { diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java index 1201395c72..4644d8d901 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogItemDtoAbstract.java @@ -20,7 +20,6 @@ import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Set; @@ -37,6 +36,8 @@ import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.core.flags.FlagUtils; import org.apache.brooklyn.util.core.flags.SetFromFlag; +import org.apache.brooklyn.util.osgi.VersionedName; +import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +53,7 @@ public abstract class CatalogItemDtoAbstract extends AbstractBrooklynO private @SetFromFlag String symbolicName; private @SetFromFlag String version = BasicBrooklynCatalog.NO_VERSION; + private @SetFromFlag String containingBundle; private @SetFromFlag String displayName; private @SetFromFlag String description; @@ -120,6 +122,11 @@ public String getName() { public String getRegisteredTypeName() { return getSymbolicName(); } + + @Override + public String getContainingBundle() { + return containingBundle; + } @Override public String getDisplayName() { @@ -191,7 +198,7 @@ public String getPlanYaml() { @Override public int hashCode() { - return Objects.hashCode(symbolicName, planYaml, javaType, nullIfEmpty(libraries), version, getCatalogItemId(), + return Objects.hashCode(symbolicName, containingBundle, planYaml, javaType, nullIfEmpty(libraries), version, getCatalogItemId(), getCatalogItemIdSearchPath()); } @@ -202,6 +209,7 @@ public boolean equals(Object obj) { if (getClass() != obj.getClass()) return false; CatalogItemDtoAbstract other = (CatalogItemDtoAbstract) obj; if (!Objects.equal(symbolicName, other.symbolicName)) return false; + if (!Objects.equal(containingBundle, other.containingBundle)) return false; if (!Objects.equal(planYaml, other.planYaml)) return false; if (!Objects.equal(javaType, other.javaType)) return false; if (!Objects.equal(nullIfEmpty(libraries), nullIfEmpty(other.libraries))) return false; @@ -346,6 +354,10 @@ protected void setVersion(String version) { this.version = version; } + public void setContainingBundle(VersionedName versionedName) { + this.containingBundle = Strings.toString(versionedName); + } + protected void setDescription(String description) { this.description = description; } @@ -421,4 +433,5 @@ private static String stringValOrNull(Map map, String key) { Object val = map.get(key); return val != null ? String.valueOf(val) : null; } + } diff --git a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java index 776894f5fa..47dab6d5f7 100644 --- a/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java +++ b/core/src/main/java/org/apache/brooklyn/core/catalog/internal/CatalogUtils.java @@ -39,13 +39,16 @@ import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContextSequential; import org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext; import org.apache.brooklyn.core.mgmt.classloading.OsgiBrooklynClassLoadingContext; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.ha.OsgiManager; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.rebind.RebindManagerImpl.RebindTracker; import org.apache.brooklyn.core.objs.BrooklynObjectInternal; +import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.core.typereg.RegisteredTypeLoadingContexts; import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; import org.apache.brooklyn.core.typereg.RegisteredTypes; +import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Time; @@ -169,13 +172,24 @@ public static void installLibraries(ManagementContext managementContext, @Nullab "Loading bundles in {}: {}", new Object[] {managementContext, Joiner.on(", ").join(libraries)}); Stopwatch timer = Stopwatch.createStarted(); + List results = MutableList.of(); for (CatalogBundle bundleUrl : libraries) { - osgi.get().registerBundle(bundleUrl); + OsgiBundleInstallationResult result = osgi.get().installDeferredStart(BasicManagedBundle.of(bundleUrl), null).get(); + if (log.isDebugEnabled()) { + logDebugOrTraceIfRebinding(log, "Installation of library "+bundleUrl+": "+result); + } + results.add(result); } - if (log.isDebugEnabled()) + for (OsgiBundleInstallationResult r: results) { + if (r.getDeferredStart()!=null) { + r.getDeferredStart().run(); + } + } + if (log.isDebugEnabled()) { logDebugOrTraceIfRebinding(log, "Registered {} bundles in {}", new Object[]{libraries.size(), Time.makeTimeStringRounded(timer)}); + } } } diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java new file mode 100644 index 0000000000..7d2267aa8e --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiArchiveInstaller.java @@ -0,0 +1,427 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.mgmt.ha; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.brooklyn.api.catalog.CatalogItem; +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult.ResultCode; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.core.typereg.BasicManagedBundle; +import org.apache.brooklyn.util.core.ResourceUtils; +import org.apache.brooklyn.util.core.osgi.BundleMaker; +import org.apache.brooklyn.util.core.osgi.Osgis; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; +import org.apache.brooklyn.util.guava.Maybe; +import org.apache.brooklyn.util.os.Os; +import org.apache.brooklyn.util.osgi.VersionedName; +import org.apache.brooklyn.util.stream.Streams; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.VersionComparator; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Objects; + +// package-private so we can move this one if/when we move OsgiManager +class OsgiArchiveInstaller { + + private static final Logger log = LoggerFactory.getLogger(OsgiArchiveInstaller.class); + + // must be 1.0; see bottom of + // http://www.eclipse.org/virgo/documentation/virgo-documentation-3.7.0.M01/docs/virgo-user-guide/html/ch02s02.html + private static final String OSGI_MANIFEST_VERSION_VALUE = "1.0"; + + final private OsgiManager osgiManager; + private ManagedBundle suppliedKnownBundleMetadata; + private InputStream zipIn; + + private boolean start = true; + private boolean loadCatalogBom = true; + private boolean force = false; + private boolean deferredStart = false; + + private File zipFile; + private Manifest discoveredManifest; + private VersionedName discoveredBomVersionedName; + OsgiBundleInstallationResult result; + + private ManagedBundle inferredMetadata; + private final boolean inputStreamSupplied; + + OsgiArchiveInstaller(OsgiManager osgiManager, ManagedBundle knownBundleMetadata, InputStream zipIn) { + this.osgiManager = osgiManager; + this.suppliedKnownBundleMetadata = knownBundleMetadata; + this.zipIn = zipIn; + inputStreamSupplied = zipIn!=null; + } + + public void setStart(boolean start) { + this.start = start; + } + + public void setLoadCatalogBom(boolean loadCatalogBom) { + this.loadCatalogBom = loadCatalogBom; + } + + public void setForce(boolean force) { + this.force = force; + } + + public void setDeferredStart(boolean deferredStart) { + this.deferredStart = deferredStart; + } + + private ManagementContextInternal mgmt() { + return (ManagementContextInternal) osgiManager.mgmt; + } + + private synchronized void init() { + if (result!=null) { + if (zipFile!=null || zipIn==null) return; + throw new IllegalStateException("This installer instance has already been used and the input stream discarded"); + } + result = new OsgiBundleInstallationResult(); + inferredMetadata = suppliedKnownBundleMetadata==null ? new BasicManagedBundle() : suppliedKnownBundleMetadata; + } + + private synchronized void makeLocalZipFileFromInputStreamOrUrl() { + if (zipIn==null) { + Maybe installedBundle = Maybe.absent(); + if (suppliedKnownBundleMetadata!=null) { + // if no input stream, look for a URL and/or a matching bundle + if (!suppliedKnownBundleMetadata.isNameResolved()) { + ManagedBundle mbFromUrl = osgiManager.getManagedBundleFromUrl(suppliedKnownBundleMetadata.getUrl()); + if (mbFromUrl!=null) { + // user supplied just a URL (eg brooklyn.libraries), but we recognise it, + // so don't try to reload it, just record the info we know about it to retrieve the bundle + ((BasicManagedBundle)suppliedKnownBundleMetadata).setSymbolicName(mbFromUrl.getSymbolicName()); + ((BasicManagedBundle)suppliedKnownBundleMetadata).setVersion(mbFromUrl.getVersion()); + } + } + if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.getOsgiUniqueUrl()!=null) { + installedBundle = Osgis.bundleFinder(osgiManager.framework).requiringFromUrl(suppliedKnownBundleMetadata.getOsgiUniqueUrl()).find(); + } + if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.getUrl()!=null) { + installedBundle = Osgis.bundleFinder(osgiManager.framework).requiringFromUrl(suppliedKnownBundleMetadata.getUrl()).find(); + } + if (installedBundle.isAbsent() && suppliedKnownBundleMetadata.isNameResolved()) { + installedBundle = Osgis.bundleFinder(osgiManager.framework).symbolicName(suppliedKnownBundleMetadata.getSymbolicName()).version(suppliedKnownBundleMetadata.getVersion()).find(); + } + if (suppliedKnownBundleMetadata.getUrl()!=null) { + if (installedBundle.isAbsent() || force) { + // reload + zipIn = ResourceUtils.create(mgmt()).getResourceFromUrl(suppliedKnownBundleMetadata.getUrl()); + } + } + } + + if (zipIn==null) { + if (installedBundle.isPresent()) { + // no way to install (no url), or no need to install (not forced); just ignore it + result.metadata = osgiManager.getManagedBundle(new VersionedName(installedBundle.get())); + if (result.metadata==null) { + // low-level installed bundle + result.metadata = new BasicManagedBundle(installedBundle.get().getSymbolicName(), installedBundle.get().getVersion().toString(), + suppliedKnownBundleMetadata!=null ? suppliedKnownBundleMetadata.getUrl() : null); + } + result.setIgnoringAlreadyInstalled(); + return; + } + result.metadata = suppliedKnownBundleMetadata; + throw new IllegalArgumentException("No input stream available and no URL could be found; nothing to install"); + } + result.bundle = installedBundle.orNull(); + } + + zipFile = Os.newTempFile("brooklyn-bundle-transient-"+suppliedKnownBundleMetadata, "zip"); + try (FileOutputStream fos = new FileOutputStream(zipFile)) { + Streams.copy(zipIn, fos); + zipIn.close(); + } catch (Exception e) { + throw Exceptions.propagate(e); + } finally { + zipIn = null; + } + } + + private void discoverManifestFromCatalogBom(boolean isCatalogBomRequired) { + discoveredManifest = new BundleMaker(mgmt()).getManifest(zipFile); + ZipFile zf = null; + try { + try { + zf = new ZipFile(zipFile); + } catch (IOException e) { + throw new IllegalArgumentException("Invalid ZIP/JAR archive: "+e); + } + ZipEntry bom = zf.getEntry("catalog.bom"); + if (bom==null) { + bom = zf.getEntry("/catalog.bom"); + } + if (bom==null) { + if (isCatalogBomRequired) { + throw new IllegalArgumentException("Archive must contain a catalog.bom file in the root"); + } else { + return; + } + } + String bomS; + try { + bomS = Streams.readFullyString(zf.getInputStream(bom)); + } catch (IOException e) { + throw new IllegalArgumentException("Error reading catalog.bom from ZIP/JAR archive: "+e); + } + discoveredBomVersionedName = BasicBrooklynCatalog.getVersionedName( BasicBrooklynCatalog.getCatalogMetadata(bomS), false ); + } finally { + Streams.closeQuietly(zf); + } + } + + private void updateManifestFromAllSourceInformation() { + if (discoveredBomVersionedName!=null) { + matchSetOrFail("catalog.bom in archive", discoveredBomVersionedName.getSymbolicName(), discoveredBomVersionedName.getVersion().toString()); + } + + boolean manifestNeedsUpdating = false; + if (discoveredManifest==null) { + discoveredManifest = new Manifest(); + manifestNeedsUpdating = true; + } + if (!matchSetOrFail("MANIFEST.MF in archive", discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME), + discoveredManifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION) )) { + manifestNeedsUpdating = true; + discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, inferredMetadata.getSymbolicName()); + discoveredManifest.getMainAttributes().putValue(Constants.BUNDLE_VERSION, inferredMetadata.getVersion()); + } + if (Strings.isBlank(inferredMetadata.getSymbolicName())) { + throw new IllegalArgumentException("Missing bundle symbolic name in BOM or MANIFEST"); + } + if (Strings.isBlank(inferredMetadata.getVersion())) { + throw new IllegalArgumentException("Missing bundle version in BOM or MANIFEST"); + } + if (discoveredManifest.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION)==null) { + discoveredManifest.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), OSGI_MANIFEST_VERSION_VALUE); + manifestNeedsUpdating = true; + } + if (manifestNeedsUpdating) { + File zf2 = new BundleMaker(mgmt()).copyAddingManifest(zipFile, discoveredManifest); + zipFile.delete(); + zipFile = zf2; + } + } + + private synchronized void close() { + if (zipFile!=null) { + zipFile.delete(); + zipFile = null; + } + } + + /** + * Installs a bundle, taking from ZIP input stream if supplied, falling back to URL in the {@link ManagedBundle} metadata supplied. + * It will take metadata from any of: a MANIFEST.MF in the ZIP; a catalog.bom in the ZIP; the {@link ManagedBundle} metadata supplied. + * If metadata is supplied in multiple such places, it must match. + * Appropriate metadata will be added to the ZIP and installation attempted. + *

+ * If a matching bundle is already installed, the installation will stop with a {@link ResultCode#IGNORING_BUNDLE_AREADY_INSTALLED} + * unless the bundle is a snapshot or "force" is specified. + * In the latter two cases, if there is an installed matching bundle, that bundle will be updated with the input stream here, + * with any catalog items from the old bundle removed and those from this bundle installed. + *

+ * Default behaviour is {@link #setLoadCatalogBom(boolean)} true and {@link #setForce(boolean)} false. + *

+ * The return value is extensive but should be self-evident, and will include a list of any registered types (catalog items) installed. + */ + public ReferenceWithError install() { + boolean startedInstallation = false; + + try { + init(); + makeLocalZipFileFromInputStreamOrUrl(); + if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result); + discoverManifestFromCatalogBom(false); + if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result); + updateManifestFromAllSourceInformation(); + if (result.code!=null) return ReferenceWithError.newInstanceWithoutError(result); + assert inferredMetadata.isNameResolved() : "Should have resolved "+inferredMetadata; + + final boolean updating; + result.metadata = osgiManager.getManagedBundle(inferredMetadata.getVersionedName()); + if (result.getMetadata()!=null) { + // already have a managed bundle - check if this is using a new/different URL + if (suppliedKnownBundleMetadata!=null && suppliedKnownBundleMetadata.getUrl()!=null) { + String knownIdForThisUrl = osgiManager.managedBundlesRecord.getManagedBundleIdFromUrl(suppliedKnownBundleMetadata.getUrl()); + if (knownIdForThisUrl==null) { + // it's a new URL, but a bundle we already know about + log.warn("Request to install from "+suppliedKnownBundleMetadata.getUrl()+" which is not recognized but "+ + "appears to match "+result.getMetadata()+"; now associating with the latter"); + osgiManager.managedBundlesRecord.setManagedBundleUrl(suppliedKnownBundleMetadata.getUrl(), result.getMetadata().getId()); + } else if (!knownIdForThisUrl.equals(result.getMetadata().getId())) { + log.warn("Request to install from "+suppliedKnownBundleMetadata.getUrl()+" which is associated to "+knownIdForThisUrl+" but "+ + "appears to match "+result.getMetadata()+"; now associating with the latter"); + osgiManager.managedBundlesRecord.setManagedBundleUrl(suppliedKnownBundleMetadata.getUrl(), result.getMetadata().getId()); + } + } + if (canUpdate()) { + result.bundle = osgiManager.framework.getBundleContext().getBundle(result.getMetadata().getOsgiUniqueUrl()); + if (result.getBundle()==null) { + throw new IllegalStateException("Detected already managing bundle "+result.getMetadata().getVersionedName()+" but framework cannot find it"); + } + updating = true; + } else { + result.setIgnoringAlreadyInstalled(); + return ReferenceWithError.newInstanceWithoutError(result); + } + } else { + result.metadata = inferredMetadata; + // no such managed bundle + Maybe b = Osgis.bundleFinder(osgiManager.framework).symbolicName(result.getMetadata().getSymbolicName()).version(result.getMetadata().getVersion()).find(); + if (b.isPresent()) { + // if it's non-brooklyn installed then fail + // (e.g. someone trying to install brooklyn or guice through this mechanism!) + result.bundle = b.get(); + result.code = OsgiBundleInstallationResult.ResultCode.ERROR_INSTALLING_BUNDLE; + throw new IllegalStateException("Bundle "+result.getMetadata().getVersionedName()+" already installed in framework but not managed by Brooklyn; cannot install or update through Brooklyn"); + } + // normal install + updating = false; + } + + startedInstallation = true; + try (InputStream fin = new FileInputStream(zipFile)) { + if (!updating) { + // install new + assert result.getBundle()==null; + result.bundle = osgiManager.framework.getBundleContext().installBundle(result.getMetadata().getOsgiUniqueUrl(), fin); + } else { + result.bundle.update(fin); + } + } + + osgiManager.checkCorrectlyInstalled(result.getMetadata(), result.bundle); + ((BasicManagedBundle)result.getMetadata()).setTempLocalFileWhenJustUploaded(zipFile); + zipFile = null; // don't close/delete it here, we'll use it for uploading, then it will delete it + + if (!updating) { + osgiManager.managedBundlesRecord.addManagedBundle(result); + result.code = OsgiBundleInstallationResult.ResultCode.INSTALLED_NEW_BUNDLE; + result.message = "Installed "+result.getMetadata().getVersionedName()+" with ID "+result.getMetadata().getId(); + mgmt().getRebindManager().getChangeListener().onManaged(result.getMetadata()); + } else { + result.code = OsgiBundleInstallationResult.ResultCode.UPDATED_EXISTING_BUNDLE; + result.message = "Updated "+result.getMetadata().getVersionedName()+" as existing ID "+result.getMetadata().getId(); + mgmt().getRebindManager().getChangeListener().onChanged(result.getMetadata()); + } + log.info(result.message); + + // setting the above before the code below means if there is a problem starting or loading catalog items + // a user has to remove then add again, or forcibly reinstall; + // that seems fine and probably better than allowing bundles to start and catalog items to be installed + // when brooklyn isn't aware it is supposed to be managing it + + // starting here flags wiring issues earlier + // but may break some things running from the IDE + // eg if it doesn't have OSGi deps, or if it doesn't have camp parser, + // or if caller is installing multiple things that depend on each other + // eg rebind code, brooklyn.libraries list -- deferred start allows caller to + // determine whether not to start or to start all after things are installed + Runnable startRunnable = new Runnable() { + public void run() { + if (start) { + try { + result.bundle.start(); + } catch (BundleException e) { + throw Exceptions.propagate(e); + } + } + + if (loadCatalogBom) { + if (updating) { + osgiManager.uninstallCatalogItemsFromBundle( result.getVersionedName() ); + // (ideally removal and addition would be atomic) + } + for (CatalogItem ci: osgiManager.loadCatalogBom(result.bundle)) { + result.catalogItemsInstalled.add(ci.getId()); + } + } + } + }; + if (deferredStart) { + result.deferredStart = startRunnable; + } else { + startRunnable.run(); + } + + return ReferenceWithError.newInstanceWithoutError(result); + + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + result.code = startedInstallation ? OsgiBundleInstallationResult.ResultCode.ERROR_INSTALLING_BUNDLE : OsgiBundleInstallationResult.ResultCode.ERROR_PREPARING_BUNDLE; + result.message = "Bundle "+inferredMetadata+" failed "+ + (startedInstallation ? "installation" : "preparation") + ": " + Exceptions.collapseText(e); + return ReferenceWithError.newInstanceThrowingError(result, new IllegalStateException(result.message, e)); + } finally { + close(); + } + } + + private boolean canUpdate() { + // only update if forced, or it's a snapshot for which a byte stream is supplied + // (IE don't update a snapshot verison every time its URL is referenced in a 'libraries' section) + return force || (VersionComparator.isSnapshot(inferredMetadata.getVersion()) && inputStreamSupplied); + } + + /** true if the supplied name and version are complete; updates if the known data is incomplete; + * throws if there is a mismatch; false if the supplied data is incomplete */ + private boolean matchSetOrFail(String source, String name, String version) { + boolean suppliedIsComplete = true; + if (Strings.isBlank(name)) { + suppliedIsComplete = false; + } else if (Strings.isBlank(inferredMetadata.getSymbolicName())) { + ((BasicManagedBundle)inferredMetadata).setSymbolicName(name); + } else if (!Objects.equal(inferredMetadata.getSymbolicName(), name)){ + throw new IllegalArgumentException("Symbolic name mismatch '"+name+"' from "+source+" (expected '"+inferredMetadata.getSymbolicName()+"')"); + } + + if (Strings.isBlank(version)) { + suppliedIsComplete = false; + } else if (Strings.isBlank(inferredMetadata.getVersion())) { + ((BasicManagedBundle)inferredMetadata).setVersion(version); + } else if (!Objects.equal(inferredMetadata.getVersion(), version)){ + throw new IllegalArgumentException("Bundle version mismatch '"+version+"' from "+source+" (expected '"+inferredMetadata.getVersion()+"')"); + } + + return suppliedIsComplete; + } + +} diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java new file mode 100644 index 0000000000..c3a725a6bb --- /dev/null +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiBundleInstallationResult.java @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.core.mgmt.ha; + +import java.util.List; + +import org.apache.brooklyn.api.typereg.ManagedBundle; +import org.apache.brooklyn.util.collections.MutableList; +import org.apache.brooklyn.util.osgi.VersionedName; +import org.osgi.framework.Bundle; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableList; + +@Beta +public class OsgiBundleInstallationResult { + String message; + ManagedBundle metadata; + Bundle bundle; + ResultCode code; + Runnable deferredStart; + + public enum ResultCode { + INSTALLED_NEW_BUNDLE, + UPDATED_EXISTING_BUNDLE, + IGNORING_BUNDLE_AREADY_INSTALLED, + ERROR_PREPARING_BUNDLE, + ERROR_INSTALLING_BUNDLE + } + final List catalogItemsInstalled = MutableList.of(); + + public String getMessage() { + return message; + } + public Bundle getBundle() { + return bundle; + } + public ManagedBundle getMetadata() { + return metadata; + } + public ResultCode getCode() { + return code; + } + public List getCatalogItemsInstalled() { + return ImmutableList.copyOf(catalogItemsInstalled); + } + public VersionedName getVersionedName() { + if (getMetadata()==null) return null; + return getMetadata().getVersionedName(); + } + public Runnable getDeferredStart() { + return deferredStart; + } + + void setIgnoringAlreadyInstalled() { + code = OsgiBundleInstallationResult.ResultCode.IGNORING_BUNDLE_AREADY_INSTALLED; + message = "Bundle "+getMetadata().getVersionedName()+" already installed as "+getMetadata().getId(); + } + + @Override + public String toString() { + return OsgiBundleInstallationResult.class.getSimpleName()+"["+code+", "+metadata+", "+message+"]"; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java index 6f6d230cc6..2f61d16de9 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/ha/OsgiManager.java @@ -19,8 +19,6 @@ package org.apache.brooklyn.core.mgmt.ha; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.InputStream; import java.net.URL; import java.util.Arrays; @@ -33,11 +31,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; + import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.typereg.ManagedBundle; import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; +import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.BrooklynFeatureEnablement; import org.apache.brooklyn.core.BrooklynVersion; @@ -46,7 +47,6 @@ import org.apache.brooklyn.core.mgmt.persist.OsgiClassPrefixer; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.core.server.BrooklynServerPaths; -import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.collections.MutableSet; @@ -54,12 +54,13 @@ import org.apache.brooklyn.util.core.osgi.Osgis.BundleFinder; import org.apache.brooklyn.util.core.osgi.SystemFrameworkLoader; import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; import org.apache.brooklyn.util.exceptions.UserFacingException; import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.os.Os.DeletionResult; +import org.apache.brooklyn.util.osgi.VersionedName; import org.apache.brooklyn.util.repeat.Repeater; -import org.apache.brooklyn.util.stream.Streams; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.time.Duration; import org.osgi.framework.Bundle; @@ -73,6 +74,7 @@ import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; @@ -93,17 +95,57 @@ public class OsgiManager { /* see `Osgis` class for info on starting framework etc */ - protected final ManagementContext mgmt; - protected final OsgiClassPrefixer osgiClassPrefixer; - protected Framework framework; - protected boolean reuseFramework; + final ManagementContext mgmt; + final OsgiClassPrefixer osgiClassPrefixer; + Framework framework; + + private boolean reuseFramework; private Set bundlesAtStartup; - protected File osgiCacheDir; - protected Map managedBundles = MutableMap.of(); - protected AtomicInteger numberOfReusableFrameworksCreated = new AtomicInteger(); + private File osgiCacheDir; + final ManagedBundlesRecord managedBundlesRecord = new ManagedBundlesRecord(); + + static class ManagedBundlesRecord { + private Map managedBundles = MutableMap.of(); + private Map managedBundlesByName = MutableMap.of(); + private Map managedBundlesByUrl = MutableMap.of(); + + synchronized Map getManagedBundles() { + return ImmutableMap.copyOf(managedBundles); + } + synchronized String getManagedBundleId(VersionedName vn) { + return managedBundlesByName.get(vn); + } + + synchronized ManagedBundle getManagedBundle(VersionedName vn) { + return managedBundles.get(managedBundlesByName.get(vn)); + } + + synchronized String getManagedBundleIdFromUrl(String url) { + return managedBundlesByUrl.get(url); + } + + synchronized ManagedBundle getManagedBundleFromUrl(String url) { + String id = getManagedBundleIdFromUrl(url); + if (id==null) return null; + return managedBundles.get(id); + } + + synchronized void setManagedBundleUrl(String url, String id) { + managedBundlesByUrl.put(url, id); + } + + synchronized void addManagedBundle(OsgiBundleInstallationResult result) { + managedBundles.put(result.getMetadata().getId(), result.getMetadata()); + managedBundlesByName.put(result.getMetadata().getVersionedName(), result.getMetadata().getId()); + if (Strings.isNonBlank(result.getMetadata().getUrl())) { + managedBundlesByUrl.put(result.getMetadata().getUrl(), result.getMetadata().getId()); + } + } + } - protected static final List OSGI_FRAMEWORK_CONTAINERS_FOR_REUSE = MutableList.of(); + private static AtomicInteger numberOfReusableFrameworksCreated = new AtomicInteger(); + private static final List OSGI_FRAMEWORK_CONTAINERS_FOR_REUSE = MutableList.of(); public OsgiManager(ManagementContext mgmt) { this.mgmt = mgmt; @@ -209,54 +251,104 @@ public Boolean call() { } public Map getManagedBundles() { - return ImmutableMap.copyOf(managedBundles); + return managedBundlesRecord.getManagedBundles(); + } + + public String getManagedBundleId(VersionedName vn) { + return managedBundlesRecord.getManagedBundleId(vn); } - public Bundle installUploadedBundle(ManagedBundle bundleMetadata, InputStream zipIn, boolean loadCatalogBom) { - try { - Bundle alreadyBundle = checkBundleInstalledThrowIfInconsistent(bundleMetadata, false); - if (alreadyBundle!=null) { - return alreadyBundle; - } + public ManagedBundle getManagedBundle(VersionedName vn) { + return managedBundlesRecord.getManagedBundle(vn); + } - File zipF = Os.newTempFile("brooklyn-bundle-transient-"+bundleMetadata, "zip"); - FileOutputStream fos = new FileOutputStream(zipF); - Streams.copy(zipIn, fos); - zipIn.close(); - fos.close(); - - Bundle bundleInstalled = framework.getBundleContext().installBundle(bundleMetadata.getOsgiUniqueUrl(), - new FileInputStream(zipF)); - checkCorrectlyInstalled(bundleMetadata, bundleInstalled); - if (!bundleMetadata.isNameResolved()) { - ((BasicManagedBundle)bundleMetadata).setSymbolicName(bundleInstalled.getSymbolicName()); - ((BasicManagedBundle)bundleMetadata).setVersion(bundleInstalled.getVersion().toString()); - } - ((BasicManagedBundle)bundleMetadata).setTempLocalFileWhenJustUploaded(zipF); - - synchronized (managedBundles) { - managedBundles.put(bundleMetadata.getId(), bundleMetadata); - } - mgmt.getRebindManager().getChangeListener().onChanged(bundleMetadata); - - // starting here flags wiring issues earlier - // but may break some things running from the IDE - bundleInstalled.start(); + /** For bundles which are installed by a URL, see whether a bundle has been installed from that URL */ + public ManagedBundle getManagedBundleFromUrl(String url) { + return managedBundlesRecord.getManagedBundleFromUrl(url); + } + + /** See {@link OsgiArchiveInstaller#install()}, using default values */ + public ReferenceWithError install(InputStream zipIn) { + return new OsgiArchiveInstaller(this, null, zipIn).install(); + } - if (loadCatalogBom) { - loadCatalogBom(bundleInstalled); + /** See {@link OsgiArchiveInstaller#install()}, but deferring the start and catalog load */ + public ReferenceWithError installDeferredStart(@Nullable ManagedBundle knownBundleMetadata, @Nullable InputStream zipIn) { + OsgiArchiveInstaller installer = new OsgiArchiveInstaller(this, knownBundleMetadata, zipIn); + installer.setDeferredStart(true); + + return installer.install(); + } + + /** See {@link OsgiArchiveInstaller#install()} - this exposes custom options */ + @Beta + public ReferenceWithError install(@Nullable ManagedBundle knownBundleMetadata, @Nullable InputStream zipIn, + boolean start, boolean loadCatalogBom, boolean forceUpdateOfNonSnapshots) { + + OsgiArchiveInstaller installer = new OsgiArchiveInstaller(this, knownBundleMetadata, zipIn); + installer.setStart(start); + installer.setLoadCatalogBom(loadCatalogBom); + installer.setForce(forceUpdateOfNonSnapshots); + + return installer.install(); + } + + /** + * Removes this bundle from Brooklyn management, + * removes all catalog items it defined, + * and then uninstalls the bundle from OSGi. + *

+ * No checking is done whether anything is using the bundle; + * behaviour of such things is not guaranteed. They will work for many things + * but attempts to load new classes may fail. + *

+ * Callers should typically fail if anything from this bundle is in use. + */ + public void uninstallUploadedBundle(ManagedBundle bundleMetadata) { + synchronized (managedBundlesRecord) { + ManagedBundle metadata = managedBundlesRecord.managedBundles.remove(bundleMetadata.getId()); + if (metadata==null) { + throw new IllegalStateException("No such bundle registered: "+bundleMetadata); } - - return bundleInstalled; - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - throw new IllegalStateException("Bundle "+bundleMetadata+" failed to install: " + Exceptions.collapseText(e), e); + managedBundlesRecord.managedBundlesByName.remove(bundleMetadata.getVersionedName()); + managedBundlesRecord.managedBundlesByUrl.remove(bundleMetadata.getUrl()); + } + mgmt.getRebindManager().getChangeListener().onUnmanaged(bundleMetadata); + + uninstallCatalogItemsFromBundle( bundleMetadata.getVersionedName() ); + + Bundle bundle = framework.getBundleContext().getBundle(bundleMetadata.getOsgiUniqueUrl()); + if (bundle==null) { + throw new IllegalStateException("No such bundle installed: "+bundleMetadata); + } + try { + bundle.stop(); + bundle.uninstall(); + } catch (BundleException e) { + throw Exceptions.propagate(e); } } + + @Beta + public void uninstallCatalogItemsFromBundle(VersionedName bundle) { + List thingsFromHere = ImmutableList.copyOf(getTypesFromBundle( bundle )); + for (RegisteredType t: thingsFromHere) { + mgmt.getCatalog().deleteCatalogItem(t.getSymbolicName(), t.getVersion()); + } + } + + protected Iterable getTypesFromBundle(final VersionedName vn) { + final String bundleId = vn.toString(); + return mgmt.getTypeRegistry().getMatching(new Predicate() { + @Override + public boolean apply(RegisteredType input) { + return bundleId.equals(input.getContainingBundle()); + } + }); + } - // TODO uninstall bundle, and call change listener onRemoved ? - // TODO on snapshot install, uninstall old equivalent snapshots (items in use might stay in use though?) - + /** @deprecated since 0.12.0 use {@link #install(ManagedBundle, InputStream, boolean, boolean)} */ + @Deprecated public synchronized Bundle registerBundle(CatalogBundle bundleMetadata) { try { Bundle alreadyBundle = checkBundleInstalledThrowIfInconsistent(bundleMetadata, true); @@ -280,12 +372,11 @@ public synchronized Bundle registerBundle(CatalogBundle bundleMetadata) { // possibly remove that other capability, so that bundles with BOMs _have_ to be installed via this method. // (load order gets confusing with auto-scanning...) public List> loadCatalogBom(Bundle bundle) { - List> catalogItems = MutableList.of(); - loadCatalogBom(mgmt, bundle, catalogItems); - return catalogItems; + return MutableList.copyOf(loadCatalogBom(mgmt, bundle)); } - private static Iterable> loadCatalogBom(ManagementContext mgmt, Bundle bundle, Iterable> catalogItems) { + private static Iterable> loadCatalogBom(ManagementContext mgmt, Bundle bundle) { + Iterable> catalogItems = MutableList.of(); 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) try { @@ -296,18 +387,21 @@ public List> loadCatalogBom(Bundle bundle) { catalogItems = new CatalogBundleLoader(applicationsPermitted, mgmt).scanForCatalog(bundle); } catch (RuntimeException ex) { - try { - bundle.uninstall(); - } catch (BundleException e) { - log.error("Cannot uninstall bundle " + bundle.getSymbolicName() + ":" + bundle.getVersion(), e); - } + // TODO confirm -- as of May 2017 we no longer uninstall the bundle if install of catalog items fails; + // caller needs to upgrade, or uninstall then reinstall + // (this uninstall wouldn't have unmanaged it in brooklyn in any case) +// try { +// bundle.uninstall(); +// } catch (BundleException e) { +// log.error("Cannot uninstall bundle " + bundle.getSymbolicName() + ":" + bundle.getVersion()+" (after error installing catalog items)", e); +// } throw new IllegalArgumentException("Error installing catalog items", ex); } } return catalogItems; } - private void checkCorrectlyInstalled(OsgiBundleWithUrl bundle, Bundle b) { + void checkCorrectlyInstalled(OsgiBundleWithUrl bundle, Bundle b) { String nv = b.getSymbolicName()+":"+b.getVersion().toString(); if (!isBundleNameEqualOrAbsent(bundle, b)) { @@ -427,21 +521,39 @@ public Maybe> tryResolveClass(String type, Iterable findBundle(OsgiBundleWithUrl catalogBundle) { - //Either fail at install time when the user supplied name:version is different - //from the one reported from the bundle - //or - //Ignore user supplied name:version when URL is supplied to be able to find the - //bundle even if it's with a different version. - // - //For now we just log a warning if there's a version discrepancy at install time, - //so prefer URL if supplied. - BundleFinder bundleFinder = Osgis.bundleFinder(framework); + // Prefer OSGi Location as URL or the managed bundle recorded URL, + // not bothering to check name:version if supplied here (eg to forgive snapshot version discrepancies); + // but fall back to name/version if URL is not known. + // Version checking may be stricter at install time. + Maybe result = null; if (catalogBundle.getUrl() != null) { + BundleFinder bundleFinder = Osgis.bundleFinder(framework); bundleFinder.requiringFromUrl(catalogBundle.getUrl()); - } else { + result = bundleFinder.find(); + if (result.isPresent()) { + return result; + } + + ManagedBundle mb = getManagedBundleFromUrl(catalogBundle.getUrl()); + if (mb!=null) { + bundleFinder.requiringFromUrl(null); + bundleFinder.symbolicName(mb.getSymbolicName()).version(mb.getVersion()); + result = bundleFinder.find(); + if (result.isPresent()) { + return result; + } + } + } + + if (catalogBundle.getSymbolicName()!=null) { + BundleFinder bundleFinder = Osgis.bundleFinder(framework); bundleFinder.symbolicName(catalogBundle.getSymbolicName()).version(catalogBundle.getVersion()); + return bundleFinder.find(); } - return bundleFinder.find(); + if (result!=null) { + return result; + } + return Maybe.absent("Insufficient information in "+catalogBundle+" to find bundle"); } /** @@ -484,5 +596,4 @@ public Iterable getResources(String name, Iterable catalogItem) { } // we don't track register/unregister of bundles; it isn't needed as it happens so early - public void installBundle(ManagedBundle bundle, InputStream zipInput) { - ((LocalManagementContext)mgmt).getOsgiManager().get().installUploadedBundle(bundle, zipInput, true); + // but we do need to know which ones to start subsequently + public OsgiBundleInstallationResult installBundle(ManagedBundle bundle, InputStream zipInput) { + return ((ManagementContextInternal)mgmt).getOsgiManager().get().installDeferredStart(bundle, zipInput).get(); } + public void startBundle(OsgiBundleInstallationResult br) throws BundleException { + if (br.getDeferredStart()!=null) { + br.getDeferredStart().run(); + } + } + public void unregisterPolicy(Policy policy) { policies.remove(policy.getId()); diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java index 34c795cbe9..e92a774dbe 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/RebindIteration.java @@ -34,8 +34,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import com.google.common.collect.ImmutableList; - import org.apache.brooklyn.api.catalog.BrooklynCatalog; import org.apache.brooklyn.api.catalog.CatalogItem; import org.apache.brooklyn.api.entity.Application; @@ -82,6 +80,7 @@ import org.apache.brooklyn.core.location.internal.LocationInternal; import org.apache.brooklyn.core.mgmt.classloading.BrooklynClassLoadingContextSequential; import org.apache.brooklyn.core.mgmt.classloading.JavaBrooklynClassLoadingContext; +import org.apache.brooklyn.core.mgmt.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.internal.BrooklynObjectManagementMode; import org.apache.brooklyn.core.mgmt.internal.BrooklynObjectManagerInternal; import org.apache.brooklyn.core.mgmt.internal.EntityManagerInternal; @@ -115,6 +114,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -325,15 +325,24 @@ protected void installBundlesAndRebuildCatalog() { // Install bundles if (rebindManager.persistBundlesEnabled) { + List installs = MutableList.of(); logRebindingDebug("RebindManager installing bundles: {}", mementoManifest.getBundleIds()); for (ManagedBundleMemento bundleM : mementoManifest.getBundles().values()) { logRebindingDebug("RebindManager installing bundle {}", bundleM.getId()); try (InputStream in = bundleM.getJarContent().openStream()) { - rebindContext.installBundle(instantiator.newManagedBundle(bundleM), in); + installs.add(rebindContext.installBundle(instantiator.newManagedBundle(bundleM), in)); } catch (Exception e) { exceptionHandler.onCreateFailed(BrooklynObjectType.MANAGED_BUNDLE, bundleM.getId(), bundleM.getSymbolicName(), e); } } + // Start them all after we've installed them + for (OsgiBundleInstallationResult br: installs) { + try { + rebindContext.startBundle(br); + } catch (Exception e) { + exceptionHandler.onCreateFailed(BrooklynObjectType.MANAGED_BUNDLE, br.getMetadata().getId(), br.getMetadata().getSymbolicName(), e); + } + } } else { logRebindingDebug("Not rebinding bundles; feature disabled: {}", mementoManifest.getBundleIds()); } diff --git a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformer.java b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformer.java index 78c8e9192d..5f2330d6e1 100644 --- a/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformer.java +++ b/core/src/main/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformer.java @@ -28,7 +28,6 @@ import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoPersister; import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData; import org.apache.brooklyn.api.objs.BrooklynObjectType; -import org.apache.brooklyn.core.mgmt.persist.BrooklynMementoPersisterToObjectStore; import org.apache.brooklyn.core.mgmt.rebind.transformer.impl.XsltTransformer; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; @@ -46,6 +45,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; +import com.google.common.io.ByteSource; @Beta public class CompoundTransformer { @@ -265,6 +265,7 @@ public BrooklynMementoRawData transform(BrooklynMementoRawData rawData) throws E Map feeds = MutableMap.copyOf(rawData.getFeeds()); Map catalogItems = MutableMap.copyOf(rawData.getCatalogItems()); Map bundles = MutableMap.copyOf(rawData.getBundles()); + Map bundleJars = MutableMap.copyOf(rawData.getBundleJars()); // TODO @neykov asks whether transformers should be run in registration order, // rather than in type order. TBD. (would be an easy change.) @@ -328,7 +329,11 @@ public BrooklynMementoRawData transform(BrooklynMementoRawData rawData) throws E LOG.warn("Unable to delete " + type + " id"+Strings.s(missing.size())+" ("+missing+"), " + "because not found in persisted state (continuing)"); } + // bundles have to be supplied by ID, but if so they can be deleted along with the jars bundles.keySet().removeAll(itemsToDelete); + for (String item: itemsToDelete) { + bundleJars.remove(item+".jar"); + } break; case UNKNOWN: break; // no-op @@ -372,9 +377,12 @@ public BrooklynMementoRawData transform(BrooklynMementoRawData rawData) throws E } break; case MANAGED_BUNDLE: - for (Map.Entry entry : bundles.entrySet()) { - entry.setValue(transformer.transform(entry.getValue())); - } + // transform of bundles and JARs not supported - you can delete, that's all + // TODO we should support a better way of adding/removing bundles, + // e.g. start in management mode where you can edit brooklyn-managed bundles +// for (Map.Entry entry : bundles.entrySet()) { +// entry.setValue(transformer.transform(entry.getValue())); +// } break; case UNKNOWN: break; // no-op @@ -393,6 +401,7 @@ public BrooklynMementoRawData transform(BrooklynMementoRawData rawData) throws E .feeds(feeds) .catalogItems(catalogItems) .bundles(bundles) + .bundleJars(bundleJars) .build(); } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java index 1ee8aa51b6..6c0fa3549f 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicManagedBundle.java @@ -21,6 +21,7 @@ import java.io.File; import java.util.Map; +import org.apache.brooklyn.api.catalog.CatalogItem.CatalogBundle; import org.apache.brooklyn.api.mgmt.rebind.RebindSupport; import org.apache.brooklyn.api.typereg.ManagedBundle; import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; @@ -28,6 +29,8 @@ import org.apache.brooklyn.core.mgmt.rebind.BasicManagedBundleRebindSupport; import org.apache.brooklyn.core.objs.AbstractBrooklynObject; import org.apache.brooklyn.core.objs.BrooklynObjectInternal; +import org.apache.brooklyn.util.osgi.VersionedName; +import org.osgi.framework.Version; import com.google.common.annotations.Beta; import com.google.common.base.MoreObjects; @@ -51,7 +54,8 @@ public BasicManagedBundle(String name, String version, String url) { Preconditions.checkNotNull(name, "Either a URL or both name and version are required"); Preconditions.checkNotNull(version, "Either a URL or both name and version are required"); } - + Version.parseVersion(version); + this.symbolicName = name; this.version = version; this.url = url; @@ -80,6 +84,12 @@ public void setVersion(String version) { this.version = version; } + @Override + public VersionedName getVersionedName() { + if (symbolicName==null) return null; + return new VersionedName(symbolicName, Version.parseVersion(version)); + } + @Override public String getUrl() { return url; @@ -172,4 +182,7 @@ protected BrooklynObjectInternal configure(Map flags) { throw new UnsupportedOperationException(); } + public static ManagedBundle of(CatalogBundle bundleUrl) { + return new BasicManagedBundle(bundleUrl.getSymbolicName(), bundleUrl.getVersion(), bundleUrl.getUrl()); + } } diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicRegisteredType.java b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicRegisteredType.java index e0e2305ff4..f345ec9625 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/BasicRegisteredType.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/BasicRegisteredType.java @@ -39,6 +39,7 @@ public class BasicRegisteredType implements RegisteredType { final RegisteredTypeKind kind; final String symbolicName; final String version; + String containingBundle; final List bundles = MutableList.of(); String displayName; @@ -83,6 +84,11 @@ public String getVersion() { return version; } + @Override + public String getContainingBundle() { + return containingBundle; + } + @Override public Collection getLibraries() { return ImmutableSet.copyOf(bundles); diff --git a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java index b3886bd650..72feeddedd 100644 --- a/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java +++ b/core/src/main/java/org/apache/brooklyn/core/typereg/RegisteredTypes.java @@ -33,6 +33,7 @@ import org.apache.brooklyn.api.objs.BrooklynObject; import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry; import org.apache.brooklyn.api.typereg.BrooklynTypeRegistry.RegisteredTypeKind; +import org.apache.brooklyn.api.typereg.ManagedBundle; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.api.typereg.RegisteredType.TypeImplementationPlan; import org.apache.brooklyn.api.typereg.RegisteredTypeLoadingContext; @@ -47,6 +48,7 @@ import org.apache.brooklyn.util.guava.Maybe; import org.apache.brooklyn.util.guava.Maybe.Absent; import org.apache.brooklyn.util.text.NaturalOrderComparator; +import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.text.VersionComparator; import org.apache.brooklyn.util.yaml.Yamls; import org.slf4j.Logger; @@ -100,6 +102,7 @@ public static RegisteredType of(CatalogItem item) { } BasicRegisteredType type = (BasicRegisteredType) spec(item.getSymbolicName(), item.getVersion(), impl, item.getCatalogItemJavaType()); + type.containingBundle = item.getContainingBundle(); type.displayName = item.getDisplayName(); type.description = item.getDescription(); type.iconUrl = item.getIconUrl(); @@ -166,6 +169,13 @@ public static Class loadActualJavaType(String javaTypeName, ManagementContext ((BasicRegisteredType)type).getCache().put(ACTUAL_JAVA_TYPE, clazz); } + @Beta + public static RegisteredType setContainingBundle(RegisteredType type, @Nullable ManagedBundle bundle) { + ((BasicRegisteredType)type).containingBundle = + bundle==null ? null : Strings.toString(bundle.getVersionedName()); + return type; + } + @Beta public static RegisteredType addSuperType(RegisteredType type, @Nullable Class superType) { if (superType!=null) { diff --git a/core/src/test/java/org/apache/brooklyn/core/BrooklynVersionTest.java b/core/src/test/java/org/apache/brooklyn/core/BrooklynVersionTest.java index ccaca98ccb..049916c156 100644 --- a/core/src/test/java/org/apache/brooklyn/core/BrooklynVersionTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/BrooklynVersionTest.java @@ -29,9 +29,10 @@ import org.apache.brooklyn.core.catalog.internal.CatalogItemBuilder; import org.apache.brooklyn.core.catalog.internal.CatalogItemDtoAbstract; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; -import org.apache.brooklyn.util.osgi.OsgiTestResources; import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; import org.apache.brooklyn.test.support.TestResourceUnavailableException; +import org.apache.brooklyn.util.core.osgi.OsgiTestBase; +import org.apache.brooklyn.util.osgi.OsgiTestResources; import org.apache.brooklyn.util.text.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -101,6 +102,7 @@ public void testGetFeatures() throws Exception { String version = "0.1.0"; String type = "brooklyn.osgi.tests.SimpleEntity"; List libraries = Lists.newArrayList("classpath:" + OsgiTestResources.BROOKLYN_TEST_OSGI_ENTITIES_PATH); + OsgiTestBase.preinstallLibrariesLowLevelToPreventCatalogBomParsing(mgmt, libraries.toArray(new String[1])); CatalogEntityItemDto c1 = CatalogItemBuilder.newEntity(symName, version) .javaType(type) diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/osgi/OsgiVersionMoreEntityTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/osgi/OsgiVersionMoreEntityTest.java index 83b48c21d6..0e19f52c92 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/osgi/OsgiVersionMoreEntityTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/osgi/OsgiVersionMoreEntityTest.java @@ -50,6 +50,7 @@ import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.core.osgi.OsgiTestBase; import org.apache.brooklyn.util.core.osgi.Osgis; +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.osgi.OsgiTestResources; @@ -138,9 +139,10 @@ protected RegisteredType addCatalogItemWithTypeAsName(String type, String versio protected RegisteredType addCatalogItemWithNameAndType(String symName, String version, String type, String ...libraries) { return addCatalogItemWithNameAndType(mgmt, symName, version, type, libraries); } - + @SuppressWarnings("deprecation") static RegisteredType addCatalogItemWithNameAndType(ManagementContext mgmt, String symName, String version, String type, String ...libraries) { + OsgiTestBase.preinstallLibrariesLowLevelToPreventCatalogBomParsing(mgmt, libraries); CatalogEntityItemDto c1 = newCatalogItemWithNameAndType(symName, version, type, libraries); mgmt.getCatalog().addItem(c1); RegisteredType c2 = mgmt.getTypeRegistry().get(symName, version); diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/PersistenceStoreObjectAccessorWriterTestFixture.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/PersistenceStoreObjectAccessorWriterTestFixture.java index b4ac83fbc2..47e42045e6 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/PersistenceStoreObjectAccessorWriterTestFixture.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/PersistenceStoreObjectAccessorWriterTestFixture.java @@ -104,7 +104,7 @@ public void testLastModifiedTime() throws Exception { Date write1 = accessor.getLastModifiedDate(); Assert.assertNotNull(write1); - Time.sleep(getLastModifiedResolution().times(2)); + Time.sleep(getLastModifiedResolution().multiply(2)); accessor.put("abc"); accessor.waitForCurrentWrites(TIMEOUT); Date write2 = accessor.getLastModifiedDate(); diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java index 9b8ccae4a4..59ea8f2f4a 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/persist/XmlMementoSerializerTest.java @@ -531,9 +531,7 @@ public void testRenamedOsgiClassWithoutBundlePrefixInRename() throws Exception { bundlePrefix + ":" + classname, bundlePrefix + ":" + oldClassname)); } - // TODO This doesn't get the bundleName - should we expect it to? Is this because of - // how we're using Felix? Would it also be true in Karaf? - @Test(groups="Broken") + @Test public void testOsgiBundleNamePrefixIncludedForDownstreamDependency() throws Exception { mgmt = LocalManagementContextForTests.builder(true).enableOsgiReusable().build(); serializer.setLookupContext(newEmptyLookupManagementContext(mgmt, true)); @@ -546,9 +544,14 @@ public void testOsgiBundleNamePrefixIncludedForDownstreamDependency() throws Exc assertSerializeAndDeserialize(obj); // i.e. prepended with bundle name - String expectedForm = "<"+bundleName+":"+classname+">ALWAYS_TRUE"; + String expectedFormInFelix = "<"+classname+">ALWAYS_TRUE"; + String expectedFormInKaraf = "<"+bundleName+":"+classname+">ALWAYS_TRUE"; String serializedForm = serializer.toString(obj); - assertEquals(serializedForm.trim(), expectedForm.trim()); + // TODO we don't currently have a way to test this with karaf or check if we are karaf or felix + // so this test isn't yet of much value, but other tests assert the full form for installed bundles + // so I think we're alright + Assert.assertTrue(serializedForm.trim().equals(expectedFormInFelix) || serializedForm.trim().equals(expectedFormInKaraf), + "Should have matched either the karaf or the felix form, but was "+serializedForm); } @Test diff --git a/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformerTest.java b/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformerTest.java index 9f8e5826cf..05969ec53c 100644 --- a/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformerTest.java +++ b/core/src/test/java/org/apache/brooklyn/core/mgmt/rebind/transformer/CompoundTransformerTest.java @@ -38,7 +38,6 @@ import org.apache.brooklyn.api.objs.BrooklynObjectType; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.BasicConfigKey; -import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.mgmt.persist.BrooklynMementoPersisterToObjectStore; import org.apache.brooklyn.core.mgmt.persist.FileBasedObjectStore; import org.apache.brooklyn.core.mgmt.persist.PersistMode; diff --git a/core/src/test/java/org/apache/brooklyn/util/core/osgi/OsgiTestBase.java b/core/src/test/java/org/apache/brooklyn/util/core/osgi/OsgiTestBase.java index db66c1341a..0e6d751f87 100644 --- a/core/src/test/java/org/apache/brooklyn/util/core/osgi/OsgiTestBase.java +++ b/core/src/test/java/org/apache/brooklyn/util/core/osgi/OsgiTestBase.java @@ -17,6 +17,10 @@ import java.io.File; import java.io.IOException; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; +import org.apache.brooklyn.util.exceptions.Exceptions; import org.apache.brooklyn.util.os.Os; import org.apache.commons.io.FileUtils; import org.osgi.framework.BundleException; @@ -53,4 +57,18 @@ public static void tearDownOsgiFramework(Framework framework, File storageTempDi } } + public static void preinstallLibrariesLowLevelToPreventCatalogBomParsing(ManagementContext mgmt, String ...libraries) { + // catalog BOM CAMP syntax not available in core; need to pre-install + // to prevent Brooklyn from installing BOMs in those libraries + for (String lib: libraries) { + // install libs manually to prevent catalog BOM loading + // (could do OsgiManager.installDeferredStart also, then just ignore the start) + try { + Osgis.install(((ManagementContextInternal)mgmt).getOsgiManager().get().getFramework(), lib); + } catch (BundleException e) { + throw Exceptions.propagate(e); + } + } + } + } 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 972c2a60aa..fe2346fa7c 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 @@ -88,13 +88,22 @@ public Response createFromYaml( @Valid String yaml); @Beta + /* TODO the polymorphic return type dependent on 'detail' is ugly, + * but we're stuck in this API because backwards compatibility expects the types map + * whereas typical usage wants more feedback. we should introduce a + * /registry and/or /types and/or /bundles endpoint that always provides details + * (and an approach to handling types more aligned with BrooklynTypeRegistry and OSGi bundling). + * Not too concerned here as this method is beta and the above switch will probably naturally happen soon. + * The main client who cares about this is the Go CLI. */ @POST @Consumes({"application/x-zip", "application/x-jar"}) @ApiOperation( value = "Add a catalog items (e.g. new type of entity, policy or location) by uploading a ZIP/JAR archive.", notes = "Accepts either an OSGi bundle JAR, or ZIP which will be turned into bundle JAR. Bother format must " + "contain a catalog.bom at the root of the archive, which must contain the bundle and version key." - + "Return value is map of ID to CatalogItemSummary.", + + "Return value is map of ID to CatalogItemSummary unless detail=true is passed as a parameter in which " + + "case the return value is a BundleInstallationRestResult map containing the types map in types along " + + "with a message, bundle, and code.", response = String.class, hidden = true) @ApiResponses(value = { @@ -106,7 +115,10 @@ public Response createFromArchive( name = "archive", value = "Bundle to install, in ZIP or JAR format, requiring catalog.bom containing bundle name and version", required = true) - byte[] archive); + byte[] archive, + @ApiParam(name="detail", value="Provide a wrapping details map", required=false) + @QueryParam("detail") @DefaultValue("false") + boolean detail); @Beta @POST diff --git a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java index bec524bb1c..a2a183414d 100644 --- a/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java +++ b/rest/rest-api/src/main/java/org/apache/brooklyn/rest/domain/ApiError.java @@ -72,6 +72,7 @@ public static Builder builderFromThrowable(Throwable t) { public static class Builder { private String message; private String details; + private Object data; private Integer errorCode; public Builder message(String message) { @@ -80,7 +81,12 @@ public Builder message(String message) { } public Builder details(String details) { - this.details = checkNotNull(details, "details"); + this.details = details; + return this; + } + + public Builder data(Object data) { + this.data = data; return this; } @@ -111,7 +117,7 @@ public Builder prefixMessage(String prefix, String separatorIfMessageNotBlank) { } public ApiError build() { - return new ApiError(message, details, errorCode); + return new ApiError(message, details, data, errorCode); } /** @deprecated since 0.7.0; use {@link #copy(ApiError)} */ @@ -136,18 +142,23 @@ public String getMessage() { @JsonInclude(JsonInclude.Include.NON_EMPTY) private final String details; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private final Object data; @JsonInclude(JsonInclude.Include.NON_NULL) private final Integer error; public ApiError(String message) { this(message, null); } - public ApiError(String message, String details) { this(message, details, null); } + public ApiError(String message, String details) { this(message, details, null, null); } public ApiError( @JsonProperty("message") String message, @JsonProperty("details") String details, + @JsonProperty("data") Object data, @JsonProperty("error") Integer error) { this.message = checkNotNull(message, "message"); this.details = details != null ? details : ""; + this.data = data; this.error = error; } @@ -159,6 +170,10 @@ public String getDetails() { return details; } + public Object getData() { + return data; + } + public Integer getError() { return error; } 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 74e16a6cac..2657c5431d 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 @@ -19,9 +19,6 @@ package org.apache.brooklyn.rest.resources; import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.util.ArrayList; @@ -29,8 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.jar.Attributes; -import java.util.jar.Manifest; import javax.annotation.Nullable; import javax.ws.rs.DefaultValue; @@ -45,21 +40,22 @@ import org.apache.brooklyn.api.entity.EntitySpec; import org.apache.brooklyn.api.location.Location; import org.apache.brooklyn.api.location.LocationSpec; +import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.policy.Policy; import org.apache.brooklyn.api.policy.PolicySpec; import org.apache.brooklyn.api.sensor.Enricher; import org.apache.brooklyn.api.sensor.EnricherSpec; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.catalog.CatalogPredicates; -import org.apache.brooklyn.core.catalog.internal.BasicBrooklynCatalog; 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.ha.OsgiBundleInstallationResult; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; -import org.apache.brooklyn.core.typereg.BasicManagedBundle; import org.apache.brooklyn.core.typereg.RegisteredTypePredicates; import org.apache.brooklyn.rest.api.CatalogApi; +import org.apache.brooklyn.rest.domain.ApiError; import org.apache.brooklyn.rest.domain.CatalogEnricherSummary; import org.apache.brooklyn.rest.domain.CatalogEntitySummary; import org.apache.brooklyn.rest.domain.CatalogItemSummary; @@ -67,23 +63,17 @@ 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.BrooklynRestResourceUtils; 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.os.Os; -import org.apache.brooklyn.util.osgi.VersionedName; -import org.apache.brooklyn.util.stream.Streams; +import org.apache.brooklyn.util.exceptions.ReferenceWithError; import org.apache.brooklyn.util.text.StringPredicates; import org.apache.brooklyn.util.text.Strings; import org.apache.brooklyn.util.yaml.Yamls; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; -import org.osgi.framework.Bundle; -import org.osgi.framework.Constants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -139,7 +129,7 @@ public Response createFromUpload(byte[] item) { return createFromYaml(new String(item)); } - return createFromArchive(item); + return createFromArchive(item, false); } @Override @@ -164,100 +154,58 @@ public Response createFromYaml(String yaml) { } } + public static class BundleInstallationRestResult { + // as Osgi result, but without bundle, and with maps of catalog items installed + + String message; + String bundle; + OsgiBundleInstallationResult.ResultCode code; + + Map types; + + public String getMessage() { + return message; + } + + @SuppressWarnings("deprecation") + public static BundleInstallationRestResult of(OsgiBundleInstallationResult in, ManagementContext mgmt, BrooklynRestResourceUtils brooklynU, UriInfo ui) { + BundleInstallationRestResult result = new BundleInstallationRestResult(); + result.message = in.getMessage(); + result.bundle = in.getMetadata().getVersionedName().toString(); + result.code = in.getCode(); + if (in.getCatalogItemsInstalled()!=null) { + result.types = MutableMap.of(); + for (String id: in.getCatalogItemsInstalled()) { + // TODO prefer to use RegisteredType, but we need transformer for those in REST + //RegisteredType ci = mgmt.getTypeRegistry().get(id); + + CatalogItem ci = CatalogUtils.getCatalogItemOptionalVersion(mgmt, id); + CatalogItemSummary summary = CatalogTransformer.catalogItemSummary(brooklynU, ci, ui.getBaseUriBuilder()); + result.types.put(id, summary); + } + } + return result; + } + } + @Override @Beta - public Response createFromArchive(byte[] zipInput) { + public Response createFromArchive(byte[] zipInput, boolean detail) { 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(mgmtInternal()); - File f=null, f2=null; - try { - f = Os.newTempFile("brooklyn-posted-archive", "zip"); - try { - Files.write(zipInput, f); - } catch (IOException e) { - Exceptions.propagate(e); - } - - ZipFile zf; - try { - zf = new ZipFile(f); - } catch (IOException e) { - throw new IllegalArgumentException("Invalid ZIP/JAR archive: "+e); - } - ZipArchiveEntry bom = zf.getEntry("catalog.bom"); - if (bom==null) { - bom = zf.getEntry("/catalog.bom"); - } - if (bom==null) { - throw new IllegalArgumentException("Archive must contain a catalog.bom file in the root"); - } - String bomS; - try { - bomS = Streams.readFullyString(zf.getInputStream(bom)); - } catch (IOException e) { - throw new IllegalArgumentException("Error reading catalog.bom from ZIP/JAR archive: "+e); - } - - try { - zf.close(); - } catch (IOException e) { - log.debug("Swallowed exception closing zipfile. Full error logged at trace: {}", e.getMessage()); - log.trace("Exception closing zipfile", e); - } - - VersionedName vn = BasicBrooklynCatalog.getVersionedName( BasicBrooklynCatalog.getCatalogMetadata(bomS) ); - - Manifest mf = bm.getManifest(f); - if (mf==null) { - mf = new Manifest(); - } - String bundleNameInMF = mf.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME); - if (Strings.isNonBlank(bundleNameInMF)) { - if (!bundleNameInMF.equals(vn.getSymbolicName())) { - throw new IllegalArgumentException("JAR MANIFEST symbolic-name '"+bundleNameInMF+"' does not match '"+vn.getSymbolicName()+"' defined in BOM"); - } - } else { - bundleNameInMF = vn.getSymbolicName(); - mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, bundleNameInMF); - } - - String bundleVersionInMF = mf.getMainAttributes().getValue(Constants.BUNDLE_VERSION); - if (Strings.isNonBlank(bundleVersionInMF)) { - if (!bundleVersionInMF.equals(vn.getVersion().toString())) { - throw new IllegalArgumentException("JAR MANIFEST version '"+bundleVersionInMF+"' does not match '"+vn.getVersion()+"' defined in BOM"); - } - } else { - bundleVersionInMF = vn.getVersion().toString(); - mf.getMainAttributes().putValue(Constants.BUNDLE_VERSION, bundleVersionInMF); - } - if (mf.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION)==null) { - mf.getMainAttributes().putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0"); - } - - f2 = bm.copyAddingManifest(f, mf); - - BasicManagedBundle bundleMetadata = new BasicManagedBundle(bundleNameInMF, bundleVersionInMF, null); - Bundle bundle; - try (FileInputStream f2in = new FileInputStream(f2)) { - bundle = ((ManagementContextInternal)mgmt()).getOsgiManager().get().installUploadedBundle(bundleMetadata, f2in, false); - } catch (Exception e) { - throw Exceptions.propagate(e); - } - - Iterable> catalogItems = MutableList.copyOf( - ((ManagementContextInternal)mgmt()).getOsgiManager().get().loadCatalogBom(bundle) ); - - return buildCreateResponse(catalogItems); - } catch (RuntimeException ex) { - throw WebResourceUtils.badRequest(ex); - } finally { - if (f!=null) f.delete(); - if (f2!=null) f2.delete(); + ReferenceWithError result = ((ManagementContextInternal)mgmt()).getOsgiManager().get() + .install(new ByteArrayInputStream(zipInput)); + + if (result.hasError()) { + return ApiError.builder().errorCode(Status.BAD_REQUEST).message(result.getWithoutError().getMessage()) + .data(BundleInstallationRestResult.of(result.getWithoutError(), mgmt(), brooklyn(), ui)).build().asJsonResponse(); } + + BundleInstallationRestResult resultR = BundleInstallationRestResult.of(result.get(), mgmt(), brooklyn(), ui); + return Response.status(Status.CREATED).entity( detail ? resultR : resultR.types ).build(); } private Response buildCreateResponse(Iterable> catalogItems) { 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 e3ad896174..46d1c9c382 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 @@ -61,6 +61,7 @@ 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.Asserts; import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.collections.MutableMap; import org.apache.brooklyn.util.core.ResourceUtils; @@ -582,7 +583,7 @@ public void testInvalidArchive() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("Invalid ZIP/JAR archive")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "zip file is empty"); } @Test @@ -594,7 +595,7 @@ public void testArchiveWithoutBom() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("Archive must contain a catalog.bom file in the root")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); } @Test @@ -613,7 +614,7 @@ public void testArchiveWithoutBundleAndVersion() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("Catalog BOM must define bundle and version")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Missing bundle symbolic name in BOM or MANIFEST"); } @Test @@ -633,7 +634,8 @@ public void testArchiveWithoutBundle() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("Catalog BOM must define bundle")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "Missing bundle symbolic name in BOM or MANIFEST"); } @Test @@ -653,7 +655,7 @@ public void testArchiveWithoutVersion() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("Catalog BOM must define version")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), "Catalog BOM must define version"); } @Test @@ -661,6 +663,7 @@ public void testJarWithoutMatchingBundle() throws Exception { String name = "My Catalog App"; String bundle = "org.apache.brooklyn.test"; String version = "0.1.0"; + String wrongBundleName = "org.apache.brooklyn.test2"; File f = createJar(ImmutableMap.of( "catalog.bom", Joiner.on("\n").join( "brooklyn.catalog:", @@ -675,7 +678,7 @@ public void testJarWithoutMatchingBundle() throws Exception { "META-INF/MANIFEST.MF", Joiner.on("\n").join( "Manifest-Version: 1.0", "Bundle-Name: " + name, - "Bundle-SymbolicName: org.apache.brooklyn.test2", + "Bundle-SymbolicName: "+wrongBundleName, "Bundle-Version: " + version, "Bundle-ManifestVersion: " + version))); @@ -684,7 +687,9 @@ public void testJarWithoutMatchingBundle() throws Exception { .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("JAR MANIFEST symbolic-name 'org.apache.brooklyn.test2' does not match '"+bundle+"' defined in BOM")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "symbolic name mismatch", + wrongBundleName, bundle); } @Test @@ -692,6 +697,7 @@ public void testJarWithoutMatchingVersion() throws Exception { String name = "My Catalog App"; String bundle = "org.apache.brooklyn.test"; String version = "0.1.0"; + String wrongVersion = "0.3.0"; File f = createJar(ImmutableMap.of( "catalog.bom", Joiner.on("\n").join( "brooklyn.catalog:", @@ -707,15 +713,17 @@ public void testJarWithoutMatchingVersion() throws Exception { "Manifest-Version: 1.0", "Bundle-Name: " + name, "Bundle-SymbolicName: " + bundle, - "Bundle-Version: 0.3.0", - "Bundle-ManifestVersion: 0.3.0"))); + "Bundle-Version: " + wrongVersion, + "Bundle-ManifestVersion: " + wrongVersion))); Response response = client().path("/catalog") .header(HttpHeaders.CONTENT_TYPE, "application/x-jar") .post(Streams.readFully(new FileInputStream(f))); assertEquals(response.getStatus(), Response.Status.BAD_REQUEST.getStatusCode()); - assertTrue(response.readEntity(String.class).contains("JAR MANIFEST version '0.3.0' does not match '"+version+"' defined in BOM")); + Asserts.assertStringContainsIgnoreCase(response.readEntity(String.class), + "version mismatch", + wrongVersion, version); } @Test diff --git a/utils/common/dependencies/osgi/more-entities-v2/src/main/resources/catalog.bom b/utils/common/dependencies/osgi/more-entities-v2/src/main/resources/catalog.bom index 3ae38e5ce1..630750898c 100644 --- a/utils/common/dependencies/osgi/more-entities-v2/src/main/resources/catalog.bom +++ b/utils/common/dependencies/osgi/more-entities-v2/src/main/resources/catalog.bom @@ -38,6 +38,7 @@ brooklyn.catalog: - id: org.apache.brooklyn.test.osgi.entities.more.MoreTemplate itemType: template item: - type: org.apache.brooklyn.test.osgi.entities.more.MoreTemplate - name: More Template - description: Cataliog item OSGi test template + services: + - type: org.apache.brooklyn.test.osgi.entities.more.MoreTemplate + name: More Template + description: Catalog item OSGi test template diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java b/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java index b9de2f139b..248b72e25f 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/maven/MavenArtifact.java @@ -26,6 +26,7 @@ import org.apache.brooklyn.util.collections.MutableList; import org.apache.brooklyn.util.javalang.JavaClassNames; import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.text.VersionComparator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -100,7 +101,7 @@ public String getPackaging() { } public boolean isSnapshot() { - return getVersion().toUpperCase().contains("SNAPSHOT"); + return VersionComparator.isSnapshot(getVersion()); } /** @see #customFileNameAfterArtifactMarker */ diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/os/Os.java b/utils/common/src/main/java/org/apache/brooklyn/util/os/Os.java index bd285589da..f1c35dca73 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/os/Os.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/os/Os.java @@ -533,10 +533,13 @@ private static boolean testForMicrosoftWindows() { /** creates a private temp file which will be deleted on exit; * either prefix or ext may be null; - * if ext is non-empty and not > 4 chars and not starting with a ., then a dot will be inserted */ + * if ext is non-empty and not > 4 chars and not starting with a ., then a dot will be inserted; + * if either name part is too long it will be shortened to prevent filesystem errors */ public static File newTempFile(String prefix, String ext) { String sanitizedPrefix = (Strings.isNonEmpty(prefix) ? Strings.makeValidFilename(prefix) + "-" : ""); + if (sanitizedPrefix.length()>101) sanitizedPrefix = sanitizedPrefix.substring(0, 100)+"--"; String extWithPrecedingSeparator = (Strings.isNonEmpty(ext) ? ext.startsWith(".") || ext.length()>4 ? ext : "."+ext : ""); + if (extWithPrecedingSeparator.length()>13) sanitizedPrefix = sanitizedPrefix.substring(0, 12)+"--"; try { File tempFile = File.createTempFile(sanitizedPrefix, extWithPrecedingSeparator, new File(tmp())); tempFile.deleteOnExit(); diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java index 2d89be278d..5807904027 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/osgi/VersionedName.java @@ -76,4 +76,11 @@ public boolean equals(Object other) { return Objects.equal(symbolicName, o.symbolicName) && Objects.equal(version, o.version); } + public static VersionedName fromString(String nameOptionalColonVersion) { + if (nameOptionalColonVersion==null) return null; + int colon = nameOptionalColonVersion.indexOf(':'); + if (colon<0) throw new IllegalArgumentException("Versioned name '"+nameOptionalColonVersion+"' must be of form 'name:version'"); + return new VersionedName(nameOptionalColonVersion.substring(0, colon), Version.parseVersion(nameOptionalColonVersion.substring(colon+1))); + } + } diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java b/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java index 94553b0dd5..071f591363 100644 --- a/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java +++ b/utils/common/src/main/java/org/apache/brooklyn/util/text/VersionComparator.java @@ -52,14 +52,19 @@ public static VersionComparator getInstance() { return INSTANCE; } + public static boolean isSnapshot(String version) { + if (version==null) return false; + return version.toUpperCase().contains(SNAPSHOT); + } + @Override public int compare(String v1, String v2) { if (v1==null && v2==null) return 0; if (v1==null) return -1; if (v2==null) return 1; - boolean isV1Snapshot = v1.toUpperCase().contains(SNAPSHOT); - boolean isV2Snapshot = v2.toUpperCase().contains(SNAPSHOT); + boolean isV1Snapshot = isSnapshot(v1); + boolean isV2Snapshot = isSnapshot(v2); if (isV1Snapshot == isV2Snapshot) { // if snapshot status is the same, look at dot-split parts first return compareDotSplitParts(splitOnDot(v1), splitOnDot(v2)); diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/osgi/VersionedNameTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/osgi/VersionedNameTest.java new file mode 100644 index 0000000000..c838258115 --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/osgi/VersionedNameTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.brooklyn.util.osgi; + +import org.apache.brooklyn.util.javalang.coerce.TypeCoercerExtensible; +import org.osgi.framework.Version; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class VersionedNameTest { + + @Test + public void testVersionedNameFromString() { + VersionedName foo1 = new VersionedName("foo", new Version("1.0")); + Assert.assertEquals(foo1, VersionedName.fromString("foo:1.0")); + Assert.assertEquals(foo1, TypeCoercerExtensible.newDefault().coerce("foo:1.0", VersionedName.class)); + } + + @Test(expectedExceptions=IllegalArgumentException.class) + public void testDoesNotAcceptInvalidVersions() { + Assert.assertEquals(new VersionedName("foo", new Version("1.0.0.alpha")), VersionedName.fromString("foo:1.0-alpha")); + } + + @Test + public void testManuallyCorrectingVersion() { + Assert.assertEquals(new VersionedName("foo", new Version("1.0.0.alpha")), VersionedName.fromString("foo:"+ + OsgiUtils.toOsgiVersion("1.0-alpha"))); + } + +} diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java index 3a4a71c35c..6c65a8a92e 100644 --- a/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java +++ b/utils/common/src/test/java/org/apache/brooklyn/util/text/VersionComparatorTest.java @@ -92,6 +92,13 @@ public void testComparison() { assertVersionOrder("0.10.0-SNAPSHOT", "0.10.0.SNAPSHOT", "0.10.0-GA", "0.10.0.GA", "0.10.0"); } + @Test + public void testIsSnapshot() { + Assert.assertTrue(VersionComparator.isSnapshot("0.10.0-SNAPSHOT")); + Assert.assertTrue(VersionComparator.isSnapshot("0.10.0.snapshot")); + Assert.assertFalse(VersionComparator.isSnapshot("0.10.0")); + } + private static void assertVersionOrder(String v1, String v2, String ...otherVersions) { List versions = MutableList.of().append(v1, v2, otherVersions); diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-com-example-entities.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-com-example-entities.jar index 86dfd8bbc5..51cf119bc4 100644 Binary files a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-com-example-entities.jar and b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-com-example-entities.jar differ diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar index 4f00f803c8..6d0cf2dbc6 100644 Binary files a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar and b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-entities.jar differ diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar index 9994cfb559..84fc378d05 100644 Binary files a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar and b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.1.0.jar differ diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar index 3d1ce7b1c8..24113f6a40 100644 Binary files a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar and b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_0.2.0.jar differ diff --git a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar index f00a115a52..21a114a4b2 100644 Binary files a/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar and b/utils/common/src/test/resources/brooklyn/osgi/brooklyn-test-osgi-more-entities_evil-twin_0.2.0.jar differ