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
This commit is contained in:
parent
44a806180b
commit
d190247ede
|
@ -10,6 +10,11 @@ CHANGELOG
|
||||||
providers can now be used with full ARNs rather than just Aliases
|
providers can now be used with full ARNs rather than just Aliases
|
||||||
[#5138](https://github.com/pulumi/pulumi/pull/5138)
|
[#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)
|
## 2.8.2 (2020-08-07)
|
||||||
|
|
||||||
- Add nuget badge to README [#5117](https://github.com/pulumi/pulumi/pull/5117)
|
- Add nuget badge to README [#5117](https://github.com/pulumi/pulumi/pull/5117)
|
||||||
|
|
|
@ -15,24 +15,15 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/v2/backend"
|
|
||||||
"github.com/pulumi/pulumi/pkg/v2/backend/display"
|
"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/resource/config"
|
||||||
"github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil"
|
"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 {
|
func newHistoryCmd() *cobra.Command {
|
||||||
var stack string
|
var stack string
|
||||||
var jsonOut bool
|
var jsonOut bool
|
||||||
|
@ -41,10 +32,12 @@ func newHistoryCmd() *cobra.Command {
|
||||||
Use: "history",
|
Use: "history",
|
||||||
Aliases: []string{"hist"},
|
Aliases: []string{"hist"},
|
||||||
SuggestFor: []string{"updates"},
|
SuggestFor: []string{"updates"},
|
||||||
Short: "[PREVIEW] Update history for a stack",
|
Hidden: true,
|
||||||
Long: `Update history for a stack
|
Short: "[DEPRECATED] Display history for a stack",
|
||||||
|
Long: "Display history for a stack.\n\n" +
|
||||||
This command lists data about previous updates for a stack.`,
|
"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,
|
Args: cmdutil.NoArgs,
|
||||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||||
opts := display.Options{
|
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")
|
&jsonOut, "json", "j", false, "Emit output as JSON")
|
||||||
return cmd
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -180,6 +180,7 @@ func newStackCmd() *cobra.Command {
|
||||||
cmd.AddCommand(newStackTagCmd())
|
cmd.AddCommand(newStackTagCmd())
|
||||||
cmd.AddCommand(newStackRenameCmd())
|
cmd.AddCommand(newStackRenameCmd())
|
||||||
cmd.AddCommand(newStackChangeSecretsProviderCmd())
|
cmd.AddCommand(newStackChangeSecretsProviderCmd())
|
||||||
|
cmd.AddCommand(newStackHistoryCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
194
pkg/cmd/pulumi/stack_history.go
Normal file
194
pkg/cmd/pulumi/stack_history.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue