diff --git a/changelog/pending/20221031--auto-dotnet--support-for-remote-operations.yaml b/changelog/pending/20221031--auto-dotnet--support-for-remote-operations.yaml new file mode 100644 index 000000000000..0aa1a0201442 --- /dev/null +++ b/changelog/pending/20221031--auto-dotnet--support-for-remote-operations.yaml @@ -0,0 +1,4 @@ +changes: +- type: feat + scope: auto/dotnet + description: Support for remote operations diff --git a/sdk/dotnet/Pulumi.Automation.Tests/RemoteWorkspaceTests.cs b/sdk/dotnet/Pulumi.Automation.Tests/RemoteWorkspaceTests.cs new file mode 100644 index 000000000000..bebe63a916d1 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation.Tests/RemoteWorkspaceTests.cs @@ -0,0 +1,24 @@ +// Copyright 2016-2022, Pulumi Corporation + +using Xunit; + +namespace Pulumi.Automation.Tests +{ + public class RemoteWorkspaceTests + { + [Theory] + [InlineData("owner/project/stack", true)] + [InlineData("", false)] + [InlineData("name", false)] + [InlineData("owner/name", false)] + [InlineData("/", false)] + [InlineData("//", false)] + [InlineData("///", false)] + [InlineData("owner/project/stack/wat", false)] + public void IsFullyQualifiedStackName(string input, bool expected) + { + var actual = RemoteWorkspace.IsFullyQualifiedStackName(input); + Assert.Equal(expected, actual); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/EnvironmentVariableValue.cs b/sdk/dotnet/Pulumi.Automation/EnvironmentVariableValue.cs new file mode 100644 index 000000000000..5aaf4edb3475 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/EnvironmentVariableValue.cs @@ -0,0 +1,19 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + public class EnvironmentVariableValue + { + public string Value { get; set; } + + public bool IsSecret { get; set; } + + public EnvironmentVariableValue( + string value, + bool isSecret = false) + { + Value = value; + IsSecret = isSecret; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs index a811720c4deb..567be86e1799 100644 --- a/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs +++ b/sdk/dotnet/Pulumi.Automation/LocalWorkspace.cs @@ -1,4 +1,4 @@ -// Copyright 2016-2021, Pulumi Corporation +// Copyright 2016-2022, Pulumi Corporation using System; using System.Collections.Generic; @@ -33,10 +33,15 @@ 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 Task _readyTask; - private static readonly SemVersion _minimumVersion = new SemVersion(3, 1, 0); + private readonly RemoteGitProgramArgs? _remoteGitProgramArgs; + private readonly IDictionary? _remoteEnvironmentVariables; + private readonly IList? _remotePreRunCommands; + + internal Task ReadyTask { get; } /// public override string WorkDir { get; } @@ -60,6 +65,11 @@ public sealed class LocalWorkspace : Workspace /// public override IDictionary? EnvironmentVariables { get; set; } + /// + /// Whether this workspace is a remote workspace. + /// + internal bool Remote { get; } + /// /// Creates a workspace using the specified options. Used for maximal control and /// customization of the underlying environment before any stacks are created or selected. @@ -74,7 +84,7 @@ public sealed class LocalWorkspace : Workspace new LocalPulumiCmd(), options, cancellationToken); - await ws._readyTask.ConfigureAwait(false); + await ws.ReadyTask.ConfigureAwait(false); return ws; } @@ -278,7 +288,7 @@ public static Task CreateOrSelectStackAsync(LocalProgramArgs arg new LocalPulumiCmd(), args, cancellationToken); - await ws._readyTask.ConfigureAwait(false); + await ws.ReadyTask.ConfigureAwait(false); return await initFunc(args.StackName, ws, cancellationToken).ConfigureAwait(false); } @@ -292,7 +302,7 @@ public static Task CreateOrSelectStackAsync(LocalProgramArgs arg new LocalPulumiCmd(), args, cancellationToken); - await ws._readyTask.ConfigureAwait(false); + await ws.ReadyTask.ConfigureAwait(false); return await initFunc(args.StackName, ws, cancellationToken).ConfigureAwait(false); } @@ -315,9 +325,20 @@ public static Task CreateOrSelectStackAsync(LocalProgramArgs arg this.Program = options.Program; this.Logger = options.Logger; this.SecretsProvider = options.SecretsProvider; + this.Remote = options.Remote; + this._remoteGitProgramArgs = options.RemoteGitProgramArgs; if (options.EnvironmentVariables != null) this.EnvironmentVariables = new Dictionary(options.EnvironmentVariables); + + if (options.RemoteEnvironmentVariables != null) + this._remoteEnvironmentVariables = + new Dictionary(options.RemoteEnvironmentVariables); + + if (options.RemotePreRunCommands != null) + { + this._remotePreRunCommands = new List(options.RemotePreRunCommands); + } } if (string.IsNullOrWhiteSpace(dir)) @@ -346,7 +367,7 @@ public static Task CreateOrSelectStackAsync(LocalProgramArgs arg readyTasks.Add(this.SaveStackSettingsAsync(pair.Key, pair.Value, cancellationToken)); } - this._readyTask = Task.WhenAll(readyTasks); + ReadyTask = Task.WhenAll(readyTasks); } private async Task InitializeProjectSettingsAsync(ProjectSettings projectSettings, @@ -380,6 +401,18 @@ private async Task PopulatePulumiVersionAsync(CancellationToken cancellationToke var hasSkipEnvVar = this.EnvironmentVariables?.ContainsKey(SkipVersionCheckVar) ?? false; var optOut = hasSkipEnvVar || Environment.GetEnvironmentVariable(SkipVersionCheckVar) != null; this._pulumiVersion = ParseAndValidatePulumiVersion(_minimumVersion, versionString, optOut); + + // If remote was specified, ensure the CLI supports it. + if (!optOut && Remote) + { + // See if `--remote` is present in `pulumi preview --help`'s output. + var args = new[] { "preview", "--help" }; + var previewResult = await RunCommandAsync(args, cancellationToken).ConfigureAwait(false); + if (!previewResult.StandardOutput.Contains("--remote")) + { + throw new InvalidOperationException("The Pulumi CLI does not support remote operations. Please update the Pulumi CLI."); + } + } } internal static SemVersion? ParseAndValidatePulumiVersion(SemVersion minVersion, string currentVersion, bool optOut) @@ -582,12 +615,30 @@ public override Task CreateStackAsync(string stackName, CancellationToken cancel if (!string.IsNullOrWhiteSpace(this.SecretsProvider)) args.AddRange(new[] { "--secrets-provider", this.SecretsProvider }); + if (Remote) + args.Add("--no-select"); + return this.RunCommandAsync(args, cancellationToken); } /// public override Task SelectStackAsync(string stackName, CancellationToken cancellationToken) - => this.RunCommandAsync(new[] { "stack", "select", stackName }, cancellationToken); + { + // If this is a remote workspace, we don't want to actually select the stack (which would modify + // global state); but we will ensure the stack exists by calling `pulumi stack`. + var args = new List + { + "stack", + }; + if (!Remote) + { + args.Add("select"); + } + args.Add("--stack"); + args.Add(stackName); + + return RunCommandAsync(args, cancellationToken); + } /// public override Task RemoveStackAsync(string stackName, CancellationToken cancellationToken = default) @@ -730,5 +781,89 @@ public override void Dispose() } } } + + internal IReadOnlyList GetRemoteArgs() + { + if (!Remote) + { + return Array.Empty(); + } + + var args = new List + { + "--remote" + }; + + if (_remoteGitProgramArgs != null) + { + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Url)) + { + args.Add(_remoteGitProgramArgs.Url); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.ProjectPath)) + { + args.Add("--remote-git-repo-dir"); + args.Add(_remoteGitProgramArgs.ProjectPath); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Branch)) + { + args.Add("--remote-git-branch"); + args.Add(_remoteGitProgramArgs.Branch); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.CommitHash)) + { + args.Add("--remote-git-commit"); + args.Add(_remoteGitProgramArgs.CommitHash); + } + if (_remoteGitProgramArgs.Auth != null) + { + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Auth.PersonalAccessToken)) + { + args.Add("--remote-git-auth-access-token"); + args.Add(_remoteGitProgramArgs.Auth.PersonalAccessToken); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Auth.SshPrivateKey)) + { + args.Add("--remote-git-auth-ssh-private-key"); + args.Add(_remoteGitProgramArgs.Auth.SshPrivateKey); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Auth.SshPrivateKeyPath)) + { + args.Add("--remote-git-auth-ssh-private-key-path"); + args.Add(_remoteGitProgramArgs.Auth.SshPrivateKeyPath); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Auth.Password)) + { + args.Add("--remote-git-auth-password"); + args.Add(_remoteGitProgramArgs.Auth.Password); + } + if (!string.IsNullOrEmpty(_remoteGitProgramArgs.Auth.Username)) + { + args.Add("--remote-git-username"); + args.Add(_remoteGitProgramArgs.Auth.Username); + } + } + } + + if (_remoteEnvironmentVariables != null) + { + foreach (var (name, value) in _remoteEnvironmentVariables) + { + args.Add(value.IsSecret ? "--remote-env-secret" : "--remote-env"); + args.Add($"{name}={value.Value}"); + } + } + + if (_remotePreRunCommands != null) + { + foreach (var command in _remotePreRunCommands) + { + args.Add("--remote-pre-run-command"); + args.Add(command); + } + } + + return args; + } } } diff --git a/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs b/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs index d15c09513239..ad5e04ad60fd 100644 --- a/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs +++ b/sdk/dotnet/Pulumi.Automation/LocalWorkspaceOptions.cs @@ -1,4 +1,4 @@ -// Copyright 2016-2021, Pulumi Corporation +// Copyright 2016-2022, Pulumi Corporation using System.Collections.Generic; using Microsoft.Extensions.Logging; @@ -64,5 +64,25 @@ public class LocalWorkspaceOptions /// . /// public IDictionary? StackSettings { get; set; } + + /// + /// Whether the workspace is a remote workspace. + /// + internal bool Remote { get; set; } + + /// + /// Args for remote workspace with Git source. + /// + internal RemoteGitProgramArgs? RemoteGitProgramArgs { get; set; } + + /// + /// Environment values scoped to the remote workspace. These will be passed to remote operations. + /// + internal IDictionary? RemoteEnvironmentVariables { get; set; } + + /// + /// An optional list of arbitrary commands to run before a remote Pulumi operation is invoked. + /// + internal IList? RemotePreRunCommands { get; set; } } } diff --git a/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt b/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt index f3084bfd95a4..f519b5518324 100644 --- a/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt +++ b/sdk/dotnet/Pulumi.Automation/PublicAPI.Shipped.txt @@ -13,6 +13,12 @@ Pulumi.Automation.DestroyOptions Pulumi.Automation.DestroyOptions.DestroyOptions() -> void Pulumi.Automation.DestroyOptions.TargetDependents.get -> bool? Pulumi.Automation.DestroyOptions.TargetDependents.set -> void +Pulumi.Automation.EnvironmentVariableValue +Pulumi.Automation.EnvironmentVariableValue.EnvironmentVariableValue(string value, bool isSecret = false) -> void +Pulumi.Automation.EnvironmentVariableValue.IsSecret.get -> bool +Pulumi.Automation.EnvironmentVariableValue.IsSecret.set -> void +Pulumi.Automation.EnvironmentVariableValue.Value.get -> string +Pulumi.Automation.EnvironmentVariableValue.Value.set -> void Pulumi.Automation.Events.CancelEvent Pulumi.Automation.Events.DiagnosticEvent Pulumi.Automation.Events.DiagnosticEvent.Color.get -> string @@ -255,6 +261,63 @@ Pulumi.Automation.RefreshOptions Pulumi.Automation.RefreshOptions.ExpectNoChanges.get -> bool? Pulumi.Automation.RefreshOptions.ExpectNoChanges.set -> void Pulumi.Automation.RefreshOptions.RefreshOptions() -> void +Pulumi.Automation.RemoteDestroyOptions +Pulumi.Automation.RemoteDestroyOptions.RemoteDestroyOptions() -> void +Pulumi.Automation.RemoteGitAuthArgs +Pulumi.Automation.RemoteGitAuthArgs.Password.get -> string? +Pulumi.Automation.RemoteGitAuthArgs.Password.set -> void +Pulumi.Automation.RemoteGitAuthArgs.PersonalAccessToken.get -> string? +Pulumi.Automation.RemoteGitAuthArgs.PersonalAccessToken.set -> void +Pulumi.Automation.RemoteGitAuthArgs.SshPrivateKey.get -> string? +Pulumi.Automation.RemoteGitAuthArgs.SshPrivateKey.set -> void +Pulumi.Automation.RemoteGitAuthArgs.SshPrivateKeyPath.get -> string? +Pulumi.Automation.RemoteGitAuthArgs.SshPrivateKeyPath.set -> void +Pulumi.Automation.RemoteGitAuthArgs.Username.get -> string? +Pulumi.Automation.RemoteGitAuthArgs.Username.set -> void +Pulumi.Automation.RemoteGitProgramArgs +Pulumi.Automation.RemoteGitProgramArgs.Auth.get -> Pulumi.Automation.RemoteGitAuthArgs? +Pulumi.Automation.RemoteGitProgramArgs.Auth.set -> void +Pulumi.Automation.RemoteGitProgramArgs.Branch.get -> string? +Pulumi.Automation.RemoteGitProgramArgs.Branch.set -> void +Pulumi.Automation.RemoteGitProgramArgs.CommitHash.get -> string? +Pulumi.Automation.RemoteGitProgramArgs.CommitHash.set -> void +Pulumi.Automation.RemoteGitProgramArgs.ProjectPath.get -> string? +Pulumi.Automation.RemoteGitProgramArgs.ProjectPath.set -> void +Pulumi.Automation.RemoteGitProgramArgs.RemoteGitProgramArgs(string stackName, string url) -> void +Pulumi.Automation.RemoteGitProgramArgs.StackName.get -> string +Pulumi.Automation.RemoteGitProgramArgs.Url.get -> string +Pulumi.Automation.RemotePreviewOptions +Pulumi.Automation.RemotePreviewOptions.RemotePreviewOptions() -> void +Pulumi.Automation.RemoteRefreshOptions +Pulumi.Automation.RemoteRefreshOptions.RemoteRefreshOptions() -> void +Pulumi.Automation.RemoteUpOptions +Pulumi.Automation.RemoteUpOptions.RemoteUpOptions() -> void +Pulumi.Automation.RemoteUpdateOptions +Pulumi.Automation.RemoteUpdateOptions.OnEvent.get -> System.Action +Pulumi.Automation.RemoteUpdateOptions.OnEvent.set -> void +Pulumi.Automation.RemoteUpdateOptions.OnStandardError.get -> System.Action +Pulumi.Automation.RemoteUpdateOptions.OnStandardError.set -> void +Pulumi.Automation.RemoteUpdateOptions.OnStandardOutput.get -> System.Action +Pulumi.Automation.RemoteUpdateOptions.OnStandardOutput.set -> void +Pulumi.Automation.RemoteUpdateOptions.RemoteUpdateOptions() -> void +Pulumi.Automation.RemoteWorkspace +Pulumi.Automation.RemoteWorkspaceOptions +Pulumi.Automation.RemoteWorkspaceOptions.RemoteWorkspaceOptions() -> void +Pulumi.Automation.RemoteWorkspaceOptions.EnvironmentVariables.get -> System.Collections.Generic.IDictionary? +Pulumi.Automation.RemoteWorkspaceOptions.EnvironmentVariables.set -> void +Pulumi.Automation.RemoteWorkspaceOptions.PreRunCommands.get -> System.Collections.Generic.IList? +Pulumi.Automation.RemoteWorkspaceOptions.PreRunCommands.set -> void +Pulumi.Automation.RemoteWorkspaceStack +Pulumi.Automation.RemoteWorkspaceStack.DestroyAsync(Pulumi.Automation.RemoteDestroyOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.RemoteWorkspaceStack.Dispose() -> void +Pulumi.Automation.RemoteWorkspaceStack.ExportStackAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.RemoteWorkspaceStack.GetHistoryAsync(Pulumi.Automation.HistoryOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +Pulumi.Automation.RemoteWorkspaceStack.GetOutputsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task> +Pulumi.Automation.RemoteWorkspaceStack.ImportStackAsync(Pulumi.Automation.StackDeployment state, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.RemoteWorkspaceStack.Name.get -> string +Pulumi.Automation.RemoteWorkspaceStack.PreviewAsync(Pulumi.Automation.RemotePreviewOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.RemoteWorkspaceStack.RefreshAsync(Pulumi.Automation.RemoteRefreshOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +Pulumi.Automation.RemoteWorkspaceStack.UpAsync(Pulumi.Automation.RemoteUpOptions options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task Pulumi.Automation.StackSettings Pulumi.Automation.StackSettings.Config.get -> System.Collections.Generic.IDictionary Pulumi.Automation.StackSettings.Config.set -> void @@ -454,6 +517,12 @@ static Pulumi.Automation.PulumiFn.Create(System.Func Pulumi.Automation.PulumiFn static Pulumi.Automation.PulumiFn.Create() -> Pulumi.Automation.PulumiFn static Pulumi.Automation.PulumiFn.Create(System.IServiceProvider serviceProvider) -> Pulumi.Automation.PulumiFn +static Pulumi.Automation.RemoteWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.RemoteGitProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.RemoteWorkspace.CreateOrSelectStackAsync(Pulumi.Automation.RemoteGitProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.RemoteWorkspace.CreateStackAsync(Pulumi.Automation.RemoteGitProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.RemoteWorkspace.CreateStackAsync(Pulumi.Automation.RemoteGitProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Pulumi.Automation.RemoteWorkspace.SelectStackAsync(Pulumi.Automation.RemoteGitProgramArgs args) -> System.Threading.Tasks.Task +static Pulumi.Automation.RemoteWorkspace.SelectStackAsync(Pulumi.Automation.RemoteGitProgramArgs args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task static Pulumi.Automation.WorkspaceStack.CreateAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task static Pulumi.Automation.WorkspaceStack.CreateOrSelectAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task static Pulumi.Automation.WorkspaceStack.SelectAsync(string name, Pulumi.Automation.Workspace workspace, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml index 79f2492159d2..890605c9452a 100644 --- a/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml +++ b/sdk/dotnet/Pulumi.Automation/Pulumi.Automation.xml @@ -361,6 +361,11 @@ + + + Whether this workspace is a remote workspace. + + Creates a workspace using the specified options. Used for maximal control and @@ -675,6 +680,26 @@ . + + + Whether the workspace is a remote workspace. + + + + + Args for remote workspace with Git source. + + + + + Environment values scoped to the remote workspace. These will be passed to remote operations. + + + + + An optional list of arbitrary commands to run before a remote Pulumi operation is invoked. + + If true, force installation of an exact version match (usually >= is accepted). @@ -855,6 +880,273 @@ Show config secrets when they appear. + + + Options controlling the behavior of an operation. + + + + + Authentication options for the repository that can be specified for a private Git repo. + There are three different authentication paths: + + + Personal accesstoken + SSH private key (and its optional password) + Basic auth username and password + + + Only one authentication path is valid. + + + + + The absolute path to a private key for access to the git repo. + + + + + The (contents) private key for access to the git repo. + + + + + The password that pairs with a username or as part of an SSH Private Key. + + + + + PersonalAccessToken is a Git personal access token in replacement of your password. + + + + + Username is the username to use when authenticating to a git repository. + + + + + Description of a stack backed by a remote Pulumi program in a Git repository. + + + + + The name of the associated Stack. + + + + + The URL of the repository. + + + + + Optional path relative to the repo root specifying location of the Pulumi program. + + + + + Optional branch to checkout. + + + + + Optional commit to checkout. + + + + + Authentication options for the repository. + + + + + Options controlling the behavior of an operation. + + + + + Options controlling the behavior of an operation. + + + + + Common options controlling the behavior of update actions taken + against an instance of . + + + + + Optional callback which is invoked whenever StandardOutput is written into + + + + + Optional callback which is invoked whenever StandardError is written into + + + + + Optional callback which is invoked with the engine events + + + + + Options controlling the behavior of an operation. + + + + + PREVIEW: Creates a Stack backed by a RemoteWorkspace with source code from the specified Git repository. + Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + + + + PREVIEW: Creates a Stack backed by a RemoteWorkspace with source code from the specified Git repository. + Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + A cancellation token. + + + + PREVIEW: Selects an existing Stack backed by a RemoteWorkspace with source code from the specified Git + repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + + + + PREVIEW: Selects an existing Stack backed by a RemoteWorkspace with source code from the specified Git + repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + A cancellation token. + + + + PREVIEW: Creates or selects an existing Stack backed by a RemoteWorkspace with source code from the specified + Git repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + + + + PREVIEW: Creates or selects an existing Stack backed by a RemoteWorkspace with source code from the specified + Git repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + + + A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + + A cancellation token. + + + + Extensibility options to configure a RemoteWorkspace. + + + + + Environment values scoped to the remote workspace. These will be passed to remote operations. + + + + + An optional list of arbitrary commands to run before a remote Pulumi operation is invoked. + + + + + The name identifying the Stack. + + + + + Creates or updates the resources in a stack by executing the program in the Workspace. + This operation runs remotely. + + https://www.pulumi.com/docs/reference/cli/pulumi_up/ + + Options to customize the behavior of the update. + A cancellation token. + + + + Performs a dry-run update to a stack, returning pending changes. + This operation runs remotely. + + https://www.pulumi.com/docs/reference/cli/pulumi_preview/ + + Options to customize the behavior of the update. + A cancellation token. + + + + Compares the current stack’s resource state with the state known to exist in the actual + cloud provider. Any such changes are adopted into the current stack. + This operation runs remotely. + + Options to customize the behavior of the refresh. + A cancellation token. + + + + Destroy deletes all resources in a stack, leaving all history and configuration intact. + This operation runs remotely. + + Options to customize the behavior of the destroy. + A cancellation token. + + + + Gets the current set of Stack outputs from the last . + + + + + Returns a list summarizing all previews and current results from Stack lifecycle operations (up/preview/refresh/destroy). + + Options to customize the behavior of the fetch history action. + A cancellation token. + + + + Exports the deployment state of the stack. + + This can be combined with ImportStackAsync to edit a + stack's state (such as recovery from failed deployments). + + + + + Imports the specified deployment state into a pre-existing stack. + + This can be combined with ExportStackAsync to edit a + stack's state (such as recovery from failed deployments). + + + + + Cancel stops a stack's currently running update. It throws + an exception if no update is currently running. Note that + this operation is _very dangerous_, and may leave the + stack in an inconsistent state if a resource operation was + pending when the update was canceled. This command is not + supported for local backends. + + Represents the state of a stack deployment as used by @@ -949,6 +1241,11 @@ Print detailed debugging output during resource operations + + + Format standard output as JSON not text. + + Options controlling the behavior of an operation. diff --git a/sdk/dotnet/Pulumi.Automation/RemoteDestroyOptions.cs b/sdk/dotnet/Pulumi.Automation/RemoteDestroyOptions.cs new file mode 100644 index 000000000000..0dca1f258e2f --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteDestroyOptions.cs @@ -0,0 +1,11 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class RemoteDestroyOptions : RemoteUpdateOptions + { + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteGitAuthArgs.cs b/sdk/dotnet/Pulumi.Automation/RemoteGitAuthArgs.cs new file mode 100644 index 000000000000..79c8dcc5d305 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteGitAuthArgs.cs @@ -0,0 +1,44 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Authentication options for the repository that can be specified for a private Git repo. + /// There are three different authentication paths: + /// + /// + /// Personal accesstoken + /// SSH private key (and its optional password) + /// Basic auth username and password + /// + /// + /// Only one authentication path is valid. + /// + public class RemoteGitAuthArgs + { + /// + /// The absolute path to a private key for access to the git repo. + /// + public string? SshPrivateKeyPath { get; set; } + + /// + /// The (contents) private key for access to the git repo. + /// + public string? SshPrivateKey { get; set; } + + /// + /// The password that pairs with a username or as part of an SSH Private Key. + /// + public string? Password { get; set; } + + /// + /// PersonalAccessToken is a Git personal access token in replacement of your password. + /// + public string? PersonalAccessToken { get; set; } + + /// + /// Username is the username to use when authenticating to a git repository. + /// + public string? Username { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteGitProgramArgs.cs b/sdk/dotnet/Pulumi.Automation/RemoteGitProgramArgs.cs new file mode 100644 index 000000000000..71bb6dbdab53 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteGitProgramArgs.cs @@ -0,0 +1,48 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Description of a stack backed by a remote Pulumi program in a Git repository. + /// + public class RemoteGitProgramArgs : RemoteWorkspaceOptions + { + /// + /// The name of the associated Stack. + /// + public string StackName { get; } + + /// + /// The URL of the repository. + /// + public string Url { get; } + + public RemoteGitProgramArgs( + string stackName, + string url) + { + StackName = stackName; + Url = url; + } + + /// + /// Optional path relative to the repo root specifying location of the Pulumi program. + /// + public string? ProjectPath { get; set; } + + /// + /// Optional branch to checkout. + /// + public string? Branch { get; set; } + + /// + /// Optional commit to checkout. + /// + public string? CommitHash { get; set; } + + /// + /// Authentication options for the repository. + /// + public RemoteGitAuthArgs? Auth { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemotePreviewOptions.cs b/sdk/dotnet/Pulumi.Automation/RemotePreviewOptions.cs new file mode 100644 index 000000000000..d2658faf2842 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemotePreviewOptions.cs @@ -0,0 +1,11 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class RemotePreviewOptions : RemoteUpdateOptions + { + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteRefreshOptions.cs b/sdk/dotnet/Pulumi.Automation/RemoteRefreshOptions.cs new file mode 100644 index 000000000000..4ef8ec1cd505 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteRefreshOptions.cs @@ -0,0 +1,11 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class RemoteRefreshOptions : RemoteUpdateOptions + { + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteUpOptions.cs b/sdk/dotnet/Pulumi.Automation/RemoteUpOptions.cs new file mode 100644 index 000000000000..caae5efab85a --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteUpOptions.cs @@ -0,0 +1,11 @@ +// Copyright 2016-2022, Pulumi Corporation + +namespace Pulumi.Automation +{ + /// + /// Options controlling the behavior of an operation. + /// + public sealed class RemoteUpOptions : RemoteUpdateOptions + { + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteUpdateOptions.cs b/sdk/dotnet/Pulumi.Automation/RemoteUpdateOptions.cs new file mode 100644 index 000000000000..5366bc081d80 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteUpdateOptions.cs @@ -0,0 +1,29 @@ +// Copyright 2016-2022, Pulumi Corporation + +using System; +using Pulumi.Automation.Events; + +namespace Pulumi.Automation +{ + /// + /// Common options controlling the behavior of update actions taken + /// against an instance of . + /// + public class RemoteUpdateOptions + { + /// + /// Optional callback which is invoked whenever StandardOutput is written into + /// + public Action? OnStandardOutput { get; set; } + + /// + /// Optional callback which is invoked whenever StandardError is written into + /// + public Action? OnStandardError { get; set; } + + /// + /// Optional callback which is invoked with the engine events + /// + public Action? OnEvent { get; set; } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteWorkspace.cs b/sdk/dotnet/Pulumi.Automation/RemoteWorkspace.cs new file mode 100644 index 000000000000..c818f37e7304 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteWorkspace.cs @@ -0,0 +1,136 @@ +// Copyright 2016-2022, Pulumi Corporation + +using System; +using System.Threading; +using System.Threading.Tasks; +using Pulumi.Automation.Commands; + +namespace Pulumi.Automation +{ + public static class RemoteWorkspace + { + /// + /// PREVIEW: Creates a Stack backed by a RemoteWorkspace with source code from the specified Git repository. + /// Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + public static Task CreateStackAsync(RemoteGitProgramArgs args) + => CreateStackAsync(args, default); + + /// + /// PREVIEW: Creates a Stack backed by a RemoteWorkspace with source code from the specified Git repository. + /// Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + /// A cancellation token. + public static Task CreateStackAsync(RemoteGitProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateAsync, cancellationToken); + + /// + /// PREVIEW: Selects an existing Stack backed by a RemoteWorkspace with source code from the specified Git + /// repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + public static Task SelectStackAsync(RemoteGitProgramArgs args) + => SelectStackAsync(args, default); + + /// + /// PREVIEW: Selects an existing Stack backed by a RemoteWorkspace with source code from the specified Git + /// repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + /// A cancellation token. + public static Task SelectStackAsync(RemoteGitProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.SelectAsync, cancellationToken); + + /// + /// PREVIEW: Creates or selects an existing Stack backed by a RemoteWorkspace with source code from the specified + /// Git repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + public static Task CreateOrSelectStackAsync(RemoteGitProgramArgs args) + => CreateOrSelectStackAsync(args, default); + + /// + /// PREVIEW: Creates or selects an existing Stack backed by a RemoteWorkspace with source code from the specified + /// Git repository. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. + /// + /// + /// A set of arguments to initialize a RemoteStack with a remote Pulumi program from a Git repository. + /// + /// A cancellation token. + public static Task CreateOrSelectStackAsync(RemoteGitProgramArgs args, CancellationToken cancellationToken) + => CreateStackHelperAsync(args, WorkspaceStack.CreateOrSelectAsync, cancellationToken); + + private static async Task CreateStackHelperAsync( + RemoteGitProgramArgs args, + Func> initFunc, + CancellationToken cancellationToken) + { + if (!IsFullyQualifiedStackName(args.StackName)) + { + throw new ArgumentException($"{nameof(args.StackName)} not fully qualified."); + } + if (string.IsNullOrWhiteSpace(args.Url)) + { + throw new ArgumentException($"{nameof(args.Url)} is required."); + } + if (!string.IsNullOrWhiteSpace(args.CommitHash) && !string.IsNullOrWhiteSpace(args.Branch)) + { + throw new ArgumentException($"{nameof(args.CommitHash)} and {nameof(args.Branch)} cannot both be specified."); + } + if (string.IsNullOrWhiteSpace(args.CommitHash) && string.IsNullOrWhiteSpace(args.Branch)) + { + throw new ArgumentException($"either {nameof(args.CommitHash)} or {nameof(args.Branch)} is required."); + } + if (!(args.Auth is null)) + { + if (!string.IsNullOrWhiteSpace(args.Auth.SshPrivateKey) && + !string.IsNullOrWhiteSpace(args.Auth.SshPrivateKeyPath)) + { + throw new ArgumentException($"{nameof(args.Auth.SshPrivateKey)} and {nameof(args.Auth.SshPrivateKeyPath)} cannot both be specified."); + } + } + + var localArgs = new LocalWorkspaceOptions + { + Remote = true, + RemoteGitProgramArgs = args, + RemoteEnvironmentVariables = args.EnvironmentVariables, + RemotePreRunCommands = args.PreRunCommands, + }; + + var ws = new LocalWorkspace( + new LocalPulumiCmd(), + localArgs, + cancellationToken); + await ws.ReadyTask.ConfigureAwait(false); + + var stack = await initFunc(args.StackName, ws, cancellationToken).ConfigureAwait(false); + return new RemoteWorkspaceStack(stack); + } + + internal static bool IsFullyQualifiedStackName(string stackName) + { + if (string.IsNullOrWhiteSpace(stackName)) + { + return false; + } + var split = stackName.Split("/"); + return split.Length == 3 + && !string.IsNullOrWhiteSpace(split[0]) + && !string.IsNullOrWhiteSpace(split[1]) + && !string.IsNullOrWhiteSpace(split[2]); + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceOptions.cs b/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceOptions.cs new file mode 100644 index 000000000000..c52eabd9f7b7 --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceOptions.cs @@ -0,0 +1,33 @@ +// Copyright 2016-2022, Pulumi Corporation + +using System.Collections.Generic; + +namespace Pulumi.Automation +{ + /// + /// Extensibility options to configure a RemoteWorkspace. + /// + public class RemoteWorkspaceOptions + { + private IDictionary? _environmentVariables; + private IList? _preRunCommands; + + /// + /// Environment values scoped to the remote workspace. These will be passed to remote operations. + /// + public IDictionary EnvironmentVariables + { + get => _environmentVariables ??= new Dictionary(); + set => _environmentVariables = value; + } + + /// + /// An optional list of arbitrary commands to run before a remote Pulumi operation is invoked. + /// + public IList PreRunCommands + { + get => _preRunCommands ??= new List(); + set => _preRunCommands = value; + } + } +} diff --git a/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceStack.cs b/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceStack.cs new file mode 100644 index 000000000000..4877f2acbbbc --- /dev/null +++ b/sdk/dotnet/Pulumi.Automation/RemoteWorkspaceStack.cs @@ -0,0 +1,155 @@ +// Copyright 2016-2022, Pulumi Corporation + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Pulumi.Automation +{ + public sealed class RemoteWorkspaceStack : IDisposable + { + private readonly WorkspaceStack _stack; + + /// + /// The name identifying the Stack. + /// + public string Name => _stack.Name; + + internal RemoteWorkspaceStack(WorkspaceStack stack) + { + _stack = stack; + } + + /// + /// Creates or updates the resources in a stack by executing the program in the Workspace. + /// This operation runs remotely. + /// + /// https://www.pulumi.com/docs/reference/cli/pulumi_up/ + /// + /// Options to customize the behavior of the update. + /// A cancellation token. + public Task UpAsync( + RemoteUpOptions? options = null, + CancellationToken cancellationToken = default) + { + var upOptions = new UpOptions + { + OnStandardOutput = options?.OnStandardOutput, + OnStandardError = options?.OnStandardError, + OnEvent = options?.OnEvent, + }; + return _stack.UpAsync(upOptions, cancellationToken); + } + + /// + /// Performs a dry-run update to a stack, returning pending changes. + /// This operation runs remotely. + /// + /// https://www.pulumi.com/docs/reference/cli/pulumi_preview/ + /// + /// Options to customize the behavior of the update. + /// A cancellation token. + public Task PreviewAsync( + RemotePreviewOptions? options = null, + CancellationToken cancellationToken = default) + { + var previewOptions = new PreviewOptions + { + OnStandardOutput = options?.OnStandardOutput, + OnStandardError = options?.OnStandardError, + OnEvent = options?.OnEvent, + }; + return _stack.PreviewAsync(previewOptions, cancellationToken); + } + + /// + /// Compares the current stack’s resource state with the state known to exist in the actual + /// cloud provider. Any such changes are adopted into the current stack. + /// This operation runs remotely. + /// + /// Options to customize the behavior of the refresh. + /// A cancellation token. + public Task RefreshAsync( + RemoteRefreshOptions? options = null, + CancellationToken cancellationToken = default) + { + var refreshOptions = new RefreshOptions + { + OnStandardOutput = options?.OnStandardOutput, + OnStandardError = options?.OnStandardError, + OnEvent = options?.OnEvent, + }; + return _stack.RefreshAsync(refreshOptions, cancellationToken); + } + + /// + /// Destroy deletes all resources in a stack, leaving all history and configuration intact. + /// This operation runs remotely. + /// + /// Options to customize the behavior of the destroy. + /// A cancellation token. + public Task DestroyAsync( + RemoteDestroyOptions? options = null, + CancellationToken cancellationToken = default) + { + var destroyOptions = new DestroyOptions + { + OnStandardOutput = options?.OnStandardOutput, + OnStandardError = options?.OnStandardError, + OnEvent = options?.OnEvent, + }; + return _stack.DestroyAsync(destroyOptions, cancellationToken); + } + + /// + /// Gets the current set of Stack outputs from the last . + /// + public Task> GetOutputsAsync(CancellationToken cancellationToken = default) + => _stack.GetOutputsAsync(cancellationToken); + + /// + /// Returns a list summarizing all previews and current results from Stack lifecycle operations (up/preview/refresh/destroy). + /// + /// Options to customize the behavior of the fetch history action. + /// A cancellation token. + public Task> GetHistoryAsync( + HistoryOptions? options = null, + CancellationToken cancellationToken = default) + { + return _stack.GetHistoryAsync(options, cancellationToken); + } + + /// + /// Exports the deployment state of the stack. + /// + /// This can be combined with ImportStackAsync to edit a + /// stack's state (such as recovery from failed deployments). + /// + public Task ExportStackAsync(CancellationToken cancellationToken = default) + => _stack.ExportStackAsync(cancellationToken); + + /// + /// Imports the specified deployment state into a pre-existing stack. + /// + /// This can be combined with ExportStackAsync to edit a + /// stack's state (such as recovery from failed deployments). + /// + public Task ImportStackAsync(StackDeployment state, CancellationToken cancellationToken = default) + => _stack.ImportStackAsync(state, cancellationToken); + + /// + /// Cancel stops a stack's currently running update. It throws + /// an exception if no update is currently running. Note that + /// this operation is _very dangerous_, and may leave the + /// stack in an inconsistent state if a resource operation was + /// pending when the update was canceled. This command is not + /// supported for local backends. + /// + public Task CancelAsync(CancellationToken cancellationToken = default) + => _stack.CancelAsync(cancellationToken); + + public void Dispose() + => _stack.Dispose(); + } +} diff --git a/sdk/dotnet/Pulumi.Automation/Workspace.cs b/sdk/dotnet/Pulumi.Automation/Workspace.cs index 60639806b492..698724c0c11f 100644 --- a/sdk/dotnet/Pulumi.Automation/Workspace.cs +++ b/sdk/dotnet/Pulumi.Automation/Workspace.cs @@ -1,4 +1,4 @@ -// Copyright 2016-2021, Pulumi Corporation +// Copyright 2016-2022, Pulumi Corporation using System; using System.Collections.Generic; @@ -326,6 +326,9 @@ public Task InstallPluginAsync(string name, string version, PluginKind kind, Can if (!string.IsNullOrWhiteSpace(this.PulumiHome)) env["PULUMI_HOME"] = this.PulumiHome; + if (this is LocalWorkspace localWorkspace && localWorkspace.Remote) + env["PULUMI_EXPERIMENTAL"] = "true"; + if (this.EnvironmentVariables != null) { foreach (var pair in this.EnvironmentVariables) diff --git a/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs b/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs index d6a29b0fae9b..b77a1371af0e 100644 --- a/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs +++ b/sdk/dotnet/Pulumi.Automation/WorkspaceStack.cs @@ -1,4 +1,4 @@ -// Copyright 2016-2021, Pulumi Corporation +// Copyright 2016-2022, Pulumi Corporation using System; using System.Collections.Generic; @@ -217,6 +217,8 @@ public Task RemoveAllConfigAsync(IEnumerable keys, CancellationToken can "--skip-preview", }; + args.AddRange(GetRemoteArgs()); + if (options != null) { if (options.Program != null) @@ -289,7 +291,10 @@ public Task RemoveAllConfigAsync(IEnumerable keys, CancellationToken can } var output = await this.GetOutputsAsync(cancellationToken).ConfigureAwait(false); - var summary = await this.GetInfoAsync(cancellationToken, options?.ShowSecrets).ConfigureAwait(false); + // If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to + // load the project file. + var showSecrets = Remote ? false : options?.ShowSecrets; + var summary = await this.GetInfoAsync(cancellationToken, showSecrets).ConfigureAwait(false); return new UpResult( upResult.StandardOutput, upResult.StandardError, @@ -321,6 +326,8 @@ public Task RemoveAllConfigAsync(IEnumerable keys, CancellationToken can var logger = this.Workspace.Logger; var args = new List() { "preview" }; + args.AddRange(GetRemoteArgs()); + if (options != null) { if (options.Program != null) @@ -443,6 +450,8 @@ void OnPreviewEvent(EngineEvent @event) "--skip-preview", }; + args.AddRange(GetRemoteArgs()); + if (options != null) { if (options.ExpectNoChanges is true) @@ -456,7 +465,10 @@ void OnPreviewEvent(EngineEvent @event) args.Add(execKind); var result = await this.RunCommandAsync(args, options?.OnStandardOutput, options?.OnStandardError, options?.OnEvent, cancellationToken).ConfigureAwait(false); - var summary = await this.GetInfoAsync(cancellationToken, options?.ShowSecrets).ConfigureAwait(false); + // If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to + // load the project file. + var showSecrets = Remote ? false : options?.ShowSecrets; + var summary = await this.GetInfoAsync(cancellationToken, showSecrets).ConfigureAwait(false); return new UpdateResult( result.StandardOutput, result.StandardError, @@ -479,6 +491,8 @@ void OnPreviewEvent(EngineEvent @event) "--skip-preview", }; + args.AddRange(GetRemoteArgs()); + if (options != null) { if (options.TargetDependents is true) @@ -492,7 +506,10 @@ void OnPreviewEvent(EngineEvent @event) args.Add(execKind); var result = await this.RunCommandAsync(args, options?.OnStandardOutput, options?.OnStandardError, options?.OnEvent, cancellationToken).ConfigureAwait(false); - var summary = await this.GetInfoAsync(cancellationToken, options?.ShowSecrets).ConfigureAwait(false); + // If it's a remote workspace, explicitly set showSecrets to false to prevent attempting to + // load the project file. + var showSecrets = Remote ? false : options?.ShowSecrets; + var summary = await this.GetInfoAsync(cancellationToken, showSecrets).ConfigureAwait(false); return new UpdateResult( result.StandardOutput, result.StandardError, @@ -806,5 +823,11 @@ static void ApplyUpdateOptions(UpdateOptions options, List args) args.Add("--json"); } } + + private bool Remote + => Workspace is LocalWorkspace localWorkspace && localWorkspace.Remote; + + private IReadOnlyList GetRemoteArgs() + => Workspace is LocalWorkspace localWorkspace ? localWorkspace.GetRemoteArgs() : Array.Empty(); } }