Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CustomResource for Java SDK #3020

Merged
merged 15 commits into from
May 28, 2024
Merged

CustomResource for Java SDK #3020

merged 15 commits into from
May 28, 2024

Conversation

EronWright
Copy link
Contributor

@EronWright EronWright commented May 23, 2024

Proposed changes

Implements CustomResource for Java SDK as an overlay, for parity with other SDKs. An overlay is necessary for this reason.

Features

Supports two usage modes. Note that each SDK is slightly different in which mode(s) it supports.

  • untyped, where you use CustomResource directly and set arbitrary fields on the object.
  • typed, where you subclass CustomResource to create a strongly-typed wrapper representing a CRD.

A "Patch" variant is also provided.

Provides a "getter" method in both untyped and typed mode. Note that the patch variant doesn't have a getter in most SDKs.

Summary of Changes

  • new example: examples/java/customresource
  • new resource: apiextensions.CustomResource
  • new resource: apiextensions.CustomResourcePatch
  • new dependency: net.bytebuddy:byte-buddy:1.14.15
  • new dependency:com.google.guava:guava:32.1.2-jre (used by core already)

TODOs:

Related issues (optional)

Closes #2787

API

  • CustomResource - to be used directly or subclassed to expose typed output properties
  • CustomResourceArgs - the final class to be used directly in the untyped use-case
  • CustomResourceArgsBase - the abstract base class for custom resource args, to expose typed input properties
  • CustomResourceArgsBase.Builder<T,B> - the base class for your custom args builder
  • CustomResourcePatch - to be used directly or subclassed to expose typed output properties
  • CustomResourcePatchArgs - the final class to be used directly in the untyped use-case
  • CustomResourcePatchArgsBase - the abstract base class for custom resource args, to expose typed input properties
  • CustomResourcePatchArgsBase.Builder<T,B> - the base class for your custom args builder

Implementation Details

Working with Untyped Inputs

The core Pulumi Java SDK has no support for dynamic inputs; it relies exclusively on reflection of the supplied InputArgs subclass (see: InputArgs::toMapAsync). To support the "untyped" mode, this implementation codegens a class at runtime using bytebuddy.

Builder Inheritance

The Java SDK leans on the fluent builder pattern, and there are special challenges in designing a builder that is amenable to inheritance. This implementation uses generics as seen here.

Example

Here's an example program to deploy two cert-manager issuers.

  • issuer1 is untyped, and calls otherFields(...) on the builder to set the spec.
  • issuer2 is typed, calls spec(...) on a subclassed builder to set the spec, and uses the typed spec output. Note that the apiVersion and kind are set automatically.

The code seen in Inputs and Outputs section would, in practice, be generated by pulumi-java-gen based on a schema file.

The untyped and typed getter variants are also demonstrated.

package myproject;

import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.annotation.Nullable;

import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.core.annotations.CustomType;
import com.pulumi.core.annotations.Export;
import com.pulumi.core.annotations.Import;
import com.pulumi.kubernetes.apiextensions.CustomResource;
import com.pulumi.kubernetes.apiextensions.CustomResourceArgs;
import com.pulumi.kubernetes.apiextensions.CustomResourceArgsBase;
import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var issuer1 = new CustomResource("issuer1", CustomResourceArgs.builder()
                    .apiVersion("cert-manager.io/v1")
                    .kind("Issuer")
                    .metadata(ObjectMetaArgs.builder().build())
                    .otherFields(Map.of("spec", Map.of(
                            "selfSigned", Map.of())))
                    .build());
            ctx.export("issuer1_name", issuer1.metadata().applyValue(s -> s.name()));

            var get1 = CustomResource.get("get1", "cert-manager.io/v1", "Issuer", issuer1.id(), null);
                
            var issuer2 = new Issuer("issuer2", IssuerArgs.builder()
                    .metadata(ObjectMetaArgs.builder().build())
                    .spec(Inputs.IssuerSpecArgs.builder()
                            .selfSigned(Inputs.SelfSignedArgs.builder().build())
                            .build())
                    .build());

            ctx.export("issuer2_name", issuer2.metadata().applyValue(s -> s.name()));
            ctx.export("issuer2_selfsigned", issuer2.spec().applyValue(s -> s.selfSigned().isPresent()));

            var get2 = Issuer.get("get2", issuer2.id(), null);
            ctx.export("get2_selfsigned", get2.spec().applyValue(s -> s.selfSigned().isPresent()));
        });
    }
}

class Issuer extends CustomResource {
    /**
     * The spec of the Issuer.
     */
    @Export(name = "spec", refs = { Outputs.IssuerSpec.class }, tree = "[0]")
    private Output<Outputs.IssuerSpec> spec;

    public Output<Outputs.IssuerSpec> spec() {
        return this.spec;
    }

    public Issuer(String name, @Nullable IssuerArgs args) {
        super(name, makeArgs(args));
    }

    public Issuer(String name, @Nullable IssuerArgs args,
            @Nullable com.pulumi.resources.CustomResourceOptions options) {
        super(name, makeArgs(args), options);
    }

