/*
* 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.camp.brooklyn.catalog;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.brooklyn.api.catalog.CatalogItem;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoPersister;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.sensor.Enricher;
import org.apache.brooklyn.api.typereg.RegisteredType;
import org.apache.brooklyn.camp.brooklyn.AbstractYamlRebindTest;
import org.apache.brooklyn.core.BrooklynFeatureEnablement;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.StartableApplication;
import org.apache.brooklyn.core.mgmt.persist.BrooklynMementoPersisterToObjectStore;
import org.apache.brooklyn.core.mgmt.persist.PersistenceObjectStore;
import org.apache.brooklyn.core.mgmt.persist.PersistenceObjectStore.StoreObjectAccessor;
import org.apache.brooklyn.core.mgmt.rebind.RebindOptions;
import org.apache.brooklyn.core.test.policy.TestEnricher;
import org.apache.brooklyn.core.test.policy.TestPolicy;
import org.apache.brooklyn.entity.stock.BasicEntity;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.text.Strings;
import org.testng.annotations.Test;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
public class CatalogYamlRebindTest extends AbstractYamlRebindTest {
// TODO Other tests (relating to https://issues.apache.org/jira/browse/BROOKLYN-149) include:
// - entities cannot be instantiated because class no longer on classpath (e.g. was OSGi)
// - config/attribute cannot be instantiated (e.g. because class no longer on classpath)
// - entity file corrupt
enum RebindWithCatalogTestMode {
NO_OP,
STRIP_DEPRECATION_AND_ENABLEMENT_FROM_CATALOG_ITEM,
DEPRECATE_CATALOG,
DISABLE_CATALOG,
DELETE_CATALOG,
REPLACE_CATALOG_WITH_NEWER_VERSION;
}
@Test
public void testRebindWithCatalogAndApp() throws Exception {
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.NO_OP);
}
@Test
public void testRebindWithCatalogDeprecatedAndAppExisting() throws Exception {
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.DEPRECATE_CATALOG);
}
@Test
public void testRebindWithCatalogDisabledAndAppExisting() throws Exception {
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.DISABLE_CATALOG);
}
// See https://issues.apache.org/jira/browse/BROOKLYN-149.
// Deletes the catalog item before rebind, but the referenced types are still on the
// default classpath.
// Will fallback to loading from classpath.
@Test
public void testRebindWithCatalogDeletedAndAppExisting() throws Exception {
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.DELETE_CATALOG);
}
// Upgrades the catalog item before rebind, deleting the old version.
// Will automatically upgrade.
@Test
public void testRebindWithCatalogUpgradedWithOldDeletedAndAppExisting() throws Exception {
BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_AUTO_FIX_CATALOG_REF_ON_REBIND);
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.REPLACE_CATALOG_WITH_NEWER_VERSION);
}
/**
* Old persisted state for catalog items may not have a "deprecated" or "disabled"
* value. Need to check that their absence will default to false.
*/
@Test
public void testRebindWithCatalogPropertiesForDeprecationAndEnablementAbsent() throws Exception {
runRebindWithCatalogAndApp(RebindWithCatalogTestMode.STRIP_DEPRECATION_AND_ENABLEMENT_FROM_CATALOG_ITEM);
}
@SuppressWarnings({ "unused", "deprecation" })
protected void runRebindWithCatalogAndApp(RebindWithCatalogTestMode mode) throws Exception {
String appSymbolicName = "my.catalog.app.id.load";
String appVersion = "0.1.0";
String appCatalogFormat = Joiner.on("\n").join(
"brooklyn.catalog:",
" id: " + appSymbolicName,
" version: %s",
" item:",
" type: "+ BasicEntity.class.getName(),
" brooklyn.enrichers:",
" - type: "+TestEnricher.class.getName(),
" brooklyn.policies:",
" - type: "+TestPolicy.class.getName());
String locSymbolicName = "my.catalog.loc.id.load";
String locVersion = "1.0.0";
String locCatalogFormat = Joiner.on("\n").join(
"brooklyn.catalog:",
" id: " + locSymbolicName,
" version: %s",
" item.type: location",
" item:",
" type: localhost");
// Create the catalog items
CatalogItem<?, ?> appItem = Iterables.getOnlyElement(addCatalogItems(String.format(appCatalogFormat, appVersion)));
CatalogItem<?, ?> locItem = Iterables.getOnlyElement(addCatalogItems(String.format(locCatalogFormat, locVersion)));
final String appItemId = appItem.getId();
final String locItemId = locItem.getId();
// Create an app, using that catalog item
String yaml = "name: simple-app-yaml\n" +
"location: \"brooklyn.catalog:"+CatalogUtils.getVersionedId(locSymbolicName, locVersion)+"\"\n" +
"services: \n" +
"- type: "+CatalogUtils.getVersionedId(appSymbolicName, appVersion);
origApp = (StartableApplication) createAndStartApplication(yaml);
BasicEntity origEntity = (BasicEntity) Iterables.getOnlyElement(origApp.getChildren());
TestPolicy origPolicy = (TestPolicy) Iterables.getOnlyElement(origEntity.policies());
TestEnricher origEnricher = (TestEnricher) Iterables.tryFind(origEntity.enrichers(), Predicates.instanceOf(TestEnricher.class)).get();
assertEquals(origEntity.getCatalogItemId(), appSymbolicName+":"+appVersion);
// Depending on test-mode, delete the catalog item, and then rebind
switch (mode) {
case DEPRECATE_CATALOG:
CatalogUtils.setDeprecated(mgmt(), appSymbolicName, appVersion, true);
CatalogUtils.setDeprecated(mgmt(), locSymbolicName, locVersion, true);
break;
case DISABLE_CATALOG:
CatalogUtils.setDisabled(mgmt(), appSymbolicName, appVersion, true);
CatalogUtils.setDisabled(mgmt(), locSymbolicName, locVersion, true);
break;
case DELETE_CATALOG:
mgmt().getCatalog().deleteCatalogItem(appSymbolicName, appVersion);
mgmt().getCatalog().deleteCatalogItem(locSymbolicName, locVersion);
break;
case REPLACE_CATALOG_WITH_NEWER_VERSION:
mgmt().getCatalog().deleteCatalogItem(appSymbolicName, appVersion);
mgmt().getCatalog().deleteCatalogItem(locSymbolicName, locVersion);
appVersion = "0.2.0";
locVersion = "1.1.0";
addCatalogItems(String.format(appCatalogFormat, appVersion));
addCatalogItems(String.format(locCatalogFormat, locVersion));
break;
case STRIP_DEPRECATION_AND_ENABLEMENT_FROM_CATALOG_ITEM:
// set everything false -- then below we rebind with these fields removed to ensure that we can rebind
CatalogUtils.setDeprecated(mgmt(), appSymbolicName, appVersion, false);
CatalogUtils.setDeprecated(mgmt(), locSymbolicName, locVersion, false);
CatalogUtils.setDisabled(mgmt(), appSymbolicName, appVersion, false);
CatalogUtils.setDisabled(mgmt(), locSymbolicName, locVersion, false);
break;
case NO_OP:
break; // no-op
default:
throw new IllegalStateException("Unknown mode: "+mode);
}
// Rebind
if (mode == RebindWithCatalogTestMode.STRIP_DEPRECATION_AND_ENABLEMENT_FROM_CATALOG_ITEM) {
// Edit the persisted state to remove the "deprecated" and "enablement" tags for our catalog items
rebind(RebindOptions.create()
.stateTransformer(new Function<BrooklynMementoPersister, Void>() {
@Override public Void apply(BrooklynMementoPersister input) {
PersistenceObjectStore objectStore = ((BrooklynMementoPersisterToObjectStore)input).getObjectStore();
StoreObjectAccessor appItemAccessor = objectStore.newAccessor("catalog/"+Strings.makeValidFilename(appItemId));
StoreObjectAccessor locItemAccessor = objectStore.newAccessor("catalog/"+Strings.makeValidFilename(locItemId));
String appItemMemento = checkNotNull(appItemAccessor.get(), "appItem in catalog");
String locItemMemento = checkNotNull(locItemAccessor.get(), "locItem in catalog");
String newAppItemMemento = removeFromXml(appItemMemento, ImmutableList.of("catalogItem/deprecated", "catalogItem/disabled"));
String newLocItemMemento = removeFromXml(locItemMemento, ImmutableList.of("catalogItem/deprecated", "catalogItem/disabled"));
appItemAccessor.put(newAppItemMemento);
locItemAccessor.put(newLocItemMemento);
return null;
}}));
} else {
rebind();
}
// Ensure app is still there, and that it is usable - e.g. "stop" effector functions as expected
BasicEntity newEntity = (BasicEntity) Iterables.getOnlyElement(newApp.getChildren());
Policy newPolicy = Iterables.getOnlyElement(newEntity.policies());
Enricher newEnricher = Iterables.tryFind(newEntity.enrichers(), Predicates.instanceOf(TestEnricher.class)).get();
assertEquals(newEntity.getCatalogItemId(), appSymbolicName+":"+appVersion);
newApp.stop();
assertFalse(Entities.isManaged(newApp));
assertFalse(Entities.isManaged(newEntity));
// Ensure catalog item is as expecpted
RegisteredType newAppItem = mgmt().getTypeRegistry().get(appSymbolicName, appVersion);
RegisteredType newLocItem = mgmt().getTypeRegistry().get(locSymbolicName, locVersion);
boolean itemDeployable;
switch (mode) {
case DISABLE_CATALOG:
assertTrue(newAppItem.isDisabled());
assertTrue(newLocItem.isDisabled());
itemDeployable = false;
break;
case DELETE_CATALOG:
assertNull(newAppItem);
assertNull(newLocItem);
itemDeployable = false;
break;
case DEPRECATE_CATALOG:
assertTrue(newAppItem.isDeprecated());
assertTrue(newLocItem.isDeprecated());
itemDeployable = true;
break;
case NO_OP:
case STRIP_DEPRECATION_AND_ENABLEMENT_FROM_CATALOG_ITEM:
case REPLACE_CATALOG_WITH_NEWER_VERSION:
assertNotNull(newAppItem);
assertNotNull(newLocItem);
assertFalse(newAppItem.isDeprecated());
assertFalse(newAppItem.isDisabled());
assertFalse(newLocItem.isDeprecated());
assertFalse(newLocItem.isDisabled());
itemDeployable = true;
break;
default:
throw new IllegalStateException("Unknown mode: "+mode);
}
// Try to deploy a new app
String yaml2 = "name: simple-app-yaml2\n" +
"location: \"brooklyn.catalog:"+CatalogUtils.getVersionedId(locSymbolicName, locVersion)+"\"\n" +
"services: \n" +
"- type: "+CatalogUtils.getVersionedId(appSymbolicName, appVersion);
if (itemDeployable) {
StartableApplication app2 = (StartableApplication) createAndStartApplication(yaml2);
BasicEntity entity2 = (BasicEntity) Iterables.getOnlyElement(app2.getChildren());
assertEquals(entity2.getCatalogItemId(), appSymbolicName+":"+appVersion);
} else {
try {
StartableApplication app2 = (StartableApplication) createAndStartApplication(yaml2);
fail();
} catch (Exception e) {
if (mode == RebindWithCatalogTestMode.DELETE_CATALOG) {
if (!e.toString().contains("cannot be matched")) throw e;
} else {
assertEquals(mode, RebindWithCatalogTestMode.DISABLE_CATALOG);
if (!e.toString().contains("cannot be matched")) throw e;
}
}
}
}
/**
* Given the "/"-separated path for the elements to be removed, it removes these from the xml
* and returns the transformed XML.
*/
private String removeFromXml(String xml, List<String> elementsToRemove) {
try {
InputSource source = new InputSource(new StringReader(xml));
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(source);
for (String elementToRemove : elementsToRemove) {
Node current = null;
boolean first = true;
for (String tag : elementToRemove.split("/")) {
NodeList matches;
if (first) {
matches = doc.getElementsByTagName(tag);
first = false;
} else {
matches = ((Element)current).getElementsByTagName(tag);
}
if (matches.getLength() > 0) {
current = matches.item(0);
} else {
current = null;
break;
}
}
if (current != null) {
current.getParentNode().removeChild(current);
}
}
DOMSource domSource = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(domSource, result);
return writer.toString();
} catch (Exception e) {
throw Exceptions.propagate(e);
}
}
}