Skip to content

Commit 7751f63

Browse files
julienpFrassle
andauthored
[auto] Add new API to install the Pulumi CLI from the Automation API (#226)
* [auto] Add new API to install the Pulumi CLI from the Automation API * Move async initialization code to CreateAsync * Rename PulumiCmd -> PulumiCommand * Get Go SDK version from pulumi-language-dotnet/go.mod * Implement InstallWindowsAsync * Fix download script extension * Review comments * Change IPulumiCommand interface to abstract class PulumiCommand * Seal CommandResult * Look for pulumi.exe in windows tests * Update PulumiEnvironment test for windows * Use LocalPulumiCommand.SkipVersionCheckVar in LocalWorkspace * Update build/Program.fs Co-authored-by: Fraser Waters <[email protected]> * Update sdk/Pulumi.Automation/Pulumi.Automation.csproj Co-authored-by: Fraser Waters <[email protected]> * Update build/Program.fs Co-authored-by: Fraser Waters <[email protected]> * Update build/Program.fs Co-authored-by: Fraser Waters <[email protected]> --------- Co-authored-by: Fraser Waters <[email protected]>
1 parent 557d710 commit 7751f63

15 files changed

+724
-321
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
component: sdk/auto
2+
kind: Improvements
3+
body: Add new API to install the Pulumi CLI from the Automation API
4+
time: 2024-01-25T13:32:17.304538+01:00
5+
custom:
6+
PR: "226"

build/Program.fs

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
open System
22
open System.IO
33
open System.Linq
4+
open System.Text.RegularExpressions
45
open Fake.IO
56
open Fake.Core
67
open Publish
@@ -27,6 +28,25 @@ let pulumiFSharp = Path.Combine(sdk, "Pulumi.FSharp")
2728
let integrationTests = Path.Combine(repositoryRoot, "integration_tests")
2829
let pulumiLanguageDotnet = Path.Combine(repositoryRoot, "pulumi-language-dotnet")
2930

31+
// Find the version of the Pulumi Go SDK that we are using for the language plugin.
32+
let findGoSDKVersion =
33+
let goMod = Path.Combine(pulumiLanguageDotnet, "go.mod")
34+
try
35+
let lines = File.ReadAllLines(goMod)
36+
let patternRegex = new Regex("^\\s*github.com/pulumi/pulumi/sdk", RegexOptions.IgnoreCase)
37+
match Array.tryFind (patternRegex.IsMatch) lines with
38+
| Some(matchingLine) ->
39+
let version = matchingLine.Split(' ')[1]
40+
let version = version.TrimStart('v')
41+
Some(version)
42+
| None ->
43+
None
44+
with
45+
| ex ->
46+
printfn "Error while trying to find the Go SDK version: %s" ex.Message
47+
48+
None
49+
3050
/// Runs `dotnet clean` command against the solution file,
3151
/// then proceeds to delete the `bin` and `obj` directory of each project in the solution
3252
let cleanSdk() =
@@ -79,9 +99,13 @@ let listIntegrationTests() =
7999
let buildSdk() =
80100
cleanSdk()
81101
restoreSdk()
82-
printfn "Building Pulumi SDK"
83-
if Shell.Exec("dotnet", "build --configuration Release", sdk) <> 0
84-
then failwith "build failed"
102+
match findGoSDKVersion with
103+
| None -> failwith "Could not find the Pulumi SDK version in go.mod"
104+
| Some(version) ->
105+
printfn "Building Pulumi SDK"
106+
if Shell.Exec("dotnet", "build --configuration Release -p:PulumiSdkVersion=" + version, sdk) <> 0
107+
108+
then failwith "build failed"
85109

86110
/// Publishes packages for Pulumi, Pulumi.Automation and Pulumi.FSharp to nuget.
87111
/// Requires NUGET_PUBLISH_KEY and PULUMI_VERSION environment variables.
@@ -148,10 +172,14 @@ let testPulumiSdk coverage =
148172
let testPulumiAutomationSdk coverage =
149173
cleanSdk()
150174
restoreSdk()
151-
printfn "Testing Pulumi Automation SDK"
152-
let coverageArgs = if coverage then $" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput={coverageDir}/coverage.pulumi.automation.xml" else ""
153-
if Shell.Exec("dotnet", "test --configuration Release" + coverageArgs, pulumiAutomationSdkTests) <> 0
154-
then failwith "automation tests failed"
175+
match findGoSDKVersion with
176+
| None -> failwith "Could not find the Pulumi SDK version in go.mod"
177+
| Some(version) ->
178+
printfn "Testing Pulumi Automation SDK"
179+
let coverageArgs = if coverage then $" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput={coverageDir}/coverage.pulumi.automation.xml" else ""
180+
if Shell.Exec("dotnet", $"test --configuration Release -p:PulumiSdkVersion={version} {coverageArgs}", pulumiAutomationSdkTests) <> 0
181+
182+
then failwith "automation tests failed"
155183

156184
let syncProtoFiles() = GitSync.repository {
157185
remoteRepository = "https://github.com/pulumi/pulumi.git"

sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs

-62
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright 2016-2024, Pulumi Corporation
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Runtime.InteropServices;
8+
using System.Threading.Tasks;
9+
using Pulumi.Automation.Commands;
10+
using Semver;
11+
using Xunit;
12+
13+
namespace Pulumi.Automation.Tests
14+
{
15+
public class LocalPulumiCommandTests
16+
{
17+
[Fact]
18+
public async Task CheckVersionCommand()
19+
{
20+
var localCmd = await LocalPulumiCommand.CreateAsync();
21+
var extraEnv = new Dictionary<string, string?>();
22+
var args = new[] { "version" };
23+
24+
var stdoutLines = new List<string>();
25+
var stderrLines = new List<string>();
26+
27+
// NOTE: not testing onEngineEvent arg as that is not
28+
// supported for "version"; to test it one needs
29+
// workspace-aware commands such as up or preview;
30+
// currently this is covered by
31+
// LocalWorkspaceTests.HandlesEvents.
32+
33+
var result = await localCmd.RunAsync(
34+
args, ".", extraEnv,
35+
onStandardOutput: line => stdoutLines.Add(line),
36+
onStandardError: line => stderrLines.Add(line));
37+
38+
Assert.Equal(0, result.Code);
39+
40+
Assert.Matches(@"^v?\d+\.\d+\.\d+", result.StandardOutput);
41+
// stderr must strictly begin with the version warning message or be an empty string:
42+
if (result.StandardError.Length > 0)
43+
{
44+
Assert.StartsWith("warning: A new version of Pulumi", result.StandardError);
45+
}
46+
47+
// If these tests begin failing, it may be because the automation output now emits CRLF
48+
// (\r\n) on Windows.
49+
//
50+
// If so, update the Lines method to split on Environment.NewLine instead of "\n".
51+
Assert.Equal(Lines(result.StandardOutput), stdoutLines.Select(x => x.Trim()).ToList());
52+
Assert.Equal(Lines(result.StandardError), stderrLines.Select(x => x.Trim()).ToList());
53+
}
54+
55+
private List<string> Lines(string s)
56+
{
57+
return s.Split("\n",
58+
StringSplitOptions.RemoveEmptyEntries)
59+
.Select(x => x.Trim())
60+
.ToList();
61+
}
62+
63+
[Fact]
64+
public async Task InstallDefaultRoot()
65+
{
66+
var requestedVersion = new SemVersion(3, 102, 0);
67+
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion });
68+
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
69+
var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi");
70+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
71+
{
72+
pulumiBin += ".exe";
73+
}
74+
Assert.True(File.Exists(pulumiBin));
75+
}
76+
77+
[Fact]
78+
public async Task InstallTwice()
79+
{
80+
var tempDir = Path.Combine(Path.GetTempPath(), "automation-test-" + Guid.NewGuid().ToString());
81+
Directory.CreateDirectory(tempDir);
82+
try
83+
{
84+
var requestedVersion = new SemVersion(3, 102, 0);
85+
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion, Root = tempDir });
86+
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
87+
var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi");
88+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
89+
{
90+
pulumiBin += ".exe";
91+
}
92+
var t1 = File.GetCreationTime(pulumiBin);
93+
// Install again with the same options
94+
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion, Root = tempDir });
95+
var t2 = File.GetCreationTime(pulumiBin);
96+
Assert.Equal(t1, t2);
97+
}
98+
finally
99+
{
100+
Directory.Delete(tempDir, true);
101+
}
102+
103+
}
104+
105+
[Fact]
106+
public async Task VersionCheck()
107+
{
108+
var dirPath = Path.Combine(Path.GetTempPath(), "automation-test-" + Guid.NewGuid().ToString());
109+
var dir = Directory.CreateDirectory(dirPath);
110+
try
111+
{
112+
// Install an old version
113+
var installed_version = new SemVersion(3, 99, 0);
114+
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = installed_version, Root = dirPath });
115+
116+
// Try to create a command with a more recent version
117+
var requested_version = new SemVersion(3, 102, 0);
118+
await Assert.ThrowsAsync<InvalidOperationException>(() => LocalPulumiCommand.CreateAsync(new LocalPulumiCommandOptions
119+
{
120+
Version = requested_version,
121+
Root = dirPath
122+
}));
123+
124+
// Opting out of the version check works
125+
await LocalPulumiCommand.CreateAsync(new LocalPulumiCommandOptions
126+
{
127+
Version = requested_version,
128+
Root = dirPath,
129+
SkipVersionCheck = true
130+
});
131+
}
132+
finally
133+
{
134+
dir.Delete(true);
135+
}
136+
}
137+
138+
[Fact]
139+
public void PulumiEnvironment()
140+
{
141+
// Plain "pulumi" command
142+
var env = new Dictionary<string, string?> { { "PATH", "/usr/bin" } };
143+
var newEnv = LocalPulumiCommand.PulumiEnvironment(env, "pulumi", false);
144+
Assert.Equal("/usr/bin", newEnv["PATH"]);
145+
146+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
147+
{
148+
env = new Dictionary<string, string?> { { "PATH", "%SystemRoot%\\system32" } };
149+
newEnv = LocalPulumiCommand.PulumiEnvironment(env, "C:\\some\\install\\root\\bin\\pulumi", false);
150+
Assert.Equal("C:\\some\\install\\root\\bin;%SystemRoot%\\system32", newEnv["PATH"]);
151+
}
152+
else
153+
{
154+
env = new Dictionary<string, string?> { { "PATH", "/usr/bin" } };
155+
newEnv = LocalPulumiCommand.PulumiEnvironment(env, "/some/install/root/bin/pulumi", false);
156+
Assert.Equal("/some/install/root/bin:/usr/bin", newEnv["PATH"]);
157+
}
158+
}
159+
160+
[Theory]
161+
[InlineData("100.0.0", true, false)]
162+
[InlineData("1.0.0", true, false)]
163+
[InlineData("2.22.0", false, false)]
164+
[InlineData("2.1.0", true, false)]
165+
[InlineData("2.21.2", false, false)]
166+
[InlineData("2.21.1", false, false)]
167+
[InlineData("2.21.0", true, false)]
168+
// Note that prerelease < release so this case should error
169+
[InlineData("2.21.1-alpha.1234", true, false)]
170+
[InlineData("2.20.0", false, true)]
171+
[InlineData("2.22.0", false, true)]
172+
// Invalid version check
173+
[InlineData("invalid", false, true)]
174+
[InlineData("invalid", true, false)]
175+
public void ValidVersionTheory(string currentVersion, bool errorExpected, bool optOut)
176+
{
177+
var testMinVersion = new SemVersion(2, 21, 1);
178+
179+
if (errorExpected)
180+
{
181+
void ValidatePulumiVersion() => LocalPulumiCommand.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
182+
Assert.Throws<InvalidOperationException>(ValidatePulumiVersion);
183+
}
184+
else
185+
{
186+
LocalPulumiCommand.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
187+
}
188+
}
189+
190+
}
191+
192+
}

sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs

-32
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,13 @@
1414
using Pulumi.Automation.Commands.Exceptions;
1515
using Pulumi.Automation.Events;
1616
using Pulumi.Automation.Exceptions;
17-
using Semver;
1817
using Serilog;
1918
using Serilog.Extensions.Logging;
2019
using Xunit;
2120
using Xunit.Abstractions;
2221
using ILogger = Microsoft.Extensions.Logging.ILogger;
2322

2423
using static Pulumi.Automation.Tests.Utility;
25-
using Xunit.Sdk;
2624

2725
namespace Pulumi.Automation.Tests
2826
{
@@ -1682,36 +1680,6 @@ public async Task PulumiVersionTest()
16821680
Assert.Matches("(\\d+\\.)(\\d+\\.)(\\d+)(-.*)?", workspace.PulumiVersion);
16831681
}
16841682

1685-
[Theory]
1686-
[InlineData("100.0.0", true, false)]
1687-
[InlineData("1.0.0", true, false)]
1688-
[InlineData("2.22.0", false, false)]
1689-
[InlineData("2.1.0", true, false)]
1690-
[InlineData("2.21.2", false, false)]
1691-
[InlineData("2.21.1", false, false)]
1692-
[InlineData("2.21.0", true, false)]
1693-
// Note that prerelease < release so this case should error
1694-
[InlineData("2.21.1-alpha.1234", true, false)]
1695-
[InlineData("2.20.0", false, true)]
1696-
[InlineData("2.22.0", false, true)]
1697-
// Invalid version check
1698-
[InlineData("invalid", false, true)]
1699-
[InlineData("invalid", true, false)]
1700-
public void ValidVersionTheory(string currentVersion, bool errorExpected, bool optOut)
1701-
{
1702-
var testMinVersion = new SemVersion(2, 21, 1);
1703-
1704-
if (errorExpected)
1705-
{
1706-
void ValidatePulumiVersion() => LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
1707-
Assert.Throws<InvalidOperationException>(ValidatePulumiVersion);
1708-
}
1709-
else
1710-
{
1711-
LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
1712-
}
1713-
}
1714-
17151683
[Fact]
17161684
public async Task RespectsProjectSettingsTest()
17171685
{

sdk/Pulumi.Automation/Commands/CommandResult.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Pulumi.Automation.Commands
66
{
7-
internal class CommandResult
7+
public sealed class CommandResult
88
{
99
public int Code { get; }
1010

0 commit comments

Comments
 (0)