[automation/*] Add support for getting stack outputs using Workspace (#6859)

This commit is contained in:
Ville Penttinen 2021-04-27 02:32:30 +03:00 committed by GitHub
parent 3cbfddf870
commit daa6045381
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 184 additions and 104 deletions

View file

@ -4,7 +4,8 @@
### Enhancements
- [automation/*] Add support for getting stack outputs using Workspace
[#6859](https://github.com/pulumi/pulumi/pull/6859)
### Bug Fixes

View file

@ -649,6 +649,31 @@ namespace Pulumi.Automation
return plugins.ToImmutableList();
}
/// <inheritdoc/>
public override async Task<ImmutableDictionary<string, OutputValue>> GetStackOutputsAsync(string stackName, CancellationToken cancellationToken = default)
{
// TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
var maskedResult = await this.RunCommandAsync(new[] { "stack", "output", "--json", "--stack", stackName }, cancellationToken).ConfigureAwait(false);
var plaintextResult = await this.RunCommandAsync(new[] { "stack", "output", "--json", "--show-secrets", "--stack", stackName }, cancellationToken).ConfigureAwait(false);
var maskedOutput = string.IsNullOrWhiteSpace(maskedResult.StandardOutput)
? new Dictionary<string, object>()
: _serializer.DeserializeJson<Dictionary<string, object>>(maskedResult.StandardOutput);
var plaintextOutput = string.IsNullOrWhiteSpace(plaintextResult.StandardOutput)
? new Dictionary<string, object>()
: _serializer.DeserializeJson<Dictionary<string, object>>(plaintextResult.StandardOutput);
var output = new Dictionary<string, OutputValue>();
foreach (var (key, value) in plaintextOutput)
{
var secret = maskedOutput[key] is string maskedValue && maskedValue == "[secret]";
output[key] = new OutputValue(value, secret);
}
return output.ToImmutableDictionary();
}
public override void Dispose()
{
base.Dispose();

View file

@ -0,0 +1,2 @@
abstract Pulumi.Automation.Workspace.GetStackOutputsAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Immutable.ImmutableDictionary<string, Pulumi.Automation.OutputValue>>
override Pulumi.Automation.LocalWorkspace.GetStackOutputsAsync(string stackName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Collections.Immutable.ImmutableDictionary<string, Pulumi.Automation.OutputValue>>

View file

@ -599,6 +599,9 @@
<member name="M:Pulumi.Automation.LocalWorkspace.ListPluginsAsync(System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="M:Pulumi.Automation.LocalWorkspace.GetOutputsAsync(System.String,System.Threading.CancellationToken)">
<inheritdoc/>
</member>
<member name="T:Pulumi.Automation.LocalWorkspaceOptions">
<summary>
Extensibility options to configure a LocalWorkspace; e.g: settings to seed
@ -1089,6 +1092,13 @@
Returns a list of all plugins installed in the Workspace.
</summary>
</member>
<member name="M:Pulumi.Automation.Workspace.GetOutputsAsync(System.String,System.Threading.CancellationToken)">
<summary>
Gets the current set of Stack outputs from the last <see cref="M:Pulumi.Automation.WorkspaceStack.UpAsync(Pulumi.Automation.UpOptions,System.Threading.CancellationToken)"/>.
</summary>
<param name="stackName">The name of the stack.</param>
<param name="cancellationToken">A cancellation token.</param>
</member>
<member name="T:Pulumi.Automation.WorkspaceStack">
<summary>
<see cref="T:Pulumi.Automation.WorkspaceStack"/> is an isolated, independently configurable instance of a

View file

@ -267,6 +267,13 @@ namespace Pulumi.Automation
/// </summary>
public abstract Task<ImmutableList<PluginInfo>> ListPluginsAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Gets the current set of Stack outputs from the last <see cref="WorkspaceStack.UpAsync(UpOptions?, CancellationToken)"/>.
/// </summary>
/// <param name="stackName">The name of the stack.</param>
/// <param name="cancellationToken">A cancellation token.</param>
public abstract Task<ImmutableDictionary<string, OutputValue>> GetStackOutputsAsync(string stackName, CancellationToken cancellationToken = default);
internal async Task<CommandResult> RunStackCommandAsync(
string stackName,
IEnumerable<string> args,

View file

@ -526,30 +526,8 @@ namespace Pulumi.Automation
/// <summary>
/// Gets the current set of Stack outputs from the last <see cref="UpAsync(UpOptions?, CancellationToken)"/>.
/// </summary>
public async Task<ImmutableDictionary<string, OutputValue>> GetOutputsAsync(CancellationToken cancellationToken = default)
{
// TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
var maskedResult = await this.RunCommandAsync(new[] { "stack", "output", "--json" }, null, null, null, cancellationToken).ConfigureAwait(false);
var plaintextResult = await this.RunCommandAsync(new[] { "stack", "output", "--json", "--show-secrets" }, null, null, null, cancellationToken).ConfigureAwait(false);
var jsonOptions = LocalSerializer.BuildJsonSerializerOptions();
var maskedOutput = string.IsNullOrWhiteSpace(maskedResult.StandardOutput)
? new Dictionary<string, object>()
: JsonSerializer.Deserialize<Dictionary<string, object>>(maskedResult.StandardOutput, jsonOptions);
var plaintextOutput = string.IsNullOrWhiteSpace(plaintextResult.StandardOutput)
? new Dictionary<string, object>()
: JsonSerializer.Deserialize<Dictionary<string, object>>(plaintextResult.StandardOutput, jsonOptions);
var output = new Dictionary<string, OutputValue>();
foreach (var (key, value) in plaintextOutput)
{
var secret = maskedOutput[key] is string maskedValue && maskedValue == "[secret]";
output[key] = new OutputValue(value, secret);
}
return output.ToImmutableDictionary();
}
public Task<ImmutableDictionary<string, OutputValue>> GetOutputsAsync(CancellationToken cancellationToken = default)
=> this.Workspace.GetStackOutputsAsync(this.Name, cancellationToken);
/// <summary>
/// Returns a list summarizing all previews and current results from Stack lifecycle operations (up/preview/refresh/destroy).

View file

@ -446,6 +446,45 @@ func (l *LocalWorkspace) ImportStack(ctx context.Context, stackName string, stat
return nil
}
// Outputs get the current set of Stack outputs from the last Stack.Up().
func (l *LocalWorkspace) StackOutputs(ctx context.Context, stackName string) (OutputMap, error) {
// standard outputs
outStdout, outStderr, code, err := l.runPulumiCmdSync(ctx, "stack", "output", "--json", "--stack", stackName)
if err != nil {
return nil, newAutoError(errors.Wrap(err, "could not get outputs"), outStdout, outStderr, code)
}
// secret outputs
secretStdout, secretStderr, code, err := l.runPulumiCmdSync(ctx,
"stack", "output", "--json", "--show-secrets", "--stack", stackName,
)
if err != nil {
return nil, newAutoError(errors.Wrap(err, "could not get secret outputs"), outStdout, outStderr, code)
}
var outputs map[string]interface{}
var secrets map[string]interface{}
if err = json.Unmarshal([]byte(outStdout), &outputs); err != nil {
return nil, errors.Wrapf(err, "error unmarshalling outputs: %s", secretStderr)
}
if err = json.Unmarshal([]byte(secretStdout), &secrets); err != nil {
return nil, errors.Wrapf(err, "error unmarshalling secret outputs: %s", secretStderr)
}
res := make(OutputMap)
for k, v := range secrets {
isSecret := outputs[k] == secretSentinel
res[k] = OutputValue{
Value: v,
Secret: isSecret,
}
}
return res, nil
}
func (l *LocalWorkspace) getPulumiVersion(ctx context.Context) (semver.Version, error) {
stdout, stderr, errCode, err := l.runPulumiCmdSync(ctx, "version")
if err != nil {

View file

@ -515,43 +515,7 @@ func (s *Stack) Destroy(ctx context.Context, opts ...optdestroy.Option) (Destroy
// Outputs get the current set of Stack outputs from the last Stack.Up().
func (s *Stack) Outputs(ctx context.Context) (OutputMap, error) {
// standard outputs
outStdout, outStderr, code, err := s.runPulumiCmdSync(ctx, nil, /* additionalOutputs */
"stack", "output", "--json",
)
if err != nil {
return nil, newAutoError(errors.Wrap(err, "could not get outputs"), outStdout, outStderr, code)
}
// secret outputs
secretStdout, secretStderr, code, err := s.runPulumiCmdSync(ctx, nil, /* additionalOutputs */
"stack", "output", "--json", "--show-secrets",
)
if err != nil {
return nil, newAutoError(errors.Wrap(err, "could not get secret outputs"), outStdout, outStderr, code)
}
var outputs map[string]interface{}
var secrets map[string]interface{}
if err = json.Unmarshal([]byte(outStdout), &outputs); err != nil {
return nil, errors.Wrapf(err, "error unmarshalling outputs: %s", secretStderr)
}
if err = json.Unmarshal([]byte(secretStdout), &secrets); err != nil {
return nil, errors.Wrapf(err, "error unmarshalling secret outputs: %s", secretStderr)
}
res := make(OutputMap)
for k, v := range secrets {
isSecret := outputs[k] == secretSentinel
res[k] = OutputValue{
Value: v,
Secret: isSecret,
}
}
return res, nil
return s.Workspace().StackOutputs(ctx, s.Name())
}
// History returns a list summarizing all previous and current results from Stack lifecycle operations

