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:
Chris Smith 2018-09-13 20:54:42 -07:00 committed by GitHub
parent 3642cca98f
commit 792c316e5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 89 deletions

View file

@ -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(

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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
}

View file

@ -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) {

View file

@ -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() != "" {

View file

@ -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.

View file

@ -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
}