    protected Issuer(String name, Output<String> id,
            @Nullable com.pulumi.resources.CustomResourceOptions options) {
        super(name, "cert-manager.io/v1", "Issuer", id, options);
    }

    private static IssuerArgs makeArgs(@Nullable IssuerArgs args) {
        var builder = args == null ? IssuerArgs.builder() : IssuerArgs.builder(args);
        return builder
            .apiVersion("cert-manager.io/v1")
            .kind("Issuer")
            .build();
    }

    public static Issuer get(String name, Output<String> id, @Nullable com.pulumi.resources.CustomResourceOptions options) {
        return new Issuer(name, id, options);
    }
}

class IssuerArgs extends CustomResourceArgsBase {
    /**
     * The spec of the Issuer.
     */
    @Import(name = "spec", required = true)
    @Nullable 
    private Output<Inputs.IssuerSpecArgs> spec;

    public static Builder builder() {
        return new Builder();
    }

    public static Builder builder(IssuerArgs defaults) {
        return new Builder(defaults);
    }

    static class Builder extends CustomResourceArgsBase.Builder<IssuerArgs, Builder> {
        public Builder() {
            super(new IssuerArgs());
        }

        public Builder(IssuerArgs defaults) {
            super(new IssuerArgs(), defaults);
        }

        public Builder spec(@Nullable Output<Inputs.IssuerSpecArgs> spec) {
            $.spec = spec;
            return this;
        }

        public Builder spec(Inputs.IssuerSpecArgs spec) {
            return spec(Output.of(spec));
        }

        @Override
        protected void copy(IssuerArgs args) {
            super.copy(args);
            $.spec = args.spec;
        }
    }
}

class Inputs {
    public static final class IssuerSpecArgs extends com.pulumi.resources.ResourceArgs {

        public static final IssuerSpecArgs Empty = new IssuerSpecArgs();

        @Import(name = "selfSigned", required = true)
        private @Nullable Output<SelfSignedArgs> selfSigned;

        public Optional<Output<SelfSignedArgs>> selfSigned() {
            return Optional.ofNullable(this.selfSigned);
        }

        private IssuerSpecArgs() {
        }

        private IssuerSpecArgs(IssuerSpecArgs $) {
            this.selfSigned = $.selfSigned;
        }

        public static Builder builder() {
            return new Builder();
        }

        public static Builder builder(IssuerSpecArgs defaults) {
            return new Builder(defaults);
        }

        public static final class Builder {
            private IssuerSpecArgs $;

            public Builder() {
                $ = new IssuerSpecArgs();
            }

            public Builder(IssuerSpecArgs defaults) {
                $ = new IssuerSpecArgs(Objects.requireNonNull(defaults));
            }

            public Builder selfSigned(@Nullable Output<SelfSignedArgs> selfSigned) {
                $.selfSigned = selfSigned;
                return this;
            }

            public Builder selfSigned(SelfSignedArgs selfSigned) {
                return selfSigned(Output.of(selfSigned));
            }

            public IssuerSpecArgs build() {
                return $;
            }
        }
    }

    public static final class SelfSignedArgs extends com.pulumi.resources.ResourceArgs {

        public static final SelfSignedArgs Empty = new SelfSignedArgs();

        private SelfSignedArgs() {
        }

        private SelfSignedArgs(SelfSignedArgs $) {
        }

        public static Builder builder() {
            return new Builder();
        }

        public static Builder builder(SelfSignedArgs defaults) {
            return new Builder(defaults);
        }

        public static final class Builder {
            private SelfSignedArgs $;

            public Builder() {
                $ = new SelfSignedArgs();
            }

            public Builder(SelfSignedArgs defaults) {
                $ = new SelfSignedArgs(Objects.requireNonNull(defaults));
            }

            public SelfSignedArgs build() {
                return $;
            }
        }
    }
}

class Outputs {
    @CustomType
    static final class IssuerSpec {

        private @Nullable SelfSigned selfSigned;

        private IssuerSpec() {
        }

        public Optional<SelfSigned> selfSigned() {
            return Optional.ofNullable(this.selfSigned);
        }

        public static Builder builder() {
            return new Builder();
        }

        public static Builder builder(IssuerSpec defaults) {
            return new Builder(defaults);
        }

        @CustomType.Builder
        public static final class Builder {
            private @Nullable SelfSigned selfSigned;

            public Builder() {
            }

            public Builder(IssuerSpec defaults) {
                Objects.requireNonNull(defaults);
                this.selfSigned = defaults.selfSigned;
            }

            @CustomType.Setter
            public Builder selfSigned(@Nullable SelfSigned selfSigned) {
                this.selfSigned = selfSigned;
                return this;
            }

            public IssuerSpec build() {
                final var _resultValue = new IssuerSpec();
                _resultValue.selfSigned = selfSigned;
                return _resultValue;
            }
        }
    }

    @CustomType
    static final class SelfSigned {

        private SelfSigned() {
        }

        public static Builder builder() {
            return new Builder();
        }

        public static Builder builder(SelfSigned defaults) {
            return new Builder(defaults);
        }

        @CustomType.Builder
        public static final class Builder {
            public Builder() {
            }

