diff --git a/sdk/java/.gitignore b/sdk/java/.gitignore index 783fbf7d129..4fb4952bafa 100644 --- a/sdk/java/.gitignore +++ b/sdk/java/.gitignore @@ -1,3 +1,5 @@ # Ignore Gradle build output directory build .gradle +/pulumi/.pulumi/ +/pulumi/Pulumi.json diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/Pulumi.java b/sdk/java/pulumi/src/main/java/com/pulumi/Pulumi.java index e216de77aed..cc5b2855a4a 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/Pulumi.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/Pulumi.java @@ -48,7 +48,7 @@ static CompletableFuture runAsync(Consumer stack) { * @see #runAsync(Consumer) */ static Pulumi.API withOptions(StackOptions options) { - return PulumiInternal.fromEnvironment(options); + return PulumiInternal.APIInternal.fromEnvironment(options); } /** diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/GlobalOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/GlobalOptions.java new file mode 100644 index 00000000000..0b6b233be1a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/GlobalOptions.java @@ -0,0 +1,143 @@ +package com.pulumi.automation; + +public class GlobalOptions { + + protected Color color; + protected boolean logFlow; + protected int logVerbosity; + protected boolean logToStdErr; + protected String tracing; + protected boolean debug; + protected boolean json; + + protected GlobalOptions() { /* empty */ } + + /** + * Colorize output + * + * @return output colorization option + */ + public Color color() { + return color; + } + + /** + * Flow log settings to child processes (like plugins) + * + * @return whether the flow log setting is active + */ + public boolean logFlow() { + return logFlow; + } + + /** + * Enable verbose logging (e.g., v=3); anything greater than 3 is very verbose + * + * @return the verbosity level + */ + public int logVerbosity() { + return logVerbosity; + } + + /** + * Log to stderr instead of to files + * + * @return whether the logging to stderr is active + */ + public boolean logToStdErr() { + return logToStdErr; + } + + /** + * Emit tracing to the specified endpoint. Use the file: scheme to write tracing data to a local file + * + * @return the tracing endpoint + */ + public String tracing() { + return tracing; + } + + /** + * Print detailed debugging output during resource operations + * + * @return whether debugging output is active + */ + public boolean debug() { + return debug; + } + + /** + * Format standard output as JSON not text. + * + * @return whether JSON output is active + */ + public boolean json() { + return json; + } + + /** + * Colorization options + */ + public enum Color { + Always, + Never, + Raw, + Auto + } + + protected static abstract class Builder> { + + protected final T options; + + protected Builder(T options) { + this.options = options; + } + + public B color(Color color) { + this.options.color = color; + //noinspection unchecked + return (B) this; + } + + public B logFlow(boolean logFlow) { + this.options.logFlow = logFlow; + //noinspection unchecked + return (B) this; + } + + public B logVerbosity(int logVerbosity) { + this.options.logVerbosity = logVerbosity; + //noinspection unchecked + return (B) this; + } + + public B logToStdErr(boolean logToStdErr) { + this.options.logToStdErr = logToStdErr; + //noinspection unchecked + return (B) this; + } + + public B tracing(String tracing) { + this.options.tracing = tracing; + //noinspection unchecked + return (B) this; + } + + public B debug(boolean debug) { + this.options.debug = debug; + //noinspection unchecked + return (B) this; + } + + /** + * @see GlobalOptions#json() + * @param json if true JSON output is active + * @return the {@link Builder} instance + */ + public B json(boolean json) { + this.options.json = json; + //noinspection unchecked + return (B) this; + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java new file mode 100644 index 00000000000..171401e8f5d --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspace.java @@ -0,0 +1,69 @@ +package com.pulumi.automation; + +import com.google.common.collect.ImmutableMap; +import com.pulumi.Context; + +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +/** + * LocalWorkspace is a default implementation of the {@link Workspace} interface. + *

+ * LocalWorkspace relies on {@code Pulumi.yaml} and {@code Pulumi..yaml} + * as the intermediate format for Project and Stack settings. + * Modifying ProjectSettings will alter the Workspace {@code Pulumi.yaml} file, + * and setting config on a Stack will modify the {@code Pulumi..yaml} file. + * This is identical to the behavior of Pulumi CLI driven workspaces. + *

