From 387c2eb35369a0f69029dfc6735134a9a9069274 Mon Sep 17 00:00:00 2001 From: Julien Poissonnier Date: Thu, 25 Jan 2024 13:28:11 +0100 Subject: [PATCH] [auto] Add new API to install the Pulumi CLI from the Automation API --- .changes/unreleased/Improvements-226.yaml | 6 + .../LocalPulumiCmdTests.cs | 129 +++++++++- .../LocalWorkspaceTests.cs | 32 --- .../Commands/CommandResult.cs | 2 +- sdk/Pulumi.Automation/Commands/IPulumiCmd.cs | 5 +- .../Commands/LocalPulumiCmd.cs | 238 +++++++++++++++++- sdk/Pulumi.Automation/LocalWorkspace.cs | 67 ++--- .../LocalWorkspaceOptions.cs | 6 + sdk/Pulumi.Automation/RemoteWorkspace.cs | 2 +- sdk/Pulumi.Automation/Workspace.cs | 2 +- 10 files changed, 405 insertions(+), 84 deletions(-) create mode 100644 .changes/unreleased/Improvements-226.yaml diff --git a/.changes/unreleased/Improvements-226.yaml b/.changes/unreleased/Improvements-226.yaml new file mode 100644 index 00000000..ec4a8b61 --- /dev/null +++ b/.changes/unreleased/Improvements-226.yaml @@ -0,0 +1,6 @@ +component: sdk/auto +kind: Improvements +body: Add new API to install the Pulumi CLI from the Automation API +time: 2024-01-25T13:32:17.304538+01:00 +custom: + PR: "226" diff --git a/sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs b/sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs index a45c3c03..48893dd7 100644 --- a/sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs +++ b/sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs @@ -2,10 +2,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text.RegularExpressions; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Pulumi.Automation.Commands; +using Semver; using Xunit; namespace Pulumi.Automation.Tests @@ -15,7 +17,7 @@ public class LocalPulumiCmdTests [Fact] public async Task CheckVersionCommand() { - var localCmd = new LocalPulumiCmd(); + var localCmd = await LocalPulumiCmd.CreateAsync(); var extraEnv = new Dictionary(); var args = new[] { "version" }; @@ -57,6 +59,129 @@ private List Lines(string s) .Select(x => x.Trim()) .ToList(); } + + [Fact] + public async Task InstallDefaultRoot() + { + var requestedVersion = new SemVersion(3, 102, 0); + await LocalPulumiCmd.Install(new LocalPulumiCmdOptions { Version = requestedVersion }); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi"); + Assert.True(File.Exists(pulumiBin)); + } + + [Fact] + public async Task InstallTwice() + { + var tempDir = Path.Combine(Path.GetTempPath(), "automation-test-" + Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + try + { + var requestedVersion = new SemVersion(3, 102, 0); + await LocalPulumiCmd.Install(new LocalPulumiCmdOptions { Version = requestedVersion, Root = tempDir }); + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi"); + FileInfo fi1 = new FileInfo(pulumiBin); + var t1 = fi1.CreationTime; + // Install again with the same options + await LocalPulumiCmd.Install(new LocalPulumiCmdOptions { Version = requestedVersion, Root = tempDir }); + FileInfo fi2 = new FileInfo(pulumiBin); + var t2 = fi2.CreationTime; + Assert.Equal(t1, t2); + } + finally + { + Directory.Delete(tempDir, true); + } + + } + + [Fact] + public async Task VersionCheck() + { + var dirPath = Path.Combine(Path.GetTempPath(), "automation-test-" + Guid.NewGuid().ToString()); + var dir = Directory.CreateDirectory(dirPath); + try + { + // Install an old version + var installed_version = new SemVersion(3, 99, 0); + await LocalPulumiCmd.Install(new LocalPulumiCmdOptions { Version = installed_version, Root = dirPath }); + + // Try to create a command with a more recent version + var requested_version = new SemVersion(3, 102, 0); + await Assert.ThrowsAsync(() => LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions + { + Version = requested_version, + Root = dirPath + })); + + // Opting out of the version check works + await LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions + { + Version = requested_version, + Root = dirPath, + SkipVersionCheck = true + }); + } + finally + { + dir.Delete(true); + } + } + + [Fact] + public void PulumiEnvironment() + { + var env = new Dictionary{ + {"PATH", "/usr/bin"} + }; + var newEnv = LocalPulumiCmd.PulumiEnvironment(env, "pulumi", false); + Assert.Equal("/usr/bin", newEnv["PATH"]); + + env = new Dictionary{ + {"PATH", "/usr/bin"} + }; + newEnv = LocalPulumiCmd.PulumiEnvironment(env, "/some/install/root/bin/pulumi", false); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Equal("/some/install/root/bin;/usr/bin", newEnv["PATH"]); + } + else + { + Assert.Equal("/some/install/root/bin:/usr/bin", newEnv["PATH"]); + } + } + + [Theory] + [InlineData("100.0.0", true, false)] + [InlineData("1.0.0", true, false)] + [InlineData("2.22.0", false, false)] + [InlineData("2.1.0", true, false)] + [InlineData("2.21.2", false, false)] + [InlineData("2.21.1", false, false)] + [InlineData("2.21.0", true, false)] + // Note that prerelease < release so this case should error + [InlineData("2.21.1-alpha.1234", true, false)] + [InlineData("2.20.0", false, true)] + [InlineData("2.22.0", false, true)] + // Invalid version check + [InlineData("invalid", false, true)] + [InlineData("invalid", true, false)] + public void ValidVersionTheory(string currentVersion, bool errorExpected, bool optOut) + { + var testMinVersion = new SemVersion(2, 21, 1); + + if (errorExpected) + { + void ValidatePulumiVersion() => LocalPulumiCmd.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); + Assert.Throws(ValidatePulumiVersion); + } + else + { + LocalPulumiCmd.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); + } + } + } } diff --git a/sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs b/sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs index bb38589e..091196e4 100644 --- a/sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs +++ b/sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs @@ -14,7 +14,6 @@ using Pulumi.Automation.Commands.Exceptions; using Pulumi.Automation.Events; using Pulumi.Automation.Exceptions; -using Semver; using Serilog; using Serilog.Extensions.Logging; using Xunit; @@ -22,7 +21,6 @@ using ILogger = Microsoft.Extensions.Logging.ILogger; using static Pulumi.Automation.Tests.Utility; -using Xunit.Sdk; namespace Pulumi.Automation.Tests { @@ -1682,36 +1680,6 @@ public async Task PulumiVersionTest() Assert.Matches("(\\d+\\.)(\\d+\\.)(\\d+)(-.*)?", workspace.PulumiVersion); } - [Theory] - [InlineData("100.0.0", true, false)] - [InlineData("1.0.0", true, false)] - [InlineData("2.22.0", false, false)] - [InlineData("2.1.0", true, false)] - [InlineData("2.21.2", false, false)] - [InlineData("2.21.1", false, false)] - [InlineData("2.21.0", true, false)] - // Note that prerelease < release so this case should error - [InlineData("2.21.1-alpha.1234", true, false)] - [InlineData("2.20.0", false, true)] - [InlineData("2.22.0", false, true)] - // Invalid version check - [InlineData("invalid", false, true)] - [InlineData("invalid", true, false)] - public void ValidVersionTheory(string currentVersion, bool errorExpected, bool optOut) - { - var testMinVersion = new SemVersion(2, 21, 1); - - if (errorExpected) - { - void ValidatePulumiVersion() => LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); - Assert.Throws(ValidatePulumiVersion); - } - else - { - LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut); - } - } - [Fact] public async Task RespectsProjectSettingsTest() { diff --git a/sdk/Pulumi.Automation/Commands/CommandResult.cs b/sdk/Pulumi.Automation/Commands/CommandResult.cs index 1a453147..0ed0dd37 100644 --- a/sdk/Pulumi.Automation/Commands/CommandResult.cs +++ b/sdk/Pulumi.Automation/Commands/CommandResult.cs @@ -4,7 +4,7 @@ namespace Pulumi.Automation.Commands { - internal class CommandResult + public class CommandResult { public int Code { get; } diff --git a/sdk/Pulumi.Automation/Commands/IPulumiCmd.cs b/sdk/Pulumi.Automation/Commands/IPulumiCmd.cs index 5a0ea79d..0cf28141 100644 --- a/sdk/Pulumi.Automation/Commands/IPulumiCmd.cs +++ b/sdk/Pulumi.Automation/Commands/IPulumiCmd.cs @@ -5,11 +5,14 @@ using System.Threading; using System.Threading.Tasks; using Pulumi.Automation.Events; +using Semver; namespace Pulumi.Automation.Commands { - internal interface IPulumiCmd + public interface IPulumiCmd { + SemVersion? Version { get; } + Task RunAsync( IList args, string workingDir, diff --git a/sdk/Pulumi.Automation/Commands/LocalPulumiCmd.cs b/sdk/Pulumi.Automation/Commands/LocalPulumiCmd.cs index e0413130..880f2cf5 100644 --- a/sdk/Pulumi.Automation/Commands/LocalPulumiCmd.cs +++ b/sdk/Pulumi.Automation/Commands/LocalPulumiCmd.cs @@ -5,18 +5,243 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using Semver; using CliWrap; +using CliWrap.Buffered; using Pulumi.Automation.Commands.Exceptions; using Pulumi.Automation.Events; + namespace Pulumi.Automation.Commands { - internal class LocalPulumiCmd : IPulumiCmd + /// + /// Options to configure a instance. + /// + public class LocalPulumiCmdOptions + { + /// + /// The version of the Pulumi CLI to install or the minimum version requirement for an existing installation. + /// + public SemVersion? Version { get; set; } + /// + /// The directory where to install the Pulumi CLI to or where to find an existing installation. + /// + public string? Root { get; set; } + /// + /// If true, skips the version validation that checks if an existing Pulumi CLI installation is compatible with the SDK. + /// + public bool SkipVersionCheck { get; set; } + } + + /// + /// A implementation that uses a locally installed Pulumi CLI. + /// + public class LocalPulumiCmd : IPulumiCmd { + // TODO: move to shared place with LocalWorkspace + private static string SkipVersionCheckVar = "PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK"; + private static readonly SemVersion _minimumVersion = new SemVersion(3, 1, 0); + internal Task ReadyTask { get; } + private readonly string _command; + private SemVersion? _version; + + /// + /// The version of the Pulumi CLI that is being used. + /// + public SemVersion? Version { get => _version; } + + /// + /// Creates a new LocalPulumiCmd instance. + /// + /// Options to configure the LocalPulumiCmd. + /// A cancellation token. + /// + public static async Task CreateAsync( + LocalPulumiCmdOptions? options = null, + CancellationToken cancellationToken = default) + { + var cmd = new LocalPulumiCmd(options, cancellationToken); + await cmd.ReadyTask.ConfigureAwait(false); + return cmd; + } + + private LocalPulumiCmd(LocalPulumiCmdOptions? options, CancellationToken cancellationToken) + { + var readyTasks = new List(); + + if (options?.Root != null) + { + _command = Path.Combine(options.Root, "bin", "pulumi"); + } + else + { + _command = "pulumi"; + } + + var minimumVersion = _minimumVersion; + if (options?.Version != null && options.Version > minimumVersion) + { + minimumVersion = options.Version; + } + + var optOut = options?.SkipVersionCheck ?? Environment.GetEnvironmentVariable(SkipVersionCheckVar) != null; + readyTasks.Add(SetPulumiVersionAsync(minimumVersion, _command, optOut, cancellationToken)); + ReadyTask = Task.WhenAll(readyTasks); + } + + private async Task SetPulumiVersionAsync(SemVersion minimumVersion, string command, bool optOut, CancellationToken cancellationToken) + { + var result = await Cli.Wrap(command) + .WithArguments("version") + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(cancellationToken); + if (result.ExitCode != 0) + { + throw new Exception($"failed to get pulumi version: {result.StandardOutput ?? ""}"); + } + var version = result.StandardOutput.Trim().TrimStart('v'); + _version = ParseAndValidatePulumiVersion(minimumVersion, version, optOut); + } + + /// + /// Installs the Pulumi CLI if it is not already installed and returns a new LocalPulumiCmd instance. + /// + /// Options to configure the LocalPulumiCmd. + /// A cancellation token. + public static async Task Install(LocalPulumiCmdOptions? options = null, CancellationToken cancellationToken = default) + { + var _assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version; + if (_assemblyVersion == null) + { + throw new Exception("Failed to get assembly version."); + } + var assemblyVersion = new SemVersion(_assemblyVersion.Major, _assemblyVersion.Minor, _assemblyVersion.Build); + var version = options?.Version ?? assemblyVersion; + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + var optionsWithDefaults = new LocalPulumiCmdOptions + { + Version = version, + Root = options?.Root ?? Path.Combine(home, ".pulumi", "versions", version.ToString()) + }; + + try + { + return await CreateAsync(optionsWithDefaults, cancellationToken); + } + catch (Exception) + { + // Ignore + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + await InstallWindowsAsync(optionsWithDefaults.Version, optionsWithDefaults.Root, cancellationToken); + } + else + { + await InstallPosixAsync(optionsWithDefaults.Version, optionsWithDefaults.Root, cancellationToken); + } + + return await CreateAsync(optionsWithDefaults, cancellationToken); + } + + private static async Task InstallWindowsAsync(SemVersion version, string root, CancellationToken cancellationToken) + { + var scriptPath = await DownloadToTmpFileAsync("https://get.pulumi.com/install.sh", "install-*.ps1", cancellationToken); + // TODO: + throw new NotImplementedException(); + } + + private static async Task InstallPosixAsync(SemVersion version, string root, CancellationToken cancellationToken) + { + var scriptPath = await DownloadToTmpFileAsync("https://get.pulumi.com/install.sh", "install-*.sh", cancellationToken); + try + { + var args = new string[] { "--no-edit-path", "--install-root", root, "--version", version.ToString() }; + var result = await Cli.Wrap(scriptPath).WithArguments(args, escape: true) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(cancellationToken); + if (result.ExitCode != 0) + { + throw new Exception($"Failed to install Pulumi ${version} in ${root}: ${result.StandardError}"); + } + } + finally + { + File.Delete(scriptPath); + } + } + + private static async Task DownloadToTmpFileAsync(string url, string extension, CancellationToken cancellationToken) + { + var response = await new HttpClient().GetAsync(url); + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Failed to download {url}"); + } + var scriptData = await response.Content.ReadAsByteArrayAsync(); + string tempFile = Path.GetTempFileName(); + string tempFileWithExtension = Path.ChangeExtension(tempFile, extension); + File.Move(tempFile, tempFileWithExtension); + try + { + + await File.WriteAllBytesAsync(tempFileWithExtension, scriptData); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // TODO: In .net7 there is File.SetUnixFileMode https://learn.microsoft.com/en-us/dotnet/api/system.io.file.setunixfilemode?view=net-7.0 + var args = new string[] { "u+x", tempFileWithExtension }; + var result = await Cli.Wrap("chmod") + .WithArguments(args, escape: true) + .WithValidation(CommandResultValidation.None) + .ExecuteAsync(cancellationToken); + if (result.ExitCode != 0) + { + throw new Exception($"Failed to chmod u+x {tempFileWithExtension}"); + } + } + + return tempFileWithExtension; + } + catch (Exception) + { + File.Delete(tempFileWithExtension); + throw; + } + } + + internal static SemVersion? ParseAndValidatePulumiVersion(SemVersion minVersion, string currentVersion, bool optOut) + { + if (!SemVersion.TryParse(currentVersion, SemVersionStyles.Any, out SemVersion? version)) + { + version = null; + } + if (optOut) + { + return version; + } + if (version == null) + { + throw new InvalidOperationException("Failed to get Pulumi version. This is probably a pulumi error. You can override by version checking by setting {SkipVersionCheckVar}=true."); + } + if (minVersion.Major < version.Major) + { + throw new InvalidOperationException($"Major version mismatch. You are using Pulumi CLI version {version} with Automation SDK v{minVersion.Major}. Please update the SDK."); + } + if (minVersion > version) + { + throw new InvalidOperationException($"Minimum version requirement failed. The minimum CLI version requirement is {minVersion}, your current CLI version is {version}. Please update the Pulumi CLI."); + } + return version; + } public async Task RunAsync( IList args, @@ -53,6 +278,7 @@ private async Task RunAsyncInner( EventLogFile? eventLogFile = null, CancellationToken cancellationToken = default) { + var stdOutBuffer = new StringBuilder(); var stdOutPipe = PipeTarget.ToStringBuilder(stdOutBuffer); if (onStandardOutput != null) @@ -70,7 +296,7 @@ private async Task RunAsyncInner( var pulumiCmd = Cli.Wrap("pulumi") .WithArguments(PulumiArgs(args, eventLogFile), escape: true) .WithWorkingDirectory(workingDir) - .WithEnvironmentVariables(PulumiEnvironment(additionalEnv, debugCommands: eventLogFile != null)) + .WithEnvironmentVariables(PulumiEnvironment(additionalEnv, _command, debugCommands: eventLogFile != null)) .WithStandardOutputPipe(stdOutPipe) .WithStandardErrorPipe(stdErrPipe) .WithValidation(CommandResultValidation.None); // we check non-0 exit code ourselves @@ -90,7 +316,7 @@ private async Task RunAsyncInner( return result; } - private static IReadOnlyDictionary PulumiEnvironment(IDictionary additionalEnv, bool debugCommands) + internal static IReadOnlyDictionary PulumiEnvironment(IDictionary additionalEnv, string command, bool debugCommands) { var env = new Dictionary(additionalEnv); @@ -101,6 +327,12 @@ private async Task RunAsyncInner( env["PULUMI_DEBUG_COMMANDS"] = "true"; } + // Prefix PATH with the directory of the pulumi command being run to ensure we prioritize the bundled plugins from the CLI installation. + if (Path.IsPathRooted(command)) + { + env["PATH"] = Path.GetDirectoryName(command) + Path.PathSeparator + env["PATH"]; + } + return env; } diff --git a/sdk/Pulumi.Automation/LocalWorkspace.cs b/sdk/Pulumi.Automation/LocalWorkspace.cs index c5204217..f84b5908 100644 --- a/sdk/Pulumi.Automation/LocalWorkspace.cs +++ b/sdk/Pulumi.Automation/LocalWorkspace.cs @@ -33,8 +33,6 @@ namespace Pulumi.Automation /// public sealed class LocalWorkspace : Workspace { - private static readonly SemVersion _minimumVersion = new SemVersion(3, 1, 0); - private readonly LocalSerializer _serializer = new LocalSerializer(); private readonly bool _ownsWorkingDir; private readonly RemoteGitProgramArgs? _remoteGitProgramArgs; @@ -50,9 +48,8 @@ public sealed class LocalWorkspace : Workspace /// public override string? PulumiHome { get; } - private SemVersion? _pulumiVersion; /// - public override string PulumiVersion => _pulumiVersion?.ToString() ?? throw new InvalidOperationException("Failed to get Pulumi version."); + public override string PulumiVersion => _cmd.Version?.ToString() ?? throw new InvalidOperationException("Failed to get Pulumi version."); /// public override string? SecretsProvider { get; } @@ -81,8 +78,12 @@ public static async Task CreateAsync( LocalWorkspaceOptions? options = null, CancellationToken cancellationToken = default) { + var cmd = options?.PulumiCmd ?? await LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions + { + SkipVersionCheck = OptOutOfVersionCheck(options?.EnvironmentVariables) + }, cancellationToken); var ws = new LocalWorkspace( - new LocalPulumiCmd(), + cmd, options, cancellationToken); await ws.ReadyTask.ConfigureAwait(false); @@ -286,7 +287,10 @@ private static async Task CreateStackHelperAsync( throw new ArgumentNullException(nameof(args.ProjectSettings)); var ws = new LocalWorkspace( - new LocalPulumiCmd(), + await LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions + { + SkipVersionCheck = OptOutOfVersionCheck(), + }, cancellationToken), args, cancellationToken); await ws.ReadyTask.ConfigureAwait(false); @@ -300,7 +304,10 @@ private static async Task CreateStackHelperAsync( CancellationToken cancellationToken) { var ws = new LocalWorkspace( - new LocalPulumiCmd(), + await LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions + { + SkipVersionCheck = OptOutOfVersionCheck() + }, cancellationToken), args, cancellationToken); await ws.ReadyTask.ConfigureAwait(false); @@ -356,7 +363,7 @@ internal LocalWorkspace( this.WorkDir = dir; - readyTasks.Add(this.PopulatePulumiVersionAsync(cancellationToken)); + readyTasks.Add(this.CheckRemoteSupport(cancellationToken)); if (options?.ProjectSettings != null) { @@ -394,18 +401,17 @@ private async Task InitializeProjectSettingsAsync(ProjectSettings projectSetting private static readonly string[] _settingsExtensions = { ".yaml", ".yml", ".json" }; - private async Task PopulatePulumiVersionAsync(CancellationToken cancellationToken) + private static bool OptOutOfVersionCheck(IDictionary? EnvironmentVariables = null) { - var result = await this.RunCommandAsync(new[] { "version" }, cancellationToken).ConfigureAwait(false); - var versionString = result.StandardOutput.Trim(); - versionString = versionString.TrimStart('v'); - - var hasSkipEnvVar = this.EnvironmentVariables?.ContainsKey(SkipVersionCheckVar) ?? false; + var hasSkipEnvVar = EnvironmentVariables?.ContainsKey(SkipVersionCheckVar) ?? false; var optOut = hasSkipEnvVar || Environment.GetEnvironmentVariable(SkipVersionCheckVar) != null; - this._pulumiVersion = ParseAndValidatePulumiVersion(_minimumVersion, versionString, optOut); + return optOut; + } + private async Task CheckRemoteSupport(CancellationToken cancellationToken) + { // If remote was specified, ensure the CLI supports it. - if (!optOut && Remote) + if (!OptOutOfVersionCheck(this.EnvironmentVariables) && Remote) { // See if `--remote` is present in `pulumi preview --help`'s output. var args = new[] { "preview", "--help" }; @@ -417,31 +423,6 @@ private async Task PopulatePulumiVersionAsync(CancellationToken cancellationToke } } - internal static SemVersion? ParseAndValidatePulumiVersion(SemVersion minVersion, string currentVersion, bool optOut) - { - if (!SemVersion.TryParse(currentVersion, SemVersionStyles.Any, out SemVersion? version)) - { - version = null; - } - if (optOut) - { - return version; - } - if (version == null) - { - throw new InvalidOperationException("Failed to get Pulumi version. This is probably a pulumi error. You can override by version checking by setting {SkipVersionCheckVar}=true."); - } - if (minVersion.Major < version.Major) - { - throw new InvalidOperationException($"Major version mismatch. You are using Pulumi CLI version {version} with Automation SDK v{minVersion.Major}. Please update the SDK."); - } - if (minVersion > version) - { - throw new InvalidOperationException($"Minimum version requirement failed. The minimum CLI version requirement is {minVersion}, your current CLI version is {version}. Please update the Pulumi CLI."); - } - return version; - } - /// public override async Task GetProjectSettingsAsync(CancellationToken cancellationToken = default) { @@ -690,7 +671,7 @@ public override async Task> RefreshConf /// public override async Task WhoAmIAsync(CancellationToken cancellationToken = default) { - var version = this._pulumiVersion; + 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. @@ -983,7 +964,7 @@ internal IReadOnlyList GetRemoteArgs() private void CheckSupportsEnvironmentsCommands() { - var version = this._pulumiVersion ?? new SemVersion(3, 0); + var version = _cmd.Version ?? new SemVersion(3, 0); // 3.95 added this command (https://github.com/pulumi/pulumi/releases/tag/v3.95.0) if (version < new SemVersion(3, 95)) diff --git a/sdk/Pulumi.Automation/LocalWorkspaceOptions.cs b/sdk/Pulumi.Automation/LocalWorkspaceOptions.cs index 1af7d7c9..3e83f23e 100644 --- a/sdk/Pulumi.Automation/LocalWorkspaceOptions.cs +++ b/sdk/Pulumi.Automation/LocalWorkspaceOptions.cs @@ -21,6 +21,12 @@ public class LocalWorkspaceOptions /// public string? PulumiHome { get; set; } + + /// + /// The Pulumi CLI installation to use. + /// + public Commands.IPulumiCmd? PulumiCmd { get; set; } + /// /// The secrets provider to user for encryption and decryption of stack secrets. /// diff --git a/sdk/Pulumi.Automation/RemoteWorkspace.cs b/sdk/Pulumi.Automation/RemoteWorkspace.cs index 2b8e15f8..34835d46 100644 --- a/sdk/Pulumi.Automation/RemoteWorkspace.cs +++ b/sdk/Pulumi.Automation/RemoteWorkspace.cs @@ -112,7 +112,7 @@ private static async Task CreateStackHelperAsync( }; var ws = new LocalWorkspace( - new LocalPulumiCmd(), + await LocalPulumiCmd.CreateAsync(new LocalPulumiCmdOptions(), cancellationToken), localArgs, cancellationToken); await ws.ReadyTask.ConfigureAwait(false); diff --git a/sdk/Pulumi.Automation/Workspace.cs b/sdk/Pulumi.Automation/Workspace.cs index 561a17a1..a9aa1d15 100644 --- a/sdk/Pulumi.Automation/Workspace.cs +++ b/sdk/Pulumi.Automation/Workspace.cs @@ -23,7 +23,7 @@ namespace Pulumi.Automation /// public abstract class Workspace : IDisposable { - private readonly IPulumiCmd _cmd; + internal readonly IPulumiCmd _cmd; internal Workspace(IPulumiCmd cmd) {