            public Builder(SelfSigned defaults) {
                Objects.requireNonNull(defaults);
            }

            public SelfSigned build() {
                final var _resultValue = new SelfSigned();
                return _resultValue;
            }
        }
    }
}

Gives the expected output:

❯ pulumi preview --diff
Previewing update (dev)

+ pulumi:pulumi:Stack: (create)
    [urn=urn:pulumi:dev::issue-2787-javaa::pulumi:pulumi:Stack::issue-2787-javaa-dev]
    + kubernetes:cert-manager.io/v1:Issuer: (create)
        [urn=urn:pulumi:dev::issue-2787-javaa::kubernetes:cert-manager.io/v1:Issuer::issuer1]
        [provider=urn:pulumi:dev::issue-2787-javaa::pulumi:providers:kubernetes::default_0_0_16_SNAPSHOT::04da6b54-80e4-46f7-96ec-b56ff0331ba9]
        apiVersion: "cert-manager.io/v1"
        kind      : "Issuer"
        metadata  : {
            annotations: {
                pulumi.com/autonamed: "true"
            }
            name       : "issuer1-dcda28b8"
            namespace  : "default"
        }
        spec      : {
            selfSigned: {}
        }
    + kubernetes:cert-manager.io/v1:Issuer: (create)
        [urn=urn:pulumi:dev::issue-2787-javaa::kubernetes:cert-manager.io/v1:Issuer::issuer2]
        [provider=urn:pulumi:dev::issue-2787-javaa::pulumi:providers:kubernetes::default_0_0_16_SNAPSHOT::04da6b54-80e4-46f7-96ec-b56ff0331ba9]
        apiVersion: "cert-manager.io/v1"
        kind      : "Issuer"
        metadata  : {
            annotations: {
                pulumi.com/autonamed: "true"
            }
            name       : "issuer2-a0d8c527"
            namespace  : "default"
        }
        spec      : {
            selfSigned: {}
        }
    --outputs:--        
    issuer1_name      : "issuer1-dcda28b8"
    issuer2_name      : "issuer2-a0d8c527"
    issuer2_selfsigned: true

Copy link

Does the PR have any schema changes?

Looking good! No breaking changes found.
No new resources/functions.

Copy link

codecov bot commented May 23, 2024

Codecov Report

Attention: Patch coverage is 0% with 4 lines in your changes are missing coverage. Please review.

Project coverage is 36.43%. Comparing base (491c036) to head (91e82b2).

Files Patch % Lines
provider/pkg/gen/schema.go 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3020      +/-   ##
==========================================
- Coverage   36.47%   36.43%   -0.04%     
==========================================
  Files          70       70              
  Lines        9163     9167       +4     
==========================================
- Hits         3342     3340       -2     
- Misses       5492     5497       +5     
- Partials      329      330       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@mjeffryes mjeffryes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion on this, but FWIW, I noticed that the dotnet provider doesn't have an "untyped" option, you always have to subclass the CustomResourceArgs class, so there's precedent here for just doing what's easiest if we're shy about adding the dependency.

@EronWright
Copy link
Contributor Author

EronWright commented May 23, 2024

I'm advocating for supporting the untyped option because the typed option is heavyweight to use, given you must develop or generate custom input/output types. I don't have any concerns about using bytebuddy but felt I should solicit feedback.

If at some point in the future we enhance the core SDK to support dynamic input values, I do believe the codegen stuff will simply fall away.

@t0yv0
Copy link
Member

t0yv0 commented May 23, 2024

This looks good! Assuming this can't be schematized so needs an overlay.

This is unfortunate but may be an easy-ish fix in the Java SDK that can unlock this and other scenarios, let's maybe track it and consider that for Java SDK GA?

The core Pulumi Java SDK has no support for dynamic inputs; it relies exclusively on reflection of the supplied InputArgs subclass (see: [InputArgs::toMapAsync](https://github.com/pulumi/pulumi-java/blob/f887fbc869974ae7d9049cb4a5b62f51b1151dcb/sdk/java/pulumi/src/main/java/com/pulumi/resources/InputArgs.java#L63)). To support the "untyped" mode, this implementation codegens a class at runtime using bytebuddy.

@EronWright EronWright marked this pull request as ready for review May 24, 2024 22:15
@EronWright EronWright enabled auto-merge (squash) May 28, 2024 21:10
@EronWright EronWright merged commit 41b0d90 into master May 28, 2024
18 checks passed
@EronWright EronWright deleted the issue-2787 branch May 28, 2024 21:21
@EronWright EronWright mentioned this pull request Jun 4, 2024
EronWright added a commit that referenced this pull request Jun 4, 2024
### Added
- Kustomize Directory v2 resource
(#3036)
- CustomResource for Java SDK
(#3020)

### Changed
- Update to pulumi-java v0.12.0
(#3025)

### Fixed
- Fixed Chart v4 fails on update
(#3046)
@pulumi-bot
Copy link
Contributor

This PR has been shipped in release v4.13.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Develop Component resource for: kubernetes:apiextensions.k8s.io:CustomResource
4 participants