/* * 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.rest.resources; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import java.awt.*; import java.io.IOException; import java.net.URI; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.brooklyn.api.typereg.OsgiBundleWithUrl; import org.apache.brooklyn.api.typereg.RegisteredType; import org.apache.brooklyn.core.catalog.internal.CatalogUtils; import org.apache.brooklyn.core.mgmt.osgi.OsgiStandaloneTest; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.policy.autoscaling.AutoScalerPolicy; import org.apache.brooklyn.rest.domain.CatalogEntitySummary; import org.apache.brooklyn.rest.domain.CatalogItemSummary; import org.apache.brooklyn.rest.domain.CatalogLocationSummary; import org.apache.brooklyn.rest.domain.CatalogPolicySummary; import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest; import org.apache.brooklyn.test.support.TestResourceUnavailableException; import org.apache.brooklyn.util.javalang.Reflections; import org.apache.http.HttpHeaders; import org.apache.http.entity.ContentType; import org.eclipse.jetty.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.testng.reporters.Files; import com.google.common.base.Joiner; import com.google.common.collect.Iterables; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.GenericType; public class CatalogResourceTest extends BrooklynRestResourceTest { private static final Logger log = LoggerFactory.getLogger(CatalogResourceTest.class); private static String TEST_VERSION = "0.1.2"; @BeforeClass(alwaysRun=true) @Override public void setUp() throws Exception { useLocalScannedCatalog(); super.setUp(); } @Override protected void addBrooklynResources() { addResource(new CatalogResource()); } @Test /** based on CampYamlLiteTest */ public void testRegisterCustomEntityTopLevelSyntaxWithBundleWhereEntityIsFromCoreAndIconFromBundle() { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); String symbolicName = "my.catalog.entity.id"; String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; String yaml = "brooklyn.catalog:\n"+ " id: " + symbolicName + "\n"+ " name: My Catalog App\n"+ " description: My description\n"+ " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif\n"+ " version: " + TEST_VERSION + "\n"+ " libraries:\n"+ " - url: " + bundleUrl + "\n"+ "\n"+ "services:\n"+ "- type: org.apache.brooklyn.core.test.entity.TestEntity\n"; ClientResponse response = client().resource("/v1/catalog") .post(ClientResponse.class, yaml); assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode()); CatalogEntitySummary entityItem = client().resource("/v1/catalog/entities/"+symbolicName + "/" + TEST_VERSION) .get(CatalogEntitySummary.class); Assert.assertNotNull(entityItem.getPlanYaml()); Assert.assertTrue(entityItem.getPlanYaml().contains("org.apache.brooklyn.core.test.entity.TestEntity")); assertEquals(entityItem.getId(), ver(symbolicName)); assertEquals(entityItem.getSymbolicName(), symbolicName); assertEquals(entityItem.getVersion(), TEST_VERSION); // and internally let's check we have libraries RegisteredType item = getManagementContext().getTypeRegistry().get(symbolicName, TEST_VERSION); Assert.assertNotNull(item); Collection<OsgiBundleWithUrl> libs = item.getLibraries(); assertEquals(libs.size(), 1); assertEquals(Iterables.getOnlyElement(libs).getUrl(), bundleUrl); // now let's check other things on the item assertEquals(entityItem.getName(), "My Catalog App"); assertEquals(entityItem.getDescription(), "My description"); assertEquals(entityItem.getIconUrl(), "/v1/catalog/icon/" + symbolicName + "/" + entityItem.getVersion()); assertEquals(item.getIconUrl(), "classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif"); // an InterfacesTag should be created for every catalog item assertEquals(entityItem.getTags().size(), 1); Object tag = entityItem.getTags().iterator().next(); List<String> actualInterfaces = ((Map<String, List<String>>) tag).get("traits"); List<Class<?>> expectedInterfaces = Reflections.getAllInterfaces(TestEntity.class); assertEquals(actualInterfaces.size(), expectedInterfaces.size()); for (Class<?> expectedInterface : expectedInterfaces) { assertTrue(actualInterfaces.contains(expectedInterface.getName())); } byte[] iconData = client().resource("/v1/catalog/icon/" + symbolicName + "/" + TEST_VERSION).get(byte[].class); assertEquals(iconData.length, 43); } @Test public void testRegisterOsgiPolicyTopLevelSyntax() { TestResourceUnavailableException.throwIfResourceUnavailable(getClass(), OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_PATH); String symbolicName = "my.catalog.policy.id"; String policyType = "org.apache.brooklyn.test.osgi.entities.SimplePolicy"; String bundleUrl = OsgiStandaloneTest.BROOKLYN_TEST_OSGI_ENTITIES_URL; String yaml = "brooklyn.catalog:\n"+ " id: " + symbolicName + "\n"+ " name: My Catalog App\n"+ " description: My description\n"+ " version: " + TEST_VERSION + "\n" + " libraries:\n"+ " - url: " + bundleUrl + "\n"+ "\n"+ "brooklyn.policies:\n"+ "- type: " + policyType; CatalogPolicySummary entityItem = Iterables.getOnlyElement( client().resource("/v1/catalog") .post(new GenericType<Map<String,CatalogPolicySummary>>() {}, yaml).values() ); Assert.assertNotNull(entityItem.getPlanYaml()); Assert.assertTrue(entityItem.getPlanYaml().contains(policyType)); assertEquals(entityItem.getId(), ver(symbolicName)); assertEquals(entityItem.getSymbolicName(), symbolicName); assertEquals(entityItem.getVersion(), TEST_VERSION); } @Test public void testListAllEntities() { List<CatalogEntitySummary> entities = client().resource("/v1/catalog/entities") .get(new GenericType<List<CatalogEntitySummary>>() {}); assertTrue(entities.size() > 0); } @Test public void testListAllEntitiesAsItem() { // ensure things are happily downcasted and unknown properties ignored (e.g. sensors, effectors) List<CatalogItemSummary> entities = client().resource("/v1/catalog/entities") .get(new GenericType<List<CatalogItemSummary>>() {}); assertTrue(entities.size() > 0); } @Test public void testFilterListOfEntitiesByName() { List<CatalogEntitySummary> entities = client().resource("/v1/catalog/entities") .queryParam("fragment", "brOOkLynENTITYmiRrOr").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(entities.size(), 1); log.info("BrooklynEntityMirror-like entities are: " + entities); List<CatalogEntitySummary> entities2 = client().resource("/v1/catalog/entities") .queryParam("regex", "[Bb]ro+klynEntityMi[ro]+").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(entities2.size(), 1); assertEquals(entities, entities2); List<CatalogEntitySummary> entities3 = client().resource("/v1/catalog/entities") .queryParam("fragment", "bweqQzZ").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(entities3.size(), 0); List<CatalogEntitySummary> entities4 = client().resource("/v1/catalog/entities") .queryParam("regex", "bweq+z+").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(entities4.size(), 0); } @Test @Deprecated // If we move to using a yaml catalog item, the details will be of the wrapping app, // not of the entity itself, so the test won't make sense any more. public void testGetCatalogEntityDetails() { CatalogEntitySummary details = client() .resource(URI.create("/v1/catalog/entities/org.apache.brooklyn.entity.brooklynnode.BrooklynNode")) .get(CatalogEntitySummary.class); assertTrue(details.toString().contains("download.url"), "expected more config, only got: "+details); } @Test @Deprecated // If we move to using a yaml catalog item, the details will be of the wrapping app, // not of the entity itself, so the test won't make sense any more. public void testGetCatalogEntityPlusVersionDetails() { CatalogEntitySummary details = client() .resource(URI.create("/v1/catalog/entities/org.apache.brooklyn.entity.brooklynnode.BrooklynNode:0.0.0.SNAPSHOT")) .get(CatalogEntitySummary.class); assertTrue(details.toString().contains("download.url"), "expected more config, only got: "+details); } @Test public void testGetCatalogEntityIconDetails() throws IOException { String catalogItemId = "testGetCatalogEntityIconDetails"; addTestCatalogItemBrooklynNodeAsEntity(catalogItemId); ClientResponse response = client().resource(URI.create("/v1/catalog/icon/" + catalogItemId + "/" + TEST_VERSION)) .get(ClientResponse.class); response.bufferEntity(); Assert.assertEquals(response.getStatus(), 200); Assert.assertEquals(response.getType(), MediaType.valueOf("image/jpeg")); Image image = Toolkit.getDefaultToolkit().createImage(Files.readFile(response.getEntityInputStream())); Assert.assertNotNull(image); } private void addTestCatalogItemBrooklynNodeAsEntity(String catalogItemId) { addTestCatalogItem(catalogItemId, null, TEST_VERSION, "org.apache.brooklyn.entity.brooklynnode.BrooklynNode"); } private void addTestCatalogItem(String catalogItemId, String itemType, String version, String service) { String yaml = "brooklyn.catalog:\n"+ " id: " + catalogItemId + "\n"+ " name: My Catalog App\n"+ (itemType!=null ? " item_type: "+itemType+"\n" : "")+ " description: My description\n"+ " icon_url: classpath:///brooklyn-test-logo.jpg\n"+ " version: " + version + "\n"+ "\n"+ "services:\n"+ "- type: " + service + "\n"; client().resource("/v1/catalog").post(yaml); } private enum DeprecateStyle { NEW_STYLE, LEGACY_STYLE } private void deprecateCatalogItem(DeprecateStyle style, String symbolicName, String version, boolean deprecated) { String id = String.format("%s:%s", symbolicName, version); ClientResponse response; if (style == DeprecateStyle.NEW_STYLE) { response = client().resource(String.format("/v1/catalog/entities/%s/deprecated", id)) .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON) .post(ClientResponse.class, deprecated); } else { response = client().resource(String.format("/v1/catalog/entities/%s/deprecated/%s", id, deprecated)) .post(ClientResponse.class); } assertEquals(response.getStatus(), Response.Status.NO_CONTENT.getStatusCode()); } private void disableCatalogItem(String symbolicName, String version, boolean disabled) { String id = String.format("%s:%s", symbolicName, version); ClientResponse getDisableResponse = client().resource(String.format("/v1/catalog/entities/%s/disabled", id)) .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON) .post(ClientResponse.class, disabled); assertEquals(getDisableResponse.getStatus(), Response.Status.NO_CONTENT.getStatusCode()); } @Test public void testListPolicies() { Set<CatalogPolicySummary> policies = client().resource("/v1/catalog/policies") .get(new GenericType<Set<CatalogPolicySummary>>() {}); assertTrue(policies.size() > 0); CatalogItemSummary asp = null; for (CatalogItemSummary p : policies) { if (AutoScalerPolicy.class.getName().equals(p.getType())) asp = p; } Assert.assertNotNull(asp, "didn't find AutoScalerPolicy"); } @Test public void testLocationAddGetAndRemove() { String symbolicName = "my.catalog.location.id"; String locationType = "localhost"; String yaml = Joiner.on("\n").join( "brooklyn.catalog:", " id: " + symbolicName, " name: My Catalog Location", " description: My description", " version: " + TEST_VERSION, "", "brooklyn.locations:", "- type: " + locationType); // Create location item Map<String, CatalogLocationSummary> items = client().resource("/v1/catalog") .post(new GenericType<Map<String,CatalogLocationSummary>>() {}, yaml); CatalogLocationSummary locationItem = Iterables.getOnlyElement(items.values()); Assert.assertNotNull(locationItem.getPlanYaml()); Assert.assertTrue(locationItem.getPlanYaml().contains(locationType)); assertEquals(locationItem.getId(), ver(symbolicName)); assertEquals(locationItem.getSymbolicName(), symbolicName); assertEquals(locationItem.getVersion(), TEST_VERSION); // Retrieve location item CatalogLocationSummary location = client().resource("/v1/catalog/locations/"+symbolicName+"/"+TEST_VERSION) .get(CatalogLocationSummary.class); assertEquals(location.getSymbolicName(), symbolicName); // Retrieve all locations Set<CatalogLocationSummary> locations = client().resource("/v1/catalog/locations") .get(new GenericType<Set<CatalogLocationSummary>>() {}); boolean found = false; for (CatalogLocationSummary contender : locations) { if (contender.getSymbolicName().equals(symbolicName)) { found = true; break; } } Assert.assertTrue(found, "contenders="+locations); // Delete ClientResponse deleteResponse = client().resource("/v1/catalog/locations/"+symbolicName+"/"+TEST_VERSION) .delete(ClientResponse.class); assertEquals(deleteResponse.getStatus(), Response.Status.NO_CONTENT.getStatusCode()); ClientResponse getPostDeleteResponse = client().resource("/v1/catalog/locations/"+symbolicName+"/"+TEST_VERSION) .get(ClientResponse.class); assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); } @Test public void testDeleteCustomEntityFromCatalog() { String symbolicName = "my.catalog.app.id.to.subsequently.delete"; String yaml = "name: "+symbolicName+"\n"+ // FIXME name above should be unnecessary when brooklyn.catalog below is working "brooklyn.catalog:\n"+ " id: " + symbolicName + "\n"+ " name: My Catalog App To Be Deleted\n"+ " description: My description\n"+ " version: " + TEST_VERSION + "\n"+ "\n"+ "services:\n"+ "- type: org.apache.brooklyn.core.test.entity.TestEntity\n"; client().resource("/v1/catalog") .post(ClientResponse.class, yaml); ClientResponse deleteResponse = client().resource("/v1/catalog/entities/"+symbolicName+"/"+TEST_VERSION) .delete(ClientResponse.class); assertEquals(deleteResponse.getStatus(), Response.Status.NO_CONTENT.getStatusCode()); ClientResponse getPostDeleteResponse = client().resource("/v1/catalog/entities/"+symbolicName+"/"+TEST_VERSION) .get(ClientResponse.class); assertEquals(getPostDeleteResponse.getStatus(), Response.Status.NOT_FOUND.getStatusCode()); } @Test public void testSetDeprecated() { runSetDeprecated(DeprecateStyle.NEW_STYLE); } // Uses old-style "/v1/catalog/{itemId}/deprecated/true", rather than the "true" in the request body. @Test @Deprecated public void testSetDeprecatedLegacy() { runSetDeprecated(DeprecateStyle.LEGACY_STYLE); } protected void runSetDeprecated(DeprecateStyle style) { String symbolicName = "my.catalog.item.id.for.deprecation"; String serviceType = "org.apache.brooklyn.entity.stock.BasicApplication"; addTestCatalogItem(symbolicName, "template", TEST_VERSION, serviceType); addTestCatalogItem(symbolicName, "template", "2.0", serviceType); try { List<CatalogEntitySummary> applications = client().resource("/v1/catalog/applications") .queryParam("fragment", symbolicName).queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applications.size(), 2); CatalogItemSummary summary0 = applications.get(0); CatalogItemSummary summary1 = applications.get(1); // Deprecate: that app should be excluded deprecateCatalogItem(style, summary0.getSymbolicName(), summary0.getVersion(), true); List<CatalogEntitySummary> applicationsAfterDeprecation = client().resource("/v1/catalog/applications") .queryParam("fragment", "basicapp").queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applicationsAfterDeprecation.size(), 1); assertTrue(applicationsAfterDeprecation.contains(summary1)); // Un-deprecate: that app should be included again deprecateCatalogItem(style, summary0.getSymbolicName(), summary0.getVersion(), false); List<CatalogEntitySummary> applicationsAfterUnDeprecation = client().resource("/v1/catalog/applications") .queryParam("fragment", "basicapp").queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applications, applicationsAfterUnDeprecation); } finally { client().resource("/v1/catalog/entities/"+symbolicName+"/"+TEST_VERSION) .delete(ClientResponse.class); client().resource("/v1/catalog/entities/"+symbolicName+"/"+"2.0") .delete(ClientResponse.class); } } @Test public void testSetDisabled() { String symbolicName = "my.catalog.item.id.for.disabling"; String serviceType = "org.apache.brooklyn.entity.stock.BasicApplication"; addTestCatalogItem(symbolicName, "template", TEST_VERSION, serviceType); addTestCatalogItem(symbolicName, "template", "2.0", serviceType); try { List<CatalogEntitySummary> applications = client().resource("/v1/catalog/applications") .queryParam("fragment", symbolicName).queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applications.size(), 2); CatalogItemSummary summary0 = applications.get(0); CatalogItemSummary summary1 = applications.get(1); // Disable: that app should be excluded disableCatalogItem(summary0.getSymbolicName(), summary0.getVersion(), true); List<CatalogEntitySummary> applicationsAfterDisabled = client().resource("/v1/catalog/applications") .queryParam("fragment", "basicapp").queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applicationsAfterDisabled.size(), 1); assertTrue(applicationsAfterDisabled.contains(summary1)); // Un-disable: that app should be included again disableCatalogItem(summary0.getSymbolicName(), summary0.getVersion(), false); List<CatalogEntitySummary> applicationsAfterUnDisabled = client().resource("/v1/catalog/applications") .queryParam("fragment", "basicapp").queryParam("allVersions", "true").get(new GenericType<List<CatalogEntitySummary>>() {}); assertEquals(applications, applicationsAfterUnDisabled); } finally { client().resource("/v1/catalog/entities/"+symbolicName+"/"+TEST_VERSION) .delete(ClientResponse.class); client().resource("/v1/catalog/entities/"+symbolicName+"/"+"2.0") .delete(ClientResponse.class); } } @Test public void testAddUnreachableItem() { addInvalidCatalogItem("http://0.0.0.0/can-not-connect"); } @Test public void testAddInvalidItem() { //equivalent to HTTP response 200 text/html addInvalidCatalogItem("classpath://not-a-jar-file.txt"); } @Test public void testAddMissingItem() { //equivalent to HTTP response 404 text/html addInvalidCatalogItem("classpath://missing-jar-file.txt"); } private void addInvalidCatalogItem(String bundleUrl) { String symbolicName = "my.catalog.entity.id"; String yaml = "brooklyn.catalog:\n"+ " id: " + symbolicName + "\n"+ " name: My Catalog App\n"+ " description: My description\n"+ " icon_url: classpath:/org/apache/brooklyn/test/osgi/entities/icon.gif\n"+ " version: " + TEST_VERSION + "\n"+ " libraries:\n"+ " - url: " + bundleUrl + "\n"+ "\n"+ "services:\n"+ "- type: org.apache.brooklyn.core.test.entity.TestEntity\n"; ClientResponse response = client().resource("/v1/catalog") .post(ClientResponse.class, yaml); assertEquals(response.getStatus(), HttpStatus.INTERNAL_SERVER_ERROR_500); } private static String ver(String id) { return CatalogUtils.getVersionedId(id, TEST_VERSION); } }