From d190247ede3df64cb0ad6a30e806a1839c0103ca Mon Sep 17 00:00:00 2001 From: stack72 Date: Tue, 11 Aug 2020 12:11:45 +0100 Subject: [PATCH] The history command should be a subcommand of stack Fixes: #5134 This ensures that `pulumi history` has been deprecated in favor of the new `pulumi stack history` command. The deprecated command will be removed in v3.0.0 of Pulumi --- CHANGELOG.md | 5 + pkg/cmd/pulumi/history.go | 141 ++--------------------- pkg/cmd/pulumi/stack.go | 1 + pkg/cmd/pulumi/stack_history.go | 194 ++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 134 deletions(-) create mode 100644 pkg/cmd/pulumi/stack_history.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d914705..59deb4fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ CHANGELOG providers can now be used with full ARNs rather than just Aliases [#5138](https://github.com/pulumi/pulumi/pull/5138) +- Ensure the 'history' command is a subcommand of 'stack'. + This means that `pulumi history` has been deprecated in favour + of `pulumi stack history`. + [#5158](https://github.com/pulumi/pulumi/pull/5158) + ## 2.8.2 (2020-08-07) - Add nuget badge to README [#5117](https://github.com/pulumi/pulumi/pull/5117) diff --git a/pkg/cmd/pulumi/history.go b/pkg/cmd/pulumi/history.go index 622f03edf..c1eae6c09 100644 --- a/pkg/cmd/pulumi/history.go +++ b/pkg/cmd/pulumi/history.go @@ -15,24 +15,15 @@ package main import ( - "encoding/json" - "fmt" - "sort" - "strings" - "time" - - "github.com/dustin/go-humanize" "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/pulumi/pulumi/pkg/v2/backend" "github.com/pulumi/pulumi/pkg/v2/backend/display" - "github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors" "github.com/pulumi/pulumi/sdk/v2/go/common/resource/config" "github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil" - "github.com/pulumi/pulumi/sdk/v2/go/common/util/contract" ) +// TO-DO: Remove as part of Pulumi v3.0.0 func newHistoryCmd() *cobra.Command { var stack string var jsonOut bool @@ -41,10 +32,12 @@ func newHistoryCmd() *cobra.Command { Use: "history", Aliases: []string{"hist"}, SuggestFor: []string{"updates"}, - Short: "[PREVIEW] Update history for a stack", - Long: `Update history for a stack - -This command lists data about previous updates for a stack.`, + Hidden: true, + Short: "[DEPRECATED] Display history for a stack", + Long: "Display history for a stack.\n\n" + + "This command displays data about previous updates for a stack.\n\n" + + "This command is now DEPRECATED, please use `pulumi stack history`.\n" + + "The command will be removed in a future release", Args: cmdutil.NoArgs, Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { opts := display.Options{ @@ -85,123 +78,3 @@ This command lists data about previous updates for a stack.`, &jsonOut, "json", "j", false, "Emit output as JSON") return cmd } - -// updateInfoJSON is the shape of the --json output for a configuration value. While we can add fields to this -// structure in the future, we should not change existing fields. -type updateInfoJSON struct { - Kind string `json:"kind"` - StartTime string `json:"startTime"` - Message string `json:"message"` - Environment map[string]string `json:"environment"` - Config map[string]configValueJSON `json:"config"` - Result string `json:"result,omitempty"` - - // These values are only present once the update finishes - EndTime *string `json:"endTime,omitempty"` - ResourceChanges *map[string]int `json:"resourceChanges,omitempty"` -} - -func displayUpdatesJSON(updates []backend.UpdateInfo, decrypter config.Decrypter) error { - makeStringRef := func(s string) *string { - return &s - } - - updatesJSON := make([]updateInfoJSON, len(updates)) - for idx, update := range updates { - info := updateInfoJSON{ - Kind: string(update.Kind), - StartTime: time.Unix(update.StartTime, 0).UTC().Format(timeFormat), - Message: update.Message, - Environment: update.Environment, - } - - info.Config = make(map[string]configValueJSON) - for k, v := range update.Config { - configValue := configValueJSON{ - Secret: v.Secure(), - } - if !v.Secure() || (v.Secure() && decrypter != nil) { - value, err := v.Value(decrypter) - contract.AssertNoError(err) - configValue.Value = makeStringRef(value) - - if v.Object() { - var obj interface{} - if err := json.Unmarshal([]byte(value), &obj); err != nil { - return err - } - configValue.ObjectValue = obj - } - } - - info.Config[k.String()] = configValue - } - info.Result = string(update.Result) - if update.Result != backend.InProgressResult { - info.EndTime = makeStringRef(time.Unix(update.EndTime, 0).UTC().Format(timeFormat)) - resourceChanges := make(map[string]int) - for k, v := range update.ResourceChanges { - resourceChanges[string(k)] = v - } - info.ResourceChanges = &resourceChanges - } - updatesJSON[idx] = info - } - - return printJSON(updatesJSON) -} - -func displayUpdatesConsole(updates []backend.UpdateInfo, opts display.Options) error { - if len(updates) == 0 { - fmt.Println("Stack has never been updated") - return nil - } - - printResourceChanges := func(background, text, sign, reset string, amount int) { - msg := opts.Color.Colorize(fmt.Sprintf("%s%s%s%v%s", background, text, sign, amount, reset)) - fmt.Print(msg) - } - - for _, update := range updates { - - fmt.Printf("UpdateKind: %v\n", update.Kind) - if update.Result == "succeeded" { - fmt.Print(opts.Color.Colorize(fmt.Sprintf("%sStatus: %v%s\n", colors.Green, update.Result, colors.Reset))) - } else { - fmt.Print(opts.Color.Colorize(fmt.Sprintf("%sStatus: %v%s\n", colors.Red, update.Result, colors.Reset))) - } - fmt.Printf("Message: %v\n", update.Message) - - printResourceChanges(colors.GreenBackground, colors.Black, "+", colors.Reset, update.ResourceChanges["create"]) - printResourceChanges(colors.RedBackground, colors.Black, "-", colors.Reset, update.ResourceChanges["delete"]) - printResourceChanges(colors.YellowBackground, colors.Black, "~", colors.Reset, update.ResourceChanges["update"]) - printResourceChanges(colors.BlueBackground, colors.Black, " ", colors.Reset, update.ResourceChanges["same"]) - - timeStart := time.Unix(update.StartTime, 0) - timeCreated := humanize.Time(timeStart) - timeEnd := time.Unix(update.EndTime, 0) - duration := timeEnd.Sub(timeStart) - fmt.Printf("%sUpdated %s took %s\n", " ", timeCreated, duration) - - isEmpty := func(s string) bool { - return len(strings.TrimSpace(s)) == 0 - } - var keys []string - for k := range update.Environment { - keys = append(keys, k) - } - sort.Strings(keys) - indent := 4 - for _, k := range keys { - if k == backend.GitHead && !isEmpty(update.Environment[k]) { - fmt.Print(opts.Color.Colorize( - fmt.Sprintf("%*s%s%s: %s%s\n", indent, "", colors.Yellow, k, update.Environment[k], colors.Reset))) - } else if !isEmpty(update.Environment[k]) { - fmt.Printf("%*s%s: %s\n", indent, "", k, update.Environment[k]) - } - } - fmt.Println("") - } - - return nil -} diff --git a/pkg/cmd/pulumi/stack.go b/pkg/cmd/pulumi/stack.go index 6157edb67..3aa5391ad 100644 --- a/pkg/cmd/pulumi/stack.go +++ b/pkg/cmd/pulumi/stack.go @@ -180,6 +180,7 @@ func newStackCmd() *cobra.Command { cmd.AddCommand(newStackTagCmd()) cmd.AddCommand(newStackRenameCmd()) cmd.AddCommand(newStackChangeSecretsProviderCmd()) + cmd.AddCommand(newStackHistoryCmd()) return cmd } diff --git a/pkg/cmd/pulumi/stack_history.go b/pkg/cmd/pulumi/stack_history.go new file mode 100644 index 000000000..3bb5bf63c --- /dev/null +++ b/pkg/cmd/pulumi/stack_history.go @@ -0,0 +1,194 @@ +package main + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/dustin/go-humanize" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/pulumi/pulumi/pkg/v2/backend" + "github.com/pulumi/pulumi/pkg/v2/backend/display" + "github.com/pulumi/pulumi/sdk/v2/go/common/diag/colors" + "github.com/pulumi/pulumi/sdk/v2/go/common/resource/config" + "github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil" + "github.com/pulumi/pulumi/sdk/v2/go/common/util/contract" +) + +func newStackHistoryCmd() *cobra.Command { + var stack string + var jsonOut bool + var showSecrets bool + + cmd := &cobra.Command{ + Use: "history", + Aliases: []string{"hist"}, + SuggestFor: []string{"updates"}, + Short: "[PREVIEW] Display history for a stack", + Long: `Display history for a stack + +This command displays data about previous updates for a stack.`, + Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { + opts := display.Options{ + Color: cmdutil.GetGlobalColorization(), + } + s, err := requireStack(stack, false /*offerNew */, opts, false /*setCurrent*/) + if err != nil { + return err + } + b := s.Backend() + updates, err := b.GetHistory(commandContext(), s.Ref()) + if err != nil { + return errors.Wrap(err, "getting history") + } + var decrypter config.Decrypter + if showSecrets { + crypter, err := getStackDecrypter(s) + if err != nil { + return errors.Wrap(err, "decrypting secrets") + } + decrypter = crypter + } + + if jsonOut { + return displayUpdatesJSON(updates, decrypter) + } + + return displayUpdatesConsole(updates, opts) + }), + } + + cmd.PersistentFlags().StringVarP( + &stack, "stack", "s", "", + "Choose a stack other than the currently selected one") + cmd.Flags().BoolVar( + &showSecrets, "show-secrets", false, + "Show secret values when listing config instead of displaying blinded values") + cmd.PersistentFlags().BoolVarP( + &jsonOut, "json", "j", false, "Emit output as JSON") + return cmd +} + +// updateInfoJSON is the shape of the --json output for a configuration value. While we can add fields to this +// structure in the future, we should not change existing fields. +type updateInfoJSON struct { + Kind string `json:"kind"` + StartTime string `json:"startTime"` + Message string `json:"message"` + Environment map[string]string `json:"environment"` + Config map[string]configValueJSON `json:"config"` + Result string `json:"result,omitempty"` + + // These values are only present once the update finishes + EndTime *string `json:"endTime,omitempty"` + ResourceChanges *map[string]int `json:"resourceChanges,omitempty"` +} + +func displayUpdatesJSON(updates []backend.UpdateInfo, decrypter config.Decrypter) error { + makeStringRef := func(s string) *string { + return &s + } + + updatesJSON := make([]updateInfoJSON, len(updates)) + for idx, update := range updates { + info := updateInfoJSON{ + Kind: string(update.Kind), + StartTime: time.Unix(update.StartTime, 0).UTC().Format(timeFormat), + Message: update.Message, + Environment: update.Environment, + } + + info.Config = make(map[string]configValueJSON) + for k, v := range update.Config { + configValue := configValueJSON{ + Secret: v.Secure(), + } + if !v.Secure() || (v.Secure() && decrypter != nil) { + value, err := v.Value(decrypter) + contract.AssertNoError(err) + configValue.Value = makeStringRef(value) + + if v.Object() { + var obj interface{} + if err := json.Unmarshal([]byte(value), &obj); err != nil { + return err + } + configValue.ObjectValue = obj + } + } + + info.Config[k.String()] = configValue + } + info.Result = string(update.Result) + if update.Result != backend.InProgressResult { + info.EndTime = makeStringRef(time.Unix(update.EndTime, 0).UTC().Format(timeFormat)) + resourceChanges := make(map[string]int) + for k, v := range update.ResourceChanges { + resourceChanges[string(k)] = v + } + info.ResourceChanges = &resourceChanges + } + updatesJSON[idx] = info + } + + return printJSON(updatesJSON) +} + +func displayUpdatesConsole(updates []backend.UpdateInfo, opts display.Options) error { + if len(updates) == 0 { + fmt.Println("Stack has never been updated") + return nil + } + + printResourceChanges := func(background, text, sign, reset string, amount int) { + msg := opts.Color.Colorize(fmt.Sprintf("%s%s%s%v%s", background, text, sign, amount, reset)) + fmt.Print(msg) + } + + for _, update := range updates { + + fmt.Printf("UpdateKind: %v\n", update.Kind) + if update.Result == "succeeded" { + fmt.Print(opts.Color.Colorize(fmt.Sprintf("%sStatus: %v%s\n", colors.Green, update.Result, colors.Reset))) + } else { + fmt.Print(opts.Color.Colorize(fmt.Sprintf("%sStatus: %v%s\n", colors.Red, update.Result, colors.Reset))) + } + fmt.Printf("Message: %v\n", update.Message) + + printResourceChanges(colors.GreenBackground, colors.Black, "+", colors.Reset, update.ResourceChanges["create"]) + printResourceChanges(colors.RedBackground, colors.Black, "-", colors.Reset, update.ResourceChanges["delete"]) + printResourceChanges(colors.YellowBackground, colors.Black, "~", colors.Reset, update.ResourceChanges["update"]) + printResourceChanges(colors.BlueBackground, colors.Black, " ", colors.Reset, update.ResourceChanges["same"]) + + timeStart := time.Unix(update.StartTime, 0) + timeCreated := humanize.Time(timeStart) + timeEnd := time.Unix(update.EndTime, 0) + duration := timeEnd.Sub(timeStart) + fmt.Printf("%sUpdated %s took %s\n", " ", timeCreated, duration) + + isEmpty := func(s string) bool { + return len(strings.TrimSpace(s)) == 0 + } + var keys []string + for k := range update.Environment { + keys = append(keys, k) + } + sort.Strings(keys) + indent := 4 + for _, k := range keys { + if k == backend.GitHead && !isEmpty(update.Environment[k]) { + fmt.Print(opts.Color.Colorize( + fmt.Sprintf("%*s%s%s: %s%s\n", indent, "", colors.Yellow, k, update.Environment[k], colors.Reset))) + } else if !isEmpty(update.Environment[k]) { + fmt.Printf("%*s%s: %s\n", indent, "", k, update.Environment[k]) + } + } + fmt.Println("") + } + + return nil +}