Filter secrets from Pulumi's outputs

When a stack has secrets, we now take the secret values and construct
a regular expression which is just an alternation of all the secret
values. Then, before pushing any string data into an Event, we run the
regular expression and replace all matches with '[secret]'.

Fixes #747
This commit is contained in:
Matt Ellis 2018-03-05 11:39:50 -08:00
parent aa482a124a
commit 96d39b60d1
7 changed files with 147 additions and 63 deletions

View file

@ -36,13 +36,15 @@ func Deploy(update Update, events chan<- Event, opts UpdateOptions) (ResourceCha
}
defer info.Close()
emitter := makeEventEmitter(events, update)
return deployLatest(info, deployOptions{
UpdateOptions: opts,
Destroy: false,
Events: events,
Diag: newEventSink(events),
Events: emitter,
Diag: newEventSink(emitter),
})
}
@ -52,7 +54,7 @@ type deployOptions struct {
Destroy bool // true if we are destroying the stack.
DOT bool // true if we should print the DOT file for this plan.
Events chan<- Event // the channel to write events from the engine to.
Events eventEmitter // the channel to write events from the engine to.
Diag diag.Sink // the sink to use for diag'ing.
}
@ -81,7 +83,7 @@ func deployLatest(info *planContext, opts deployOptions) (ResourceChanges, error
}
} else {
// Otherwise, we will actually deploy the latest bits.
opts.Events <- preludeEvent(opts.DryRun, result.Info.Update.GetTarget().Config)
opts.Events.preludeEvent(opts.DryRun, result.Info.Update.GetTarget().Config)
// Walk the plan, reporting progress and executing the actual operations as we go.
start := time.Now()
@ -95,7 +97,7 @@ func deployLatest(info *planContext, opts deployOptions) (ResourceChanges, error
// Print out the total number of steps performed (and their kinds), the duration, and any summary info.
resourceChanges = ResourceChanges(actions.Ops)
opts.Events <- updateSummaryEvent(actions.MaybeCorrupt, time.Since(start), resourceChanges)
opts.Events.updateSummaryEvent(actions.MaybeCorrupt, time.Since(start), resourceChanges)
if err != nil {
return resourceChanges, err
@ -136,7 +138,7 @@ func (acts *deployActions) OnResourceStepPre(step deploy.Step) (interface{}, err
indent := getIndent(step, acts.Seen)
summary := getResourcePropertiesSummary(step, indent)
details := getResourcePropertiesDetails(step, indent, false, acts.Opts.Debug)
acts.Opts.Events <- resourcePreEvent(step, indent, summary, details)
acts.Opts.Events.resourcePreEvent(step, indent, summary, details)
// Inform the snapshot service that we are about to perform a step.
return acts.Update.BeginMutation()
@ -155,7 +157,7 @@ func (acts *deployActions) OnResourceStepPost(ctx interface{},
// Issue a true, bonafide error.
acts.Opts.Diag.Errorf(diag.ErrorPlanApplyFailed, err)
acts.Opts.Events <- resourceOperationFailedEvent(step, status, acts.Steps)
acts.Opts.Events.resourceOperationFailedEvent(step, status, acts.Steps)
} else {
if step.Logical() {
// Increment the counters.
@ -166,7 +168,7 @@ func (acts *deployActions) OnResourceStepPost(ctx interface{},
// Also show outputs here, since there might be some from the initial registration.
indent := getIndent(step, acts.Seen)
text := getResourceOutputsPropertiesString(step, indent, false, acts.Opts.Debug)
acts.Opts.Events <- resourceOutputsEvent(step, indent, text)
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
}
// Write out the current snapshot. Note that even if a failure has occurred, we should still have a
@ -179,7 +181,7 @@ func (acts *deployActions) OnResourceOutputs(step deploy.Step) error {
indent := getIndent(step, acts.Seen)
text := getResourceOutputsPropertiesString(step, indent, false, acts.Opts.Debug)
acts.Opts.Events <- resourceOutputsEvent(step, indent, text)
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
// There's a chance there are new outputs that weren't written out last time.
// We need to perform another snapshot write to ensure they get written out.

View file

@ -17,12 +17,12 @@ func Destroy(update Update, events chan<- Event, opts UpdateOptions) (ResourceCh
}
defer info.Close()
emitter := makeEventEmitter(events, update)
return deployLatest(info, deployOptions{
UpdateOptions: opts,
Destroy: true,
Events: events,
Diag: newEventSink(events),
Destroy: true,
Events: emitter,
Diag: newEventSink(emitter),
})
}

View file

@ -3,6 +3,8 @@
package engine
import (
"bytes"
"regexp"
"time"
"github.com/pulumi/pulumi/pkg/diag"
@ -102,6 +104,42 @@ type StepEventStateMetadata struct {
Protect bool // true to "protect" this resource (protected resources cannot be deleted).
}
func makeEventEmitter(events chan<- Event, update Update) eventEmitter {
var f filter = &nopFilter{}
target := update.GetTarget()
if target.Config.HasSecureValue() {
var b bytes.Buffer
for _, v := range target.Config {
if !v.Secure() {
continue
}
if b.Len() > 0 {
b.WriteRune('|')
}
secret, err := v.Value(target.Decrypter)
contract.AssertNoError(err)
b.WriteString(regexp.QuoteMeta(secret))
}
f = &regexFilter{re: regexp.MustCompile(b.String())}
}
return eventEmitter{
Chan: events,
Filter: f,
}
}
type eventEmitter struct {
Chan chan<- Event
Filter filter
}
func makeStepEventMetadata(step deploy.Step) StepEventMetdata {
return StepEventMetdata{
Op: step.Op(),
@ -130,8 +168,29 @@ func makeStepEventStateMetadata(state *resource.State) *StepEventStateMetadata {
}
}
func resourceOperationFailedEvent(step deploy.Step, status resource.Status, steps int) Event {
return Event{
type filter interface {
Filter(s string) string
}
type nopFilter struct {
}
func (f *nopFilter) Filter(s string) string {
return s
}
type regexFilter struct {
re *regexp.Regexp
}
func (f *regexFilter) Filter(s string) string {
return f.re.ReplaceAllLiteralString(s, "[secret]")
}
func (e *eventEmitter) resourceOperationFailedEvent(step deploy.Step, status resource.Status, steps int) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: ResourceOperationFailed,
Payload: ResourceOperationFailedPayload{
Metadata: makeStepEventMetadata(step),
@ -141,30 +200,36 @@ func resourceOperationFailedEvent(step deploy.Step, status resource.Status, step
}
}
func resourceOutputsEvent(step deploy.Step, indent int, text string) Event {
return Event{
func (e *eventEmitter) resourceOutputsEvent(step deploy.Step, indent int, text string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: ResourceOutputsEvent,
Payload: ResourceOutputsEventPayload{
Metadata: makeStepEventMetadata(step),
Indent: indent,
Text: text,
Text: e.Filter.Filter(text),
},
}
}
func resourcePreEvent(step deploy.Step, indent int, summary string, details string) Event {
return Event{
func (e *eventEmitter) resourcePreEvent(step deploy.Step, indent int, summary string, details string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: ResourcePreEvent,
Payload: ResourcePreEventPayload{
Metadata: makeStepEventMetadata(step),
Indent: indent,
Summary: summary,
Details: details,
Summary: e.Filter.Filter(summary),
Details: e.Filter.Filter(details),
},
}
}
func preludeEvent(isPreview bool, cfg config.Map) Event {
func (e *eventEmitter) preludeEvent(isPreview bool, cfg config.Map) {
contract.Requiref(e != nil, "e", "!= nil")
configStringMap := make(map[string]string, len(cfg))
for k, v := range cfg {
keyString := k.String()
@ -173,7 +238,7 @@ func preludeEvent(isPreview bool, cfg config.Map) Event {
configStringMap[keyString] = valueString
}
return Event{
e.Chan <- Event{
Type: PreludeEvent,
Payload: PreludeEventPayload{
IsPreview: isPreview,
@ -182,8 +247,10 @@ func preludeEvent(isPreview bool, cfg config.Map) Event {
}
}
func previewSummaryEvent(resourceChanges ResourceChanges) Event {
return Event{
func (e *eventEmitter) previewSummaryEvent(resourceChanges ResourceChanges) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: SummaryEvent,
Payload: SummaryEventPayload{
IsPreview: true,
@ -194,8 +261,11 @@ func previewSummaryEvent(resourceChanges ResourceChanges) Event {
}
}
func updateSummaryEvent(maybeCorrupt bool, duration time.Duration, resourceChanges ResourceChanges) Event {
return Event{
func (e *eventEmitter) updateSummaryEvent(maybeCorrupt bool,
duration time.Duration, resourceChanges ResourceChanges) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: SummaryEvent,
Payload: SummaryEventPayload{
IsPreview: false,
@ -206,55 +276,65 @@ func updateSummaryEvent(maybeCorrupt bool, duration time.Duration, resourceChang
}
}
func diagDebugEvent(msg string) Event {
return Event{
func (e *eventEmitter) diagDebugEvent(msg string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: DiagEvent,
Payload: DiagEventPayload{
Message: msg,
Message: e.Filter.Filter(msg),
Color: colors.Raw,
Severity: diag.Debug,
},
}
}
func diagInfoEvent(msg string) Event {
return Event{
func (e *eventEmitter) diagInfoEvent(msg string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: DiagEvent,
Payload: DiagEventPayload{
Message: msg,
Message: e.Filter.Filter(msg),
Color: colors.Raw,
Severity: diag.Info,
},
}
}
func diagInfoerrEvent(msg string) Event {
return Event{
func (e *eventEmitter) diagInfoerrEvent(msg string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: DiagEvent,
Payload: DiagEventPayload{
Message: msg,
Message: e.Filter.Filter(msg),
Color: colors.Raw,
Severity: diag.Infoerr,
},
}
}
func diagErrorEvent(msg string) Event {
return Event{
func (e *eventEmitter) diagErrorEvent(msg string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: DiagEvent,
Payload: DiagEventPayload{
Message: msg,
Message: e.Filter.Filter(msg),
Color: colors.Raw,
Severity: diag.Error,
},
}
}
func diagWarningEvent(msg string) Event {
return Event{
func (e *eventEmitter) diagWarningEvent(msg string) {
contract.Requiref(e != nil, "e", "!= nil")
e.Chan <- Event{
Type: DiagEvent,
Payload: DiagEventPayload{
Message: msg,
Message: e.Filter.Filter(msg),
Color: colors.Raw,
Severity: diag.Warning,
},

View file

@ -13,9 +13,7 @@ import (
"github.com/pulumi/pulumi/pkg/util/contract"
)
func newEventSink(events chan<- Event) diag.Sink {
contract.Require(events != nil, "events")
func newEventSink(events eventEmitter) diag.Sink {
return &eventSink{
events: events,
counts: make(map[diag.Severity]int),
@ -26,7 +24,7 @@ const eventSinkIDPrefix = "PU"
// eventSink is a sink which writes all events to a channel
type eventSink struct {
events chan<- Event // the channel to emit events into.
events eventEmitter // the channel to emit events into.
counts map[diag.Severity]int // the number of messages that have been issued per severity.
mutex sync.RWMutex // a mutex for guarding updates to the counts map
}
@ -63,7 +61,7 @@ func (s *eventSink) Debugf(d *diag.Diag, args ...interface{}) {
if glog.V(9) {
glog.V(9).Infof("eventSink::Debug(%v)", msg[:len(msg)-1])
}
s.events <- diagDebugEvent(msg)
s.events.diagDebugEvent(msg)
s.incrementCount(diag.Debug)
}
@ -72,7 +70,7 @@ func (s *eventSink) Infof(d *diag.Diag, args ...interface{}) {
if glog.V(5) {
glog.V(5).Infof("eventSink::Info(%v)", msg[:len(msg)-1])
}
s.events <- diagInfoEvent(msg)
s.events.diagInfoEvent(msg)
s.incrementCount(diag.Info)
}
@ -81,7 +79,7 @@ func (s *eventSink) Infoerrf(d *diag.Diag, args ...interface{}) {
if glog.V(5) {
glog.V(5).Infof("eventSink::Infoerr(%v)", msg[:len(msg)-1])
}
s.events <- diagInfoerrEvent(msg)
s.events.diagInfoerrEvent(msg)
s.incrementCount(diag.Infoerr)
}
@ -90,7 +88,7 @@ func (s *eventSink) Errorf(d *diag.Diag, args ...interface{}) {
if glog.V(5) {
glog.V(5).Infof("eventSink::Error(%v)", msg[:len(msg)-1])
}
s.events <- diagErrorEvent(msg)
s.events.diagErrorEvent(msg)
s.incrementCount(diag.Error)
}
@ -99,7 +97,7 @@ func (s *eventSink) Warningf(d *diag.Diag, args ...interface{}) {
if glog.V(5) {
glog.V(5).Infof("eventSink::Warning(%v)", msg[:len(msg)-1])
}
s.events <- diagWarningEvent(msg)
s.events.diagWarningEvent(msg)
s.incrementCount(diag.Warning)
}

View file

@ -183,7 +183,8 @@ func (res *planResult) Close() error {
// printPlan prints the plan's result to the plan's Options.Events stream.
func printPlan(result *planResult) (ResourceChanges, error) {
result.Options.Events <- preludeEvent(result.Options.DryRun, result.Info.Update.GetTarget().Config)
result.Options.Events.preludeEvent(result.Options.DryRun,
result.Info.Update.GetTarget().Config)
// Walk the plan's steps and and pretty-print them out.
actions := newPreviewActions(result.Options)
@ -200,7 +201,7 @@ func printPlan(result *planResult) (ResourceChanges, error) {
// Emit an event with a summary of operation counts.
changes := ResourceChanges(actions.Ops)
result.Options.Events <- previewSummaryEvent(changes)
result.Options.Events.previewSummaryEvent(changes)
return changes, nil
}

View file

@ -25,13 +25,15 @@ func Preview(update Update, events chan<- Event, opts UpdateOptions) error {
// should elide unknown input/output properties when interacting with the language and resource providers and we
// will produce unexpected results.
opts.DryRun = true
emitter := makeEventEmitter(events, update)
return previewLatest(info, deployOptions{
UpdateOptions: opts,
Destroy: false,
Events: events,
Diag: newEventSink(events),
Events: emitter,
Diag: newEventSink(emitter),
})
}
@ -82,7 +84,7 @@ func (acts *previewActions) OnResourceStepPre(step deploy.Step) (interface{}, er
indent := getIndent(step, acts.Seen)
summary := getResourcePropertiesSummary(step, indent)
details := getResourcePropertiesDetails(step, indent, true, acts.Opts.Debug)
acts.Opts.Events <- resourcePreEvent(step, indent, summary, details)
acts.Opts.Events.resourcePreEvent(step, indent, summary, details)
return nil, nil
}
@ -108,6 +110,7 @@ func (acts *previewActions) OnResourceOutputs(step deploy.Step) error {
indent := getIndent(step, acts.Seen)
text := getResourceOutputsPropertiesString(step, indent, true, acts.Opts.Debug)
acts.Opts.Events <- resourceOutputsEvent(step, indent, text)
acts.Opts.Events.resourceOutputsEvent(step, indent, text)
return nil
}

View file

@ -33,15 +33,15 @@ type Crypter interface {
}
// A nopDecrypter simply returns the ciphertext as-is.
type nopDecrypter int
type nopDecrypter struct{}
var NopDecrypter Decrypter = nopDecrypter(0)
var NopDecrypter Decrypter = nopDecrypter{}
func (nopDecrypter) DecryptValue(ciphertext string) (string, error) {
return ciphertext, nil
}
// NewBlindingDecrypter returns a Decrypter that instead of decrypting data, just returns "********", it can
// NewBlindingDecrypter returns a Decrypter that instead of decrypting data, just returns "[secret]", it can
// be used when you want to display configuration information to a user but don't want to prompt for a password
// so secrets will not be decrypted.
func NewBlindingDecrypter() Decrypter {
@ -51,7 +51,7 @@ func NewBlindingDecrypter() Decrypter {
type blindingDecrypter struct{}
func (b blindingDecrypter) DecryptValue(ciphertext string) (string, error) {
return "********", nil
return "[secret]", nil
}
// NewPanicCrypter returns a new config crypter that will panic if used.