[CLI] Adding the ability to create a default org for backends that support orgs (#8352)

This commit is contained in:
Paul Stack 2021-11-12 12:44:51 -06:00 committed by GitHub
parent 0a38bc295c
commit 74ba28ad55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 5 deletions

View file

@ -1,5 +1,9 @@
### Improvements
- [CLI] Adding the ability to use `pulumi org set [name]` to set a default org
to use when creating a stacks in the Pulumi Service backend or Self -hosted Service
[#8352](https://github.com/pulumi/pulumi/pull/8352)
- [schema] Add IsOverlay option to disable codegen for particular types
[#8338](https://github.com/pulumi/pulumi/pull/8338)

View file

@ -229,6 +229,10 @@ func loginWithBrowser(ctx context.Context, d diag.Sink, cloudURL string, opts di
return New(d, cloudURL)
}
func SetDefaultOrg(url string, orgName string) error {
return workspace.SetBackendConfigDefaultOrg(url, orgName)
}
// Login logs into the target cloud URL and returns the cloud backend for it.
func Login(ctx context.Context, d diag.Sink, cloudURL string, opts display.Options) (Backend, error) {
cloudURL = ValueOrDefaultURL(cloudURL)

View file

@ -33,6 +33,7 @@ import (
func newLoginCmd() *cobra.Command {
var cloudURL string
var defaultOrg string
var localMode bool
cmd := &cobra.Command{
@ -55,6 +56,9 @@ func newLoginCmd() *cobra.Command {
"to log in to a self-hosted Pulumi service running at the api.pulumi.acmecorp.com domain.\n" +
"\n" +
"For `https://` URLs, the CLI will speak REST to a service that manages state and concurrency control.\n" +
"You can specify a default org to use when logging into the Pulumi service backend or a " +
"self-hosted Pulumi service.\n" +
"\n" +
"[PREVIEW] If you prefer to operate Pulumi independently of a service, and entirely local to your computer,\n" +
"pass `file://<path>`, where `<path>` will be where state checkpoints will be stored. For instance,\n" +
"\n" +
@ -127,8 +131,21 @@ func newLoginCmd() *cobra.Command {
var err error
if filestate.IsFileStateBackendURL(cloudURL) {
be, err = filestate.Login(cmdutil.Diag(), cloudURL)
if defaultOrg != "" {
return fmt.Errorf("unable to set default org for this type of backend")
}
} else {
be, err = httpstate.Login(commandContext(), cmdutil.Diag(), cloudURL, displayOptions)
// if the user has specified a default org to associate with the backend
if defaultOrg != "" {
cloudURL, err := workspace.GetCurrentCloudURL()
if err != nil {
return err
}
if err := httpstate.SetDefaultOrg(cloudURL, defaultOrg); err != nil {
return err
}
}
}
if err != nil {
return errors.Wrapf(err, "problem logging in")
@ -145,6 +162,8 @@ func newLoginCmd() *cobra.Command {
}
cmd.PersistentFlags().StringVarP(&cloudURL, "cloud-url", "c", "", "A cloud URL to log in to")
cmd.PersistentFlags().StringVar(&defaultOrg, "default-org", "", "A default org to associate with the login. "+
"Please note, currently, only the managed and self-hosted backends support organizations")
cmd.PersistentFlags().BoolVarP(&localMode, "local", "l", false, "Use Pulumi in local-only mode")
return cmd

View file

@ -172,7 +172,11 @@ func runNew(args newArgs) error {
// created via the web app.
var s backend.Stack
if args.stack != "" && strings.Count(args.stack, "/") == 2 {
existingStack, existingName, existingDesc, err := getStack(args.stack, opts)
stackName, err := buildStackName(args.stack)
if err != nil {
return err
}
existingStack, existingName, existingDesc, err := getStack(stackName, opts)
if err != nil {
return err
}
@ -518,7 +522,11 @@ func promptAndCreateStack(prompt promptForValueFunc,
}
if stack != "" {
s, err := stackInit(b, stack, setCurrent, secretsProvider)
stackName, err := buildStackName(stack)
if err != nil {
return nil, err
}
s, err := stackInit(b, stackName, setCurrent, secretsProvider)
if err != nil {
return nil, err
}
@ -536,7 +544,14 @@ func promptAndCreateStack(prompt promptForValueFunc,
if err != nil {
return nil, err
}
s, err := stackInit(b, stackName, setCurrent, secretsProvider)
formattedStackName, err := buildStackName(stackName)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
s, err := stackInit(b, formattedStackName, setCurrent, secretsProvider)
if err != nil {
if !yes {
// Let the user know about the error and loop around to try again.

153
pkg/cmd/pulumi/org.go Normal file
View file

@ -0,0 +1,153 @@
// Copyright 2016-2021, 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 main
import (
"fmt"
"github.com/spf13/cobra"
"github.com/pulumi/pulumi/pkg/v3/backend/display"
"github.com/pulumi/pulumi/pkg/v3/backend/httpstate"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)
func newOrgCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "org",
Short: "Manage Organization configuration",
Long: "Manage Organization configuration.\n" +
"\n" +
"Use this command to manage organization configuration, " +
"e.g. setting the default organization for a backend",
Args: cmdutil.NoArgs,
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
cloudURL, err := workspace.GetCurrentCloudURL()
if err != nil {
return err
}
defaultOrg, err := workspace.GetBackendConfigDefaultOrg()
if err != nil {
return err
}
fmt.Printf("Current Backend: %s\n", cloudURL)
if defaultOrg != "" {
fmt.Printf("Default Org: %s", defaultOrg)
} else {
fmt.Println("No Default Org Specified")
}
return nil
}),
}
cmd.AddCommand(newOrgSetDefaultCmd())
cmd.AddCommand(newOrgGetDefaultCmd())
return cmd
}
func newOrgSetDefaultCmd() *cobra.Command {
var orgName string
var cmd = &cobra.Command{
Use: "set-default [NAME]",
Args: cmdutil.ExactArgs(1),
Short: "Set the default organization for the current backend",
Long: "Set the default organization for the current backend.\n" +
"\n" +
"This command is used to set the default organization in which to create \n" +
"projects and stacks for the current backend.\n" +
"\n" +
"Currently, only the managed and self-hosted backends support organizations. " +
"If you try and set a default organization for a backend that does not \n" +
"support create organizations, then an error will be returned by the CLI",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
displayOpts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
orgName = args[0]
currentBe, err := currentBackend(displayOpts)
if err != nil {
return err
}
if !currentBe.SupportsOrganizations() {
return fmt.Errorf("unable to set a default organization for backend type: %s",
currentBe.Name())
}
if _, ok := currentBe.(httpstate.Backend); ok {
cloudURL, err := workspace.GetCurrentCloudURL()
if err != nil {
return err
}
if err := httpstate.SetDefaultOrg(cloudURL, orgName); err != nil {
return err
}
}
return nil
}),
}
return cmd
}
func newOrgGetDefaultCmd() *cobra.Command {
var cmd = &cobra.Command{
Use: "get-default",
Short: "Get the default org for the current backend",
Long: "Get the default org for the current backend.\n" +
"\n" +
"This command is used to get the default organization for which and stacks are created in " +
"the current backend.\n" +
"\n" +
"Currently, only the managed and self-hosted backends support organizations.",
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
displayOpts := display.Options{
Color: cmdutil.GetGlobalColorization(),
}
currentBe, err := currentBackend(displayOpts)
if err != nil {
return err
}
if !currentBe.SupportsOrganizations() {
return fmt.Errorf("backends of this type %q do not support organizations",
currentBe.Name())
}
defaultOrg, err := workspace.GetBackendConfigDefaultOrg()
if err != nil {
return err
}
if defaultOrg != "" {
fmt.Println(defaultOrg)
} else {
fmt.Println("No Default Org Specified")
}
return nil
}),
}
return cmd
}

View file

@ -210,6 +210,7 @@ func NewPulumiCmd() *cobra.Command {
cmd.AddCommand(newConsoleCmd())
cmd.AddCommand(newAboutCmd())
cmd.AddCommand(newSchemaCmd())
cmd.AddCommand(newOrgCmd())
// Less common, and thus hidden, commands:
cmd.AddCommand(newGenCompletionCmd(cmd))

View file

@ -107,11 +107,15 @@ func newStackInitCmd() *cobra.Command {
return errors.New("missing stack name")
}
if err := b.ValidateStackName(stackName); err != nil {
formattedStackName, err := buildStackName(stackName)
if err != nil {
return err
}
if err := b.ValidateStackName(formattedStackName); err != nil {
return err
}
stackRef, err := b.ParseStackReference(stackName)
stackRef, err := b.ParseStackReference(formattedStackName)
if err != nil {
return err
}

View file

@ -862,3 +862,20 @@ func getRefreshOption(proj *workspace.Project, refresh string) (bool, error) {
// the default functionality right now is to always skip a refresh
return false, nil
}
func buildStackName(stackName string) (string, error) {
if strings.Count(stackName, "/") == 2 {
return stackName, nil
}
defaultOrg, err := workspace.GetBackendConfigDefaultOrg()
if err != nil {
return "", err
}
if defaultOrg != "" {
return fmt.Sprintf("%s/%s", defaultOrg, stackName), nil
}
return stackName, nil
}

View file

@ -114,6 +114,8 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.6 // indirect
github.com/mattn/go-ieproxy v0.0.1 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
@ -130,6 +132,7 @@ require (
github.com/pierrec/lz4 v2.6.0+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 // indirect

View file

@ -455,6 +455,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
@ -580,11 +582,15 @@ github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w
github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/ryboe/q v1.0.15 h1:atR2S58tRbVv5+t+Kx5qf+VvT2rpXYQPGAn5QtyB5jc=
github.com/ryboe/q v1.0.15/go.mod h1:ecdh6eECsYWI/cWgtRaYjWb8fbz5YndR22B+xpAcHY8=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 h1:TToq11gyfNlrMFZiYujSekIsPd9AmsA2Bj/iv+s4JHE=

View file

@ -251,3 +251,123 @@ func StoreCredentials(creds Credentials) error {
return nil
}
type BackendConfig struct {
DefaultOrg string `json:"defaultOrg,omitempty"` // The default org for this backend config.
}
type PulumiConfig struct {
BackendConfig map[string]BackendConfig `json:"backends,omitempty"` // a map of arbitrary backends configs.
}
func getConfigFilePath() (string, error) {
// Allow the folder we use to store config in to be overridden by tests
pulumiFolder := os.Getenv(PulumiCredentialsPathEnvVar)
if pulumiFolder == "" {
folder, err := GetPulumiHomeDir()
if err != nil {
return "", errors.Wrapf(err, "failed to get the home path")
}
pulumiFolder = folder
}
err := os.MkdirAll(pulumiFolder, 0700)
if err != nil {
return "", errors.Wrapf(err, "failed to create '%s'", pulumiFolder)
}
return filepath.Join(pulumiFolder, "config.json"), nil
}
func GetPulumiConfig() (PulumiConfig, error) {
configFile, err := getConfigFilePath()
if err != nil {
return PulumiConfig{}, err
}
c, err := ioutil.ReadFile(configFile)
if err != nil {
if os.IsNotExist(err) {
return PulumiConfig{}, nil
}
return PulumiConfig{}, errors.Wrapf(err, "reading '%s'", configFile)
}
var config PulumiConfig
if err = json.Unmarshal(c, &config); err != nil {
return PulumiConfig{}, errors.Wrapf(err, "failed to read Pulumi config file")
}
return config, nil
}
func StorePulumiConfig(config PulumiConfig) error {
configFile, err := getConfigFilePath()
if err != nil {
return err
}
raw, err := json.MarshalIndent(config, "", " ")
if err != nil {
return errors.Wrapf(err, "marshalling config object")
}
// Use a temporary file and atomic os.Rename to ensure the file contents are
// updated atomically to ensure concurrent `pulumi` CLI operations are safe.
tempConfigFile, err := ioutil.TempFile(filepath.Dir(configFile), "config-*.json")
if err != nil {
return err
}
_, err = tempConfigFile.Write(raw)
if err != nil {
return err
}
err = tempConfigFile.Close()
if err != nil {
return err
}
err = os.Rename(tempConfigFile.Name(), configFile)
if err != nil {
contract.IgnoreError(os.Remove(tempConfigFile.Name()))
return err
}
return nil
}
func SetBackendConfigDefaultOrg(backendURL, defaultOrg string) error {
config, err := GetPulumiConfig()
if err != nil && !os.IsNotExist(err) {
return err
}
if config.BackendConfig == nil {
config.BackendConfig = make(map[string]BackendConfig)
}
config.BackendConfig[backendURL] = BackendConfig{
DefaultOrg: defaultOrg,
}
return StorePulumiConfig(config)
}
func GetBackendConfigDefaultOrg() (string, error) {
config, err := GetPulumiConfig()
if err != nil && !os.IsNotExist(err) {
return "", err
}
backendURL, err := GetCurrentCloudURL()
if err != nil {
return "", err
}
if beConfig, ok := config.BackendConfig[backendURL]; ok {
if beConfig.DefaultOrg != "" {
return beConfig.DefaultOrg, nil
}
}
return "", nil
}