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; + } + } +}