Repository: pulumi/esc
Pull request #510 (2025-04-21 21:23:30)

Enable client lookup of default orgs if not locally set
## Context

Fix https://github.com/pulumi/pulumi-service/issues/27575

A goal of Project Hockeystick is to have new Pulumi users to be able to trial premium Pulumi features immediately; as a part of this, we would want new Pulumi users to have their `esc` usage directly hook into to the trial org with Premium features enabled, rather than their individual org.

For details on the project, see the epic: https://github.com/pulumi/home/issues/3910

## Change

1. Adds integration against the new `GetDefaultOrg` API call: https://github.com/pulumi/pulumi-service/pull/27461
2. Adds fallback logic to make the API call if `~/.pulumi/config` does not have default org set for the given backend;
3. Move fallback logic of falling back to the individual org to after the API call is made.

## Local Testing




Patch:
diff --git a/cmd/esc/cli/client/apitype.go b/cmd/esc/cli/client/apitype.go
index bce8a05..ac69d8a 100644
--- a/cmd/esc/cli/client/apitype.go
+++ b/cmd/esc/cli/client/apitype.go
@@ -207,3 +207,11 @@ type RetractEnvironmentRevisionRequest struct {
 	Replacement *int   `json:"replacement,omitempty"`
 	Reason      string `json:"reason,omitempty"`
 }
