-
Notifications
You must be signed in to change notification settings - Fork 24
[CLI-2354] Add confluent organization use command for org switching
#3277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| package organization | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| pcmd "github.com/confluentinc/cli/v4/pkg/cmd" | ||
| "github.com/confluentinc/cli/v4/pkg/errors" | ||
| "github.com/confluentinc/cli/v4/pkg/output" | ||
| "github.com/confluentinc/cli/v4/pkg/resource" | ||
| ) | ||
|
|
||
| func (c *command) newUseCommand() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "use <id>", | ||
| Short: "Use a Confluent Cloud organization in subsequent commands.", | ||
| Long: "Choose a Confluent Cloud organization to be used in subsequent commands. Switching organizations clears your active environment and Kafka cluster selections.", | ||
| Args: cobra.ExactArgs(1), | ||
| ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgs), | ||
| RunE: c.use, | ||
| } | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func (c *command) validArgs(cmd *cobra.Command, args []string) []string { | ||
| if len(args) > 0 { | ||
| return nil | ||
| } | ||
|
|
||
| if err := c.PersistentPreRunE(cmd, args); err != nil { | ||
|
Check warning on line 32 in internal/organization/command_use.go
|
||
| return nil | ||
| } | ||
|
|
||
| organizations, err := c.V2Client.ListOrgOrganizations() | ||
| if err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| suggestions := make([]string, len(organizations)) | ||
| for i, org := range organizations { | ||
| suggestions[i] = fmt.Sprintf("%s\t%s", org.GetId(), org.GetDisplayName()) | ||
| } | ||
| return suggestions | ||
| } | ||
|
|
||
| func (c *command) use(_ *cobra.Command, args []string) error { | ||
| id := args[0] | ||
|
|
||
| // Use the list endpoint instead of get-by-ID because the org-scoped JWT | ||
| // only authorizes reading the *current* organization. The list endpoint | ||
| // returns every organization the user belongs to regardless of JWT scope. | ||
| organizations, err := c.V2Client.ListOrgOrganizations() | ||
| if err != nil { | ||
| return fmt.Errorf("failed to list organizations: %w", err) | ||
| } | ||
|
|
||
| found := false | ||
| for _, org := range organizations { | ||
| if org.GetId() == id { | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
| if !found { | ||
| return errors.NewErrorWithSuggestions( | ||
| fmt.Sprintf(`organization "%s" not found or access forbidden`, id), | ||
| "List available organizations with `organization list`.", | ||
| ) | ||
| } | ||
|
|
||
| if id == c.Context.GetCurrentOrganization() { | ||
| output.Printf(c.Config.EnableColor, errors.UsingResourceMsg, resource.Organization, id) | ||
| return nil | ||
| } | ||
|
|
||
| if err := c.Context.SwitchOrganization(c.Client, id); err != nil { | ||
| return fmt.Errorf("failed to switch to organization %q: %w", id, err) | ||
| } | ||
|
|
||
| if err := c.Config.Save(); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| output.Printf(c.Config.EnableColor, errors.UsingResourceMsg, resource.Organization, id) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,6 +112,42 @@ func (c *Context) UpdateAuthTokens(token, refreshToken string) error { | |
| return c.Save() | ||
| } | ||
|
|
||
| // SwitchOrganization switches the context to a different Confluent Cloud | ||
| // organization by minting a new org-scoped JWT using the existing user-scoped | ||
| // refresh token. It clears all environment-scoped state (environments, Kafka | ||
| // clusters) since those are org-scoped. The caller must call Config.Save() | ||
| // after this method returns successfully. | ||
| func (c *Context) SwitchOrganization(client *ccloudv1.Client, orgId string) error { | ||
| previousOrgId := c.LastOrgId | ||
| previousAuthToken := c.State.AuthToken | ||
| previousRefreshToken := c.State.AuthRefreshToken | ||
|
|
||
| // Set the target org before refreshing so RefreshSession picks it up | ||
| // via GetCurrentOrganization(). | ||
| c.LastOrgId = orgId | ||
|
|
||
| if err := c.RefreshSession(client); err != nil { | ||
| c.LastOrgId = previousOrgId | ||
| c.State.AuthToken = previousAuthToken | ||
| c.State.AuthRefreshToken = previousRefreshToken | ||
| return err | ||
| } | ||
|
|
||
| // Clear all environment-scoped state: environments and Kafka clusters | ||
| // belong to a specific org, so stale IDs from the previous org must not | ||
| // leak into the new context. | ||
| c.CurrentEnvironment = "" | ||
| c.Environments = map[string]*EnvironmentContext{} | ||
| if c.KafkaClusterContext != nil { | ||
| c.KafkaClusterContext.ActiveKafkaCluster = "" | ||
| c.KafkaClusterContext.ActiveKafkaClusterEndpoint = "" | ||
| c.KafkaClusterContext.KafkaClusterConfigs = map[string]*KafkaClusterConfig{} | ||
| c.KafkaClusterContext.KafkaEnvContexts = map[string]*KafkaEnvContext{} | ||
| } | ||
|
Comment on lines
+121
to
+146
|
||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *Context) IsCloud(isTest bool) bool { | ||
| if isTest && c.PlatformName == testserver.TestCloudUrl.String() { | ||
| return true | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,10 +1,14 @@ | ||||||||||||||||
| package config | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "fmt" | ||||||||||||||||
| "testing" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||||
|
|
||||||||||||||||
| ccloudv1 "github.com/confluentinc/ccloud-sdk-go-v1-public" | ||||||||||||||||
| ccloudv1mock "github.com/confluentinc/ccloud-sdk-go-v1-public/mock" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| var ( | ||||||||||||||||
|
|
@@ -77,6 +81,123 @@ | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func TestSwitchOrganization(t *testing.T) { | ||||||||||||||||
| cfg := AuthenticatedCloudConfigMock() | ||||||||||||||||
| ctx := cfg.Context() | ||||||||||||||||
|
|
||||||||||||||||
| oldRefreshToken := "old-refresh-token" | ||||||||||||||||
| ctx.State.AuthRefreshToken = oldRefreshToken | ||||||||||||||||
|
|
||||||||||||||||
| newOrgId := "org-new-id" | ||||||||||||||||
| newToken := "new-auth-token" | ||||||||||||||||
| newRefreshToken := "new-refresh-token" | ||||||||||||||||
|
|
||||||||||||||||
| auth := &ccloudv1mock.Auth{ | ||||||||||||||||
| LoginFunc: func(req *ccloudv1.AuthenticateRequest) (*ccloudv1.AuthenticateReply, error) { | ||||||||||||||||
| require.Equal(t, newOrgId, req.OrgResourceId) | ||||||||||||||||
| require.Equal(t, oldRefreshToken, req.RefreshToken) | ||||||||||||||||
| return &ccloudv1.AuthenticateReply{ | ||||||||||||||||
| Token: newToken, | ||||||||||||||||
| RefreshToken: newRefreshToken, | ||||||||||||||||
| }, nil | ||||||||||||||||
| }, | ||||||||||||||||
| } | ||||||||||||||||
| client := &ccloudv1.Client{Auth: auth} | ||||||||||||||||
|
|
||||||||||||||||
| err := ctx.SwitchOrganization(client, newOrgId) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
|
|
||||||||||||||||
| require.Equal(t, newOrgId, ctx.LastOrgId) | ||||||||||||||||
| require.Equal(t, newToken, ctx.State.AuthToken) | ||||||||||||||||
| require.Equal(t, newRefreshToken, ctx.State.AuthRefreshToken) | ||||||||||||||||
|
||||||||||||||||
| require.Equal(t, newRefreshToken, ctx.State.AuthRefreshToken) | |
| require.Equal(t, newRefreshToken, ctx.State.AuthRefreshToken) | |
| // Ensure org-scoped auth metadata is updated to reflect the new organization. | |
| require.NotNil(t, ctx.State.Auth) | |
| require.NotNil(t, ctx.State.Auth.Organization) | |
| require.Equal(t, newOrgId, ctx.State.Auth.Organization.ResourceId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as this comment : #3277 (comment)
Check warning on line 115 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Rename function "TestSwitchOrganization_ClearsEnvironmentState" to match the regular expression ^(_|[a-zA-Z0-9]+)$
[S100] Function names should comply with a naming convention
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=f736780c-f534-4310-a5d6-42bc00fb99b7&open=f736780c-f534-4310-a5d6-42bc00fb99b7
Check failure on line 121 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Define a constant instead of duplicating this literal "env-123" 7 times.
[S1192] String literals should not be duplicated
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=180e1319-044e-4a78-9ca8-beabc8edbdd1&open=180e1319-044e-4a78-9ca8-beabc8edbdd1
Check failure on line 124 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Define a constant instead of duplicating this literal "lkc-999" 3 times.
[S1192] String literals should not be duplicated
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=51095a1e-1836-4ff3-91a4-e5f7dfdad2af&open=51095a1e-1836-4ff3-91a4-e5f7dfdad2af
Check failure on line 135 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Define a constant instead of duplicating this literal "org-other" 4 times.
[S1192] String literals should not be duplicated
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=699ad6c1-b6ad-44ef-b45e-6eef8841f869&open=699ad6c1-b6ad-44ef-b45e-6eef8841f869
Check warning on line 146 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Rename function "TestSwitchOrganization_RollbackOnFailure" to match the regular expression ^(_|[a-zA-Z0-9]+)$
[S100] Function names should comply with a naming convention
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=17df5a75-f97a-4b7d-ba82-f4fc2db6320f&open=17df5a75-f97a-4b7d-ba82-f4fc2db6320f
Check warning on line 180 in pkg/config/context_test.go
SonarQube-Confluent / SonarQube Code Analysis
Rename function "TestSwitchOrganization_NilKafkaClusterContext" to match the regular expression ^(_|[a-zA-Z0-9]+)$
[S100] Function names should comply with a naming convention
See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3277&issues=d4c412d0-3258-463e-8ee4-0484f339ea32&open=d4c412d0-3258-463e-8ee4-0484f339ea32
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Using organization "abc-123". |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| Choose a Confluent Cloud organization to be used in subsequent commands. Switching organizations clears your active environment and Kafka cluster selections. | ||
|
|
||
| Usage: | ||
| confluent organization use <id> [flags] | ||
|
|
||
| Global Flags: | ||
| -h, --help Show help for this command. | ||
| --unsafe-trace Equivalent to -vvvv, but also log HTTP requests and responses which might contain plaintext secrets. | ||
| -v, --verbose count Increase verbosity (-v for warn, -vv for info, -vvv for debug, -vvvv for trace). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Error: failed to list organizations: Forbidden |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Error: organization "org-dne" not found or access forbidden | ||
|
|
||
| Suggestions: | ||
| List available organizations with `organization list`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Using organization "abc-456". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from Steven Gagniere (@sgagniere) :