Skip to content

Commit

Permalink
Wait for resources in the input property dependencies (#1583)
Browse files Browse the repository at this point in the history
Besides the explicitly provided resources from the dependsOn option, we
need to also take into account the resource dependencies from the
invoke’s arguments.


pulumi/pulumi#17747
  • Loading branch information
julienp authored Jan 27, 2025
1 parent 44a0778 commit 6d7c8fd
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Include explicit dependencies of invokes in their resulting output
- Bump core SDK versions in generated code
- Emit plugin download URL in default resource options of the generated SDKs
- Wait for resources in the input property dependencies


### Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,37 +541,45 @@ public <T> Output<T> invoke(String token, TypeShape<T> targetType, InvokeArgs ar
final CompletableFuture<String> packageRefFuture = packageRef == null
? CompletableFuture.completedFuture(null)
: packageRef;

// The expanded set of dependencies, including children of components.
var transitiveDeps = this.prepare.getAllTransitivelyReferencedResources(ImmutableSet.copyOf(options.getDependsOn()));
// If we depend on any CustomResources, we need to ensure that their
// ID is known before proceeding. If it is not known, we will return
// an unknown result.
var hasUnknownIDs = CompletableFutures.allOf(transitiveDeps
.filter(r -> r instanceof CustomResource)
.map(r -> Internal.of(((CustomResource) r).id()).isKnown())
.collect(ImmutableSet.toImmutableSet())
).thenApply(s -> s.stream().anyMatch(known -> !known));

// Wait for all values from args to be available, and then perform the RPC.
return new OutputInternal<>(CompletableFuture.allOf(
this.featureSupport.monitorSupportsResourceReferences(),
hasUnknownIDs)
.thenCompose(ignored -> {
boolean keepResources = this.featureSupport.monitorSupportsResourceReferences().join();
boolean hasUnknown = hasUnknownIDs.join();

return this.serializeInvokeArgs(token, args, keepResources)
.thenCompose(serializedArgs -> {
if (serializedArgs.containsUnknowns || hasUnknown) {
return CompletableFuture.completedFuture(OutputData.unknown());
} else {
return packageRefFuture
.thenCompose(packageRefString -> this.invokeRawAsync(token, serializedArgs, options, packageRefString))
.thenApply(result -> parseInvokeResponse(token, targetType, result).withDependencies(options.getDependsOn()));
}
});
}));

return new OutputInternal<>(this.featureSupport.monitorSupportsResourceReferences().thenCompose(
keepResources -> this.serializeInvokeArgs(token, args, keepResources)
).thenCompose(serializedArgs -> {
if (serializedArgs.containsUnknowns) {
return CompletableFuture.completedFuture(OutputData.unknown());
} else {
// If we depend on any CustomResources, we need to ensure that their
// ID is known before proceeding. If it is not known, we will return
// an unknown result.
var deps = new HashSet<Resource>(options.getDependsOn());
// Add the dependencies from the inputs to the set of resources to wait for.
var propertyDeps = serializedArgs.propertyToDependentResources.values().stream()
.flatMap(Collection::stream)
.collect(ImmutableSet.toImmutableSet());
var depsSet = new ImmutableSet.Builder<Resource>()
.addAll(deps)
.addAll(propertyDeps)
.build();
// The expanded set of dependencies, including children of components.
var transitiveDeps = this.prepare.getAllTransitivelyReferencedResources(depsSet);
// Ensure that all resource IDs are known before proceeding.
var hasUnknownIDs = CompletableFutures.allOf(transitiveDeps
.filter(r -> r instanceof CustomResource)
.map(r -> Internal.of(((CustomResource) r).id()).isKnown())
.collect(ImmutableSet.toImmutableSet())
).thenApply(s -> s.stream().anyMatch(known -> !known));

return hasUnknownIDs.thenCompose(hasUnknown -> {
if (hasUnknown) {
return CompletableFuture.completedFuture(OutputData.unknown());
} else {
return packageRefFuture
.thenCompose(packageRefString -> this.invokeRawAsync(token, serializedArgs, options, packageRefString))
.thenApply(result -> parseInvokeResponse(token, targetType, result).withDependencies(options.getDependsOn()));
}
});
}
}));
}

private <T> OutputData<T> parseInvokeResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import com.pulumi.core.TypeShape;
import com.pulumi.core.annotations.CustomType;
import com.pulumi.core.annotations.CustomType.Setter;
import com.pulumi.core.internal.OutputData;
import com.pulumi.core.internal.OutputInternal;
import com.pulumi.core.annotations.Import;
import com.pulumi.core.annotations.ResourceType;
import com.pulumi.deployment.internal.Runner;
import com.pulumi.resources.Resource;
import com.pulumi.resources.InvokeArgs;
import com.pulumi.resources.CustomResource;
import com.pulumi.resources.CustomResourceOptions;
import com.pulumi.resources.DependencyResource;
import com.pulumi.resources.ResourceArgs;
import com.pulumi.test.Mocks;
import com.pulumi.test.TestOptions;
Expand Down Expand Up @@ -111,8 +114,10 @@ public CompletableFuture<Map<String, Object>> callAsync(CallArgs args) {
.build();

var result = test.runTest(ctx -> {
var res = new MyCustomResource("r1", null, CustomResourceOptions.builder().build());
var deps = new ArrayList<Resource>();
var remote = new DependencyResource("some:urn");
deps.add(remote);
var res = new MyCustomResource("r1", null, CustomResourceOptions.builder().build());
deps.add(res);

var opts = new InvokeOutputOptions(null, null, null, deps);
Expand All @@ -126,6 +131,38 @@ public CompletableFuture<Map<String, Object>> callAsync(CallArgs args) {
assertThat(result.exitCode()).isEqualTo(Runner.ProcessExitedSuccessfully);
}

@Test
void testInvokesInputDependencies() {
var test = PulumiTestInternal.builder()
.options(TestOptions.builder().preview(true).build())
.mocks(new Mocks() {
@Override
public CompletableFuture<ResourceResult> newResourceAsync(ResourceArgs args) {
return CompletableFuture
.supplyAsync(() -> ResourceResult.of(Optional.empty(), ImmutableMap.of()));
}

@Override
public CompletableFuture<Map<String, Object>> callAsync(CallArgs args) {
return CompletableFuture.completedFuture(ImmutableMap.of());
}
})
.build();

var result = test.runTest(ctx -> {
var res = new MyCustomResource("r1", null, CustomResourceOptions.builder().build());
var text = new OutputInternal<>(OutputData.of("abc").withDependency(res));
var arg = new CustomArgs(text);
CustomInvokes.doStuff(arg, InvokeOutputOptions.Empty).applyValue(r -> {
assertFalse(true, "invoke should not be called!");
return r;
});
});
assertThat(result.exceptions()).hasSize(0);

assertThat(result.exitCode()).isEqualTo(Runner.ProcessExitedSuccessfully);
}

public static final class MyArgs extends ResourceArgs {
}

Expand Down Expand Up @@ -160,9 +197,9 @@ static class CustomArgs extends InvokeArgs {

@Import(name = "text")
@Nullable
public final String text;
public final Output<String> text;

CustomArgs(@Nullable String text) {
CustomArgs(@Nullable Output<String> text) {
this.text = text;
}
}
Expand Down

0 comments on commit 6d7c8fd

Please sign in to comment.