Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The history command should be a subcommand of stack #5158

Merged
merged 1 commit into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
141 changes: 7 additions & 134 deletions pkg/cmd/pulumi/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions pkg/cmd/pulumi/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func newStackCmd() *cobra.Command {
cmd.AddCommand(newStackTagCmd())
cmd.AddCommand(newStackRenameCmd())
cmd.AddCommand(newStackChangeSecretsProviderCmd())
cmd.AddCommand(newStackHistoryCmd())

return cmd
}
Expand Down
194 changes: 194 additions & 0 deletions pkg/cmd/pulumi/stack_history.go
Original file line number Diff line number Diff line change
@@ -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
}