/* * 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; import java.io.File; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.brooklyn.api.entity.Application; import org.apache.brooklyn.api.entity.Entity; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.api.mgmt.ha.MementoCopyMode; import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoRawData; import org.apache.brooklyn.api.sensor.AttributeSensor; import org.apache.brooklyn.api.sensor.Sensor; import org.apache.brooklyn.camp.brooklyn.spi.dsl.BrooklynDslDeferredSupplier; import org.apache.brooklyn.config.ConfigKey; import org.apache.brooklyn.core.config.ConfigKeys; import org.apache.brooklyn.core.entity.Attributes; import org.apache.brooklyn.core.entity.Entities; import org.apache.brooklyn.core.entity.EntityAsserts; import org.apache.brooklyn.core.entity.EntityInternal; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; import org.apache.brooklyn.core.mgmt.persist.BrooklynPersistenceUtils; import org.apache.brooklyn.core.mgmt.rebind.RebindTestUtils; import org.apache.brooklyn.core.sensor.Sensors; import org.apache.brooklyn.core.test.entity.TestEntity; import org.apache.brooklyn.entity.group.DynamicCluster; import org.apache.brooklyn.util.collections.MutableSet; import org.apache.brooklyn.util.core.task.Tasks; import org.apache.brooklyn.util.guava.Maybe; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.io.Files; @Test public class DslAndRebindYamlTest extends AbstractYamlTest { private static final Logger log = LoggerFactory.getLogger(DslAndRebindYamlTest.class); protected ClassLoader classLoader = getClass().getClassLoader(); protected File mementoDir; protected Set<ManagementContext> mgmtContexts = MutableSet.of(); protected ExecutorService executor; @Override protected LocalManagementContext newTestManagementContext() { if (mementoDir != null) throw new IllegalStateException("already created mgmt context"); mementoDir = Files.createTempDir(); mementoDir.deleteOnExit(); LocalManagementContext mgmt = RebindTestUtils.newPersistingManagementContext(mementoDir, classLoader, 1); mgmtContexts.add(mgmt); return mgmt; } @BeforeMethod(alwaysRun = true) @Override public void setUp() { super.setUp(); executor = Executors.newSingleThreadExecutor(); } @AfterMethod(alwaysRun = true) @Override public void tearDown() { if (executor != null) executor.shutdownNow(); for (ManagementContext mgmt : mgmtContexts) Entities.destroyAll(mgmt); super.tearDown(); mementoDir = null; mgmtContexts.clear(); } @Override protected Logger getLogger() { return log; } public Application rebind(Application app) throws Exception { RebindTestUtils.waitForPersisted(app); Application result = RebindTestUtils.rebind(mementoDir, getClass().getClassLoader()); mgmtContexts.add(result.getManagementContext()); return result; } protected Entity setupAndCheckTestEntityInBasicYamlWith(String... extras) throws Exception { Entity app = createAndStartApplication(loadYaml("test-entity-basic-template.yaml", extras)); waitForApplicationTasks(app); Assert.assertEquals(app.getDisplayName(), "test-entity-basic-template"); log.info("App started:"); Entities.dumpInfo(app); Assert.assertTrue(app.getChildren().iterator().hasNext(), "Expected app to have child entity"); Entity entity = app.getChildren().iterator().next(); Assert.assertTrue(entity instanceof TestEntity, "Expected TestEntity, found " + entity.getClass()); return entity; } public static <T> T getConfigInTask(final Entity entity, final ConfigKey<T> key) { return Entities.submit(entity, Tasks.<T>builder().body(new Callable<T>() { @Override public T call() throws Exception { return entity.getConfig(key); } }).build()).getUnchecked(); } protected <T> Future<T> getConfigInTaskAsync(final Entity entity, final ConfigKey<T> key) { // Wait for the attribute to be ready in a new Task Callable<T> configGetter = new Callable<T>() { @Override public T call() throws Exception { T s = getConfigInTask(entity, key); getLogger().info("getConfig {}={}", key, s); return s; } }; return executor.submit(configGetter); } @Test public void testDslAttributeWhenReady() throws Exception { Entity testEntity = entityWithAttributeWhenReady(); ((EntityInternal) testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar"); Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "bar"); } @Test public void testDslAttributeWhenReadyRebindWhenResolved() throws Exception { Entity testEntity = entityWithAttributeWhenReady(); ((EntityInternal) testEntity).sensors().set(Sensors.newStringSensor("foo"), "bar"); Application app2 = rebind(testEntity.getApplication()); Entity e2 = Iterables.getOnlyElement(app2.getChildren()); Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "bar"); } @Test public void testDslAttributeWhenReadyWhenNotYetResolved() throws Exception { Entity testEntity = entityWithAttributeWhenReady(); Application app2 = rebind(testEntity.getApplication()); Entity e2 = Iterables.getOnlyElement(app2.getChildren()); // Wait for the attribute to be ready in a new Task Future<String> stringFuture = getConfigInTaskAsync(e2, TestEntity.CONF_NAME); // Check that the Task is still waiting for attribute to be ready Assert.assertFalse(stringFuture.isDone()); // Set the sensor; expect that to complete e2.sensors().set(Sensors.newStringSensor("foo"), "bar"); String s = stringFuture.get(10, TimeUnit.SECONDS); // Timeout just for sanity Assert.assertEquals(s, "bar"); } @Test public void testDslAttributeWhenReadyPersistedAsDeferredSupplier() throws Exception { doDslAttributeWhenReadyPersistedAsDeferredSupplier(false); } @Test public void testDslAttributeWhenReadyPersistedWithoutLeakingResolvedValue() throws Exception { doDslAttributeWhenReadyPersistedAsDeferredSupplier(true); } protected void doDslAttributeWhenReadyPersistedAsDeferredSupplier(boolean resolvedBeforeRebind) throws Exception { Entity testEntity = entityWithAttributeWhenReady(); if (resolvedBeforeRebind) { testEntity.sensors().set(Sensors.newStringSensor("foo"), "bar"); Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "bar"); } // Persist and rebind Application app2 = rebind(testEntity.getApplication()); Entity e2 = Iterables.getOnlyElement(app2.getChildren()); Maybe<Object> maybe = ((EntityInternal) e2).config().getLocalRaw(TestEntity.CONF_NAME); Assert.assertTrue(maybe.isPresentAndNonNull()); Assert.assertTrue(BrooklynDslDeferredSupplier.class.isInstance(maybe.get())); BrooklynDslDeferredSupplier<?> deferredSupplier = (BrooklynDslDeferredSupplier<?>) maybe.get(); Assert.assertEquals(deferredSupplier.toString(), "$brooklyn:entity(\"x\").attributeWhenReady(\"foo\")"); // Assert the persisted state itself is as expected, and not too big BrooklynMementoRawData raw = BrooklynPersistenceUtils.newStateMemento(app2.getManagementContext(), MementoCopyMode.LOCAL); String persistedStateForE2 = raw.getEntities().get(e2.getId()); Matcher matcher = Pattern.compile(".*\\<test.confName\\>(.*)\\<\\/test.confName\\>.*", Pattern.DOTALL) .matcher(persistedStateForE2); Assert.assertTrue(matcher.find()); String testConfNamePersistedState = matcher.group(1); Assert.assertNotNull(testConfNamePersistedState); // should be about 200 chars long, something like: // // <test.confName> // <org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady> // <component> // <componentId>x</componentId> // <scope>GLOBAL</scope> // </component> // <sensorName>foo</sensorName> // </org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady> // </test.confName> Assert.assertTrue(testConfNamePersistedState.length() < 400, "persisted state too long: " + testConfNamePersistedState); Assert.assertFalse(testConfNamePersistedState.contains("bar"), "value 'bar' leaked in persisted state"); } @Test public void testDslAttributeWhenReadyInEntitySpecWhenNotYetResolved() throws Exception { doDslAttributeWhenReadyInEntitySpec(false); } @Test public void testDslAttributeWhenReadyInEntitySpecWhenAlreadyResolved() throws Exception { doDslAttributeWhenReadyInEntitySpec(true); } protected void doDslAttributeWhenReadyInEntitySpec(boolean resolvedBeforeRebind) throws Exception { String yaml = "location: localhost\n" + "name: Test Cluster\n" + "services:\n" + "- type: org.apache.brooklyn.entity.group.DynamicCluster\n" + " id: test-cluster\n" + " initialSize: 0\n" + " memberSpec:\n" + " $brooklyn:entitySpec:\n" + " type: org.apache.brooklyn.core.test.entity.TestEntity\n" + " brooklyn.config:\n" + " test.confName: $brooklyn:component(\"test-cluster\").attributeWhenReady(\"sensor\")"; final Entity testEntity = createAndStartApplication(yaml); DynamicCluster cluster = (DynamicCluster) Iterables.getOnlyElement(testEntity.getApplication().getChildren()); cluster.resize(1); Assert.assertEquals(cluster.getMembers().size(), 1); if (resolvedBeforeRebind) { cluster.sensors().set(Sensors.newStringSensor("sensor"), "bar"); } // Persist and rebind Application app2 = rebind(cluster.getApplication()); DynamicCluster cluster2 = (DynamicCluster) Iterables.getOnlyElement(app2.getApplication().getChildren()); // Assert the persisted state itself is as expected, and not too big BrooklynMementoRawData raw = BrooklynPersistenceUtils.newStateMemento(app2.getManagementContext(), MementoCopyMode.LOCAL); String persistedStateForE2 = raw.getEntities().get(cluster2.getId()); String expectedTag = "org.apache.brooklyn.camp.brooklyn.spi.dsl.methods.DslComponent_-AttributeWhenReady"; Matcher matcher = Pattern.compile(".*\\<"+expectedTag+"\\>(.*)\\<\\/"+expectedTag+"\\>.*", Pattern.DOTALL) .matcher(persistedStateForE2); Assert.assertTrue(matcher.find(), persistedStateForE2); String testConfNamePersistedState = matcher.group(1); Assert.assertNotNull(testConfNamePersistedState); // Can re-size to create a new member entity cluster2.resize(2); Assert.assertEquals(cluster2.getMembers().size(), 2); // Both the existing and the new member should have the DeferredSupplier config for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) { Maybe<Object> maybe = ((EntityInternal)member).config().getLocalRaw(TestEntity.CONF_NAME); Assert.assertTrue(maybe.isPresentAndNonNull()); BrooklynDslDeferredSupplier<?> deferredSupplier = (BrooklynDslDeferredSupplier<?>) maybe.get(); Assert.assertEquals(deferredSupplier.toString(), "$brooklyn:entity(\"test-cluster\").attributeWhenReady(\"sensor\")"); } if (resolvedBeforeRebind) { // All members should resolve their config for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) { String val = getConfigInTask(member, TestEntity.CONF_NAME); Assert.assertEquals(val, "bar"); } } else { List<Future<String>> futures = Lists.newArrayList(); // All members should have unresolved values for (Entity member : Iterables.filter(cluster2.getChildren(), TestEntity.class)) { // Wait for the attribute to be ready in a new Task Future<String> stringFuture = getConfigInTaskAsync(member, TestEntity.CONF_NAME); futures.add(stringFuture); // Check that the Task is still waiting for attribute to be ready Thread.sleep(100); Assert.assertFalse(stringFuture.isDone()); } // After setting the sensor, all those values should now resolve cluster2.sensors().set(Sensors.newStringSensor("sensor"), "bar"); for (Future<String> future : futures) { String s = future.get(10, TimeUnit.SECONDS); // Timeout just for sanity Assert.assertEquals(s, "bar"); } } } private Entity entityWithAttributeWhenReady() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.confName: $brooklyn:component(\"x\").attributeWhenReady(\"foo\")"); } private void doTestOnEntityWithSensor(Entity testEntity, Sensor<?> expectedSensor) throws Exception { doTestOnEntityWithSensor(testEntity, expectedSensor, true); } private void doTestOnEntityWithSensor(Entity testEntity, Sensor<?> expectedSensor, boolean inTask) throws Exception { @SuppressWarnings("rawtypes") ConfigKey<Sensor> configKey = ConfigKeys.newConfigKey(Sensor.class, "test.sensor"); Sensor<?> s; s = inTask ? getConfigInTask(testEntity, configKey) : testEntity.getConfig(configKey); Assert.assertEquals(s, expectedSensor); Application app2 = rebind(testEntity.getApplication()); Entity te2 = Iterables.getOnlyElement(app2.getChildren()); s = inTask ? getConfigInTask(te2, configKey) : te2.getConfig(configKey); Assert.assertEquals(s, expectedSensor); } @Test public void testDslSensorFromClass() throws Exception { doTestOnEntityWithSensor(entityWithSensorFromClass(), Attributes.SERVICE_UP); // without context it can still find it doTestOnEntityWithSensor(entityWithSensorFromClass(), Attributes.SERVICE_UP, false); } @Test public void testDslSensorLocal() throws Exception { doTestOnEntityWithSensor(entityWithSensorLocal(), TestEntity.SEQUENCE); // here without context it makes one up, so type info (and description etc) not present; // but context is needed to submit the DslDeferredSupplier object, so this would fail // doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, TestEntity.SEQUENCE.getName()), false); } @Test public void testDslSensorAdHoc() throws Exception { doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, "sensor.foo")); // here context has no impact, but it is needed to submit the DslDeferredSupplier object so this would fail // doTestOnEntityWithSensor(entityWithSensorAdHoc(), Sensors.newSensor(Object.class, "sensor.foo"), false); } private Entity entityWithSensorFromClass() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.sensor: $brooklyn:sensor(\"" + Attributes.class.getName() + "\", \"" + Attributes.SERVICE_UP.getName() + "\")"); } private Entity entityWithSensorLocal() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.sensor: $brooklyn:sensor(\"" + TestEntity.SEQUENCE.getName() + "\")"); } private Entity entityWithSensorAdHoc() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.sensor: $brooklyn:sensor(\"sensor.foo\")"); } @Test public void testDslConfigFromRoot() throws Exception { Entity testEntity = entityWithConfigFromRoot(); Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "bar"); } @Test public void testDslConfigFromRootRebind() throws Exception { Entity testEntity = entityWithConfigFromRoot(); Application app2 = rebind(testEntity.getApplication()); Entity e2 = Iterables.getOnlyElement(app2.getChildren()); Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "bar"); } private Entity entityWithConfigFromRoot() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.confName: $brooklyn:component(\"x\").config(\"foo\")", "brooklyn.config:", " foo: bar"); } @Test public void testDslFormatString() throws Exception { Entity testEntity = entityWithFormatString(); Assert.assertEquals(getConfigInTask(testEntity, TestEntity.CONF_NAME), "hello world"); } @Test public void testDslFormatStringRebind() throws Exception { Entity testEntity = entityWithFormatString(); Application app2 = rebind(testEntity.getApplication()); Entity e2 = Iterables.getOnlyElement(app2.getChildren()); Assert.assertEquals(getConfigInTask(e2, TestEntity.CONF_NAME), "hello world"); } private Entity entityWithFormatString() throws Exception { return setupAndCheckTestEntityInBasicYamlWith( " id: x", " brooklyn.config:", " test.confName: $brooklyn:formatString(\"hello %s\", \"world\")"); } /* - type: org.apache.brooklyn.enricher.stock.Transformer brooklyn.config: enricher.sourceSensor: $brooklyn:sensor("mongodb.server.replicaSet.primary.endpoint") enricher.targetSensor: $brooklyn:sensor("justtheport") enricher.transformation: $brooklyn:function.regexReplacement("^.*:", "") - type: org.apache.brooklyn.enricher.stock.Transformer brooklyn.config: enricher.sourceSensor: $brooklyn:sensor("mongodb.server.replicaSet.primary.endpoint") enricher.targetSensor: $brooklyn:sensor("directport") enricher.targetValue: $brooklyn:regexReplacement($brooklyn:attributeWhenReady("mongodb.server.replicaSet.primary.endpoint"), "^.*:", "foo") */ @Test public void testRegexReplacementWithStrings() throws Exception { Entity testEntity = setupAndCheckTestEntityInBasicYamlWith( " brooklyn.config:", " test.regex.config: $brooklyn:regexReplacement(\"somefooname\", \"foo\", \"bar\")" ); Assert.assertEquals("somebarname", testEntity.getConfig(ConfigKeys.newStringConfigKey("test.regex.config"))); } @Test public void testRegexReplacementWithAttributeWhenReady() throws Exception { Entity testEntity = setupAndCheckTestEntityInBasicYamlWith( " brooklyn.config:", " test.regex.config: $brooklyn:regexReplacement($brooklyn:attributeWhenReady(\"test.regex.source\"), $brooklyn:attributeWhenReady(\"test.regex.pattern\"), $brooklyn:attributeWhenReady(\"test.regex.replacement\"))" ); testEntity.sensors().set(Sensors.newStringSensor("test.regex.source"), "somefooname"); testEntity.sensors().set(Sensors.newStringSensor("test.regex.pattern"), "foo"); testEntity.sensors().set(Sensors.newStringSensor("test.regex.replacement"), "bar"); Assert.assertEquals("somebarname", testEntity.getConfig(ConfigKeys.newStringConfigKey("test.regex.config"))); } @Test public void testRegexReplacementFunctionWithStrings() throws Exception { Entity testEntity = setupAndCheckTestEntityInBasicYamlWith( " brooklyn.enrichers:", " - type: org.apache.brooklyn.enricher.stock.Transformer", " brooklyn.config:", " enricher.sourceSensor: $brooklyn:sensor(\"test.name\")", " enricher.targetSensor: $brooklyn:sensor(\"test.name.transformed\")", " enricher.transformation: $brooklyn:function.regexReplacement(\"foo\", \"bar\")" ); testEntity.sensors().set(TestEntity.NAME, "somefooname"); AttributeSensor<String> transformedSensor = Sensors.newStringSensor("test.name.transformed"); EntityAsserts.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname"); } @Test public void testRegexReplacementFunctionWithAttributeWhenReady() throws Exception { Entity testEntity = setupAndCheckTestEntityInBasicYamlWith( " brooklyn.enrichers:", " - type: org.apache.brooklyn.enricher.stock.Transformer", " brooklyn.config:", " enricher.sourceSensor: $brooklyn:sensor(\"test.name\")", " enricher.targetSensor: $brooklyn:sensor(\"test.name.transformed\")", " enricher.transformation: $brooklyn:function.regexReplacement($brooklyn:attributeWhenReady(\"test.pattern\"), $brooklyn:attributeWhenReady(\"test.replacement\"))" ); testEntity.sensors().set(Sensors.newStringSensor("test.pattern"), "foo"); testEntity.sensors().set(Sensors.newStringSensor("test.replacement"), "bar"); testEntity.sensors().set(TestEntity.NAME, "somefooname"); AttributeSensor<String> transformedSensor = Sensors.newStringSensor("test.name.transformed"); EntityAsserts.assertAttributeEqualsEventually(testEntity, transformedSensor, "somebarname"); } }