Change backend.ListStacks to return a new StackSummary interface (#1931)
* Have backend.ListStacks return a new StackSummary interface * Update filestake backend to use new type * Update httpstate backend to use new type * Update commands to use new type * lint * Address PR feedback * Lint
This commit is contained in:
parent
3642cca98f
commit
792c316e5e
|
@ -23,13 +23,11 @@ import (
|
|||
"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/util/contract"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
)
|
||||
|
||||
|
@ -57,7 +55,7 @@ func newStackLsCmd() *cobra.Command {
|
|||
Color: cmdutil.GetGlobalColorization(),
|
||||
}
|
||||
|
||||
// Get a list of all known backends, as we will query them all.
|
||||
// Get the current backend.
|
||||
b, err := currentBackend(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -75,93 +73,77 @@ func newStackLsCmd() *cobra.Command {
|
|||
packageFilter = &proj.Name
|
||||
}
|
||||
|
||||
// Now produce a list of summaries, and enumerate them sorted by name.
|
||||
var result error
|
||||
var stackNames []string
|
||||
stacks := make(map[string]backend.Stack)
|
||||
bs, err := b.ListStacks(commandContext(), packageFilter)
|
||||
// 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()
|
||||
})
|
||||
|
||||
_, showURLColumn := b.(httpstate.Backend)
|
||||
|
||||
for _, stack := range bs {
|
||||
name := stack.Ref().String()
|
||||
stacks[name] = stack
|
||||
stackNames = append(stackNames, name)
|
||||
}
|
||||
sort.Strings(stackNames)
|
||||
|
||||
// Devote 48 characters to the name width, unless there is a longer name.
|
||||
maxname := 48
|
||||
for _, name := range stackNames {
|
||||
if len(name) > maxname {
|
||||
maxname = len(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.
|
||||
|
||||
// We have to fault in snapshots for all the stacks we are going to list here, because that's the easiest
|
||||
// way to get the last update time and the resource count. Since this is an expensive operation, we'll
|
||||
// do it before printing any output so the latency happens all at once instead of line by line.
|
||||
//
|
||||
// TODO[pulumi/pulumi-service#1530]: We need a lighterweight way of fetching just the specific information
|
||||
// we want to display here.
|
||||
for _, name := range stackNames {
|
||||
stack := stacks[name]
|
||||
_, err := stack.Snapshot(commandContext())
|
||||
contract.IgnoreError(err) // If we couldn't get snapshot for the stack don't fail the overall listing.
|
||||
}
|
||||
|
||||
formatDirective := "%-" + strconv.Itoa(maxname) + "s %-24s %-18s"
|
||||
// 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 _, name := range stackNames {
|
||||
// Mark the name as current '*' if we've selected it.
|
||||
stack := stacks[name]
|
||||
for _, summary := range stackSummaries {
|
||||
const none = "n/a"
|
||||
|
||||
// Name column
|
||||
name := summary.Name().String()
|
||||
if name == current {
|
||||
name += "*"
|
||||
}
|
||||
|
||||
// Get last deployment info, provided that it exists.
|
||||
none := "n/a"
|
||||
// Last update column
|
||||
lastUpdate := none
|
||||
resourceCount := none
|
||||
snap, err := stack.Snapshot(commandContext())
|
||||
contract.IgnoreError(err) // If we couldn't get snapshot for the stack don't fail the overall listing.
|
||||
|
||||
if snap != nil {
|
||||
if t := snap.Manifest.Time; !t.IsZero() {
|
||||
lastUpdate = humanize.Time(t)
|
||||
}
|
||||
resourceCount = strconv.Itoa(len(snap.Resources))
|
||||
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 {
|
||||
var url string
|
||||
if cs, ok := stack.(httpstate.Stack); ok {
|
||||
if u, urlErr := cs.ConsoleURL(); urlErr == nil {
|
||||
url = u
|
||||
if httpBackend, ok := b.(httpstate.Backend); ok {
|
||||
if nameSuffix, err := httpBackend.StackConsoleURL(summary.Name()); err != nil {
|
||||
url = none
|
||||
} else {
|
||||
url = fmt.Sprintf("%s/%s", httpBackend.CloudURL(), nameSuffix)
|
||||
}
|
||||
}
|
||||
if url == "" {
|
||||
url = none
|
||||
}
|
||||
values = append(values, url)
|
||||
}
|
||||
|
||||
fmt.Printf(formatDirective, values...)
|
||||
}
|
||||
|
||||
return result
|
||||
return nil
|
||||
}),
|
||||
}
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
|
|
40
cmd/util.go
40
cmd/util.go
|
@ -163,7 +163,7 @@ func requireCurrentStack(offerNew bool, opts display.Options, setCurrent bool) (
|
|||
return chooseStack(b, offerNew, opts, setCurrent)
|
||||
}
|
||||
|
||||
// chooseStack will prompt the user to choose amongst the full set of stacks in the given backends. If offerNew is
|
||||
// chooseStack will prompt the user to choose amongst the full set of stacks in the given backend. If offerNew is
|
||||
// true, then the option to create an entirely new stack is provided and will create one as desired.
|
||||
func chooseStack(
|
||||
b backend.Backend, offerNew bool, opts display.Options, setCurrent bool) (backend.Stack, error) {
|
||||
|
@ -183,22 +183,20 @@ func chooseStack(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// First create a list and map of stack names.
|
||||
// List stacks as available options.
|
||||
var options []string
|
||||
stacks := make(map[string]backend.Stack)
|
||||
allStacks, err := b.ListStacks(commandContext(), &proj.Name)
|
||||
summaries, err := b.ListStacks(commandContext(), &proj.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not query backend for stacks")
|
||||
}
|
||||
for _, stack := range allStacks {
|
||||
name := stack.Ref().String()
|
||||
for _, summary := range summaries {
|
||||
name := summary.Name().String()
|
||||
options = append(options, name)
|
||||
stacks[name] = stack
|
||||
}
|
||||
sort.Strings(options)
|
||||
|
||||
// If we are offering to create a new stack, add that to the end of the list.
|
||||
newOption := "<create a new stack>"
|
||||
const newOption = "<create a new stack>"
|
||||
if offerNew {
|
||||
options = append(options, newOption)
|
||||
} else if len(options) == 0 {
|
||||
|
@ -227,7 +225,7 @@ func chooseStack(
|
|||
message = opts.Color.Colorize(colors.BrightWhite + message + colors.Reset)
|
||||
|
||||
var option string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
if err = survey.AskOne(&survey.Select{
|
||||
Message: message,
|
||||
Options: options,
|
||||
Default: current,
|
||||
|
@ -236,20 +234,30 @@ func chooseStack(
|
|||
}
|
||||
|
||||
if option == newOption {
|
||||
stackName, err := cmdutil.ReadConsole("Please enter your desired stack name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
stackName, readErr := cmdutil.ReadConsole("Please enter your desired stack name")
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
|
||||
stackRef, err := b.ParseStackReference(stackName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
stackRef, parseErr := b.ParseStackReference(stackName)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
return createStack(b, stackRef, nil, setCurrent)
|
||||
}
|
||||
|
||||
return stacks[option], nil
|
||||
// With the stack name selected, look it up from the backend.
|
||||
stackRef, err := b.ParseStackReference(option)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing selected stack")
|
||||
}
|
||||
stack, err := b.GetStack(commandContext(), stackRef)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting selected stack")
|
||||
}
|
||||
|
||||
return stack, nil
|
||||
}
|
||||
|
||||
// readProject attempts to detect and read the project for the current workspace. If an error occurs, it will be
|
||||
|
|
|
@ -18,6 +18,7 @@ package backend
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
|
@ -58,6 +59,16 @@ type StackReference interface {
|
|||
Name() tokens.QName
|
||||
}
|
||||
|
||||
// StackSummary provides a basic description of a stack, without the ability to inspect its resources or make changes.
|
||||
type StackSummary interface {
|
||||
Name() StackReference
|
||||
|
||||
// LastUpdate returns when the stack was last updated, as applicable.
|
||||
LastUpdate() *time.Time
|
||||
// ResourceCount returns the stack's resource count, as applicable.
|
||||
ResourceCount() *int
|
||||
}
|
||||
|
||||
// Backend is an interface that represents actions the engine will interact with to manage stacks of cloud resources.
|
||||
// It can be implemented any number of ways to provide pluggable backend implementations of the Pulumi Cloud.
|
||||
type Backend interface {
|
||||
|
@ -79,7 +90,7 @@ type Backend interface {
|
|||
// first boolean return value will be set to true.
|
||||
RemoveStack(ctx context.Context, stackRef StackReference, force bool) (bool, error)
|
||||
// ListStacks returns a list of stack summaries for all known stacks in the target backend.
|
||||
ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]Stack, error)
|
||||
ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]StackSummary, error)
|
||||
|
||||
// GetStackCrypter returns an encrypter/decrypter for the given stack's secret config values.
|
||||
GetStackCrypter(stackRef StackReference) (config.Crypter, error)
|
||||
|
|
|
@ -176,19 +176,22 @@ func (b *localBackend) GetStack(ctx context.Context, stackRef backend.StackRefer
|
|||
}
|
||||
}
|
||||
|
||||
func (b *localBackend) ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]backend.Stack, error) {
|
||||
func (b *localBackend) ListStacks(
|
||||
ctx context.Context, projectFilter *tokens.PackageName) ([]backend.StackSummary, error) {
|
||||
stacks, err := b.getLocalStacks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []backend.Stack
|
||||
var results []backend.StackSummary
|
||||
for _, stackName := range stacks {
|
||||
stack, err := b.GetStack(ctx, localBackendReference{name: stackName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results = append(results, stack)
|
||||
localStack, ok := stack.(*localStack)
|
||||
contract.Assertf(ok, "localBackend GetStack returned non-localStack")
|
||||
results = append(results, newLocalStackSummary(localStack))
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
|
|
@ -16,6 +16,7 @@ package filestate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
|
@ -88,3 +89,34 @@ func (s *localStack) ExportDeployment(ctx context.Context) (*apitype.UntypedDepl
|
|||
func (s *localStack) ImportDeployment(ctx context.Context, deployment *apitype.UntypedDeployment) error {
|
||||
return backend.ImportStackDeployment(ctx, s, deployment)
|
||||
}
|
||||
|
||||
type localStackSummary struct {
|
||||
s *localStack
|
||||
}
|
||||
|
||||
func newLocalStackSummary(s *localStack) localStackSummary {
|
||||
return localStackSummary{s}
|
||||
}
|
||||
|
||||
func (lss localStackSummary) Name() backend.StackReference {
|
||||
return lss.s.Ref()
|
||||
}
|
||||
|
||||
func (lss localStackSummary) LastUpdate() *time.Time {
|
||||
snap := lss.s.snapshot
|
||||
if snap != nil {
|
||||
if t := snap.Manifest.Time; !t.IsZero() {
|
||||
return &t
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lss localStackSummary) ResourceCount() *int {
|
||||
snap := lss.s.snapshot
|
||||
if snap != nil {
|
||||
count := len(snap.Resources)
|
||||
return &count
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -416,7 +416,7 @@ func serveBrowserLoginServer(l net.Listener, expectedNonce string, destinationUR
|
|||
}
|
||||
|
||||
// CloudConsoleStackPath returns the stack path components for getting to a stack in the cloud console. This path
|
||||
// must, of coursee, be combined with the actual console base URL by way of the CloudConsoleURL function above.
|
||||
// must, of course, be combined with the actual console base URL by way of the CloudConsoleURL function above.
|
||||
func (b *cloudBackend) cloudConsoleStackPath(stackID client.StackIdentifier) string {
|
||||
return path.Join(stackID.Owner, stackID.Stack)
|
||||
}
|
||||
|
@ -529,19 +529,24 @@ func (b *cloudBackend) CreateStack(ctx context.Context, stackRef backend.StackRe
|
|||
return stack, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]backend.Stack, error) {
|
||||
stacks, err := b.client.ListStacks(ctx, projectFilter)
|
||||
func (b *cloudBackend) ListStacks(
|
||||
ctx context.Context, projectFilter *tokens.PackageName) ([]backend.StackSummary, error) {
|
||||
apiSummaries, err := b.client.ListStacks(ctx, projectFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map to a summary slice.
|
||||
var results []backend.Stack
|
||||
for _, stack := range stacks {
|
||||
results = append(results, newStack(stack, b))
|
||||
// Convert []apitype.StackSummary into []backend.StackSummary.
|
||||
var backendSummaries []backend.StackSummary
|
||||
for _, apiSummary := range apiSummaries {
|
||||
backendSummary := cloudStackSummary{
|
||||
summary: apiSummary,
|
||||
b: b,
|
||||
}
|
||||
backendSummaries = append(backendSummaries, backendSummary)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
return backendSummaries, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) RemoveStack(ctx context.Context, stackRef backend.StackReference, force bool) (bool, error) {
|
||||
|
|
|
@ -124,6 +124,8 @@ func pulumiAPICall(ctx context.Context, cloudAPI, method, path string, body []by
|
|||
// backwards compatibility.
|
||||
userAgent := fmt.Sprintf("pulumi-cli/1 (%s; %s)", version.Version, runtime.GOOS)
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
// Specify the specific API version we accept.
|
||||
req.Header.Set("Accept", "application/vnd.pulumi+1")
|
||||
|
||||
// Apply credentials if provided.
|
||||
if tok.String() != "" {
|
||||
|
|
|
@ -149,11 +149,10 @@ func (pc *Client) GetCLIVersionInfo(ctx context.Context) (semver.Version, semver
|
|||
return latestSem, oldestSem, nil
|
||||
}
|
||||
|
||||
// ListStacks lists all stacks for the indicated project.
|
||||
func (pc *Client) ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]apitype.Stack, error) {
|
||||
// ListStacks lists all stacks the current user has access to, optionally filtered by project.
|
||||
func (pc *Client) ListStacks(ctx context.Context, projectFilter *tokens.PackageName) ([]apitype.StackSummary, error) {
|
||||
|
||||
// Query all stacks for the project on Pulumi.
|
||||
var stacks []apitype.Stack
|
||||
var resp apitype.ListStacksResponse
|
||||
var queryFilter interface{}
|
||||
if projectFilter != nil {
|
||||
queryFilter = struct {
|
||||
|
@ -161,11 +160,11 @@ func (pc *Client) ListStacks(ctx context.Context, projectFilter *tokens.PackageN
|
|||
}{ProjectFilter: string(*projectFilter)}
|
||||
}
|
||||
|
||||
if err := pc.restCall(ctx, "GET", "/api/user/stacks", queryFilter, nil, &stacks); err != nil {
|
||||
if err := pc.restCall(ctx, "GET", "/api/user/stacks", queryFilter, nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stacks, nil
|
||||
return resp.Stacks, nil
|
||||
}
|
||||
|
||||
// GetLatestConfiguration returns the configuration for the latest deployment of a given stack.
|
||||
|
|
|
@ -17,6 +17,7 @@ package httpstate
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
|
@ -153,3 +154,30 @@ func (s *cloudStack) ConsoleURL() (string, error) {
|
|||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// cloudStackSummary implements the backend.StackSummary interface, by wrapping
|
||||
// an apitype.StackSummary struct.
|
||||
type cloudStackSummary struct {
|
||||
summary apitype.StackSummary
|
||||
b *cloudBackend
|
||||
}
|
||||
|
||||
func (css cloudStackSummary) Name() backend.StackReference {
|
||||
return cloudBackendReference{
|
||||
owner: css.summary.OrgName,
|
||||
name: tokens.QName(css.summary.StackName),
|
||||
b: css.b,
|
||||
}
|
||||
}
|
||||
|
||||
func (css cloudStackSummary) LastUpdate() *time.Time {
|
||||
if css.summary.LastUpdate == nil {
|
||||
return nil
|
||||
}
|
||||
t := time.Unix(*css.summary.LastUpdate, 0)
|
||||
return &t
|
||||
}
|
||||
|
||||
func (css cloudStackSummary) ResourceCount() *int {
|
||||
return css.summary.ResourceCount
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue