From 4580ff54f7881c221673b09981b4a0ce6f348c38 Mon Sep 17 00:00:00 2001 From: Adesh Nalpet Adimurthy <390.adesh@gmail.com> Date: Sat, 2 May 2026 18:33:13 -0400 Subject: [PATCH 1/3] [Fix] Allow child resources to override parent's protect setting --- .../java/com/pulumi/resources/Resource.java | 7 +++++-- .../com/pulumi/resources/ResourceOptions.java | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java index e63a18c1bcc..ec66eb23035 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java @@ -204,7 +204,10 @@ protected Resource( // the 'childResources' is a Synchronized Collection, so this is safe operation parentResource.childResources.add(this); - options.protect = options.protect || parentResource.protect; // TODO: is this logic good? + // Inherit parent's protect only if child didn't set it explicitly. + if (options.protect == null) { + options.protect = parentResource.protect; + } thisProviders.putAll(options.parent.providers); } @@ -237,7 +240,7 @@ protected Resource( thisProviders.putAll(convertToProvidersMap(providerList)); } - this.protect = options.protect; + this.protect = options.protect != null && options.protect; this.provider = custom ? options.provider : null; this.version = options.version; this.providers = Map.copyOf(thisProviders); diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java index 19093fd98a9..35656395c4b 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java @@ -23,7 +23,8 @@ public abstract class ResourceOptions { protected Resource parent; @Nullable protected Output> dependsOn; - protected boolean protect; + @Nullable + protected Boolean protect; @Nullable protected List ignoreChanges; @Nullable @@ -56,7 +57,7 @@ protected ResourceOptions( @Nullable Output id, @Nullable Resource parent, @Nullable Output> dependsOn, - boolean protect, + @Nullable Boolean protect, @Nullable List ignoreChanges, @Nullable String version, @Nullable ProviderResource provider, @@ -152,6 +153,7 @@ public B dependsOn(@Nullable List dependsOn) { /** * When set to true, protect ensures this resource cannot be deleted. + * An explicit value on a child overrides the parent's value. */ public B protect(boolean protect) { options.protect = protect; @@ -159,6 +161,16 @@ public B protect(boolean protect) { return (B) this; } + /** + * @see #protect(boolean) + * Pass {@code null} to unset and inherit from the parent. + */ + public B protect(@Nullable Boolean protect) { + options.protect = protect; + //noinspection unchecked + return (B) this; + } + /** * Ignore changes to any of the specified properties. */ @@ -370,7 +382,7 @@ public Output> getDependsOn() { * @see Builder#protect(boolean) */ public boolean isProtect() { - return protect; + return protect != null && protect; } /** @@ -477,7 +489,7 @@ protected static T mergeSharedOptions(T options1, T options1.id = options2.id == null ? options1.id : options2.id; options1.parent = options2.parent == null ? options1.parent : options2.parent; - options1.protect = options1.protect || options2.protect; + options1.protect = options2.protect == null ? options1.protect : options2.protect; options1.urn = options2.urn == null ? options1.urn : options2.urn; options1.version = options2.version == null ? options1.version : options2.version; options1.provider = options2.provider == null ? options1.provider : options2.provider; From 3a95adda93916617535e061f9af3ee63f9d36a8f Mon Sep 17 00:00:00 2001 From: Adesh Nalpet Adimurthy <390.adesh@gmail.com> Date: Sun, 3 May 2026 17:17:12 -0400 Subject: [PATCH 2/3] Make protect, retainOnDelete and deleteBeforeReplace tri-state --- pkg/cmd/pulumi-language-java/language_test.go | 1 - .../src/main/java/generated_program/App.java | 4 ++- .../deployment/internal/DeploymentImpl.java | 14 +++++--- .../resources/ComponentResourceOptions.java | 4 +-- .../resources/CustomResourceOptions.java | 28 +++++++++++---- .../java/com/pulumi/resources/Resource.java | 4 --- .../com/pulumi/resources/ResourceOptions.java | 36 ++++++++++++++----- .../pulumi/resources/ResourceOptionsTest.java | 8 ++--- 8 files changed, 68 insertions(+), 31 deletions(-) diff --git a/pkg/cmd/pulumi-language-java/language_test.go b/pkg/cmd/pulumi-language-java/language_test.go index b37b2c8d9c6..5e1008f2c82 100644 --- a/pkg/cmd/pulumi-language-java/language_test.go +++ b/pkg/cmd/pulumi-language-java/language_test.go @@ -175,7 +175,6 @@ var expectedFailures = map[string]string{ "l2-component-component-resource-ref": "components with resources as inputs/outputs not supported", "l2-component-program-resource-ref": "components with resources as inputs/outputs not supported", "l2-resource-secret": "#1564 Fix l2-resource-secret", - "l2-resource-parent-inheritance": "Fix l2-resource-parent-inheritance", "l2-namespaced-provider": "components with resources as inputs/outputs not supported", "l2-component-property-deps": "compilation error", "l2-parameterized-resource": "compilation error", diff --git a/pkg/cmd/pulumi-language-java/testdata/projects/l2-resource-parent-inheritance/src/main/java/generated_program/App.java b/pkg/cmd/pulumi-language-java/testdata/projects/l2-resource-parent-inheritance/src/main/java/generated_program/App.java index c63a219a72d..9b2c0e10257 100644 --- a/pkg/cmd/pulumi-language-java/testdata/projects/l2-resource-parent-inheritance/src/main/java/generated_program/App.java +++ b/pkg/cmd/pulumi-language-java/testdata/projects/l2-resource-parent-inheritance/src/main/java/generated_program/App.java @@ -7,8 +7,8 @@ import com.pulumi.simple.Resource; import com.pulumi.simple.ResourceArgs; import com.pulumi.resources.CustomResourceOptions; -import java.util.List; import java.util.ArrayList; +import java.util.Arrays; import java.util.Map; import java.io.File; import java.nio.file.Files; @@ -42,6 +42,7 @@ public static void stack(Context ctx) { .value(true) .build(), CustomResourceOptions.builder() .protect(true) + .retainOnDelete(true) .build()); var child2 = new Resource("child2", ResourceArgs.builder() @@ -54,6 +55,7 @@ public static void stack(Context ctx) { .value(true) .build(), CustomResourceOptions.builder() .protect(false) + .retainOnDelete(false) .parent(parent2) .build()); diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/deployment/internal/DeploymentImpl.java b/sdk/java/pulumi/src/main/java/com/pulumi/deployment/internal/DeploymentImpl.java index b999ed11956..b9b3b49fe2a 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/deployment/internal/DeploymentImpl.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/deployment/internal/DeploymentImpl.java @@ -1685,14 +1685,11 @@ private RegisterResourceRequest createRegisterResourceRequest( .setType(type) .setName(name) .setCustom(custom) - .setProtect(options.isProtect()) .setVersion(options.getVersion().orElse("")) .setPluginDownloadURL(options.getPluginDownloadURL().orElse("")) .setImportId(customOpts ? ((CustomResourceOptions) options).getImportId().orElse("") : "") .setAcceptSecrets(true) .setAcceptResources(!this.disableResourceReferences) - .setDeleteBeforeReplace(customOpts && ((CustomResourceOptions) options).getDeleteBeforeReplace()) - .setDeleteBeforeReplaceDefined(true) .setCustomTimeouts( RegisterResourceRequest.CustomTimeouts.newBuilder() .setCreate(customTimeoutToGolangString.apply(CustomTimeouts::getCreate)) @@ -1701,11 +1698,18 @@ private RegisterResourceRequest createRegisterResourceRequest( .build() ) .setRemote(remote) - .setRetainOnDelete(options.isRetainOnDelete()) .setPackageRef(packageRef == null ? "" : packageRef); + options.getProtect().ifPresent(request::setProtect); + options.getRetainOnDelete().ifPresent(request::setRetainOnDelete); + if (customOpts) { - request.addAllAdditionalSecretOutputs(((CustomResourceOptions) options).getAdditionalSecretOutputs()); + var customResourceOpts = (CustomResourceOptions) options; + customResourceOpts.getDeleteBeforeReplaceOptional().ifPresent(value -> { + request.setDeleteBeforeReplace(value); + request.setDeleteBeforeReplaceDefined(true); + }); + request.addAllAdditionalSecretOutputs(customResourceOpts.getAdditionalSecretOutputs()); request.addAllReplaceOnChanges(options.getReplaceOnChanges()); } diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ComponentResourceOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ComponentResourceOptions.java index e29bc7bdde0..5850336a843 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ComponentResourceOptions.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ComponentResourceOptions.java @@ -30,7 +30,7 @@ public ComponentResourceOptions( @Nullable Output id, @Nullable Resource parent, @Nullable Output> dependsOn, - boolean protect, + @Nullable Boolean protect, @Nullable List ignoreChanges, @Nullable String version, @Nullable CustomTimeouts customTimeouts, @@ -38,7 +38,7 @@ public ComponentResourceOptions( @Nullable List> aliases, @Nullable String urn, @Nullable List replaceOnChanges, - boolean retainOnDelete, + @Nullable Boolean retainOnDelete, @Nullable String pluginDownloadURL, @Nullable List hideDiffs, @Nullable List providers, diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/CustomResourceOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/CustomResourceOptions.java index f37aa212d3b..27439dccf57 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/CustomResourceOptions.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/CustomResourceOptions.java @@ -19,7 +19,8 @@ public final class CustomResourceOptions extends ResourceOptions implements Copy public static final CustomResourceOptions Empty = CustomResourceOptions.builder().build(); - private boolean deleteBeforeReplace; + @Nullable + private Boolean deleteBeforeReplace; @Nullable private List additionalSecretOutputs; @Nullable @@ -31,7 +32,7 @@ private CustomResourceOptions( @Nullable Output id, @Nullable Resource parent, @Nullable Output> dependsOn, - boolean protect, + @Nullable Boolean protect, @Nullable List ignoreChanges, @Nullable String version, @Nullable ProviderResource provider, @@ -39,11 +40,11 @@ private CustomResourceOptions( @Nullable List resourceTransformations, @Nullable List> aliases, @Nullable String urn, - boolean deleteBeforeReplace, + @Nullable Boolean deleteBeforeReplace, @Nullable List additionalSecretOutputs, @Nullable String importId, @Nullable List replaceOnChanges, - boolean retainOnDelete, + @Nullable Boolean retainOnDelete, @Nullable String pluginDownloadURL, @Nullable List hideDiffs, @Nullable List replaceWith, @@ -75,6 +76,14 @@ public Builder deleteBeforeReplace(boolean deleteBeforeReplace) { return this; } + /** + * @see #deleteBeforeReplace(boolean) + */ + public Builder deleteBeforeReplace(@Nullable Boolean deleteBeforeReplace) { + options.deleteBeforeReplace = deleteBeforeReplace; + return this; + } + /** * The names of outputs for this resource that should be treated as secrets. This augments * the list that the resource provider and pulumi engine already determine based on inputs @@ -114,7 +123,14 @@ public CustomResourceOptions build() { * @see Builder#deleteBeforeReplace(boolean) */ public boolean getDeleteBeforeReplace() { - return this.deleteBeforeReplace; + return this.deleteBeforeReplace != null && this.deleteBeforeReplace; + } + + /** + * @see Builder#deleteBeforeReplace(Boolean) + */ + public Optional getDeleteBeforeReplaceOptional() { + return Optional.ofNullable(this.deleteBeforeReplace); } /** @@ -188,7 +204,7 @@ public static CustomResourceOptions merge( //noinspection ConstantConditions options1 = mergeSharedOptions(options1, options2, id); - options1.deleteBeforeReplace = options1.deleteBeforeReplace || options2.deleteBeforeReplace; + options1.deleteBeforeReplace = options2.deleteBeforeReplace == null ? options1.deleteBeforeReplace : options2.deleteBeforeReplace; options1.importId = options2.importId == null ? options1.importId : options2.importId; options1.additionalSecretOutputs = mergeNullableList(options1.additionalSecretOutputs, options2.additionalSecretOutputs); diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java index ec66eb23035..08fc514f2f4 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/Resource.java @@ -204,10 +204,6 @@ protected Resource( // the 'childResources' is a Synchronized Collection, so this is safe operation parentResource.childResources.add(this); - // Inherit parent's protect only if child didn't set it explicitly. - if (options.protect == null) { - options.protect = parentResource.protect; - } thisProviders.putAll(options.parent.providers); } diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java index 35656395c4b..e53f4ed977a 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/resources/ResourceOptions.java @@ -41,7 +41,8 @@ public abstract class ResourceOptions { protected String urn; @Nullable protected List replaceOnChanges; - protected boolean retainOnDelete; + @Nullable + protected Boolean retainOnDelete; @Nullable protected String pluginDownloadURL; @Nullable @@ -66,7 +67,7 @@ protected ResourceOptions( @Nullable List> aliases, @Nullable String urn, @Nullable List replaceOnChanges, - boolean retainOnDelete, + @Nullable Boolean retainOnDelete, @Nullable String pluginDownloadURL, @Nullable List hideDiffs, @Nullable List replaceWith, @@ -153,7 +154,6 @@ public B dependsOn(@Nullable List dependsOn) { /** * When set to true, protect ensures this resource cannot be deleted. - * An explicit value on a child overrides the parent's value. */ public B protect(boolean protect) { options.protect = protect; @@ -163,7 +163,6 @@ public B protect(boolean protect) { /** * @see #protect(boolean) - * Pass {@code null} to unset and inherit from the parent. */ public B protect(@Nullable Boolean protect) { options.protect = protect; @@ -297,8 +296,17 @@ public B replaceOnChanges(@Nullable List replaceOnChanges) { * If set to True, the providers Delete method will not be called for this resource. */ public B retainOnDelete(boolean retainOnDelete) { + options.retainOnDelete = retainOnDelete; //noinspection unchecked + return (B) this; + } + + /** + * @see #retainOnDelete(boolean) + */ + public B retainOnDelete(@Nullable Boolean retainOnDelete) { options.retainOnDelete = retainOnDelete; + //noinspection unchecked return (B) this; } @@ -385,6 +393,13 @@ public boolean isProtect() { return protect != null && protect; } + /** + * @see Builder#protect(Boolean) + */ + public Optional getProtect() { + return Optional.ofNullable(protect); + } + /** * @see Builder#ignoreChanges(String...) */ @@ -445,7 +460,14 @@ public List getReplaceOnChanges() { * @see Builder#retainOnDelete(boolean) */ public boolean isRetainOnDelete() { - return this.retainOnDelete; + return this.retainOnDelete != null && this.retainOnDelete; + } + + /** + * @see Builder#retainOnDelete(Boolean) + */ + public Optional getRetainOnDelete() { + return Optional.ofNullable(this.retainOnDelete); } /** @@ -499,9 +521,7 @@ protected static T mergeSharedOptions(T options1, T options1.resourceTransformations = mergeNullableList(options1.resourceTransformations, options2.resourceTransformations); options1.aliases = mergeNullableList(options1.aliases, options2.aliases); options1.replaceOnChanges = mergeNullableList(options1.replaceOnChanges, options2.replaceOnChanges); - options1.retainOnDelete = options1.retainOnDelete || options2.retainOnDelete; - options1.pluginDownloadURL = options2.pluginDownloadURL == null ? options1.pluginDownloadURL : options2.pluginDownloadURL; - options1.retainOnDelete = options1.retainOnDelete || options2.retainOnDelete; + options1.retainOnDelete = options2.retainOnDelete == null ? options1.retainOnDelete : options2.retainOnDelete; options1.pluginDownloadURL = options2.pluginDownloadURL == null ? options1.pluginDownloadURL : options2.pluginDownloadURL; options1.hideDiffs = mergeNullableList(options1.hideDiffs, options2.hideDiffs); options1.dependsOn = Output.concatList(options1.dependsOn, options2.dependsOn); diff --git a/sdk/java/pulumi/src/test/java/com/pulumi/resources/ResourceOptionsTest.java b/sdk/java/pulumi/src/test/java/com/pulumi/resources/ResourceOptionsTest.java index a02c8734f9d..f0a499974e5 100644 --- a/sdk/java/pulumi/src/test/java/com/pulumi/resources/ResourceOptionsTest.java +++ b/sdk/java/pulumi/src/test/java/com/pulumi/resources/ResourceOptionsTest.java @@ -20,8 +20,8 @@ class ResourceOptionsTest { private static Stream testMergeSharedOptions() { return Stream.of( arguments(new TestResourceOptions(), new TestResourceOptions(), new TestResourceOptions( - null, null, Output.of(List.of()), false, null, - null, null, null, null, null, null, null, false, null, null, null, null + null, null, Output.of(List.of()), null, null, + null, null, null, null, null, null, null, null, null, null, null, null )), arguments( new TestResourceOptions( null, @@ -115,7 +115,7 @@ public TestResourceOptions( @Nullable Output id, @Nullable Resource parent, @Nullable Output> dependsOn, - boolean protect, + @Nullable Boolean protect, @Nullable List ignoreChanges, @Nullable String version, @Nullable ProviderResource provider, @@ -124,7 +124,7 @@ public TestResourceOptions( @Nullable List> aliases, @Nullable String urn, @Nullable List replaceOnChanges, - boolean retainOnDelete, + @Nullable Boolean retainOnDelete, @Nullable String pluginDownloadURL, @Nullable List hideDiffs, @Nullable List replaceWith, From 23e8bfc70803deab9c7c80aa84e19b1bfa2d5178 Mon Sep 17 00:00:00 2001 From: Adesh Nalpet Adimurthy <390.adesh@gmail.com> Date: Mon, 11 May 2026 14:58:03 -0400 Subject: [PATCH 3/3] Add changelog entry --- .changes/unreleased/bug-fixes-2156.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/bug-fixes-2156.yaml diff --git a/.changes/unreleased/bug-fixes-2156.yaml b/.changes/unreleased/bug-fixes-2156.yaml new file mode 100644 index 00000000000..1a9144cfc66 --- /dev/null +++ b/.changes/unreleased/bug-fixes-2156.yaml @@ -0,0 +1,6 @@ +component: sdk +kind: bug-fixes +body: Fix child resources unable to override protect, retainOnDelete, and deleteBeforeReplace from parent. +time: 2026-05-11T14:57:14.03221-04:00 +custom: + PR: "2156"