View file

@ -105,6 +105,8 @@ type Workspace interface {
// ImportStack imports the specified deployment state into a pre-existing stack.
// This can be combined with ExportStack to edit a stack's state (such as recovery from failed deployments).
ImportStack(context.Context, string, apitype.UntypedDeployment) error
// Outputs get the current set of Stack outputs from the last Stack.Up().
StackOutputs(context.Context, string) (OutputMap, error)
}
// ConfigValue is a configuration value used by a Pulumi program.

View file

@ -22,7 +22,7 @@ import { CommandResult, runPulumiCmd } from "./cmd";
import { ConfigMap, ConfigValue } from "./config";
import { minimumVersion } from "./minimumVersion";
import { ProjectSettings } from "./projectSettings";
import { Stack } from "./stack";
import { OutputMap, Stack } from "./stack";
import { StackSettings, stackSettingsSerDeKeys } from "./stackSettings";
import { Deployment, PluginInfo, PulumiFn, StackSummary, WhoAmIResult, Workspace } from "./workspace";
@ -544,6 +544,26 @@ export class LocalWorkspace implements Workspace {
await this.runPulumiCmd(["stack", "import", "--file", filepath, "--stack", stackName]);
fs.unlinkSync(filepath);
}
/**
* Gets the current set of Stack outputs from the last Stack.up().
* @param stackName the name of the stack.
*/
async stackOutputs(stackName: string): Promise<OutputMap> {
// TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
const maskedResult = await this.runPulumiCmd(["stack", "output", "--json", "--stack", stackName]);
const plaintextResult = await this.runPulumiCmd(["stack", "output", "--json", "--show-secrets", "--stack", stackName]);
const maskedOuts = JSON.parse(maskedResult.stdout);
const plaintextOuts = JSON.parse(plaintextResult.stdout);
const outputs: OutputMap = {};
for (const [key, value] of Object.entries(plaintextOuts)) {
const secret = maskedOuts[key] === "[secret]";
outputs[key] = { value, secret };
}
return outputs;
}
/**
* serializeArgsForOp is hook to provide additional args to every CLI commands before they are executed.
* Provided with stack name,

View file

@ -30,8 +30,6 @@ import { Deployment, PulumiFn, Workspace } from "./workspace";
const langrpc = require("../proto/language_grpc_pb.js");
const secretSentinel = "[secret]";
/**
* Stack is an isolated, independently configurable instance of a Pulumi program.
* Stack exposes methods for the full pulumi lifecycle (up/preview/refresh/destroy), as well as managing configuration.
@ -504,19 +502,7 @@ export class Stack {
* Gets the current set of Stack outputs from the last Stack.up().
*/
async outputs(): Promise<OutputMap> {
// TODO: do this in parallel after this is fixed https://github.com/pulumi/pulumi/issues/6050
const maskedResult = await this.runPulumiCmd(["stack", "output", "--json"]);
const plaintextResult = await this.runPulumiCmd(["stack", "output", "--json", "--show-secrets"]);
const maskedOuts = JSON.parse(maskedResult.stdout);
const plaintextOuts = JSON.parse(plaintextResult.stdout);
const outputs: OutputMap = {};
for (const [key, value] of Object.entries(plaintextOuts)) {
const secret = maskedOuts[key] === secretSentinel;
outputs[key] = { value, secret };
}
return outputs;
return this.workspace.stackOutputs(this.name);
}
/**
* Returns a list summarizing all previous and current results from Stack lifecycle operations

View file

@ -14,6 +14,7 @@
import { ConfigMap, ConfigValue } from "./config";
import { ProjectSettings } from "./projectSettings";
import { OutputMap } from "./stack";
import { StackSettings } from "./stackSettings";
/**
@ -206,6 +207,11 @@ export interface Workspace {
* @param state the stack state to import.
*/
importStack(stackName: string, state: Deployment): Promise<void>;
/**
* Gets the current set of Stack outputs from the last Stack.up().
* @param stackName the name of the stack.
*/
stackOutputs(stackName: string): Promise<OutputMap>;
}
/**

View file

@ -146,6 +146,11 @@ from ._workspace import (
Deployment,
)
from ._output import (
OutputMap,
OutputValue
)
from ._project_settings import (
ProjectSettings,
ProjectRuntimeInfo,
@ -218,6 +223,10 @@ __all__ = [
"Deployment",
"WhoAmIResult",
# _output
"OutputMap",
"OutputValue",
# _project_settings
"ProjectSettings",
"ProjectRuntimeInfo",

View file

@ -20,11 +20,12 @@ from typing import Optional, List, Mapping, Callable
from semver import VersionInfo
import yaml
from ._config import ConfigMap, ConfigValue
from ._config import ConfigMap, ConfigValue, _SECRET_SENTINEL
from ._project_settings import ProjectSettings
from ._stack_settings import StackSettings
from ._workspace import Workspace, PluginInfo, StackSummary, WhoAmIResult, PulumiFn, Deployment
from ._stack import _DATETIME_FORMAT, Stack
from ._output import OutputMap, OutputValue
from ._cmd import _run_pulumi_cmd, CommandResult, OnOutput
from ._minimum_version import _MINIMUM_VERSION
from .errors import InvalidVersionError
@ -270,6 +271,17 @@ class LocalWorkspace(Workspace):
self._run_pulumi_cmd_sync(["stack", "import", "--file", file.name, "--stack", stack_name])
os.remove(file.name)
def stack_outputs(self, stack_name: str) -> OutputMap:
masked_result = self._run_pulumi_cmd_sync(["stack", "output", "--json", "--stack", stack_name])
plaintext_result = self._run_pulumi_cmd_sync(["stack", "output", "--json", "--show-secrets", "--stack", stack_name])
masked_outputs = json.loads(masked_result.stdout)
plaintext_outputs = json.loads(plaintext_result.stdout)
outputs: OutputMap = {}
for key in plaintext_outputs:
secret = masked_outputs[key] == _SECRET_SENTINEL
outputs[key] = OutputValue(value=plaintext_outputs[key], secret=secret)
return outputs
def _get_pulumi_version(self) -> VersionInfo:
result = self._run_pulumi_cmd_sync(["version"])
version_string = result.stdout.strip()

View file

@ -0,0 +1,31 @@
# Copyright 2016-2021, Pulumi Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, MutableMapping
from ._config import _SECRET_SENTINEL
class OutputValue:
value: Any
secret: bool
def __init__(self, value: Any, secret: bool):
self.value = value
self.secret = secret
def __repr__(self):
return _SECRET_SENTINEL if self.secret else repr(self.value)
OutputMap = MutableMapping[str, OutputValue]

View file

@ -24,9 +24,10 @@ from typing import List, Any, Mapping, MutableMapping, Optional, Callable, Tuple
import grpc
from ._cmd import CommandResult, _run_pulumi_cmd, OnOutput
from ._config import ConfigValue, ConfigMap, _SECRET_SENTINEL
from ._config import ConfigValue, ConfigMap
from .errors import StackAlreadyExistsError
from .events import OpMap, EngineEvent, SummaryEvent
from ._output import OutputMap
from ._server import LanguageServer
from ._workspace import Workspace, PulumiFn, Deployment
from ..runtime.settings import _GRPC_CHANNEL_OPTIONS
@ -48,21 +49,6 @@ class StackInitMode(Enum):
CREATE_OR_SELECT = "create_or_select"
class OutputValue:
value: Any
secret: bool
def __init__(self, value: Any, secret: bool):
self.value = value
self.secret = secret
def __repr__(self):
return _SECRET_SENTINEL if self.secret else repr(self.value)
OutputMap = MutableMapping[str, OutputValue]
class UpdateSummary:
def __init__(self,
# pre-update info
@ -515,15 +501,7 @@ class Stack:
:returns: OutputMap
"""
masked_result = self._run_pulumi_cmd_sync(["stack", "output", "--json"])
plaintext_result = self._run_pulumi_cmd_sync(["stack", "output", "--json", "--show-secrets"])
masked_outputs = json.loads(masked_result.stdout)
plaintext_outputs = json.loads(plaintext_result.stdout)
outputs: OutputMap = {}
for key in plaintext_outputs:
secret = masked_outputs[key] == _SECRET_SENTINEL
outputs[key] = OutputValue(value=plaintext_outputs[key], secret=secret)
return outputs
return self.workspace.stack_outputs(self.name)
def history(self,
page_size: Optional[int] = None,

View file

@ -25,6 +25,7 @@ from typing import (
from ._stack_settings import StackSettings
from ._project_settings import ProjectSettings
from ._config import ConfigMap, ConfigValue
from ._output import OutputMap
PulumiFn = Callable[[], None]
@ -360,3 +361,12 @@ class Workspace(ABC):
:param stack_name: The name of the stack to import.
:param state: The deployment state to import.
"""
@abstractmethod
def stack_outputs(self, stack_name: str) -> OutputMap:
"""
Gets the current set of Stack outputs from the last Stack.up().
:param stack_name: The name of the stack.
:returns: OutputMap
"""