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

Add install command to automation api #426

Merged
merged 4 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Improvements-426.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
component: sdk/auto
kind: Improvements
body: Add `pulumi install` to Automation Api
time: 2024-12-10T23:15:49.339423812Z
custom:
PR: "426"
41 changes: 41 additions & 0 deletions sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
using ILogger = Microsoft.Extensions.Logging.ILogger;

using static Pulumi.Automation.Tests.Utility;
using Pulumi.Automation.Tests.Mocks;
using Pulumi.Automation.Commands;
using Semver;

namespace Pulumi.Automation.Tests
{
Expand Down Expand Up @@ -2440,5 +2443,43 @@ public async Task TestLifecycleRefresh()
await stack.Workspace.RemoveStackAsync(stackName);
}
}

[Fact]
public async Task InstallRunsSuccessfully()
{
var mockCommand = new PulumiCommandMock(new SemVersion(3, 130, 0), new CommandResult(0, "", ""));
var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { PulumiCommand = mockCommand });

await workspace.InstallAsync(new InstallOptions
{
NoDependencies = true,
NoPlugins = true,
Reinstall = true,
UseLanguageVersionTools = true
});

Assert.Equal(5, mockCommand.RecordedArgs.Count);
Assert.Equal("install", mockCommand.RecordedArgs[0]);
}

[Fact]
public async Task InstallRequiresSupportedVersion()
{
var mockCommand = new PulumiCommandMock(new SemVersion(3, 0, 0), new CommandResult(0, "", ""));
var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { PulumiCommand = mockCommand });

await Assert.ThrowsAsync<InvalidOperationException>(async () => await workspace.InstallAsync());
}

[Fact]
public async Task InstallLanguageVersionToolsRequiresSupportedVersion()
{
var mockCommand = new PulumiCommandMock(new SemVersion(3, 91, 0), new CommandResult(0, "", ""));
var installOptions = new InstallOptions { UseLanguageVersionTools = true };

var workspace = await LocalWorkspace.CreateAsync(new LocalWorkspaceOptions { PulumiCommand = mockCommand });

await Assert.ThrowsAsync<InvalidOperationException>(async () => await workspace.InstallAsync(installOptions));
}
}
}
52 changes: 52 additions & 0 deletions sdk/Pulumi.Automation.Tests/Mocks/PulumiCommandMock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Pulumi.Automation.Events;
using Pulumi.Automation.Commands;
using Semver;

