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

[auto] Add new API to install the Pulumi CLI from the Automation API #226

Merged
merged 16 commits into from
Jan 30, 2024
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-226.yaml
Original file line number Diff line number Diff line change
@@ -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"
42 changes: 35 additions & 7 deletions build/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
open System
open System.IO
open System.Linq
open System.Text.RegularExpressions
open Fake.IO
open Fake.Core
open Publish
Expand All @@ -27,6 +28,25 @@ let pulumiFSharp = Path.Combine(sdk, "Pulumi.FSharp")
let integrationTests = Path.Combine(repositoryRoot, "integration_tests")
let pulumiLanguageDotnet = Path.Combine(repositoryRoot, "pulumi-language-dotnet")

// Find the version of the Pulumi Go SDK that we are using for the language plugin.
let findGoSDKVersion =
let goMod = Path.Combine(pulumiLanguageDotnet, "go.mod")
try
let lines = File.ReadAllLines(goMod)
let patternRegex = new Regex("^\\s*github.com/pulumi/pulumi/sdk", RegexOptions.IgnoreCase)
match Array.tryFind (patternRegex.IsMatch) lines with
| Some(matchingLine) ->
let version = matchingLine.Split(' ')[1]
let version = version.TrimStart('v')
Some(version)
| None ->
None
with
| ex ->
printfn "Error while trying to find the Go SDK version: %s" ex.Message

None

/// Runs `dotnet clean` command against the solution file,
/// then proceeds to delete the `bin` and `obj` directory of each project in the solution
let cleanSdk() =
Expand Down Expand Up @@ -79,9 +99,13 @@ let listIntegrationTests() =
let buildSdk() =
cleanSdk()
restoreSdk()
printfn "Building Pulumi SDK"
if Shell.Exec("dotnet", "build --configuration Release", sdk) <> 0
then failwith "build failed"
match findGoSDKVersion with
| None -> failwith "Could not find the Pulumi SDK version in go.mod"
| Some(version) ->
printfn "Building Pulumi SDK"
if Shell.Exec("dotnet", "build --configuration Release -p:PulumiSdkVersion=" + version, sdk) <> 0

then failwith "build failed"

/// Publishes packages for Pulumi, Pulumi.Automation and Pulumi.FSharp to nuget.
/// Requires NUGET_PUBLISH_KEY and PULUMI_VERSION environment variables.
Expand Down Expand Up @@ -148,10 +172,14 @@ let testPulumiSdk coverage =
let testPulumiAutomationSdk coverage =
cleanSdk()
restoreSdk()
printfn "Testing Pulumi Automation SDK"
let coverageArgs = if coverage then $" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput={coverageDir}/coverage.pulumi.automation.xml" else ""
if Shell.Exec("dotnet", "test --configuration Release" + coverageArgs, pulumiAutomationSdkTests) <> 0
then failwith "automation tests failed"
match findGoSDKVersion with
| None -> failwith "Could not find the Pulumi SDK version in go.mod"
| Some(version) ->
printfn "Testing Pulumi Automation SDK"
let coverageArgs = if coverage then $" -p:CollectCoverage=true -p:CoverletOutputFormat=cobertura -p:CoverletOutput={coverageDir}/coverage.pulumi.automation.xml" else ""
if Shell.Exec("dotnet", $"test --configuration Release -p:PulumiSdkVersion={version} {coverageArgs}", pulumiAutomationSdkTests) <> 0

then failwith "automation tests failed"

