You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2023/03/24 11:44:30 UTC

[brooklyn-server] branch master updated (8b7b77994b -> 4ef6f634e3)

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git


    from 8b7b77994b Merge branch 'misc-workflow-fixes'
     new eaf265e9b8 notes on supporting a zip url scheme
     new 789c68d29c tidyup and tests of latency detector / enricher
     new 4b38854162 support target_index variable for list-based targets
     new 26b1a9a089 allow configurable interpolation mode on more workflow steps - blueprints and commands
     new a67de70822 add message for mathematical operations without spaces
     new bcd6e30fb2 add test for serializing BigDecimal
     new 3b79e7f9d6 workflow software process should start/stop children by default
     new 4ef6f634e3 Merge branch 'more-workflow-and-misc'

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../core/workflow/steps/CustomWorkflowStep.java    |  6 +-
 .../steps/appmodel/AddEntityWorkflowStep.java      | 24 +++----
 .../steps/appmodel/AddPolicyWorkflowStep.java      | 24 +++----
 .../appmodel/ApplyInitializerWorkflowStep.java     | 22 ++----
 .../appmodel/DeployApplicationWorkflowStep.java    | 25 +++----
 .../steps/appmodel/HasBlueprintWorkflowStep.java   | 78 ++++++++++++++++++++++
 .../workflow/steps/external/SshWorkflowStep.java   |  9 ++-
 .../steps/variables/SetVariableWorkflowStep.java   | 22 ++++--
 .../apache/brooklyn/util/core/ResourceUtils.java   | 28 +++++++-
 .../policy/enricher/HttpLatencyDetector.java       |  2 +-
 .../policy/enricher/HttpLatencyDetectorTest.java   | 30 ++++++++-
 .../util/json/BrooklynJacksonSerializerTest.java   |  3 +
 .../software/base/WorkflowSoftwareProcess.java     |  2 +
 .../brooklyn/location/winrm/WinrmWorkflowStep.java |  8 ++-
 14 files changed, 206 insertions(+), 77 deletions(-)
 create mode 100644 core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/HasBlueprintWorkflowStep.java


[brooklyn-server] 08/08: Merge branch 'more-workflow-and-misc'

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 4ef6f634e34b53e822d6ae2ebce793f0dd74f894
Merge: 8b7b77994b 3b79e7f9d6
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Mar 24 11:44:17 2023 +0000

    Merge branch 'more-workflow-and-misc'

 .../core/workflow/steps/CustomWorkflowStep.java    |  6 +-
 .../steps/appmodel/AddEntityWorkflowStep.java      | 24 +++----
 .../steps/appmodel/AddPolicyWorkflowStep.java      | 24 +++----
 .../appmodel/ApplyInitializerWorkflowStep.java     | 22 ++----
 .../appmodel/DeployApplicationWorkflowStep.java    | 25 +++----
 .../steps/appmodel/HasBlueprintWorkflowStep.java   | 78 ++++++++++++++++++++++
 .../workflow/steps/external/SshWorkflowStep.java   |  9 ++-
 .../steps/variables/SetVariableWorkflowStep.java   | 22 ++++--
 .../apache/brooklyn/util/core/ResourceUtils.java   | 28 +++++++-
 .../policy/enricher/HttpLatencyDetector.java       |  2 +-
 .../policy/enricher/HttpLatencyDetectorTest.java   | 30 ++++++++-
 .../util/json/BrooklynJacksonSerializerTest.java   |  3 +
 .../software/base/WorkflowSoftwareProcess.java     |  2 +
 .../brooklyn/location/winrm/WinrmWorkflowStep.java |  8 ++-
 14 files changed, 206 insertions(+), 77 deletions(-)


[brooklyn-server] 03/08: support target_index variable for list-based targets

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 4b388541622851a7171cc52c232cbeb5accb3f2e
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Mar 23 17:34:41 2023 +0000

    support target_index variable for list-based targets
---
 .../org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
index 0c87777f38..efc51fc185 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/CustomWorkflowStep.java
@@ -201,8 +201,9 @@ public class CustomWorkflowStep extends WorkflowStepDefinition implements Workfl
             targetR = MutableList.of(targetR);
         }
 
