pulumi/cmd/stack_output.go
Matt Ellis ce4b585065 Don't show secret outputs by default on the CLI
When using `pulumi stack` or `pulumi stack output`, we were showing
secret values in the worst way possible. They were displayed in our
object structure with a signature key that denoted they were secrets
but they were not encrypted, so you still saw the underlying value.

To be able to continue to leverage the mechanisms we have for
serializing property maps, we add a rewriting step where we make a
pass over the property map before we serialize it. For any secret
values we find, if `--show-secrets` was passed, we simply replace the
secret value with the underlying element it wraps (this ensures that
we don't serialize it as a rich object with the signature key). If
`--show-secrets` was not passed, we simply replace it with a new
string property with the value `[secret]`.

This mimics the behavor we see from the stack outputs we see when you
complete a `pulumi update`
2019-05-10 17:07:52 -07:00

151 lines
5 KiB
Go

// Copyright 2016-2018, 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.
package cmd
import (
"fmt"
"github.com/pulumi/pulumi/pkg/resource"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/backend/display"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/resource/stack"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
)
func newStackOutputCmd() *cobra.Command {
var jsonOut bool
var showSecrets bool
var stackName string
cmd := &cobra.Command{
Use: "output [property-name]",
Args: cmdutil.MaximumNArgs(1),
Short: "Show a stack's output properties",
Long: "Show a stack's output properties.\n" +
"\n" +
"By default, this command lists all output properties exported from a stack.\n" +
"If a specific property-name is supplied, just that property's value is shown.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
opts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
// Fetch the current stack and its output properties.
s, err := requireStack(stackName, false, opts, true /*setCurrent*/)
if err != nil {
return err
}
snap, err := s.Snapshot(commandContext())
if err != nil {
return err
}
outputs, err := getStackOutputs(snap, showSecrets)
if err != nil {
return errors.Wrap(err, "getting outputs")
}
if outputs == nil {
outputs = make(map[string]interface{})
}
// If there is an argument, just print that property. Else, print them all (similar to `pulumi stack`).
if len(args) > 0 {
name := args[0]
v, has := outputs[name]
if has {
if jsonOut {
if err := printJSON(v); err != nil {
return err
}
} else {
fmt.Printf("%v\n", stringifyOutput(v))
}
} else {
return errors.Errorf("current stack does not have output property '%v'", name)
}
} else if jsonOut {
if err := printJSON(outputs); err != nil {
return err
}
} else {
printStackOutputs(outputs)
}
return nil
}),
}
cmd.PersistentFlags().BoolVarP(
&jsonOut, "json", "j", false, "Emit output as JSON")
cmd.PersistentFlags().StringVarP(
&stackName, "stack", "s", "", "The name of the stack to operate on. Defaults to the current stack")
cmd.PersistentFlags().BoolVar(
&showSecrets, "show-secrets", false, "Display outputs which are marked as secret in plaintext")
return cmd
}
// massagePropertyValue takes a property value and strips out the secrets annotations from it. If showSecrets is
// not true any secret values are replaced with "[secret]".
func massagePropertyValue(v resource.PropertyValue, showSecrets bool) resource.PropertyValue {
switch {
case v.IsArray():
new := make([]resource.PropertyValue, len(v.ArrayValue()))
for i, e := range v.ArrayValue() {
new[i] = massagePropertyValue(e, showSecrets)
}
return resource.NewArrayProperty(new)
case v.IsObject():
new := make(resource.PropertyMap, len(v.ObjectValue()))
for k, e := range v.ObjectValue() {
new[k] = massagePropertyValue(e, showSecrets)
}
return resource.NewObjectProperty(massageSecrets(v.ObjectValue(), showSecrets))
case v.IsSecret() && showSecrets:
return massagePropertyValue(v.SecretValue().Element, showSecrets)
case v.IsSecret():
return resource.NewStringProperty("[secret]")
default:
return v
}
}
// massageSecrets takes a property map and returns a new map by transforming each value with massagePropertyValue
// This allows us to serialize the resulting map using our existing serialization logic we use for deployments, to
// produce sane output for stackOutputs. If we did not do this, SecretValues would be serialized as objects
// with the signature key and value.
func massageSecrets(m resource.PropertyMap, showSecrets bool) resource.PropertyMap {
new := make(resource.PropertyMap, len(m))
for k, e := range m {
new[k] = massagePropertyValue(e, showSecrets)
}
return new
}
func getStackOutputs(snap *deploy.Snapshot, showSecrets bool) (map[string]interface{}, error) {
state, err := stack.GetRootStackResource(snap)
if err != nil {
return nil, err
}
// massageSecrets will remove all the secrets from the property map, so it should be safe to pass a panic
// crypter. This also ensure that if for some reason we didn't remove everything, we don't accidentally disclose
// secret values!
return stack.SerializeProperties(massageSecrets(state.Outputs, showSecrets), config.NewPanicCrypter())
}