let syncProtoFiles() = GitSync.repository {
remoteRepository = "https://github.com/pulumi/pulumi.git"
Expand Down
62 changes: 0 additions & 62 deletions sdk/Pulumi.Automation.Tests/LocalPulumiCmdTests.cs

This file was deleted.

192 changes: 192 additions & 0 deletions sdk/Pulumi.Automation.Tests/LocalPulumiCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Copyright 2016-2024, Pulumi Corporation

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Pulumi.Automation.Commands;
using Semver;
using Xunit;

namespace Pulumi.Automation.Tests
{
public class LocalPulumiCommandTests
{
[Fact]
public async Task CheckVersionCommand()
{
var localCmd = await LocalPulumiCommand.CreateAsync();
var extraEnv = new Dictionary<string, string?>();
var args = new[] { "version" };

var stdoutLines = new List<string>();
var stderrLines = new List<string>();

// NOTE: not testing onEngineEvent arg as that is not
// supported for "version"; to test it one needs
// workspace-aware commands such as up or preview;
// currently this is covered by
// LocalWorkspaceTests.HandlesEvents.

var result = await localCmd.RunAsync(
args, ".", extraEnv,
onStandardOutput: line => stdoutLines.Add(line),
onStandardError: line => stderrLines.Add(line));

Assert.Equal(0, result.Code);

Assert.Matches(@"^v?\d+\.\d+\.\d+", result.StandardOutput);
// stderr must strictly begin with the version warning message or be an empty string:
if (result.StandardError.Length > 0)
{
Assert.StartsWith("warning: A new version of Pulumi", result.StandardError);
}

// If these tests begin failing, it may be because the automation output now emits CRLF
// (\r\n) on Windows.
//
// If so, update the Lines method to split on Environment.NewLine instead of "\n".
Assert.Equal(Lines(result.StandardOutput), stdoutLines.Select(x => x.Trim()).ToList());
Assert.Equal(Lines(result.StandardError), stderrLines.Select(x => x.Trim()).ToList());
}

private List<string> Lines(string s)
{
return s.Split("\n",
StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToList();
}

[Fact]
public async Task InstallDefaultRoot()
{
var requestedVersion = new SemVersion(3, 102, 0);
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion });
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi");
julienp marked this conversation as resolved.
Show resolved Hide resolved
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
pulumiBin += ".exe";
}
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 LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion, Root = tempDir });
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var pulumiBin = Path.Combine(home, ".pulumi", "versions", requestedVersion.ToString(), "bin", "pulumi");
julienp marked this conversation as resolved.
Show resolved Hide resolved
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
pulumiBin += ".exe";
}
var t1 = File.GetCreationTime(pulumiBin);
// Install again with the same options
await LocalPulumiCommand.Install(new LocalPulumiCommandOptions { Version = requestedVersion, Root = tempDir });
var t2 = File.GetCreationTime(pulumiBin);
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 LocalPulumiCommand.Install(new LocalPulumiCommandOptions { 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<InvalidOperationException>(() => LocalPulumiCommand.CreateAsync(new LocalPulumiCommandOptions
{
Version = requested_version,
Root = dirPath
}));

// Opting out of the version check works
await LocalPulumiCommand.CreateAsync(new LocalPulumiCommandOptions
{
Version = requested_version,
Root = dirPath,
SkipVersionCheck = true
});
}
finally
{
dir.Delete(true);
}
}

[Fact]
public void PulumiEnvironment()
{
// Plain "pulumi" command
var env = new Dictionary<string, string?> { { "PATH", "/usr/bin" } };
var newEnv = LocalPulumiCommand.PulumiEnvironment(env, "pulumi", false);
Assert.Equal("/usr/bin", newEnv["PATH"]);

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
env = new Dictionary<string, string?> { { "PATH", "%SystemRoot%\\system32" } };
newEnv = LocalPulumiCommand.PulumiEnvironment(env, "C:\\some\\install\\root\\bin\\pulumi", false);
Assert.Equal("C:\\some\\install\\root\\bin;%SystemRoot%\\system32", newEnv["PATH"]);
}
else
{
env = new Dictionary<string, string?> { { "PATH", "/usr/bin" } };
newEnv = LocalPulumiCommand.PulumiEnvironment(env, "/some/install/root/bin/pulumi", false);
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() => LocalPulumiCommand.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
Assert.Throws<InvalidOperationException>(ValidatePulumiVersion);
}
else
{
LocalPulumiCommand.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
}
}

}

}
32 changes: 0 additions & 32 deletions sdk/Pulumi.Automation.Tests/LocalWorkspaceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@
using Pulumi.Automation.Commands.Exceptions;
using Pulumi.Automation.Events;
using Pulumi.Automation.Exceptions;
using Semver;
using Serilog;
using Serilog.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
using ILogger = Microsoft.Extensions.Logging.ILogger;

using static Pulumi.Automation.Tests.Utility;
using Xunit.Sdk;

namespace Pulumi.Automation.Tests
{
Expand Down Expand Up @@ -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<InvalidOperationException>(ValidatePulumiVersion);
}
else
{
LocalWorkspace.ParseAndValidatePulumiVersion(testMinVersion, currentVersion, optOut);
}
}

[Fact]
public async Task RespectsProjectSettingsTest()
{
Expand Down
2 changes: 1 addition & 1 deletion sdk/Pulumi.Automation/Commands/CommandResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Pulumi.Automation.Commands
{
internal class CommandResult
public sealed class CommandResult
{
public int Code { get; }

Expand Down
Loading
Loading