+
+// GetDefaultOrganizationResponse returns the backend's opinion of which organization
+// to default to for a given user, if a default organization has not been configured.
+type GetDefaultOrganizationResponse struct {
+	// Returns the organization name.
+	// Can be an empty string, if the user is a member of no organizations
+	Organization string `json:"gitHubLogin"`
+}
diff --git a/cmd/esc/cli/client/client.go b/cmd/esc/cli/client/client.go
index ad7db9e..ed64679 100644
--- a/cmd/esc/cli/client/client.go
+++ b/cmd/esc/cli/client/client.go
@@ -68,6 +68,10 @@ type Client interface {
 	// GetRevisionNumber returns the revision number for version.
 	GetRevisionNumber(ctx context.Context, orgName, projectName, envName, version string) (int, error)
 
+	// GetDefaultOrg returns the organization if the backend has an opinion on what user organization to default to,
+	// if not configured locally by the user.
+	GetDefaultOrg(ctx context.Context) (string, error)
+
 	// ListEnvironments lists all environments that are accessible to the calling user.
 	//
 	// Each call to ListEnvironments returns a page of results and a continuation token. If there are no
@@ -453,6 +457,19 @@ func (pc *client) GetRevisionNumber(ctx context.Context, orgName, projectName, e
 	return resp.Revision, nil
 }
 
+// GetDefaultOrg returns the organization if the backend has an opinion on what user organization to default to,
+// if not configured locally by the user.
+func (pc *client) GetDefaultOrg(ctx context.Context) (string, error) {
+	var resp GetDefaultOrganizationResponse
+	if err := pc.restCall(ctx, http.MethodGet, "/api/user/organizations/default", nil, nil, &resp); err != nil {
+		if IsNotFound(err) {
+			return "", nil
+		}
+		return "", err
+	}
+	return resp.Organization, nil
+}
+
 func (pc *client) ListEnvironments(
 	ctx context.Context,
 	continuationToken string,
diff --git a/cmd/esc/cli/esc.go b/cmd/esc/cli/esc.go
index 6dcad20..7f37262 100644
--- a/cmd/esc/cli/esc.go
+++ b/cmd/esc/cli/esc.go
@@ -4,6 +4,7 @@ package cli
 
 import (
 	"bufio"
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -176,3 +177,29 @@ func (esc *escCommand) confirmPrompt(prompt, name string) bool {
 	line, _ := reader.ReadString('\n')
 	return strings.TrimSpace(line) == name
 }
+
+// Looks up the default org.
+// Prefers default org that the user has configured locally in their ~/.pulumi/config.json
+// If unset, then it will attempt to make an API call to the backend to determine the service's opinion
+// of which user organization should be the default; defaults to individual org otherwise if unset.
+func (esc *escCommand) lookupDefaultOrg(ctx context.Context, backendURL, username string) (string, error) {
+	userConfiguredDefaultOrg, err := esc.workspace.GetBackendConfigDefaultOrg(backendURL, username)
+	if err != nil {
+		return "", err
+	}
+	if userConfiguredDefaultOrg != "" {
+		return userConfiguredDefaultOrg, nil
+	}
+
+	if esc.client != nil {
+		backendDefaultOrg, err := esc.client.GetDefaultOrg(ctx)
+		if err != nil {
+			return backendDefaultOrg, err
+		} else if backendDefaultOrg != "" {
+			return backendDefaultOrg, err
+		}
+	}
+
+	// If client is unset, or if neither user nor backend have default configured, return the individual org.
+	return username, nil
+}
diff --git a/cmd/esc/cli/login.go b/cmd/esc/cli/login.go
index 20f1040..1c8ee20 100644
--- a/cmd/esc/cli/login.go
+++ b/cmd/esc/cli/login.go
@@ -172,14 +172,8 @@ func (esc *escCommand) getCachedClient(ctx context.Context) error {
 			return fmt.Errorf("could not determine current cloud: %w", err)
 		}
 
-		defaultOrg, err := esc.workspace.GetBackendConfigDefaultOrg(backendURL, nAccount.Username)
-		if err != nil {
-			return fmt.Errorf("could not determine default org: %w", err)
-		}
-
 		account = &workspace.Account{
-			Account:    *nAccount,
-			DefaultOrg: defaultOrg,
+			Account: *nAccount,
 		}
 	}
 
@@ -197,6 +191,14 @@ func (esc *escCommand) getCachedClient(ctx context.Context) error {
 	}
 
 	esc.client = esc.newClient(esc.userAgent, account.BackendURL, account.AccessToken, account.Insecure)
+
+	defaultOrg, err := esc.lookupDefaultOrg(ctx, backendURL, account.Username)
+	if err != nil {
+		return fmt.Errorf("looking up org to default to: %w", err)
+	} else if defaultOrg != "" {
+		esc.account.DefaultOrg = defaultOrg
+	}
+
 	return nil
 }
 
@@ -209,15 +211,9 @@ func (esc *escCommand) getCachedCredentials(ctx context.Context, backendURL stri
 		return false, nil
 	}
 
-	defaultOrg, err := esc.workspace.GetBackendConfigDefaultOrg(backendURL, account.Username)
-	if err != nil {
-		return false, err
-	}
-
 	esc.account = workspace.Account{
 		Account:    *account,
 		BackendURL: backendURL,
-		DefaultOrg: defaultOrg,
 	}
 	return true, nil
 }
diff --git a/cmd/esc/cli/workspace/config.go b/cmd/esc/cli/workspace/config.go
index 3c28f6b..6ac69f0 100644
--- a/cmd/esc/cli/workspace/config.go
+++ b/cmd/esc/cli/workspace/config.go
@@ -23,6 +23,7 @@ func (w *Workspace) SetBackendConfigDefaultOrg(backendURL, defaultOrg string) er
 	return w.pulumi.SetBackendConfigDefaultOrg(backendURL, defaultOrg)
 }
 
