Skip to content

Commit

Permalink
CustomResource for Java SDK (#3020)
Browse files Browse the repository at this point in the history
<!--Thanks for your contribution. See [CONTRIBUTING](CONTRIBUTING.md)
    for Pulumi's contribution guidelines.

    Help us merge your changes more quickly by adding more details such
    as labels, milestones, and reviewers.-->

### Proposed changes

<!--Give us a brief description of what you've done and what it solves.
-->
Implements `CustomResource` for Java SDK as an overlay, for parity with
other SDKs. An overlay is necessary for [this
reason](#2787 (comment)).

#### 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:

- [x] Prerequisite: Pulumi Java 0.12
(pulumi/pulumi-java#1361)
- [x] `Get` method (untyped and typed)
- [x] Patch variant
- [x] Integration test or in-tree example

### Related issues (optional)

<!--Refer to related PRs or issues: #1234, or 'Fixes #1234' or 'Closes
#1234'.
Or link to full URLs to issues or pull requests in other GitHub
repositories. -->
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`](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.

#### 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](https://egalluzzo.blogspot.com/2010/06/using-inheritance-with-fluent.html).

## Example

Here's an example program to deploy two [cert-manager
issuers](https://cert-manager.io/docs/concepts/issuer/).
- `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.

```java
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
```
  • Loading branch information
EronWright authored May 28, 2024
1 parent 491c036 commit 41b0d90
Show file tree
Hide file tree
Showing 22 changed files with 2,157 additions and 2 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Unreleased

- Update to pulumi-java v0.12.0 #3025 (https://github.com/pulumi/pulumi-kubernetes/pull/3025)
- Update to pulumi-java v0.12.0 (https://github.com/pulumi/pulumi-kubernetes/pull/3025)
- Fixed a panic that occurs when diffing Job resources containing `replaceUnready` annotations and an unreachable cluster connection. (https://github.com/pulumi/pulumi-kubernetes/pull/3024)
- CustomResource for Java SDK (https://github.com/pulumi/pulumi-kubernetes/pull/3020)

## 4.12.0 (May 21, 2024)

Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ python_sdk::

java_sdk:: PACKAGE_VERSION := $(shell pulumictl convert-version --language generic -v "$(VERSION_GENERIC)")
java_sdk:: bin/pulumi-java-gen
$(WORKING_DIR)/bin/$(JAVA_GEN) generate --schema $(SCHEMA_FILE) --out sdk/java --build gradle-nexus
$(WORKING_DIR)/bin/$(JAVA_GEN) generate --schema $(SCHEMA_FILE) --overlay provider/pkg/gen/java-templates \
--out sdk/java --build gradle-nexus
cd ${PACKDIR}/java/ && \
echo "module fake_java_module // Exclude this directory from Go tools\n\ngo 1.17" > go.mod && \
gradle --console=plain build
Expand Down
7 changes: 7 additions & 0 deletions examples/java/customresource/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: example
runtime: java
description: An example for com.pulumi.kubernetes.apiextensions.CustomResource
config:
pulumi:tags:
value:
pulumi:template: ""
97 changes: 97 additions & 0 deletions examples/java/customresource/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.pulumi</groupId>
<artifactId>issue-2787-javaa</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<encoding>UTF-8</encoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.release>11</maven.compiler.release>
<mainClass>myproject.App</mainClass>
<mainArgs/>
</properties>

<dependencies>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>pulumi</artifactId>
<version>(,1.0]</version>
</dependency>
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>kubernetes</artifactId>
<version>(,5.0]</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-my-jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>${mainClass}</mainClass>
<commandlineArgs>${mainArgs}</commandlineArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-wrapper-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mavenVersion>3.8.5</mavenVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading

0 comments on commit 41b0d90

Please sign in to comment.