diff --git a/cmd/login.go b/cmd/login.go index 64593e9e6..d6c25e0f5 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -3,8 +3,11 @@ package cmd import ( + "fmt" + "github.com/spf13/cobra" + "github.com/pulumi/pulumi/pkg/backend" "github.com/pulumi/pulumi/pkg/backend/cloud" "github.com/pulumi/pulumi/pkg/backend/local" "github.com/pulumi/pulumi/pkg/util/cmdutil" @@ -18,13 +21,21 @@ func newLoginCmd() *cobra.Command { Long: "Log into the Pulumi Cloud. You can script by using PULUMI_ACCESS_TOKEN environment variable.", Args: cmdutil.NoArgs, Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error { + var b backend.Backend + var err error + if local.IsLocalBackendURL(cloudURL) { - _, err := local.Login(cmdutil.Diag(), cloudURL) + b, err = local.Login(cmdutil.Diag(), cloudURL) + } else { + b, err = cloud.Login(cmdutil.Diag(), cloudURL) + } + + if err != nil { return err } - _, err := cloud.Login(cmdutil.Diag(), cloudURL) - return err + fmt.Printf("Logged into %s\n", b.Name()) + return nil }), } cmd.PersistentFlags().StringVarP(&cloudURL, "cloud-url", "c", "", "A cloud URL to log into") diff --git a/cmd/pulumi.go b/cmd/pulumi.go index 5746f03b5..973d2d0cb 100644 --- a/cmd/pulumi.go +++ b/cmd/pulumi.go @@ -13,11 +13,9 @@ import ( "github.com/golang/glog" "github.com/spf13/cobra" - "github.com/pulumi/pulumi/pkg/backend/cloud" "github.com/pulumi/pulumi/pkg/backend/local" "github.com/pulumi/pulumi/pkg/diag/colors" "github.com/pulumi/pulumi/pkg/util/cmdutil" - "github.com/pulumi/pulumi/pkg/workspace" ) // NewPulumiCmd creates a new Pulumi Cmd instance. @@ -50,17 +48,6 @@ func NewPulumiCmd() *cobra.Command { cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { defaultHelp(cmd, args) fmt.Println("See documentation at https://docs.pulumi.com") - - url, err := workspace.GetCurrentCloudURL() - if err == nil && url != "" && !local.IsLocalBackendURL(url) { - fmt.Printf("\n") - suffix := "" - if url != cloud.PulumiCloudURL { - suffix = fmt.Sprintf(" (%s)", url) - } - - fmt.Printf("Currently logged into the Pulumi Cloud%s%s\n", cmdutil.EmojiOr(" ☁️", ""), suffix) - } }) cmd.PersistentFlags().StringVarP(&cwd, "cwd", "C", "", "Run pulumi as if it had been started in another directory") diff --git a/cmd/stack.go b/cmd/stack.go index 981a3a9e8..9d7f0e30e 100644 --- a/cmd/stack.go +++ b/cmd/stack.go @@ -39,15 +39,17 @@ func newStackCmd() *cobra.Command { fmt.Printf("Current stack is %s:\n", s.Name()) be := s.Backend() - fmt.Printf(" Managed by %s", be.Name()) - if _, isCloud := be.(cloud.Backend); isCloud { - fmt.Printf(" ☁️\n") + cloudBe, isCloud := be.(cloud.Backend) + if !isCloud || cloudBe.CloudURL() != cloud.PulumiCloudURL { + fmt.Printf(" Managed by %s\n", be.Name()) + } + if isCloud { if cs, ok := s.(cloud.Stack); ok { - fmt.Printf(" Organization %s\n", cs.OrgName()) - fmt.Printf(" PPC %s\n", cs.CloudName()) + fmt.Printf(" Owner: %s\n", cs.OrgName()) + if !cs.RunLocally() { + fmt.Printf(" PPC: %s\n", cs.CloudName()) + } } - } else { - fmt.Printf("\n") } snap := s.Snapshot() @@ -55,7 +57,7 @@ func newStackCmd() *cobra.Command { if t := snap.Manifest.Time; t.IsZero() { fmt.Printf(" Last update time unknown\n") } else { - fmt.Printf(" Last updated %s (%v)\n", humanize.Time(t), t) + fmt.Printf(" Last updated: %s (%v)\n", humanize.Time(t), t) } var cliver string if snap.Manifest.Version == "" { @@ -63,7 +65,7 @@ func newStackCmd() *cobra.Command { } else { cliver = snap.Manifest.Version } - fmt.Printf(" Pulumi version %s\n", cliver) + fmt.Printf(" Pulumi version: %s\n", cliver) for _, plugin := range snap.Manifest.Plugins { var plugver string if plugin.Version == nil { @@ -71,7 +73,7 @@ func newStackCmd() *cobra.Command { } else { plugver = plugin.Version.String() } - fmt.Printf(" Plugin %s [%s] version %s\n", plugin.Name, plugin.Kind, plugver) + fmt.Printf(" Plugin %s [%s] version: %s\n", plugin.Name, plugin.Kind, plugver) } } else { fmt.Printf(" No updates yet; run 'pulumi update'\n") @@ -112,6 +114,15 @@ func newStackCmd() *cobra.Command { printStackOutputs(outputs) } } + + // Add a link to the pulumi.com console page for this stack, if it has one. + if cs, ok := s.(cloud.Stack); ok { + if consoleURL, err := cs.ConsoleURL(); err == nil { + fmt.Printf("\n") + fmt.Printf("More information at: %s\n", consoleURL) + } + } + fmt.Printf("\n") fmt.Printf("Use `pulumi stack select` to change stack; `pulumi stack ls` lists known ones\n") diff --git a/cmd/stack_ls.go b/cmd/stack_ls.go index 580df3c88..d7d9af41f 100644 --- a/cmd/stack_ls.go +++ b/cmd/stack_ls.go @@ -65,6 +65,8 @@ func newStackLsCmd() *cobra.Command { if err != nil { return err } + showPPCColumn := hasAnyPPCStacks(bs) + for _, stack := range bs { name := stack.Name().String() stacks[name] = stack @@ -80,8 +82,16 @@ func newStackLsCmd() *cobra.Command { } } - fmt.Printf("%-"+strconv.Itoa(maxname)+"s %-24s %-18s %-25s\n", - "NAME", "LAST UPDATE", "RESOURCE COUNT", "CLOUD") + formatDirective := "%-" + strconv.Itoa(maxname) + "s %-24s %-18s" + headers := []interface{}{"NAME", "LAST UPDATE", "RESOURCE COUNT"} + + if showPPCColumn { + formatDirective = formatDirective + " %-25s" + headers = append(headers, "PPC") + } + formatDirective = formatDirective + "\n" + + fmt.Printf(formatDirective, headers...) for _, name := range stackNames { // Mark the name as current '*' if we've selected it. stack := stacks[name] @@ -100,16 +110,19 @@ func newStackLsCmd() *cobra.Command { resourceCount = strconv.Itoa(len(snap.Resources)) } - // Print out the cloud URL. - var cloudInfo string - if cs, ok := stack.(cloud.Stack); ok { - cloudInfo = fmt.Sprintf("%s:%s/%s", cs.CloudURL(), cs.OrgName(), cs.CloudName()) - } else { - cloudInfo = none + values := []interface{}{name, lastUpdate, resourceCount} + if showPPCColumn { + // Print out the PPC name. + var cloudInfo string + if cs, ok := stack.(cloud.Stack); ok && !cs.RunLocally() { + cloudInfo = cs.CloudName() + } else { + cloudInfo = none + } + values = append(values, cloudInfo) } - fmt.Printf("%-"+strconv.Itoa(maxname)+"s %-24s %-18s %-25s\n", - name, lastUpdate, resourceCount, cloudInfo) + fmt.Printf(formatDirective, values...) } return result @@ -120,3 +133,15 @@ func newStackLsCmd() *cobra.Command { return cmd } + +func hasAnyPPCStacks(stacks []backend.Stack) bool { + for _, s := range stacks { + if cs, ok := s.(cloud.Stack); ok { + if !cs.RunLocally() { + return true + } + } + } + + return false +} diff --git a/pkg/backend/cloud/backend.go b/pkg/backend/cloud/backend.go index cde2a600a..cfec3079f 100644 --- a/pkg/backend/cloud/backend.go +++ b/pkg/backend/cloud/backend.go @@ -122,6 +122,7 @@ type Backend interface { ListTemplates() ([]workspace.Template, error) CancelCurrentUpdate(stackRef backend.StackReference) error + StackConsoleURL(stackRef backend.StackReference) (string, error) } type cloudBackend struct { @@ -187,7 +188,23 @@ func Login(d diag.Sink, cloudURL string) (Backend, error) { return New(d, cloudURL) } -func (b *cloudBackend) Name() string { return b.url } +func (b *cloudBackend) StackConsoleURL(stackRef backend.StackReference) (string, error) { + stackID, err := b.getCloudStackIdentifier(stackRef) + if err != nil { + return "", err + } + + return b.cloudConsoleStackPath(stackID), nil +} + +func (b *cloudBackend) Name() string { + if b.url == PulumiCloudURL { + return "pulumi.com" + } + + return b.url +} + func (b *cloudBackend) CloudURL() string { return b.url } func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) { @@ -369,13 +386,19 @@ func (b *cloudBackend) CreateStack(stackRef backend.StackReference, opts interfa return nil, errors.Wrap(err, "error determining initial tags") } - stack, err := b.client.CreateStack(project, cloudOpts.CloudName, string(stackRef.StackName()), tags) + apistack, err := b.client.CreateStack(project, cloudOpts.CloudName, string(stackRef.StackName()), tags) if err != nil { return nil, err } - fmt.Printf("Created stack '%s' hosted in Pulumi Cloud PPC %s\n", stackRef, stack.CloudName) - return newStack(stack, b), nil + stack := newStack(apistack, b) + fmt.Printf("Created stack '%s'", stack.Name()) + if !stack.RunLocally() { + fmt.Printf(" in PPC %s", stack.CloudName()) + } + fmt.Println() + + return stack, nil } func (b *cloudBackend) ListStacks(projectFilter *tokens.PackageName) ([]backend.Stack, error) { @@ -697,8 +720,7 @@ func (b *cloudBackend) updateStack( // Print a banner so it's clear this is going to the cloud. actionLabel := getActionLabel(string(action), dryRun) fmt.Printf( - colors.ColorizeText( - colors.BrightMagenta+"%s stack '%s' in the Pulumi Cloud"+colors.Reset+cmdutil.EmojiOr(" ☁️", "")+"\n"), + colors.ColorizeText(colors.BrightMagenta+"%s stack '%s'"+colors.Reset+"\n"), actionLabel, stack.Name()) // Create an update object (except if this won't yield an update; i.e., doing a local preview). diff --git a/pkg/backend/cloud/stack.go b/pkg/backend/cloud/stack.go index be1d87314..46a8bd318 100644 --- a/pkg/backend/cloud/stack.go +++ b/pkg/backend/cloud/stack.go @@ -5,6 +5,7 @@ package cloud import ( "fmt" + "github.com/pkg/errors" "github.com/pulumi/pulumi/pkg/apitype" "github.com/pulumi/pulumi/pkg/backend" "github.com/pulumi/pulumi/pkg/engine" @@ -19,10 +20,11 @@ import ( // Stack is a cloud stack. This simply adds some cloud-specific properties atop the standard backend stack interface. type Stack interface { backend.Stack - CloudURL() string // the URL to the cloud containing this stack. - OrgName() string // the organization that owns this stack. - CloudName() string // the PPC in which this stack is running. - RunLocally() bool // true if previews/updates/destroys targeting this stack run locally. + CloudURL() string // the URL to the cloud containing this stack. + OrgName() string // the organization that owns this stack. + CloudName() string // the PPC in which this stack is running. + RunLocally() bool // true if previews/updates/destroys targeting this stack run locally. + ConsoleURL() (string, error) // the URL to view the stack's information on Pulumi.com } // cloudStack is a cloud stack descriptor. @@ -141,3 +143,15 @@ func (s *cloudStack) ExportDeployment() (*apitype.UntypedDeployment, error) { func (s *cloudStack) ImportDeployment(deployment *apitype.UntypedDeployment) error { return backend.ImportStackDeployment(s, deployment) } + +func (s *cloudStack) ConsoleURL() (string, error) { + path, err := s.b.StackConsoleURL(s.Name()) + if err != nil { + return "", nil + } + url := s.b.CloudConsoleURL(path) + if url == "" { + return "", errors.New("could not determine clould console URL") + } + return url, nil +}