+// Returns the default org as configured in the backend, returning an empty string if unset.
 func (w *Workspace) GetBackendConfigDefaultOrg(backendURL, username string) (string, error) {
 	config, err := w.pulumi.GetPulumiConfig()
 	if err != nil && !errors.Is(err, fs.ErrNotExist) {
@@ -31,5 +32,5 @@ func (w *Workspace) GetBackendConfigDefaultOrg(backendURL, username string) (str
 	if cfg, ok := config.BackendConfig[backendURL]; ok && cfg.DefaultOrg != "" {
 		return cfg.DefaultOrg, nil
 	}
-	return username, nil
+	return "", nil
 }


Test patch:
diff --git a/cmd/esc/cli/cli_test.go b/cmd/esc/cli/cli_test.go
index 39f6bc8..a34354e 100644
--- a/cmd/esc/cli/cli_test.go
+++ b/cmd/esc/cli/cli_test.go
@@ -151,6 +151,7 @@ func (env testEnviron) Vars() []string {
 
 type testPulumiWorkspace struct {
 	credentials workspace.Credentials
+	config      workspace.PulumiConfig
 }
 
 func (w *testPulumiWorkspace) DeleteAccount(backendURL string) error {
@@ -167,12 +168,12 @@ func (w *testPulumiWorkspace) DeleteAllAccounts() error {
 	return nil
 }
 
-func (*testPulumiWorkspace) SetBackendConfigDefaultOrg(backendURL, defaultOrg string) error {
+func (w *testPulumiWorkspace) SetBackendConfigDefaultOrg(backendURL, defaultOrg string) error {
 	return nil
 }
 
-func (*testPulumiWorkspace) GetPulumiConfig() (workspace.PulumiConfig, error) {
-	return workspace.PulumiConfig{}, nil
+func (w *testPulumiWorkspace) GetPulumiConfig() (workspace.PulumiConfig, error) {
+	return w.config, nil
 }
 
 func (*testPulumiWorkspace) GetPulumiPath(elem ...string) (string, error) {
@@ -281,6 +282,7 @@ func (env *testEnvironment) latest() *testEnvironmentRevision {
 
 type testPulumiClient struct {
 	user         string
+	defaultOrg   string
 	environments map[string]*testEnvironment
 	openEnvs     map[string]*esc.Environment
 }
@@ -472,6 +474,11 @@ func (c *testPulumiClient) GetRevisionNumber(ctx context.Context, orgName, proje
 
 }
 
+func (c *testPulumiClient) GetDefaultOrg(ctx context.Context) (string, error) {
+	return c.defaultOrg, nil
+
+}
+
 func (c *testPulumiClient) ListEnvironments(
 	ctx context.Context,
 	continuationToken string,
diff --git a/cmd/esc/cli/client/client_test.go b/cmd/esc/cli/client/client_test.go
index 969e9a2..d31e72e 100644
--- a/cmd/esc/cli/client/client_test.go
+++ b/cmd/esc/cli/client/client_test.go
@@ -1124,3 +1124,26 @@ func TestDeleteEnvironmentTags(t *testing.T) {
 		assert.ErrorContains(t, err, http.StatusText(http.StatusNotFound))
 	})
 }
+
+func TestGetDefaultOrg(t *testing.T) {
+	t.Run("Not Found", func(t *testing.T) {
+		// GIVEN
+		client := newTestClient(t, http.MethodGet, "/api/user/organizations/default", func(w http.ResponseWriter, r *http.Request) {
+			w.WriteHeader(http.StatusNotFound)
+
+			err := json.NewEncoder(w).Encode(apitype.ErrorResponse{
+				Code:    http.StatusNotFound,
+				Message: http.StatusText(http.StatusNotFound),
+			})
+			require.NoError(t, err)
+		})
+
+		// WHEN
+		orgName, err := client.GetDefaultOrg(context.Background())
+
+		// THEN
+		// We should gracefully swallow the 404.
+		assert.NoError(t, err)
+		assert.Empty(t, orgName)
+	})
+}
diff --git a/cmd/esc/cli/login_test.go b/cmd/esc/cli/login_test.go
index 015cbad..6437a92 100644
--- a/cmd/esc/cli/login_test.go
+++ b/cmd/esc/cli/login_test.go
@@ -134,13 +134,21 @@ func TestEnvVarOverridesAccounts(t *testing.T) {
 			},
 		},
 	}
+
+	// Configure a default org to skip mocking a client call for default org
+	backendConfig := make(map[string]pulumi_workspace.BackendConfig, len(creds.Accounts))
+	for url := range creds.Accounts {
+		backendConfig[url] = pulumi_workspace.BackendConfig{DefaultOrg: "test-user-org"}
+	}
+
 	esc := &escCommand{
 		command: "esc",
 		login:   &testLoginManager{creds: creds},
 		newClient: func(userAgent, backendURL, accessToken string, insecure bool) client.Client {
 			return client.New(userAgent, backendURL, accessToken, insecure)
 		},
-		workspace: workspace.New(testFS{}, &testPulumiWorkspace{}),
+		workspace: workspace.New(testFS{}, &testPulumiWorkspace{
+			config: pulumi_workspace.PulumiConfig{BackendConfig: backendConfig}}),
 	}
 
 	// Verify default
@@ -162,3 +170,102 @@ func TestEnvVarOverridesAccounts(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Equal(t, esc.client.URL(), "https://api.pulumi.com")
 }
+
+func TestDefaultOrgConfiguration(t *testing.T) {
+	username := "test-user"
+	backend := "https://api.pulumi.com"
+	creds := pulumi_workspace.Credentials{
+		Current: backend,
+		Accounts: map[string]pulumi_workspace.Account{
+			backend: {
+				Username:    username,
+				AccessToken: "access-token",
+			},
+		},
+	}
+
+	t.Run("prefers user configuration", func(t *testing.T) {
+		// GIVEN
+		// The user has configured a default org:
+		userConfiguredDefaultOrg := "my-default-org"
+		testWorkspace := workspace.New(testFS{}, &testPulumiWorkspace{
+			config: pulumi_workspace.PulumiConfig{
+				BackendConfig: map[string]pulumi_workspace.BackendConfig{
+					backend: {
+						DefaultOrg: userConfiguredDefaultOrg,
+					},
+				},
+			},
+		})
+
+		testClient := testPulumiClient{}
+		esc := &escCommand{
+			command: "esc",
+			login:   &testLoginManager{creds: creds},
+			newClient: func(userAgent, backendURL, accessToken string, insecure bool) client.Client {
+				return &testClient
+			},
+			workspace: testWorkspace,
+		}
+
+		// WHEN
+		err := esc.getCachedClient(context.Background())
+
+		// THEN
+		assert.NoError(t, err)
+		assert.Equal(t, userConfiguredDefaultOrg, esc.account.DefaultOrg)
+	})
+
+	t.Run("falls back to backend client configuration", func(t *testing.T) {
+		// GIVEN
+		// The user has not configured a default org:
+		testWorkspace := workspace.New(testFS{}, &testPulumiWorkspace{})
+
+		// But the backend has an opinion on the default org:
+		serviceDefaultOrg := "service-default-org"
+		testClient := testPulumiClient{
+			defaultOrg: serviceDefaultOrg,
+		}
+
+		esc := &escCommand{
+			command: "esc",
+			login:   &testLoginManager{creds: creds},
+			newClient: func(userAgent, backendURL, accessToken string, insecure bool) client.Client {
+				return &testClient
+			},
+			workspace: testWorkspace,
+		}
+
+		// WHEN
+		err := esc.getCachedClient(context.Background())
+
+		// THEN
+		assert.NoError(t, err)
+		assert.Equal(t, serviceDefaultOrg, esc.account.DefaultOrg)
+	})
+
+	t.Run("falls back to individual org as last resort", func(t *testing.T) {
+		// GIVEN
+		// The user has not configured a default org:
+		testWorkspace := workspace.New(testFS{}, &testPulumiWorkspace{})
+
+		// And the service has no opinion:
+		testClient := testPulumiClient{defaultOrg: ""}
+
+		esc := &escCommand{
+			command: "esc",
+			login:   &testLoginManager{creds: creds},
+			newClient: func(userAgent, backendURL, accessToken string, insecure bool) client.Client {
+				return &testClient
+			},
+			workspace: testWorkspace,
+		}
+
+		// WHEN
+		err := esc.getCachedClient(context.Background())
+
+		// THEN
+		assert.NoError(t, err)
+		assert.Equal(t, username, esc.account.DefaultOrg)
+	})
+}