+ * If not provided a working directory, causing LocalWorkspace to create a temp directory, + * the temp directory will be cleaned up. + */ +public class LocalWorkspace implements Workspace { + + private final Logger logger; + private final ProjectSettings settings; + private final ImmutableMap environmentVariables; + private final LocalWorkspaceOptions options; + + public LocalWorkspace( + Logger logger, + ProjectSettings settings, + Map environmentVariables, + LocalWorkspaceOptions options + ) { + this.logger = requireNonNull(logger); + this.settings = requireNonNull(settings); + this.environmentVariables = ImmutableMap.copyOf(environmentVariables); + this.options = requireNonNull(options); + } + + @Override + public ProjectSettings projectSettings() { + return this.settings; + } + + @Override + public ImmutableMap environmentVariables() { + return this.environmentVariables; + } + + @Override + public Path workDir() { + return this.options.workDir(); + } + + @Override + public Optional> program() { + return this.options.program(); + } + + @Override + public WorkspaceStack upsertStack(StackSettings settings) { + return new WorkspaceStack(logger, this, settings); + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspaceOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspaceOptions.java new file mode 100644 index 00000000000..fe5841aaf04 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/LocalWorkspaceOptions.java @@ -0,0 +1,73 @@ +package com.pulumi.automation; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.pulumi.Context; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; + +@ParametersAreNonnullByDefault +public class LocalWorkspaceOptions { + + private final Path workDir; + @Nullable + private final Consumer program; + + public LocalWorkspaceOptions( + Path workDir, + @Nullable Consumer program + ) { + + this.workDir = requireNonNull(workDir); + this.program = program; + } + + public Path workDir() { + return this.workDir; + } + + @Nullable + public Optional> program() { + return Optional.ofNullable(this.program); + } + + public static LocalWorkspaceOptions.Builder builder() { + return new LocalWorkspaceOptions.Builder(); + } + + @ParametersAreNonnullByDefault + @CanIgnoreReturnValue + public static class Builder { + + private Path workDir; + private Consumer program; + + public Builder workDir(Path path) { + this.workDir = path; + return this; + } + + /** + * The inline Pulumi program to be used for Preview/Update operations if any. + * @see Workspace#program() + * @param program the inline Pulumi program to use + * @return the {@link Builder} instance + */ + public Builder program(@Nullable Consumer program) { + this.program = program; + return this; + } + + public LocalWorkspaceOptions build() { + return new LocalWorkspaceOptions( + this.workDir, + this.program + ); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectBackend.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectBackend.java new file mode 100644 index 00000000000..7f59a1ddd4a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectBackend.java @@ -0,0 +1,13 @@ +package com.pulumi.automation; + +public class ProjectBackend { + private final String url; + + public ProjectBackend(String url) { + this.url = url; + } + + public String url() { + return url; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectSettings.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectSettings.java new file mode 100644 index 00000000000..6b1bb00a916 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ProjectSettings.java @@ -0,0 +1,91 @@ +package com.pulumi.automation; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; + +/** + * A Pulumi project manifest. + * It describes metadata applying to all sub-stacks created from the project. + */ +@ParametersAreNonnullByDefault +public class ProjectSettings { + + private final String name; + private final String runtime; + @Nullable + private final ProjectBackend backend; + + /** + * A new {@link ProjectSettings} with the given values + * + * @param name the project name + * @param runtime the language runtime + * @param backend the optional {@link ProjectBackend} setting + */ + public ProjectSettings(String name, String runtime, @Nullable ProjectBackend backend) { + this.name = requireNonNull(name); + this.runtime = requireNonNull(runtime); + this.backend = backend; + } + + /** + * Name of the project containing alphanumeric characters, hyphens, underscores, and periods. + * @return the project name + */ + public String name() { + return name; + } + + /** + * Installed language runtime of the project: nodejs, python, go, dotnet, java or yaml. + * @return the language runtime + */ + public String runtime() { + return runtime; + } + + public static ProjectSettings.Builder builder() { + return new ProjectSettings.Builder(); + } + + public Optional backend() { + return Optional.ofNullable(backend); + } + + public static class Builder { + + private String name; + private String runtime; + @Nullable + private ProjectBackend backend; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder runtime(String runtime) { + this.runtime = runtime; + return this; + } + + public Builder backend(String url) { + this.backend = new ProjectBackend(url); + return this; + } + + public ProjectSettings build() { + if (runtime == null) { + runtime = "java"; + } + return new ProjectSettings( + this.name, + this.runtime, + this.backend + ); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/PulumiAuto.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/PulumiAuto.java new file mode 100644 index 00000000000..67fe495ccd9 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/PulumiAuto.java @@ -0,0 +1,36 @@ +package com.pulumi.automation; + +import com.pulumi.Pulumi; +import com.pulumi.automation.internal.PulumiAutoInternal; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.Map; + +@ParametersAreNonnullByDefault +public interface PulumiAuto extends Pulumi { + + static API withProjectSettings(ProjectSettings projectSettings) { + return new PulumiAutoInternal.APIInternal().withProjectSettings(projectSettings); + } + + static API withEnvironmentVariables(Map environmentVariables) { + return new PulumiAutoInternal.APIInternal().withEnvironmentVariables(environmentVariables); + } + + /** + * Pulumi Automation entrypoint operations. + */ + interface API { + /** + * The {@link ProjectSettings} object for the current project. + * + * @param projectSettings the project setting + * @return the {@link API} instance + */ + API withProjectSettings(ProjectSettings projectSettings); + + API withEnvironmentVariables(Map environmentVariables); + + LocalWorkspace localWorkspace(LocalWorkspaceOptions options); + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackSettings.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackSettings.java new file mode 100644 index 00000000000..217e9a83491 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/StackSettings.java @@ -0,0 +1,54 @@ +package com.pulumi.automation; + +import com.google.common.collect.ImmutableMap; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class StackSettings { + + private final String name; + private final ImmutableMap config; + + public StackSettings(String name, Map config) { + this.name = requireNonNull(name); + this.config = ImmutableMap.copyOf(config); + } + + public String name() { + return name; + } + + /** + * This is an optional configuration bag. + * @return stack configuration + */ + public Map config() { + return config; + } + + public static StackSettings.Builder builder() { + return new StackSettings.Builder(); + } + + public static class Builder { + + private String name; + private Map config = Map.of(); + + public Builder config(Map config) { + this.config = config; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public StackSettings build() { + return new StackSettings(this.name, this.config); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpOptions.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpOptions.java new file mode 100644 index 00000000000..58296695cc7 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpOptions.java @@ -0,0 +1,28 @@ +package com.pulumi.automation; + +/** + * Common options controlling the behavior of update actions taken + * against an instance of {@link WorkspaceStack}. + */ +public final class UpOptions extends GlobalOptions { + + private UpOptions() { /* empty */ } + + public static UpOptions.Builder builder() { + return new UpOptions.Builder(new UpOptions()); + } + + /** + * The {@link GlobalOptions} builder. + */ + public static final class Builder extends GlobalOptions.Builder { + + private Builder(UpOptions options) { + super(options); + } + + public UpOptions build() { + return this.options; + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpResult.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpResult.java new file mode 100644 index 00000000000..f06d7b78a2a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpResult.java @@ -0,0 +1,40 @@ +package com.pulumi.automation; + +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class UpResult { + private final String stdout; + private final String stderr; + private final UpdateSummary summary; + private final Map outputs; + + public UpResult( + String stdout, + String stderr, + UpdateSummary summary, + Map outputs + ) { + this.stdout = requireNonNull(stdout); + this.stderr = requireNonNull(stderr); + this.summary = requireNonNull(summary); + this.outputs = requireNonNull(outputs); + } + + public String stdout() { + return stdout; + } + + public String stderr() { + return stderr; + } + + public UpdateSummary summary() { + return summary; + } + + public Map outputs() { + return outputs; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateKind.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateKind.java new file mode 100644 index 00000000000..ee77914c13a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateKind.java @@ -0,0 +1,10 @@ +package com.pulumi.automation; + +public enum UpdateKind { + Update, + Preview, + Refresh, + Rename, + Destroy, + Import, +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateState.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateState.java new file mode 100644 index 00000000000..fce63a2850b --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateState.java @@ -0,0 +1,4 @@ +package com.pulumi.automation; + +public class UpdateState { +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateSummary.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateSummary.java new file mode 100644 index 00000000000..37d2e786c3a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/UpdateSummary.java @@ -0,0 +1,19 @@ +package com.pulumi.automation; + +import static java.util.Objects.requireNonNull; + +public class UpdateSummary { + + // pre-update information + private final UpdateKind kind; + // post-update information + private final UpdateState result; + + public UpdateSummary( + UpdateKind kind, + UpdateState result + ) { + this.kind = requireNonNull(kind); + this.result = requireNonNull(result); + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/ValueOrSecret.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ValueOrSecret.java new file mode 100644 index 00000000000..f8e8b8fe389 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/ValueOrSecret.java @@ -0,0 +1,30 @@ +package com.pulumi.automation; + +import static java.util.Objects.requireNonNull; + +public class ValueOrSecret { + + private final String value; + private final boolean isSecret; + + private ValueOrSecret(String value, boolean isSecret) { + this.value = requireNonNull(value); + this.isSecret = isSecret; + } + + public static ValueOrSecret value(String value) { + return new ValueOrSecret(value, false); + } + + public static ValueOrSecret secret(String value) { + return new ValueOrSecret(value, true); + } + + public String value() { + return value; + } + + public boolean isSecret() { + return isSecret; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java new file mode 100644 index 00000000000..fa527288770 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/Workspace.java @@ -0,0 +1,24 @@ +package com.pulumi.automation; + +import com.google.common.collect.ImmutableMap; +import com.pulumi.Context; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Workspace is the execution context containing a single Pulumi project, + * a program, and multiple stacks. + *

+ * Workspaces are used to manage the execution environment, providing various utilities + * such as plugin installation, environment configuration ($PULUMI_HOME), + * and creation, deletion, and listing of Stacks. + */ +public interface Workspace { + ProjectSettings projectSettings(); + ImmutableMap environmentVariables(); + Path workDir(); + Optional> program(); + WorkspaceStack upsertStack(StackSettings options); +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/WorkspaceStack.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/WorkspaceStack.java new file mode 100644 index 00000000000..60f4b3eec39 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/WorkspaceStack.java @@ -0,0 +1,154 @@ +package com.pulumi.automation; + +import com.pulumi.automation.internal.ExecKind; +import com.pulumi.automation.internal.LanguageRuntimeContext; +import com.pulumi.automation.internal.LanguageRuntimeServer; +import com.pulumi.automation.internal.LanguageRuntimeService; +import com.pulumi.automation.internal.Shell; +import com.pulumi.core.internal.Arrays; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +public class WorkspaceStack { + + private final Logger logger; + private final Workspace workspace; + private final StackSettings settings; + + public WorkspaceStack( + Logger logger, + Workspace workspace, + StackSettings settings + ) { + this(logger, workspace, settings, s -> defaultInitializer(s)); + } + + public WorkspaceStack( + Logger logger, + Workspace workspace, + StackSettings settings, + Consumer initializer + ) { + this.logger = requireNonNull(logger); + this.workspace = requireNonNull(workspace); + this.settings = requireNonNull(settings); + requireNonNull(initializer).accept(this); + } + + public Workspace workspace() { + return this.workspace; + } + + private ExecKind execKind() { + if (this.workspace.program().isPresent()) { + return ExecKind.Inline; + } + return ExecKind.Local; + } + + /** + * Creates or updates the resources in a stack by executing the program in the Workspace. + * + * @param options Options to customize the behavior of the update. + * @return the update result future + * @see + */ + public CompletableFuture upAsync(UpOptions options) { + requireNonNull(options); // FIXME + + var args = new String[]{ + "up", + "--yes", + "--skip-preview", + String.format("--stack=%s", this.settings.name()), + }; + + return pulumiCmd(execKind(), args).thenApply(__ -> new UpResult( + "", "", new UpdateSummary(UpdateKind.Update, new UpdateState()), Map.of() // FIXME + )); + } + + public CompletableFuture previewAsync() { + var args = new String[]{ + "preview", + String.format("--stack=%s", this.settings.name()), + }; + + return pulumiCmd(execKind(), args).thenApply(__ -> null); + } + + private CompletableFuture pulumiCmd(ExecKind kind, String... args) { + var shell = new Shell( + () -> null, + line -> System.out.println(line), + line -> System.err.println(line), + this.workspace.environmentVariables(), + this.workspace.workDir() + ); + var pulumi = new String[]{"pulumi"}; + args = Arrays.concat(pulumi, args); + if (kind != ExecKind.None) { + args = Arrays.concat(args, new String[]{ + String.format("--exec-kind=%s", kind.flag()) + }); + } + if (kind == ExecKind.Inline) { + // we need the server only for inline programs + var server = createAndStartServer(); + args = Arrays.concat(args, new String[]{ + String.format("--client=127.0.0.1:%d", server.port()), + }); + return shell.run(args).whenComplete((integer, throwable) -> { + server.shutdown(); + }); + } + return shell.run(args); + } + + private LanguageRuntimeServer createAndStartServer() { + var program = this.workspace.program().orElseThrow( + () -> new IllegalStateException("expected inline program, got none") + ); + var context = new LanguageRuntimeContext(program); + var service = new LanguageRuntimeService(this.logger, context); + var server = new LanguageRuntimeServer(this.logger, service); + try { + server.start(); + } catch (Throwable t) { + server.shutdown(); + throw t; + } + return server; + } + + private static void defaultInitializer(WorkspaceStack stack) { + if (stack.select() > 0) { + if (stack.create() > 0) { + throw new IllegalStateException("stack creation failed"); + }; + } + } + + private Integer select() { + var args = new String[]{ + "stack", + "init", + this.settings.name(), + }; + return pulumiCmd(ExecKind.None, args).join(); + } + + private Integer create() { + var args = new String[]{ + "stack", + "select", + String.format("--stack=%s", this.settings.name()), + }; + return pulumiCmd(ExecKind.None, args).join(); + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/AutoResult.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/AutoResult.java new file mode 100644 index 00000000000..be475bc5364 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/AutoResult.java @@ -0,0 +1,37 @@ +package com.pulumi.automation.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.pulumi.core.Output; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +@ParametersAreNonnullByDefault +public class AutoResult { + + private final int exitCode; + private final List exceptions; + private final Map> exports; + + public AutoResult(int exitCode, List exceptions, Map> exports) { + this.exitCode = exitCode; + this.exceptions = requireNonNull(ImmutableList.copyOf(exceptions)); + this.exports = requireNonNull(ImmutableMap.copyOf(exports)); + } + + public int exitCode() { + return exitCode; + } + + public List exceptions() { + return exceptions; + } + + public Map> exports() { + return exports; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/ExecKind.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/ExecKind.java new file mode 100644 index 00000000000..d127ca8d81a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/ExecKind.java @@ -0,0 +1,19 @@ +package com.pulumi.automation.internal; + +import static java.util.Objects.requireNonNull; + +public enum ExecKind { + None(""), + Local("auto.local"), + Inline("auto.inline"); + + private final String flag; + + ExecKind(String flag) { + this.flag = requireNonNull(flag); + } + + public String flag() { + return flag; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeContext.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeContext.java new file mode 100644 index 00000000000..54a2de56c1a --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeContext.java @@ -0,0 +1,22 @@ +package com.pulumi.automation.internal; + +import com.pulumi.Context; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; + +@ParametersAreNonnullByDefault +public class LanguageRuntimeContext { + + private final Consumer program; + + public LanguageRuntimeContext(Consumer program) { + this.program = requireNonNull(program); + } + + public Consumer program() { + return this.program; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeServer.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeServer.java new file mode 100644 index 00000000000..7221fe53fd7 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeServer.java @@ -0,0 +1,65 @@ +package com.pulumi.automation.internal; + +import io.grpc.Grpc; +import io.grpc.InsecureServerCredentials; +import io.grpc.Server; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +@ParametersAreNonnullByDefault +public class LanguageRuntimeServer { + + private static final int SHUTDOWN_TIMEOUT_IN_SECONDS = 30; + + private final Logger logger; + private final Server server; + + public LanguageRuntimeServer(Logger logger, LanguageRuntimeService service) { + this.logger = requireNonNull(logger); + this.server = Grpc.newServerBuilderForPort(0 /* random port */, InsecureServerCredentials.create()) + .addService(service) + .build(); + } + + public int port() { + var port = server.getPort(); + if (port == -1) { + throw new UnsupportedOperationException("Cannot get LanguageRuntimeServer port, got -1"); + } + return port; + } + + public int start() { + try { + server.start(); + } catch (IOException e) { + throw new UncheckedIOException("Cannot start LanguageRuntimeServer", e); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + // Use stderr here since the logger may have been reset by its JVM shutdown hook. + System.err.println("Shutting down LanguageRuntimeServer since JVM is shutting down"); + LanguageRuntimeServer.this.shutdown(); + })); + + var port = server.getPort(); + logger.finest(String.format("LanguageRuntimeServer started, listening on %d", port)); + return port; + } + + public void shutdown() { + if (server != null) { + try { + server.shutdown().awaitTermination(SHUTDOWN_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Error while awaiting for termination of LanguageRuntimeServer", e); + } + } + logger.finest("LanguageRuntimeServer shut down"); + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeService.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeService.java new file mode 100644 index 00000000000..c484404a21c --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/LanguageRuntimeService.java @@ -0,0 +1,55 @@ +package com.pulumi.automation.internal; + +import io.grpc.stub.StreamObserver; +import pulumirpc.Language.RunRequest; +import pulumirpc.Language.RunResponse; +import pulumirpc.LanguageRuntimeGrpc; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +@ParametersAreNonnullByDefault +public class LanguageRuntimeService extends LanguageRuntimeGrpc.LanguageRuntimeImplBase { + + private final Logger logger; + private final LanguageRuntimeContext context; + + public LanguageRuntimeService(Logger logger, LanguageRuntimeContext context) { + this.logger = requireNonNull(logger); + this.context = requireNonNull(context); + } + + @Override + public void run(RunRequest request, StreamObserver responseObserver) { + this.logger.finest(String.format("request: %s", request)); + try { + run(request); + } catch (Exception e) { + responseObserver.onError(e); + throw e; + } + responseObserver.onNext(RunResponse.newBuilder().build()); + responseObserver.onCompleted(); + } + + private void run(RunRequest request) { + var args = request.getArgsList(); + var engineAddress = args.size() > 0 ? args.get(0) : ""; + + var requestContext = new RunRequestContext( + engineAddress, + request.getMonitorAddress(), + request.getConfigMap(), + request.getConfigSecretKeysList(), + request.getProject(), + request.getStack(), + request.getDryRun() + ); + + try (var pulumi = PulumiAutoInternal.from(this.logger, requestContext)) { + pulumi.runAutoAsync(context.program()).join(); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/PulumiAutoInternal.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/PulumiAutoInternal.java new file mode 100644 index 00000000000..c5ed1678f28 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/PulumiAutoInternal.java @@ -0,0 +1,130 @@ +package com.pulumi.automation.internal; + +import com.google.common.collect.ImmutableMap; +import com.pulumi.Config; +import com.pulumi.Context; +import com.pulumi.automation.LocalWorkspace; +import com.pulumi.automation.LocalWorkspaceOptions; +import com.pulumi.automation.ProjectSettings; +import com.pulumi.automation.PulumiAuto; +import com.pulumi.context.internal.ConfigContextInternal; +import com.pulumi.context.internal.ContextInternal; +import com.pulumi.context.internal.LoggingContextInternal; +import com.pulumi.context.internal.OutputContextInternal; +import com.pulumi.core.internal.OutputFactory; +import com.pulumi.core.internal.annotations.InternalUse; +import com.pulumi.deployment.Deployment; +import com.pulumi.deployment.internal.DeploymentImpl; +import com.pulumi.deployment.internal.DeploymentInstanceHolder; +import com.pulumi.deployment.internal.DeploymentInstanceInternal; +import com.pulumi.deployment.internal.GrpcEngine; +import com.pulumi.deployment.internal.GrpcMonitor; +import com.pulumi.deployment.internal.Runner; +import com.pulumi.internal.PulumiInternal; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.io.Closeable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Logger; + +import static java.util.Objects.requireNonNull; + +/** + * Provides an internal Pulumi Automation entrypoint and exposes various internals for the testing purposes. + */ +@InternalUse +@ParametersAreNonnullByDefault +public class PulumiAutoInternal extends PulumiInternal implements PulumiAuto, Closeable { + + public PulumiAutoInternal( + Runner runner, + ContextInternal stackContext + ) { + super(runner, stackContext); + } + + public static PulumiAutoInternal from(Logger logger, RunRequestContext requestContext) { + var engine = new GrpcEngine(requestContext.engineAddress()); + var monitor = new GrpcMonitor(requestContext.monitorAddress()); + + var conf = new DeploymentImpl.Config( + requestContext.configMap(), + requestContext.configSecretKeys() + ); + + var projectName = requestContext.project(); + var stackName = requestContext.stack(); + var dryRun = requestContext.dryRun(); + + var state = new DeploymentImpl.DeploymentState(conf, logger, projectName, stackName, dryRun, engine, monitor); + var deployment = new DeploymentImpl(state); + DeploymentInstanceHolder.setInstance(new DeploymentInstanceInternal(deployment)); + + var instance = Deployment.getInstance(); + var runner = deployment.getRunner(); + var log = deployment.getLog(); + + Function configFactory = (name) -> new Config(instance.getConfig(), name); + var config = new ConfigContextInternal(projectName, configFactory); + var logging = new LoggingContextInternal(log); + var outputFactory = new OutputFactory(runner); + var outputs = new OutputContextInternal(outputFactory); + var ctx = new ContextInternal( + projectName, stackName, logging, config, outputs, List.of() + ); + return new PulumiAutoInternal(runner, ctx); + } + + @InternalUse + public CompletableFuture runAutoAsync( + Consumer stackCallback + ) { + return runAsyncResult(stackCallback).thenApply(r -> new AutoResult( + r.exitCode(), + r.exceptions(), + this.stackContext.exports() + )); + } + + @Override + public void close() { + // Unset the global state + DeploymentImpl.internalUnsafeDestroyInstance(); + } + + @InternalUse + @ParametersAreNonnullByDefault + public static final class APIInternal implements PulumiAuto.API { + + private final Logger standardLogger = Logger.getLogger(PulumiAuto.API.class.getName()); + + private ProjectSettings projectSettings; + private ImmutableMap environmentVariables; + + @Override + public PulumiAuto.API withProjectSettings(ProjectSettings projectSettings) { + this.projectSettings = requireNonNull(projectSettings); + return this; + } + + @Override + public PulumiAuto.API withEnvironmentVariables(Map environmentVariables) { + this.environmentVariables = ImmutableMap.copyOf(environmentVariables); + return this; + } + + @Override + public LocalWorkspace localWorkspace(LocalWorkspaceOptions options) { + return new LocalWorkspace( + this.standardLogger, + this.projectSettings, + this.environmentVariables, + options + ); + } + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/RunRequestContext.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/RunRequestContext.java new file mode 100644 index 00000000000..7f3bc9fc1ac --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/RunRequestContext.java @@ -0,0 +1,65 @@ +package com.pulumi.automation.internal; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; + +public class RunRequestContext { + private final String engineAddress; + private final String monitorAddress; + private final ImmutableMap configMap; + private final ImmutableSet configSecretKeys; + private final String project; + private final String stack; + private final boolean dryRun; + + public RunRequestContext( + String engineAddress, + String monitorAddress, + Map configMap, + List configSecretKeys, + String project, + String stack, + boolean dryRun + ) { + this.engineAddress = requireNonNull(engineAddress); + this.monitorAddress = requireNonNull(monitorAddress); + this.configMap = ImmutableMap.copyOf(configMap); + this.configSecretKeys = ImmutableSet.copyOf(configSecretKeys); + this.project = requireNonNull(project); + this.stack = requireNonNull(stack); + this.dryRun = dryRun; + } + + public String engineAddress() { + return this.engineAddress; + } + + public String monitorAddress() { + return this.monitorAddress; + } + + public ImmutableMap configMap() { + return this.configMap; + } + + public ImmutableSet configSecretKeys() { + return this.configSecretKeys; + } + + public String project() { + return this.project; + } + + public String stack() { + return this.stack; + } + + public boolean dryRun() { + return this.dryRun; + } +} diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/Shell.java b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/Shell.java new file mode 100644 index 00000000000..b4ed1456911 --- /dev/null +++ b/sdk/java/pulumi/src/main/java/com/pulumi/automation/internal/Shell.java @@ -0,0 +1,106 @@ +package com.pulumi.automation.internal; + +import com.google.common.collect.ImmutableMap; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; + +public class Shell { + + private final Supplier stdin; + private final Consumer stdout; + private final Consumer stderr; + private final Map environmentVariables; + private final Path workDir; + + public Shell( + Supplier stdin, + Consumer stdout, + Consumer stderr, + Map environmentVariables, + Path workDir) { + this.stdin = requireNonNull(stdin); + this.stdout = requireNonNull(stdout); + this.stderr = requireNonNull(stderr); + this.environmentVariables = ImmutableMap.copyOf(environmentVariables); + this.workDir = requireNonNull(workDir); + } + + public CompletableFuture run(String... command) { + return CompletableFuture.supplyAsync(() -> { + ProcessBuilder builder = new ProcessBuilder(); + builder.directory(workDir.toFile()); + builder.environment().putAll(this.environmentVariables); + builder.command(command); + try { + return builder.start(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).thenCompose(process -> { + var codeAsync = process.onExit(); + var done = CompletableFuture.allOf( + codeAsync, + redirect(process.getErrorStream(), this.stderr), + redirect(process.getInputStream(), this.stdout), + redirect(this.stdin, process.getOutputStream()) + ); + return done.thenApply(ignore -> codeAsync.join().exitValue()); + }); + } + + private CompletableFuture redirect(final InputStream stream, final Consumer lines) { + final CompletableFuture future = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + reader.lines().forEachOrdered(lines); + future.complete(null); + } catch (IOException e) { + future.completeExceptionally(e); + } + }); + return future; + } + + private CompletableFuture redirect(final Supplier lines, final OutputStream stream) { + final CompletableFuture future = new CompletableFuture<>(); + CompletableFuture.runAsync(() -> { + try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream))) { + produce(lines, writer); + future.complete(null); + } catch (IOException e) { + future.completeExceptionally(e); + } catch (UncheckedIOException e) { + future.completeExceptionally(e.getCause()); + } + }); + return future; + } + + private static void produce(final Supplier supplier, final BufferedWriter writer) { + Stream.generate(supplier).takeWhile(line -> line != null) + .forEachOrdered(line -> { + try { + writer.write(line); + writer.newLine(); + writer.flush(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } +} 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 a726b0fd3d3..0d015cd4d3c 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 @@ -50,6 +50,7 @@ import com.pulumi.serialization.internal.PropertiesSerializer; import com.pulumi.serialization.internal.PropertiesSerializer.SerializationResult; import com.pulumi.serialization.internal.Structs; +import pulumirpc.AliasOuterClass.Alias; import pulumirpc.EngineOuterClass; import pulumirpc.EngineOuterClass.LogRequest; import pulumirpc.EngineOuterClass.LogSeverity; @@ -58,7 +59,6 @@ import pulumirpc.Resource.RegisterResourceOutputsRequest; import pulumirpc.Resource.RegisterResourceRequest; import pulumirpc.Resource.SupportsFeatureRequest; -import pulumirpc.AliasOuterClass.Alias; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -69,7 +69,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.ArrayList; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -96,7 +95,6 @@ import static com.pulumi.core.internal.Strings.isNonEmptyOrNull; import static com.pulumi.resources.internal.Stack.RootPulumiStackTypeName; import static java.util.stream.Collectors.toMap; -import static java.util.stream.Collectors.toSet; @InternalUse public class DeploymentImpl extends DeploymentInstanceHolder implements Deployment, DeploymentInternal { @@ -277,7 +275,7 @@ public Config(ImmutableMap allConfig, ImmutableSet confi this.configSecretKeys = Objects.requireNonNull(configSecretKeys); } - private static Config parse() { + public static Config parse() { return new Config(parseConfig(), parseConfigSecretKeys()); } diff --git a/sdk/java/pulumi/src/main/java/com/pulumi/internal/PulumiInternal.java b/sdk/java/pulumi/src/main/java/com/pulumi/internal/PulumiInternal.java index 0d536a175fb..4493d40bda5 100644 --- a/sdk/java/pulumi/src/main/java/com/pulumi/internal/PulumiInternal.java +++ b/sdk/java/pulumi/src/main/java/com/pulumi/internal/PulumiInternal.java @@ -2,6 +2,7 @@ import com.pulumi.Config; import com.pulumi.Context; +import com.pulumi.Log; import com.pulumi.Pulumi; import com.pulumi.context.internal.ConfigContextInternal; import com.pulumi.context.internal.ContextInternal; @@ -10,6 +11,7 @@ import com.pulumi.core.internal.OutputFactory; import com.pulumi.core.internal.annotations.InternalUse; import com.pulumi.deployment.Deployment; +import com.pulumi.deployment.DeploymentInstance; import com.pulumi.deployment.internal.DeploymentImpl; import com.pulumi.deployment.internal.Runner; import com.pulumi.deployment.internal.Runner.Result; @@ -25,7 +27,7 @@ @InternalUse @ParametersAreNonnullByDefault -public class PulumiInternal implements Pulumi, Pulumi.API { +public class PulumiInternal implements Pulumi { protected final Runner runner; protected final ContextInternal stackContext; @@ -36,32 +38,7 @@ public PulumiInternal(Runner runner, ContextInternal stackContext) { this.stackContext = requireNonNull(stackContext); } - @InternalUse - public static PulumiInternal fromEnvironment(StackOptions options) { - var deployment = DeploymentImpl.fromEnvironment(); - var instance = Deployment.getInstance(); - var projectName = deployment.getProjectName(); - var stackName = deployment.getStackName(); - var runner = deployment.getRunner(); - var log = deployment.getLog(); - - Function configFactory = (name) -> new Config(instance.getConfig(), name); - var config = new ConfigContextInternal(projectName, configFactory); - var logging = new LoggingContextInternal(log); - var outputFactory = new OutputFactory(runner); - var outputs = new OutputContextInternal(outputFactory); - - var ctx = new ContextInternal( - projectName, stackName, logging, config, outputs, options.resourceTransformations() - ); - return new PulumiInternal(runner, ctx); - } - - public void run(Consumer stack) { - System.exit(runAsync(stack).join()); - } - - public CompletableFuture runAsync(Consumer stackCallback) { + protected CompletableFuture runAsync(Consumer stackCallback) { return runAsyncResult(stackCallback).thenApply(r -> r.exitCode()); } @@ -80,4 +57,54 @@ protected CompletableFuture> runAsyncResult(Consumer stac }) ); } + + private static ContextInternal contextInternal( + StackOptions options, + DeploymentInstance instance, + String projectName, + String stackName, + Runner runner, + Log log + ) { + Function configFactory = (name) -> new Config(instance.getConfig(), name); + var config = new ConfigContextInternal(projectName, configFactory); + var logging = new LoggingContextInternal(log); + var outputFactory = new OutputFactory(runner); + var outputs = new OutputContextInternal(outputFactory); + + return new ContextInternal( + projectName, stackName, logging, config, outputs, options.resourceTransformations() + ); + } + + @InternalUse + @ParametersAreNonnullByDefault + public static final class APIInternal extends PulumiInternal implements Pulumi.API { + + public APIInternal(Runner runner, ContextInternal stackContext) { + super(runner, stackContext); + } + + @Override + public void run(Consumer stackCallback) { + System.exit(runAsync(stackCallback).join()); + } + + @Override + public CompletableFuture runAsync(Consumer stackCallback) { + return runAsyncResult(stackCallback).thenApply(r -> r.exitCode()); + } + + public static APIInternal fromEnvironment(StackOptions options) { + var deployment = DeploymentImpl.fromEnvironment(); + var instance = Deployment.getInstance(); + var projectName = deployment.getProjectName(); + var stackName = deployment.getStackName(); + var runner = deployment.getRunner(); + var log = deployment.getLog(); + + ContextInternal ctx = contextInternal(options, instance, projectName, stackName, runner, log); + return new APIInternal(runner, ctx); + } + } } diff --git a/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTests.java b/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTests.java new file mode 100644 index 00000000000..1cf7dadc47d --- /dev/null +++ b/sdk/java/pulumi/src/test/java/com/pulumi/automation/LocalWorkspaceTests.java @@ -0,0 +1,103 @@ +package com.pulumi.automation; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.pulumi.Context; +import com.pulumi.core.Output; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.io.TempDir; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Consumer; + +import static com.pulumi.automation.ValueOrSecret.secret; +import static com.pulumi.automation.ValueOrSecret.value; + +public class LocalWorkspaceTests { + + // local environment, to run locally offline, make sure you set: + // export PULUMI_BACKEND_URL=file://~ + // export PULUMI_API=file://~ + // pulumi login --local + + @Test + public void testStackLifecycleInlineProgram(TestInfo testInfo, @TempDir Path tempDir) throws IOException { + var projectName = safe(testInfo.getDisplayName()) + Tests.randomSuffix(); + var stackName = Tests.randomStackName(); + + var projectConfig = ImmutableMap.of( + "name", projectName, + "runtime", "java", + "description", "test", + "backend", ImmutableMap.of( + "url", "file://~" + ) + ); + var projectFile = Path.of(tempDir.toString(), "Pulumi.json").toFile(); + projectFile.deleteOnExit(); + try (Writer writer = new FileWriter(projectFile)) { + Gson gson = new GsonBuilder().create(); + gson.toJson(projectConfig, writer); + } + var stackConfig = ImmutableMap.of( + "config", ImmutableMap.of( + projectName + ":bar", value("abc"), + projectName + ":buzz", secret("secret") + ) + ); + var stackFile = Path.of(tempDir.toString(), String.format("Pulumi.%s.json", stackName)).toFile(); + stackFile.deleteOnExit(); + try (Writer writer = new FileWriter(stackFile)) { + Gson gson = new GsonBuilder().create(); + gson.toJson(stackConfig, writer); + } + + Consumer program = ctx -> { + ctx.export("exp-static", Output.of("foo")); + ctx.export("exp-cfg", Output.of(ctx.config().require("bar"))); + ctx.export("exp-secret", Output.of(ctx.config().requireSecret("buzz"))); + }; + + var workspace = PulumiAuto + .withProjectSettings(ProjectSettings.builder() // FIXME + .name(projectName) + .backend("file://~") + .build() + ) + .withEnvironmentVariables(Map.of( + "PULUMI_CONFIG_PASSPHRASE", "test" + )) + .localWorkspace(LocalWorkspaceOptions.builder() + .workDir(tempDir) + .program(program) + .build() + ); + + var stack = workspace.upsertStack(StackSettings.builder() + .name(stackName) + .config(ImmutableMap.of( // FIXME + "bar", value("abc"), + "buzz", secret("secret") + )) + .build() + ); +// var result = stack.previewAsync(); + var result = stack.upAsync(UpOptions.builder().build()); + + result.join(); + } + + private String safe(String displayName) { + return displayName + .replace(" ", "") + .replace(",", "-") + .replace("(", "-") + .replace(")", "-"); + } +} diff --git a/sdk/java/pulumi/src/test/java/com/pulumi/automation/Tests.java b/sdk/java/pulumi/src/test/java/com/pulumi/automation/Tests.java new file mode 100644 index 00000000000..b1ed8c12f16 --- /dev/null +++ b/sdk/java/pulumi/src/test/java/com/pulumi/automation/Tests.java @@ -0,0 +1,28 @@ +package com.pulumi.automation; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collector; +import java.util.stream.Stream; + +public class Tests { + + private static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz"; + + public static String randomStackName() { + var letters = Stream.generate(() -> { + var index = ThreadLocalRandom.current().nextInt(0, LOWERCASE_LETTERS.length()); + return LOWERCASE_LETTERS.charAt(index); + }); + return letters.limit(8).collect(Collector.of( + StringBuilder::new, + StringBuilder::append, + StringBuilder::append, + StringBuilder::toString + )); + } + + public static String randomSuffix() { + Integer integer = ThreadLocalRandom.current().nextInt(); + return String.format("%08x", integer); // 8 hex characters + } +}