namespace Pulumi.Automation.Tests.Mocks
{
class PulumiCommandMock : PulumiCommand
{
private readonly CommandResult CommandResult;

public PulumiCommandMock(SemVersion version, CommandResult commandResult)
{
this.Version = version;
this.CommandResult = commandResult;
}

public override Task<CommandResult> RunAsync(
IList<string> args,
string workingDir,
IDictionary<string, string?> additionalEnv,
Action<string>? onStandardOutput = null,
Action<string>? onStandardError = null,
Action<EngineEvent>? onEngineEvent = null,
CancellationToken cancellationToken = default)
{
this.RecordedArgs = args;
return Task.FromResult(this.CommandResult);
}

public override Task<CommandResult> RunInputAsync(
IList<string> args,
string workingDir,
IDictionary<string, string?> additionalEnv,
Action<string>? onStandardOutput = null,
Action<string>? onStandardError = null,
string? stdIn = null,
Action<EngineEvent>? onEngineEvent = null,
CancellationToken cancellationToken = default)
{
this.RecordedArgs = args;
return Task.FromResult(this.CommandResult);
}

public override SemVersion? Version { get; }

public IList<string> RecordedArgs { get; private set; } = new List<string>();
}
}
25 changes: 25 additions & 0 deletions sdk/Pulumi.Automation/InstallOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Pulumi.Automation
{
public class InstallOptions
{
/// <summary>
/// Skip installing plugins.
/// </summary>
public bool NoPlugins { get; set; }

/// <summary>
/// Skip installing dependencies.
/// </summary>
public bool NoDependencies { get; set; }

/// <summary>
/// Reinstall a plugin even if it already exists.
/// </summary>
public bool Reinstall { get; set; }

/// <summary>
/// Use language version tools to setup and install the language runtime.
/// </summary>
public bool UseLanguageVersionTools { get; set; }
}
}
55 changes: 45 additions & 10 deletions sdk/Pulumi.Automation/LocalWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -669,15 +669,8 @@ public override async Task<ImmutableDictionary<string, ConfigValue>> RefreshConf
/// <inheritdoc/>
public override async Task<WhoAmIResult> WhoAmIAsync(CancellationToken cancellationToken = default)
{
var version = _cmd.Version;
if (version == null)
{
// Assume an old version. Doesn't really matter what this is as long as it's pre-3.58.
version = new SemVersion(3, 0);
}

// 3.58 added the --json flag (https://github.com/pulumi/pulumi/releases/tag/v3.58.0)
if (version >= new SemVersion(3, 58))
if (SupportsCommand(new SemVersion(3, 58)))
{
// Use the new --json style
var result = await this.RunCommandAsync(new[] { "whoami", "--json" }, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -866,6 +859,42 @@ public override Task ChangeSecretsProviderAsync(string stackName, string newSecr
return this.RunCommandAsync(args, cancellationToken);
}

public override Task InstallAsync(InstallOptions? options = default, CancellationToken cancellationToken = default)
{
if (!SupportsCommand(new SemVersion(3, 91, 0)))
{
throw new InvalidOperationException("The Pulumi CLI version does not support the install command. Please update the Pulumi CLI.");
};

var args = new List<string> { "install" };

if (options is null)
{
return this.RunCommandAsync(args, cancellationToken);
}

if (options.UseLanguageVersionTools)
{
if (!SupportsCommand(new SemVersion(3, 130, 0)))
{
throw new InvalidOperationException($"The Pulumi CLI version does not support {nameof(options.UseLanguageVersionTools)}. Please update the Pulumi CLI.");
}

args.Add("--use-language-version-tools");
}

if (options.NoDependencies)
args.Add("--no-dependencies");

if (options.Reinstall)
args.Add("--reinstall");

if (options.NoPlugins)
args.Add("--no-plugins");

return this.RunCommandAsync(args, cancellationToken);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Expand Down Expand Up @@ -978,15 +1007,21 @@ internal IReadOnlyList<string> GetRemoteArgs()
return args;
}

private void CheckSupportsEnvironmentsCommands()
private bool SupportsCommand(SemVersion minSupportedVersion)
{
var version = _cmd.Version ?? new SemVersion(3, 0);

return version >= minSupportedVersion;
}

private void CheckSupportsEnvironmentsCommands()
{
// 3.95 added this command (https://github.com/pulumi/pulumi/releases/tag/v3.95.0)
if (version < new SemVersion(3, 95))
if (!SupportsCommand(new SemVersion(3, 95)))
{
throw new InvalidOperationException("The Pulumi CLI version does not support env operations on a stack. Please update the Pulumi CLI.");
}
}

}
}
28 changes: 28 additions & 0 deletions sdk/Pulumi.Automation/Pulumi.Automation.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions sdk/Pulumi.Automation/Workspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ public Task InstallPluginAsync(string name, string version, PluginKind kind, Can
/// <param name="cancellationToken">A cancellation token.</param>
public abstract Task ChangeSecretsProviderAsync(string stackName, string newSecretsProvider, SecretsProviderOptions? secretsProviderOptions = null, CancellationToken cancellationToken = default);

/// <summary>
/// Install packages and plugins for the current program or policy pack.
/// </summary>
/// <param name="options">The options to customize the install.</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns></returns>
public abstract Task InstallAsync(InstallOptions? options = null, CancellationToken cancellationToken = default);

internal async Task<CommandResult> RunStackCommandAsync(
string stackName,
IList<string> args,
Expand Down
Loading