+        AtomicInteger index = new AtomicInteger(0);
         ((Iterable<?>) targetR).forEach(t -> {
-            WorkflowExecutionContext nw = newWorkflow(context, t);
+            WorkflowExecutionContext nw = newWorkflow(context, t, index.getAndIncrement());
             Maybe<Task<Object>> mt = nw.getTask(true);
 
             String targetS = wasList || t !=null ? " for target '"+t+"'" : "";
@@ -396,7 +397,7 @@ public class CustomWorkflowStep extends WorkflowStepDefinition implements Workfl
         return result;
     }
 
-    private WorkflowExecutionContext newWorkflow(WorkflowStepInstanceExecutionContext context, Object target) {
+    private WorkflowExecutionContext newWorkflow(WorkflowStepInstanceExecutionContext context, Object target, int targetIndex) {
         if (steps==null) throw new IllegalArgumentException("Cannot make new workflow with no steps");
 
         WorkflowExecutionContext nestedWorkflowContext = WorkflowExecutionContext.newInstanceUnpersistedWithParent(
@@ -406,6 +407,7 @@ public class CustomWorkflowStep extends WorkflowStepDefinition implements Workfl
                 ConfigBag.newInstance(getInput()), null);
         if (target!=null) {
             nestedWorkflowContext.getWorkflowScratchVariables().put("target", target);
+            nestedWorkflowContext.getWorkflowScratchVariables().put("target_index", targetIndex);
         }
         return nestedWorkflowContext;
     }


[brooklyn-server] 01/08: notes on supporting a zip url scheme

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit eaf265e9b81791150a4e15c2f6a4f121bfb6747f
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Mar 23 17:33:56 2023 +0000

    notes on supporting a zip url scheme
---
 .../apache/brooklyn/util/core/ResourceUtils.java   | 28 +++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/brooklyn/util/core/ResourceUtils.java b/core/src/main/java/org/apache/brooklyn/util/core/ResourceUtils.java
index 08a9f06ce3..253e4d3b95 100644
--- a/core/src/main/java/org/apache/brooklyn/util/core/ResourceUtils.java
+++ b/core/src/main/java/org/apache/brooklyn/util/core/ResourceUtils.java
@@ -402,7 +402,33 @@ public class ResourceUtils {
         if (u!=null) return u.openStream();
         else throw new IOException(subUrl+" not found on classpath");
     }
-    
+
+    private InputStream getResourceZip(String url) throws IOException {
+        // TODO would be nice to support ZIP-wrapping from classpath, either as a protocol or DSL or both
+        // something like the below (also cf pax wrap:mvn... but i think our need is sufficiently different)
+        // zip://output-file.zip?contents=/foo/bar
+        /* contents as string (source) or map (with includes, excludes) or list (of maps);
+           also available as DSL,as:
+           $brooklyn:zip:
+             name: output-file.zip
+             contents:
+             - source: /foo/bar
+               includes: [ ** /sub/**, ** /okay/** ]
+               excludes: [ ** /*.bak ]
+               target: ""
+             - source: /
+               includes: [ /foo/bar/** ]
+               excludes: [ ** /*.bak ]
+               target: ""
+         */
+        assert url.startsWith("zip:");
+        String subUrl = url.substring("classpath:".length());
+        while (subUrl.startsWith("/")) subUrl = subUrl.substring(1);
+        URL u = getLoader().getResource(subUrl);
+        if (u!=null) return u.openStream();
+        else throw new IOException(subUrl+" not found on classpath");
+    }
+
     private InputStream getResourceViaSftp(String url) throws IOException {
         assert url.startsWith("sftp://");
         String subUrl = url.substring("sftp://".length());


[brooklyn-server] 07/08: workflow software process should start/stop children by default

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 3b79e7f9d62c4e76d54cd0881cc508817a95f8bb
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Mar 24 10:57:02 2023 +0000

    workflow software process should start/stop children by default
---
 .../apache/brooklyn/entity/software/base/WorkflowSoftwareProcess.java   | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/WorkflowSoftwareProcess.java b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/WorkflowSoftwareProcess.java
index 09ab9aef31..3bfa5d587a 100644
--- a/software/base/src/main/java/org/apache/brooklyn/entity/software/base/WorkflowSoftwareProcess.java
+++ b/software/base/src/main/java/org/apache/brooklyn/entity/software/base/WorkflowSoftwareProcess.java
@@ -33,6 +33,8 @@ import org.apache.brooklyn.core.workflow.steps.CustomWorkflowStep;
 @ImplementedBy(WorkflowSoftwareProcessImpl.class)
 public interface WorkflowSoftwareProcess extends SoftwareProcess {
 
+    ConfigKey<ChildStartableMode> CHILDREN_STARTABLE_MODE = ConfigKeys.newConfigKeyWithDefault(SoftwareProcess.CHILDREN_STARTABLE_MODE, ChildStartableMode.FOREGROUND_LATE);
+
     @CatalogConfig(label = "Install Workflow", priority=5)
     ConfigKey<CustomWorkflowStep> INSTALL_WORKFLOW = ConfigKeys.builder(CustomWorkflowStep.class, "install.workflow")
             .description("workflow to run during the software install phase")


[brooklyn-server] 05/08: add message for mathematical operations without spaces

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit a67de70822b38198ed99af460caff57664130772
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Mar 24 10:56:20 2023 +0000

    add message for mathematical operations without spaces
---
 .../steps/variables/SetVariableWorkflowStep.java   | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java
index 44e4969fbc..82f7c3ac66 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/variables/SetVariableWorkflowStep.java
@@ -317,16 +317,28 @@ public class SetVariableWorkflowStep extends WorkflowStepDefinition {
                 Maybe<Double> rhsD = asDouble(rhs);
                 if (lhsD.isPresent() && rhsD.isPresent()) return asDouble(ifDouble.apply(lhsD.get(), rhsD.get())).get();
 
-                if (lhsD.isAbsent()) throw new IllegalArgumentException("Invalid left argument to operation '"+op+"': "+lhs0+" => "+lhs);
-                if (rhsD.isAbsent()) throw new IllegalArgumentException("Invalid right argument to operation '"+op+"': "+rhs0+" = "+rhs);
+                if (lhsD.isAbsent()) failOnInvalidArgument("left", op, lhs0, lhs);
+                if (rhsD.isAbsent()) failOnInvalidArgument("right", op, rhs0, rhs);
+            } else {
+                if (lhsI.isAbsent()) failOnInvalidArgument("left", op, lhs0, lhs);
+                if (rhsI.isAbsent()) failOnInvalidArgument("right", op, rhs0, rhs);
             }
 
-            if (lhsI.isAbsent()) throw new IllegalArgumentException("Invalid left argument to operation '"+op+"': "+lhs0+" => "+lhs);
-            if (rhsI.isAbsent()) throw new IllegalArgumentException("Invalid right argument to operation '"+op+"': "+rhs0+" = "+rhs);
-
             throw new IllegalArgumentException("Should not come here");
         }
 
+        private IllegalArgumentException failOnInvalidArgument(String side, String op, Object pre, Object post) {
+            String msg = "Invalid "+side+" argument to operation '"+op+"'";
+
+            String postS = ""+post;
+            if (postS.contains("*") || postS.contains("+") || postS.contains("+") || postS.contains("/")) {
+                // we could weaken this contraint but for now at least make it clear
+                msg += "; mathematical operations must have spaces around them for disambiguation";
+            }
+
+            throw new IllegalArgumentException(msg + ": "+pre+" => "+post);
+        }
+
         Object handleMultiply(List<String> lhs, List<String> rhs) {
             return applyMathOperator("*", lhs, rhs, (a,b)->a*b, (a,b)->a*b);
         }


[brooklyn-server] 06/08: add test for serializing BigDecimal

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit bcd6e30fb25b10ad36656c38065079c1c32fed40
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Mar 24 10:56:52 2023 +0000

    add test for serializing BigDecimal
---
 .../apache/brooklyn/rest/util/json/BrooklynJacksonSerializerTest.java  | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonSerializerTest.java b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonSerializerTest.java
index a248fceebb..71cdc497ee 100644
--- a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonSerializerTest.java
+++ b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/json/BrooklynJacksonSerializerTest.java
@@ -263,6 +263,9 @@ public class BrooklynJacksonSerializerTest {
             Asserts.assertSize((Map) x2, 2);
             Asserts.assertEquals(((Map) x2).get("int"), 1);
             Asserts.assertThat(((Map) x2).get("double"), v -> v.equals(1.0d) || v.equals(BigDecimal.valueOf(1.0d)));
+
+            Asserts.assertEquals(mapper.writer().writeValueAsString(new BigDecimal("1.01")), "1.01");
+
         } finally {
             Entities.destroyAll(mgmt);
         }


[brooklyn-server] 02/08: tidyup and tests of latency detector / enricher

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 789c68d29cbb9aec9da6021205515976d95d71da
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Thu Mar 23 17:34:16 2023 +0000

    tidyup and tests of latency detector / enricher
---
 .../policy/enricher/HttpLatencyDetector.java       |  2 +-
 .../policy/enricher/HttpLatencyDetectorTest.java   | 30 +++++++++++++++++++++-
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/policy/src/main/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetector.java b/policy/src/main/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetector.java
index d76c1fa2ea..24a7ced7da 100644
--- a/policy/src/main/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetector.java
+++ b/policy/src/main/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetector.java
@@ -231,7 +231,7 @@ public class HttpLatencyDetector extends AbstractEnricher implements Enricher {
                 Function<String, String> postProcessor = getConfig(URL_POST_PROCESSING);
                 String newVal = (postProcessor != null) ? postProcessor.apply(currentVal.toString()) : currentVal.toString();
                 if (AtomicReferences.setIfDifferent(url, newVal)) {
-                    log.debug("{} updated url on initial connectionon, to {}", this, newVal);
+                    log.debug("{} updated url on initial connection, to {}", this, newVal);
                 }
             }
         }
diff --git a/policy/src/test/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetectorTest.java b/policy/src/test/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetectorTest.java
index 248891b5cb..d731cff204 100644
--- a/policy/src/test/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetectorTest.java
+++ b/policy/src/test/java/org/apache/brooklyn/policy/enricher/HttpLatencyDetectorTest.java
@@ -24,13 +24,16 @@ import java.util.concurrent.TimeUnit;
 import org.apache.brooklyn.api.entity.Entity;
 import org.apache.brooklyn.api.entity.EntitySpec;
 import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
 import org.apache.brooklyn.core.entity.Entities;
 import org.apache.brooklyn.core.entity.EntityAsserts;
 import org.apache.brooklyn.core.sensor.Sensors;
 import org.apache.brooklyn.core.test.entity.TestApplication;
 import org.apache.brooklyn.core.test.entity.TestEntity;
 import org.apache.brooklyn.util.collections.MutableMap;
+import org.apache.brooklyn.util.core.config.ConfigBag;
 import org.apache.brooklyn.util.core.http.BetterMockWebServer;
+import org.apache.brooklyn.util.time.Duration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.testng.annotations.AfterMethod;
@@ -120,7 +123,7 @@ public class HttpLatencyDetectorTest {
     }
 
     @Test(groups="Integration")
-    public void testWaitsForServiceUp() throws Exception {
+    public void  testWaitsForServiceUp() throws Exception {
         entity.sensors().set(TestEntity.SERVICE_UP, false);
         
         entity.enrichers().add(HttpLatencyDetector.builder()
@@ -137,6 +140,31 @@ public class HttpLatencyDetectorTest {
         entity.sensors().set(TestEntity.SERVICE_UP, true);
         assertLatencyAttributesNonNull(entity); 
     }
+
+    @Test(groups="Integration")
+    public void  testWaitsForServiceUpWhenInitializedFromConfig() throws Exception {
+        entity.sensors().set(TestEntity.SERVICE_UP, false);
+        entity.sensors().set(TEST_URL, baseUrl.toString());
+
+        entity.enrichers().add(
+                new HttpLatencyDetector(ConfigBag.newInstance()
+//                        .configure((ConfigKey) HttpLatencyDetector.URL, baseUrl)
+                        .configure((ConfigKey) HttpLatencyDetector.URL_SENSOR, TEST_URL.getName())
+
+                        .configure(HttpLatencyDetector.PERIOD, Duration.millis(100))
+                        .configure(HttpLatencyDetector.ROLLUP_WINDOW_SIZE, Duration.millis(1000))
+                        .configure(HttpLatencyDetector.REQUIRE_SERVICE_UP, true)
+                        .getAllConfig()));
+
+        // nothing until url is set
+        EntityAsserts.assertAttributeEqualsContinually(
+                MutableMap.of("timeout", 200),
+                entity, HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_MOST_RECENT, null);
+
+        // gets value after url is set, and gets rolling average
+        entity.sensors().set(TestEntity.SERVICE_UP, true);
+        assertLatencyAttributesNonNull(entity);
+    }
     
     protected void assertLatencyAttributesNonNull(Entity entity) {
         EntityAsserts.assertAttributeEventuallyNonNull(entity, HttpLatencyDetector.REQUEST_LATENCY_IN_SECONDS_MOST_RECENT); 


[brooklyn-server] 04/08: allow configurable interpolation mode on more workflow steps - blueprints and commands

Posted by he...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git

commit 26b1a9a0895f5fcc349e093bfd77604025b51aa5
Author: Alex Heneveld <al...@cloudsoft.io>
AuthorDate: Fri Mar 24 08:43:22 2023 +0000

    allow configurable interpolation mode on more workflow steps - blueprints and commands
---
 .../steps/appmodel/AddEntityWorkflowStep.java      | 24 +++----
 .../steps/appmodel/AddPolicyWorkflowStep.java      | 24 +++----
 .../appmodel/ApplyInitializerWorkflowStep.java     | 22 ++----
 .../appmodel/DeployApplicationWorkflowStep.java    | 25 +++----
 .../steps/appmodel/HasBlueprintWorkflowStep.java   | 78 ++++++++++++++++++++++
 .../workflow/steps/external/SshWorkflowStep.java   |  9 ++-
 .../brooklyn/location/winrm/WinrmWorkflowStep.java |  8 ++-
 7 files changed, 123 insertions(+), 67 deletions(-)

diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddEntityWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddEntityWorkflowStep.java
index ce38235543..4713391a9f 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddEntityWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddEntityWorkflowStep.java
@@ -42,9 +42,11 @@ import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
 import org.apache.brooklyn.core.workflow.steps.appmodel.DeployApplicationWorkflowStep.StartMode;
+import org.apache.brooklyn.core.workflow.steps.variables.SetVariableWorkflowStep;
 import org.apache.brooklyn.util.collections.MutableList;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.task.Tasks;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
 import org.apache.brooklyn.util.exceptions.Exceptions;
 import org.apache.brooklyn.util.guava.Maybe;
 import org.apache.brooklyn.util.text.Identifiers;
@@ -57,24 +59,21 @@ import javax.annotation.Nullable;
 import java.util.List;
 import java.util.Map;
 
-public class AddEntityWorkflowStep extends WorkflowStepDefinition {
+public class AddEntityWorkflowStep extends WorkflowStepDefinition implements HasBlueprintWorkflowStep {
 
     private static final Logger LOG = LoggerFactory.getLogger(AddEntityWorkflowStep.class);
 
     public static final String SHORTHAND = "[ ${type} ]";
 
-    public static final ConfigKey<Object> BLUEPRINT = ConfigKeys.newConfigKey(Object.class, "blueprint");
-    public static final ConfigKey<String> TYPE = ConfigKeys.newStringConfigKey("type");
     public static final ConfigKey<String> FORMAT = ConfigKeys.newStringConfigKey("format");
 
     // sync is not completely idempotent (in the call to start it) but very useful for testing;
     // option not included in documentation, but used for tests
     public static final ConfigKey<StartMode> START = ConfigKeys.newConfigKey(StartMode.class, "start", "Default 'async'");
 
-    // don't try to instantiate 'type' here
-    @JsonDeserialize(using = JsonPassThroughDeserializer.class)
-    void setBlueprint(Object blueprint) {
-        setInput(BLUEPRINT, blueprint);
+    @Override
+    public Logger logger() {
+        return LOG;
     }
 
     @Override
@@ -85,11 +84,7 @@ public class AddEntityWorkflowStep extends WorkflowStepDefinition {
     @Override
     public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
         super.validateStep(mgmt, workflow);
-
-        boolean hasBlueprint = getInput().containsKey(BLUEPRINT.getName());
-        boolean hasType = getInput().containsKey(TYPE.getName());
-        if (!hasBlueprint && !hasType) throw new IllegalArgumentException("A '"+BLUEPRINT.getName()+"' must be defined or a type supplied in shorthand");
-        if (hasBlueprint && hasType) throw new IllegalArgumentException("Cannot provide both a '"+BLUEPRINT.getName()+"' and a type in shorthand");
+        validateStepBlueprint(mgmt, workflow);
     }
 
     @Override
@@ -102,10 +97,7 @@ public class AddEntityWorkflowStep extends WorkflowStepDefinition {
 
     @Override
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
-        Object blueprint = input.get(BLUEPRINT.getName());
-        if (blueprint == null) {
-            blueprint = "type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE));
-        }
+        Object blueprint = resolveBlueprint(context);
 
         List<String> preCreatedEntityIds = Maybe.ofDisallowingNull(getStepState(context)).map(MutableList::copyOf).orNull();
         List<String> newCreatedEntityIds = MutableList.of();
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddPolicyWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddPolicyWorkflowStep.java
index 2ec1ab42eb..8995389c25 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddPolicyWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/AddPolicyWorkflowStep.java
@@ -48,7 +48,7 @@ import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 
-public class AddPolicyWorkflowStep extends WorkflowStepDefinition {
+public class AddPolicyWorkflowStep extends WorkflowStepDefinition implements HasBlueprintWorkflowStep {
 
     private static final Logger LOG = LoggerFactory.getLogger(AddPolicyWorkflowStep.class);
 
@@ -60,24 +60,19 @@ public class AddPolicyWorkflowStep extends WorkflowStepDefinition {
     public static final ConfigKey<Object> ENTITY = ConfigKeys.newConfigKey(Object.class, "entity");
 
     @Override
-    public void populateFromShorthand(String expression) {
-        populateFromShorthandTemplate(SHORTHAND, expression);
+    public Logger logger() {
+        return LOG;
     }
 
-    // don't try to instantiate 'type' here
-    @JsonDeserialize(using = JsonPassThroughDeserializer.class)
-    void setBlueprint(Object blueprint) {
-        setInput(BLUEPRINT, blueprint);
+    @Override
+    public void populateFromShorthand(String expression) {
+        populateFromShorthandTemplate(SHORTHAND, expression);
     }
 
     @Override
     public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
         super.validateStep(mgmt, workflow);
-
-        boolean hasBlueprint = getInput().containsKey(BLUEPRINT.getName());
-        boolean hasType = getInput().containsKey(TYPE.getName());
-        if (!hasBlueprint && !hasType) throw new IllegalArgumentException("A '"+BLUEPRINT.getName()+"' must be defined or a type supplied in shorthand");
-        if (hasBlueprint && hasType) throw new IllegalArgumentException("Cannot provide both a '"+BLUEPRINT.getName()+"' and a type in shorthand");
+        validateStepBlueprint(mgmt, workflow);
     }
 
     @Override
@@ -85,10 +80,7 @@ public class AddPolicyWorkflowStep extends WorkflowStepDefinition {
         Object entityToFind = context.getInput(ENTITY);
         Entity entity = entityToFind != null ? DeleteEntityWorkflowStep.findEntity(context, entityToFind).get() : context.getEntity();
 
-        Object blueprint = input.get(BLUEPRINT.getName());
-        if (blueprint == null) {
-            blueprint = "type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE));
-        }
+        Object blueprint = resolveBlueprint(context);
 
         AbstractBrooklynObjectSpec spec = null;
         EntityAdjunct inst = null;
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ApplyInitializerWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ApplyInitializerWorkflowStep.java
index 086233982d..14d825781c 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ApplyInitializerWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/ApplyInitializerWorkflowStep.java
@@ -44,20 +44,17 @@ import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 
-public class ApplyInitializerWorkflowStep extends WorkflowStepDefinition {
+public class ApplyInitializerWorkflowStep extends WorkflowStepDefinition implements HasBlueprintWorkflowStep {
 
     private static final Logger LOG = LoggerFactory.getLogger(ApplyInitializerWorkflowStep.class);
 
     public static final String SHORTHAND = "[ ${type} [ \" at \" ${entity} ] ]";
 
-    public static final ConfigKey<Object> BLUEPRINT = ConfigKeys.newConfigKey(Object.class, "blueprint");
-    public static final ConfigKey<String> TYPE = ConfigKeys.newStringConfigKey("type");
     public static final ConfigKey<Object> ENTITY = ConfigKeys.newConfigKey(Object.class, "entity");
 
-    // don't try to instantiate 'type' here
-    @JsonDeserialize(using = JsonPassThroughDeserializer.class)
-    void setBlueprint(Object blueprint) {
-        setInput(BLUEPRINT, blueprint);
+    @Override
+    public Logger logger() {
+        return LOG;
     }
 
     @Override
@@ -68,11 +65,7 @@ public class ApplyInitializerWorkflowStep extends WorkflowStepDefinition {
     @Override
     public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
         super.validateStep(mgmt, workflow);
-
-        boolean hasBlueprint = getInput().containsKey(BLUEPRINT.getName());
-        boolean hasType = getInput().containsKey(TYPE.getName());
-        if (!hasBlueprint && !hasType) throw new IllegalArgumentException("A '"+BLUEPRINT.getName()+"' must be defined or a type supplied in shorthand");
-        if (hasBlueprint && hasType) throw new IllegalArgumentException("Cannot provide both a '"+BLUEPRINT.getName()+"' and a type in shorthand");
+        validateStepBlueprint(mgmt, workflow);
     }
 
     @Override
@@ -80,10 +73,7 @@ public class ApplyInitializerWorkflowStep extends WorkflowStepDefinition {
         Object entityToFind = context.getInput(ENTITY);
         Entity entity = entityToFind != null ? DeleteEntityWorkflowStep.findEntity(context, entityToFind).get() : context.getEntity();
 
-        Object blueprint = input.get(BLUEPRINT.getName());
-        if (blueprint == null) {
-            blueprint = "type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE));
-        }
+        Object blueprint = resolveBlueprint(context);
 
         EntityInitializer initializer;
         try {
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/DeployApplicationWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/DeployApplicationWorkflowStep.java
index da018209ae..a065534071 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/DeployApplicationWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/DeployApplicationWorkflowStep.java
@@ -42,14 +42,12 @@ import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 
-public class DeployApplicationWorkflowStep extends WorkflowStepDefinition {
+public class DeployApplicationWorkflowStep extends WorkflowStepDefinition implements HasBlueprintWorkflowStep {
 
     private static final Logger LOG = LoggerFactory.getLogger(DeployApplicationWorkflowStep.class);
 
     public static final String SHORTHAND = "[ ${type} ]";
 
-    public static final ConfigKey<Object> BLUEPRINT = ConfigKeys.newConfigKey(Object.class, "blueprint");
-    public static final ConfigKey<String> TYPE = ConfigKeys.newStringConfigKey("type");
     public static final ConfigKey<String> FORMAT = ConfigKeys.newStringConfigKey("format");
 
     // sync is not completely idempotent (in the call to start it) but very useful for testing;
@@ -61,24 +59,19 @@ public class DeployApplicationWorkflowStep extends WorkflowStepDefinition {
     }
 
     @Override
-    public void populateFromShorthand(String expression) {
-        populateFromShorthandTemplate(SHORTHAND, expression);
+    public Logger logger() {
+        return LOG;
     }
 
-    // don't try to instantiate 'type' here
-    @JsonDeserialize(using = JsonPassThroughDeserializer.class)
-    void setBlueprint(Object blueprint) {
-        setInput(BLUEPRINT, blueprint);
+    @Override
+    public void populateFromShorthand(String expression) {
+        populateFromShorthandTemplate(SHORTHAND, expression);
     }
 
     @Override
     public void validateStep(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
         super.validateStep(mgmt, workflow);
-
-        boolean hasBlueprint = getInput().containsKey(BLUEPRINT.getName());
-        boolean hasType = getInput().containsKey(TYPE.getName());
-        if (!hasBlueprint && !hasType) throw new IllegalArgumentException("A '"+BLUEPRINT.getName()+"' must be defined or a type supplied in shorthand");
-        if (hasBlueprint && hasType) throw new IllegalArgumentException("Cannot provide both a '"+BLUEPRINT.getName()+"' and a type in shorthand");
+        validateStepBlueprint(mgmt, workflow);
     }
 
     @Override
@@ -91,9 +84,7 @@ public class DeployApplicationWorkflowStep extends WorkflowStepDefinition {
 
     @Override
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
-        Object blueprint = context.getInput(BLUEPRINT);
-        if (blueprint == null)
-            blueprint = "services: [ { type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE)) + " } ]";
+        Object blueprint = resolveBlueprint(context, () -> "services: [ { type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE)) + " } ]");
 
         String createdAppId = getStepState(context);
         Application app = null;
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/HasBlueprintWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/HasBlueprintWorkflowStep.java
new file mode 100644
index 0000000000..4a7d0a8547
--- /dev/null
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/appmodel/HasBlueprintWorkflowStep.java
@@ -0,0 +1,78 @@
+/*
+ * 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.workflow.steps.appmodel;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+import org.apache.brooklyn.core.resolve.jackson.JsonPassThroughDeserializer;
+import org.apache.brooklyn.core.workflow.WorkflowExecutionContext;
+import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.steps.variables.SetVariableWorkflowStep;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
+import org.apache.brooklyn.util.text.StringEscapes;
+import org.slf4j.Logger;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+import java.util.function.Supplier;
+
+public interface HasBlueprintWorkflowStep {
+
+    ConfigKey<Object> BLUEPRINT = ConfigKeys.newConfigKey(Object.class, "blueprint");
+    ConfigKey<String> TYPE = ConfigKeys.newStringConfigKey("type");
+
+    ConfigKey<SetVariableWorkflowStep.InterpolationMode> INTERPOLATION_MODE = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_MODE, SetVariableWorkflowStep.InterpolationMode.FULL);
+    ConfigKey<TemplateProcessor.InterpolationErrorMode> INTERPOLATION_ERRORS = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_ERRORS, TemplateProcessor.InterpolationErrorMode.IGNORE);
+
+    Map<String, Object> getInput();
+    Logger logger();
+
+    default void validateStepBlueprint(@Nullable ManagementContext mgmt, @Nullable WorkflowExecutionContext workflow) {
+        boolean hasBlueprint = getInput().containsKey(BLUEPRINT.getName());
+        boolean hasType = getInput().containsKey(TYPE.getName());
+        if (!hasBlueprint && !hasType) throw new IllegalArgumentException("A '"+BLUEPRINT.getName()+"' must be defined or a type supplied");
+        if (hasBlueprint && hasType) throw new IllegalArgumentException("Cannot provide both a '"+BLUEPRINT.getName()+"' and a '"+TYPE.getName()+"'");
+    }
+
+    default Object resolveBlueprint(WorkflowStepInstanceExecutionContext context) {
+        return resolveBlueprint(context, () -> "type: " + StringEscapes.JavaStringEscapes.wrapJavaString(context.getInput(TYPE)));
+    }
+    default Object resolveBlueprint(WorkflowStepInstanceExecutionContext context, Supplier<String> defaultValue) {
+        Object blueprint = getInput().get(BLUEPRINT.getName());
+        if (blueprint == null) {
+            return defaultValue.get();
+        }
+        logger().debug("Blueprint (pre-resolution) is: "+blueprint);
+        Object result = new SetVariableWorkflowStep.ConfigurableInterpolationEvaluation(context, null, blueprint,
+                context.getInputOrDefault(INTERPOLATION_MODE), context.getInputOrDefault(INTERPOLATION_ERRORS)).evaluate();
+        logger().debug("Blueprint (post-resolution: "+context.getInputOrDefault(INTERPOLATION_MODE)+"/"+context.getInputOrDefault(INTERPOLATION_ERRORS)+") is: "+result);
+        return result;
+    }
+
+    <T> void setInput(ConfigKey<T> key, T value);
+
+    // don't try to instantiate 'type' here
+    @JsonDeserialize(using = JsonPassThroughDeserializer.class)
+    default void setBlueprint(Object blueprint) {
+        setInput(BLUEPRINT, blueprint);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/external/SshWorkflowStep.java b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/external/SshWorkflowStep.java
index 9a0d7c5092..1e08d59e6e 100644
--- a/core/src/main/java/org/apache/brooklyn/core/workflow/steps/external/SshWorkflowStep.java
+++ b/core/src/main/java/org/apache/brooklyn/core/workflow/steps/external/SshWorkflowStep.java
@@ -25,6 +25,7 @@ import org.apache.brooklyn.core.config.MapConfigKey;
 import org.apache.brooklyn.core.location.Locations;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
+import org.apache.brooklyn.core.workflow.steps.variables.SetVariableWorkflowStep;
 import org.apache.brooklyn.location.ssh.SshMachineLocation;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.json.ShellEnvironmentSerializer;
@@ -33,6 +34,7 @@ import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.core.task.ssh.SshTasks;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
 import org.apache.brooklyn.util.text.Strings;
 import org.apache.brooklyn.util.time.Duration;
 
@@ -50,6 +52,9 @@ public class SshWorkflowStep extends WorkflowStepDefinition {
     public static final ConfigKey<DslPredicates.DslPredicate<Integer>> EXIT_CODE = ConfigKeys.newConfigKey(new TypeToken<DslPredicates.DslPredicate<Integer>>() {}, "exit_code");
     public static final ConfigKey<Integer> OUTPUT_MAX_SIZE = ConfigKeys.newIntegerConfigKey("output_max_size", "Maximum size for stdout and stderr, or -1 for no limit", 100000);
 
+    ConfigKey<SetVariableWorkflowStep.InterpolationMode> INTERPOLATION_MODE = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_MODE, SetVariableWorkflowStep.InterpolationMode.FULL);
+    ConfigKey<TemplateProcessor.InterpolationErrorMode> INTERPOLATION_ERRORS = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_ERRORS, TemplateProcessor.InterpolationErrorMode.IGNORE);
+
     @Override
     public void populateFromShorthand(String expression) {
         populateFromShorthandTemplate(SHORTHAND, expression);
@@ -57,7 +62,9 @@ public class SshWorkflowStep extends WorkflowStepDefinition {
 
     @Override
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
-        String command = context.getInput(COMMAND);
+        String command = new SetVariableWorkflowStep.ConfigurableInterpolationEvaluation<>(context, TypeToken.of(String.class), getInput().get(COMMAND.getName()),
+                context.getInputOrDefault(INTERPOLATION_MODE), context.getInputOrDefault(INTERPOLATION_ERRORS)).evaluate();
+
         if (Strings.isBlank(command)) throw new IllegalStateException("'command' is required");
 
         SshMachineLocation machine;
diff --git a/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinrmWorkflowStep.java b/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinrmWorkflowStep.java
index ec93d75a27..26e05d8737 100644
--- a/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinrmWorkflowStep.java
+++ b/software/winrm/src/main/java/org/apache/brooklyn/location/winrm/WinrmWorkflowStep.java
@@ -26,12 +26,14 @@ import org.apache.brooklyn.core.location.Machines;
 import org.apache.brooklyn.core.workflow.WorkflowStepDefinition;
 import org.apache.brooklyn.core.workflow.WorkflowStepInstanceExecutionContext;
 import org.apache.brooklyn.core.workflow.steps.external.SshWorkflowStep;
+import org.apache.brooklyn.core.workflow.steps.variables.SetVariableWorkflowStep;
 import org.apache.brooklyn.util.collections.MutableMap;
 import org.apache.brooklyn.util.core.json.ShellEnvironmentSerializer;
 import org.apache.brooklyn.util.core.predicates.DslPredicates;
 import org.apache.brooklyn.util.core.task.DynamicTasks;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskFactory;
 import org.apache.brooklyn.util.core.task.system.ProcessTaskWrapper;
+import org.apache.brooklyn.util.core.text.TemplateProcessor;
 import org.apache.brooklyn.util.text.Strings;
 
 import java.util.Map;
@@ -46,6 +48,9 @@ public class WinrmWorkflowStep extends WorkflowStepDefinition {
     public static final ConfigKey<DslPredicates.DslPredicate<Integer>> EXIT_CODE = ConfigKeys.newConfigKey(new TypeToken<DslPredicates.DslPredicate<Integer>>() {}, "exit_code");
     public static final ConfigKey<Integer> OUTPUT_MAX_SIZE = ConfigKeys.newIntegerConfigKey("output_max_size", "Maximum size for stdout and stderr, or -1 for no limit", 100000);
 
+    ConfigKey<SetVariableWorkflowStep.InterpolationMode> INTERPOLATION_MODE = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_MODE, SetVariableWorkflowStep.InterpolationMode.FULL);
+    ConfigKey<TemplateProcessor.InterpolationErrorMode> INTERPOLATION_ERRORS = ConfigKeys.newConfigKeyWithDefault(SetVariableWorkflowStep.INTERPOLATION_ERRORS, TemplateProcessor.InterpolationErrorMode.IGNORE);
+
     @Override
     public void populateFromShorthand(String expression) {
         populateFromShorthandTemplate(SHORTHAND, expression);
@@ -53,7 +58,8 @@ public class WinrmWorkflowStep extends WorkflowStepDefinition {
 
     @Override
     protected Object doTaskBody(WorkflowStepInstanceExecutionContext context) {
-        String command = context.getInput(COMMAND);
+        String command = new SetVariableWorkflowStep.ConfigurableInterpolationEvaluation<>(context, TypeToken.of(String.class), getInput().get(COMMAND.getName()),
+                context.getInputOrDefault(INTERPOLATION_MODE), context.getInputOrDefault(INTERPOLATION_ERRORS)).evaluate();
         if (Strings.isBlank(command)) throw new IllegalStateException("'command' is required");
 
         WinRmMachineLocation machine;