diff --git a/tests/integration/java_integration_test.go b/tests/integration/java_integration_test.go
index ed7f9436e1d..06f5d1eb380 100644
--- a/tests/integration/java_integration_test.go
+++ b/tests/integration/java_integration_test.go
@@ -4,6 +4,7 @@ package integration
import (
"fmt"
+ "github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"os"
"path/filepath"
"testing"
@@ -19,10 +20,11 @@ func TestIntegrations(t *testing.T) {
t.Run("stack-reference", func(t *testing.T) {
dir := filepath.Join(getCwd(t), "stack-reference")
test := getJavaBase(t, integration.ProgramTestOptions{
- Dir: dir,
- Quick: true,
- DebugUpdates: false,
- DebugLogLevel: 0,
+ Dir: dir,
+ Quick: true,
+ DestroyOnCleanup: true,
+ DebugUpdates: false,
+ DebugLogLevel: 0,
Env: []string{
"PULUMI_EXCESSIVE_DEBUG_OUTPUT=false",
},
@@ -63,6 +65,18 @@ func TestIntegrations(t *testing.T) {
})
integration.ProgramTest(t, &test)
})
+ t.Run("stack-transformation", func(t *testing.T) {
+ dir := filepath.Join(getCwd(t), "stack-transformation")
+ test := getJavaBase(t, integration.ProgramTestOptions{
+ Dir: dir,
+ Quick: true,
+ DestroyOnCleanup: true,
+ DebugUpdates: false,
+ DebugLogLevel: 0,
+ ExtraRuntimeValidation: stackTransformationValidator(),
+ })
+ integration.ProgramTest(t, &test)
+ })
}
func getJavaBase(t *testing.T, testSpecificOptions integration.ProgramTestOptions) integration.ProgramTestOptions {
@@ -103,3 +117,69 @@ func getJavaBase(t *testing.T, testSpecificOptions integration.ProgramTestOption
t.Logf("Running test with opts.CloudURL: %s", opts.CloudURL)
return opts
}
+
+func stackTransformationValidator() func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+ resName := "random:index/randomString:RandomString"
+ return func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
+ foundRes1 := false
+ foundRes2Child := false
+ foundRes3 := false
+ foundRes4Child := false
+ foundRes5Child := false
+ for _, res := range stack.Deployment.Resources {
+ // "res1" has a transformation which adds additionalSecretOutputs
+ if res.URN.Name() == "res1" {
+ foundRes1 = true
+ assert.Equal(t, res.Type, tokens.Type(resName))
+ assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("length"))
+ }
+ // "res2" has a transformation which adds additionalSecretOutputs to it's
+ // "child" and sets minUpper to 2
+ if res.URN.Name() == "res2-child" {
+ foundRes2Child = true
+ assert.Equal(t, res.Type, tokens.Type(resName))
+ assert.Equal(t, res.Parent.Type(), tokens.Type("my:component:MyComponent"))
+ assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("length"))
+ assert.Contains(t, res.AdditionalSecretOutputs, resource.PropertyKey("special"))
+ minUpper := res.Inputs["minUpper"]
+ assert.NotNil(t, minUpper)
+ assert.Equal(t, 2.0, minUpper.(float64))
+ }
+ // "res3" is impacted by a global stack transformation which sets
+ // overrideSpecial to "stackvalue"
+ if res.URN.Name() == "res3" {
+ foundRes3 = true
+ assert.Equal(t, res.Type, tokens.Type(resName))
+ overrideSpecial := res.Inputs["overrideSpecial"]
+ assert.NotNil(t, overrideSpecial)
+ assert.Equal(t, "stackvalue", overrideSpecial.(string))
+ }
+ // "res4" is impacted by two component parent transformations which appends
+ // to overrideSpecial "value1" and then "value2" and also a global stack
+ // transformation which appends "stackvalue" to overrideSpecial. The end
+ // result should be "value1value2stackvalue".
+ if res.URN.Name() == "res4-child" {
+ foundRes4Child = true
+ assert.Equal(t, res.Type, tokens.Type(resName))
+ assert.Equal(t, res.Parent.Type(), tokens.Type("my:component:MyComponent"))
+ overrideSpecial := res.Inputs["overrideSpecial"]
+ assert.NotNil(t, overrideSpecial)
+ assert.Equal(t, "value1value2stackvalue", overrideSpecial.(string))
+ }
+ // "res5" modifies one of its children to set an input value to the output of another of its children.
+ if res.URN.Name() == "res5-child1" {
+ foundRes5Child = true
+ assert.Equal(t, res.Type, tokens.Type(resName))
+ assert.Equal(t, res.Parent.Type(), tokens.Type("my:component:MyComponent"))
+ length := res.Inputs["length"]
+ assert.NotNil(t, length)
+ assert.Equal(t, 6.0, length.(float64))
+ }
+ }
+ assert.True(t, foundRes1)
+ assert.True(t, foundRes2Child)
+ assert.True(t, foundRes3)
+ assert.True(t, foundRes4Child)
+ assert.True(t, foundRes5Child)
+ }
+}
diff --git a/tests/integration/stack-transformation/.gitignore b/tests/integration/stack-transformation/.gitignore
new file mode 100644
index 00000000000..de9e327568b
--- /dev/null
+++ b/tests/integration/stack-transformation/.gitignore
@@ -0,0 +1,2 @@
+.mvn/wrapper/maven-wrapper.jar
+/target/
diff --git a/tests/integration/stack-transformation/Pulumi.yaml b/tests/integration/stack-transformation/Pulumi.yaml
new file mode 100644
index 00000000000..f04349665c4
--- /dev/null
+++ b/tests/integration/stack-transformation/Pulumi.yaml
@@ -0,0 +1,3 @@
+name: stack-transformation
+runtime: java
+description: A minimal Java Pulumi program with Maven builds
diff --git a/tests/integration/stack-transformation/pom.xml b/tests/integration/stack-transformation/pom.xml
new file mode 100644
index 00000000000..249646ebc96
--- /dev/null
+++ b/tests/integration/stack-transformation/pom.xml
@@ -0,0 +1,109 @@
+
+
+ 4.0.0
+
+ com.pulumi.example
+ stacktransformation
+ 1.0-SNAPSHOT
+
+
+ UTF-8
+ 11
+ 11
+ 11
+ ${project.groupId}.${project.artifactId}.App
+
+ 0.0.1
+ 4.6.0
+
+
+
+
+ env-dependencies
+
+
+ env.PULUMI_JAVA_SDK_VERSION
+
+
+
+ ${env.PULUMI_JAVA_SDK_VERSION}
+ ${env.PULUMI_RANDOM_PROVIDER_SDK_VERSION}
+
+
+
+
+
+
+ com.pulumi
+ pulumi
+ ${pulumiSdkVersion}
+
+
+ com.pulumi
+ random
+ ${pulumiRandomVersion}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.2.2
+
+
+
+ true
+ ${mainClass}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.3.0
+
+
+
+ true
+ ${mainClass}
+
+
+
+ jar-with-dependencies
+
+
+
+
+ make-my-jar-with-dependencies
+ package
+
+ single
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.0.0
+
+ ${mainClass}
+ ${mainArgs}
+
+
+
+ org.apache.maven.plugins
+ maven-wrapper-plugin
+ 3.1.0
+
+ 3.8.5
+
+
+
+
+
diff --git a/tests/integration/stack-transformation/src/main/java/com/pulumi/example/stacktransformation/App.java b/tests/integration/stack-transformation/src/main/java/com/pulumi/example/stacktransformation/App.java
new file mode 100644
index 00000000000..3c1d94b2506
--- /dev/null
+++ b/tests/integration/stack-transformation/src/main/java/com/pulumi/example/stacktransformation/App.java
@@ -0,0 +1,208 @@
+package com.pulumi.example.stacktransformation;
+
+import com.pulumi.Context;
+import com.pulumi.Pulumi;
+import com.pulumi.core.Output;
+import com.pulumi.random.RandomString;
+import com.pulumi.random.RandomStringArgs;
+import com.pulumi.resources.ComponentResource;
+import com.pulumi.resources.ComponentResourceOptions;
+import com.pulumi.resources.CustomResourceOptions;
+import com.pulumi.resources.ResourceTransformation;
+import com.pulumi.resources.StackOptions;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public class App {
+
+ private final static String RandomStringType = "random:index/randomString:RandomString";
+
+ public static void main(String[] args) {
+ Pulumi.withOptions(StackOptions.builder()
+ .resourceTransformations(App::scenario3)
+ .build())
+ .run(App::stack);
+ }
+
+ private static void stack(Context ctx) {
+ // Scenario #1 - apply a transformation to a CustomResource
+ var res1 = new RandomString("res1",
+ RandomStringArgs.builder().length(5).build(),
+ CustomResourceOptions.builder()
+ .resourceTransformations(args -> Optional.of(new ResourceTransformation.Result(
+ args.args(),
+ CustomResourceOptions.merge(
+ (CustomResourceOptions) args.options(),
+ CustomResourceOptions.builder()
+ .additionalSecretOutputs("length")
+ .build()
+ )
+ )))
+ .build()
+ );
+
+ // Scenario #2 - apply a transformation to a Component to transform its children
+ var res2 = new MyComponent("res2",
+ ComponentResourceOptions.builder()
+ .resourceTransformations(args -> {
+ if (Objects.equals(args.resource().getResourceType(), RandomStringType)
+ && args.args() instanceof RandomStringArgs) {
+ var oldArgs = (RandomStringArgs) args.args();
+ var resultArgs = RandomStringArgs.builder()
+ .length(oldArgs.length())
+ .minUpper(2)
+ .build();
+ var resultOpts = CustomResourceOptions.merge(
+ (CustomResourceOptions) args.options(),
+ CustomResourceOptions.builder()
+ .additionalSecretOutputs("length")
+ .build()
+ );
+ return Optional.of(new ResourceTransformation.Result(resultArgs, resultOpts));
+ }
+ return Optional.empty();
+ })
+ .build()
+ );
+
+ // Scenario #3 - apply a transformation to the Stack to transform all resources in the stack.
+ var res3 = new RandomString("res3", RandomStringArgs.builder().length(5).build());
+
+ // Scenario #4 - transformations are applied in order of decreasing specificity
+ // 1. (not in this example) Child transformation
+ // 2. First parent transformation
+ // 3. Second parent transformation
+ // 4. Stack transformation
+ var res4 = new MyComponent("res4",
+ ComponentResourceOptions.builder()
+ .resourceTransformations(
+ args -> scenario4(args, "value1"),
+ args -> scenario4(args, "value2")
+ )
+ .build()
+ );
+
+ // Scenario #5 - cross-resource transformations that inject dependencies on one resource into another.
+ var res5 = new MyOtherComponent("res5",
+ ComponentResourceOptions.builder()
+ .resourceTransformations(transformChild1DependsOnChild2())
+ .build()
+ );
+ }
+
+ // Scenario #3 - apply a transformation to the Stack to transform all (future) resources in the stack
+ private static Optional scenario3(ResourceTransformation.Args args) {
+ if (Objects.equals(args.resource().getResourceType(), RandomStringType)
+ && args.args() instanceof RandomStringArgs) {
+ var oldArgs = (RandomStringArgs) args.args();
+ var resultArgs = RandomStringArgs.builder()
+ .length(oldArgs.length())
+ .minUpper(oldArgs.minUpper().orElse(null)) // TODO: see if we can make this API more consistent
+ .overrideSpecial(Output.format("%sstackvalue", oldArgs.overrideSpecial().orElse(Output.of(""))))
+ .build();
+ return Optional.of(new ResourceTransformation.Result(resultArgs, args.options()));
+ }
+ return Optional.empty();
+ }
+
+ private static Optional scenario4(ResourceTransformation.Args args, String v) {
+ if (Objects.equals(args.resource().getResourceType(), RandomStringType)
+ && args.args() instanceof RandomStringArgs) {
+ var oldArgs = (RandomStringArgs) args.args();
+ var resultArgs = RandomStringArgs.builder()
+ .length(oldArgs.length())
+ .overrideSpecial(Output.format("%s%s", oldArgs.overrideSpecial().orElse(Output.of("")), v))
+ .build();
+ return Optional.of(new ResourceTransformation.Result(resultArgs, args.options()));
+ }
+
+ return Optional.empty();
+ }
+
+ private static ResourceTransformation transformChild1DependsOnChild2() {
+ // Create a task completion source that wil be resolved once we find child2.
+ // This is needed because we do not know what order we will see the resource
+ // registrations of child1 and child2.
+ var child2ArgsSource = new CompletableFuture();
+
+ return (ResourceTransformation.Args args) -> {
+ // Return a transformation which will rewrite child1 to depend on the promise for child2, and
+ // will resolve that promise when it finds child2.
+ if (args.args() instanceof RandomStringArgs) {
+ var resourceArgs = (RandomStringArgs) args.args();
+ var resourceName = args.resource().getResourceName();
+ if (resourceName.endsWith("-child2")) {
+ // Resolve the child2 promise with the child2 resource.
+ child2ArgsSource.complete(resourceArgs);
+ return Optional.empty();
+ }
+ if (resourceName.endsWith("-child1")) {
+ var child2Length = resourceArgs.length()
+ .apply(in -> {
+ if (in != 5) {
+ // Not strictly necessary - but shows we can confirm invariants we expect to be true.
+ throw new RuntimeException("unexpected input value");
+ }
+ return Output.of(child2ArgsSource.thenApply(child2Args -> child2Args.length()));
+ })
+ .apply(out -> out);
+ var newArgs = RandomStringArgs.builder().length(child2Length).build();
+ return Optional.of(new ResourceTransformation.Result(newArgs, args.options()));
+ }
+ }
+ return Optional.empty();
+ };
+ }
+
+ static class MyComponent extends ComponentResource {
+ private final RandomString child;
+
+ public MyComponent(String name, @Nullable ComponentResourceOptions options) {
+ super("my:component:MyComponent", name, options);
+ this.child = new RandomString(String.format("%s-child", name),
+ RandomStringArgs.builder()
+ .length(5)
+ .build(),
+ CustomResourceOptions.builder()
+ .parent(this)
+ .additionalSecretOutputs("special")
+ .build()
+ );
+ }
+
+ public RandomString child() {
+ return this.child;
+ }
+ }
+
+ // Scenario #5 - cross-resource transformations that inject the output of one resource to the input
+ // of the other one.
+ static class MyOtherComponent extends ComponentResource {
+ private RandomString child1;
+ private RandomString child2;
+
+ public MyOtherComponent(String name, @Nullable ComponentResourceOptions options) {
+ super("my:component:MyComponent", name, options);
+ this.child1 = new RandomString(String.format("%s-child1", name),
+ RandomStringArgs.builder().length(5).build(),
+ CustomResourceOptions.builder().parent(this).build()
+ );
+
+ this.child2 = new RandomString(String.format("%s-child2", name),
+ RandomStringArgs.builder().length(6).build(),
+ CustomResourceOptions.builder().parent(this).build()
+ );
+ }
+
+ public RandomString child1() {
+ return child1;
+ }
+
+ public RandomString child2() {
+ return child2;
+ }
+ }
+}