When outputing JSON, if we have a fixed number of log entries (i.e. we are not `--follow`'ing, we wrap each entry in array. Otherwise, we just emit each log entry as an object at top level. As part of this change, I've adopted a slightly more precise time output format in `pulumi stack ls` when using JSON output. These times now match the default output from `console.log(new Date())`
194 lines
5.8 KiB
Go
194 lines
5.8 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"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/pulumi/pulumi/pkg/backend"
|
|
"github.com/pulumi/pulumi/pkg/backend/display"
|
|
"github.com/pulumi/pulumi/pkg/backend/httpstate"
|
|
"github.com/pulumi/pulumi/pkg/backend/state"
|
|
"github.com/pulumi/pulumi/pkg/tokens"
|
|
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
|
"github.com/pulumi/pulumi/pkg/workspace"
|
|
)
|
|
|
|
func newStackLsCmd() *cobra.Command {
|
|
var allStacks bool
|
|
var jsonOut bool
|
|
cmd := &cobra.Command{
|
|
Use: "ls",
|
|
Short: "List all known stacks",
|
|
Args: cmdutil.NoArgs,
|
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
|
var packageFilter *tokens.PackageName
|
|
if !allStacks {
|
|
// Ensure we are in a project; if not, we will fail.
|
|
projPath, err := workspace.DetectProjectPath()
|
|
if err != nil {
|
|
return errors.Wrapf(err, "could not detect current project")
|
|
} else if projPath == "" {
|
|
return errors.New("no Pulumi.yaml found; please run this command in a project directory")
|
|
}
|
|
|
|
proj, err := workspace.LoadProject(projPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not load current project")
|
|
}
|
|
packageFilter = &proj.Name
|
|
}
|
|
|
|
// Get the current backend.
|
|
b, err := currentBackend(display.Options{Color: cmdutil.GetGlobalColorization()})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the current stack so we can print a '*' next to it.
|
|
var current string
|
|
if s, _ := state.CurrentStack(commandContext(), b); s != nil {
|
|
// If we couldn't figure out the current stack, just don't print the '*' later on instead of failing.
|
|
current = s.Ref().String()
|
|
}
|
|
|
|
// List all of the stacks available.
|
|
stackSummaries, err := b.ListStacks(commandContext(), packageFilter)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Sort by stack name.
|
|
sort.Slice(stackSummaries, func(i, j int) bool {
|
|
return stackSummaries[i].Name().String() < stackSummaries[j].Name().String()
|
|
})
|
|
|
|
if jsonOut {
|
|
return formatJSON(b, current, stackSummaries)
|
|
}
|
|
|
|
return formatConsole(b, current, stackSummaries)
|
|
}),
|
|
}
|
|
cmd.PersistentFlags().BoolVarP(
|
|
&jsonOut, "json", "j", false, "Emit outputs as JSON")
|
|
cmd.PersistentFlags().BoolVarP(
|
|
&allStacks, "all", "a", false, "List all stacks instead of just stacks for the current project")
|
|
|
|
return cmd
|
|
}
|
|
|
|
// stackSummaryJSON is the shape of the --json output of this command. When --json is passed, we print an array
|
|
// of stackSummaryJSON objects. While we can add fields to this structure in the future, we should not change
|
|
// existing fields.
|
|
type stackSummaryJSON struct {
|
|
Name string `json:"name"`
|
|
Current bool `json:"current"`
|
|
LastUpdate string `json:"lastUpdate,omitEmpty"`
|
|
ResourceCount *int `json:"resourceCount,omitEmpty"`
|
|
URL string `json:"url,omitEmpty"`
|
|
}
|
|
|
|
func formatJSON(b backend.Backend, currentStack string, stackSummaries []backend.StackSummary) error {
|
|
output := make([]stackSummaryJSON, len(stackSummaries))
|
|
for idx, summary := range stackSummaries {
|
|
summaryJSON := stackSummaryJSON{
|
|
Name: summary.Name().String(),
|
|
ResourceCount: summary.ResourceCount(),
|
|
Current: summary.Name().String() == currentStack,
|
|
}
|
|
|
|
if summary.LastUpdate() != nil {
|
|
summaryJSON.LastUpdate = summary.LastUpdate().UTC().Format(timeFormat)
|
|
}
|
|
|
|
if httpBackend, ok := b.(httpstate.Backend); ok {
|
|
if consoleURL, err := httpBackend.StackConsoleURL(summary.Name()); err == nil {
|
|
summaryJSON.URL = consoleURL
|
|
}
|
|
}
|
|
|
|
output[idx] = summaryJSON
|
|
}
|
|
|
|
return printJSON(output)
|
|
}
|
|
|
|
func formatConsole(b backend.Backend, currentStack string, stackSummaries []backend.StackSummary) error {
|
|
_, showURLColumn := b.(httpstate.Backend)
|
|
|
|
// Devote 48 characters to the name width, unless there is a longer name.
|
|
maxName := 47
|
|
for _, summary := range stackSummaries {
|
|
name := summary.Name().String()
|
|
if len(name) > maxName {
|
|
maxName = len(name)
|
|
}
|
|
}
|
|
maxName++ // Account for adding the '*' to the currently selected stack.
|
|
|
|
// Header string and formatting options to align columns.
|
|
formatDirective := "%-" + strconv.Itoa(maxName) + "s %-24s %-18s"
|
|
headers := []interface{}{"NAME", "LAST UPDATE", "RESOURCE COUNT"}
|
|
if showURLColumn {
|
|
formatDirective += " %s"
|
|
headers = append(headers, "URL")
|
|
}
|
|
formatDirective = formatDirective + "\n"
|
|
|
|
fmt.Printf(formatDirective, headers...)
|
|
for _, summary := range stackSummaries {
|
|
const none = "n/a"
|
|
|
|
// Name column
|
|
name := summary.Name().String()
|
|
if name == currentStack {
|
|
name += "*"
|
|
}
|
|
|
|
// Last update column
|
|
lastUpdate := none
|
|
if stackLastUpdate := summary.LastUpdate(); stackLastUpdate != nil {
|
|
lastUpdate = humanize.Time(*stackLastUpdate)
|
|
}
|
|
|
|
// ResourceCount column
|
|
resourceCount := none
|
|
if stackResourceCount := summary.ResourceCount(); stackResourceCount != nil {
|
|
resourceCount = strconv.Itoa(*stackResourceCount)
|
|
}
|
|
|
|
// Render the columns.
|
|
values := []interface{}{name, lastUpdate, resourceCount}
|
|
if showURLColumn {
|
|
url := none
|
|
if httpBackend, ok := b.(httpstate.Backend); ok {
|
|
if consoleURL, err := httpBackend.StackConsoleURL(summary.Name()); err == nil {
|
|
url = consoleURL
|
|
}
|
|
}
|
|
values = append(values, url)
|
|
}
|
|
|
|
fmt.Printf(formatDirective, values...)
|
|
}
|
|
|
|
return nil
|
|
}
|