Skip to content

Commit d5a7cde

Browse files
authored
feat: Add team context selection via switch (#14493)
This change is focused around adding the ability to set a team context via a new `cloudquery switch` command that takes a team name, but also introduces a few other features that are adjacent to that: - Adds `cloudquery switch` (prints current team) and `cloudquery switch <team>` (switches team) - Adds a `cloudquery logout` command to log out - Adds a `--team` option to `cloudquery login` as shorthand for `cloudquery login` followed by `cloudquery switch <team>` - Introduces a `config` library for managing config files. We currently only store a `team` value, but this may be extended in the future. - Removes the `--url` parameter from `cloudquery publish` and uses an environment variable for this instead. Usually we don't use environment variables, but in this case it is intentional, because you would only ever need to set the URL in tests. - All tests now use environment variables for overriding the API and accounts URLs - Renaming `CQ_API_KEY` to `CLOUDQUERY_API_KEY` for clarity and consistency (we will have to update the publish CI job to handle this CC @erezrokah)
1 parent f902e36 commit d5a7cde

File tree

13 files changed

+508
-31
lines changed

13 files changed

+508
-31
lines changed

cli/cmd/constants.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package cmd
2+
3+
const (
4+
defaultAPIURL = "https://api.cloudquery.io"
5+
defaultAccountsURL = "https://accounts.cloudquery.io"
6+
)

cli/cmd/login.go

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,37 @@ import (
1515

1616
"github.com/cenkalti/backoff/v4"
1717
"github.com/cloudquery/cloudquery/cli/internal/auth"
18+
"github.com/cloudquery/cloudquery/cli/internal/config"
1819
"github.com/pkg/browser"
1920
"github.com/spf13/cobra"
2021
)
2122

2223
const (
24+
// login command
2325
loginShort = "Login to CloudQuery Hub."
2426
loginLong = `Login to CloudQuery Hub.
2527
2628
This is required to download plugins from CloudQuery Hub.
2729
2830
Local plugins and different registries don't need login.
2931
`
32+
loginExample = `
33+
# Log in to CloudQuery Hub
34+
cloudquery login
3035
31-
accountsURL = "https://accounts.cloudquery.io"
36+
# Log in to a specific team
37+
cloudquery login --team my-team
38+
`
3239
)
3340

3441
func newCmdLogin() *cobra.Command {
35-
cmd := &cobra.Command{
36-
Use: "login",
37-
Short: loginShort,
38-
Long: loginLong,
39-
Hidden: true,
40-
Args: cobra.MatchAll(cobra.ExactArgs(0), cobra.OnlyValidArgs),
42+
loginCmd := &cobra.Command{
43+
Use: "login",
44+
Short: loginShort,
45+
Long: loginLong,
46+
Example: loginExample,
47+
Hidden: true,
48+
Args: cobra.MatchAll(cobra.ExactArgs(0), cobra.OnlyValidArgs),
4149
RunE: func(cmd *cobra.Command, args []string) error {
4250
// Set up a channel to listen for OS signals for graceful shutdown.
4351
ctx, cancel := context.WithCancel(cmd.Context())
@@ -50,10 +58,11 @@ func newCmdLogin() *cobra.Command {
5058
cancel()
5159
}()
5260

53-
return runLogin(ctx)
61+
return runLogin(ctx, cmd)
5462
},
5563
}
56-
return cmd
64+
loginCmd.Flags().StringP("team", "t", "", "Team to login to. Specify the team name, e.g. 'my-team' (not the display name)")
65+
return loginCmd
5766
}
5867

5968
func waitForServer(ctx context.Context, url string) error {
@@ -79,7 +88,10 @@ func waitForServer(ctx context.Context, url string) error {
7988
}, backoff.WithContext(backoff.NewConstantBackOff(100*time.Millisecond), ctx))
8089
}
8190

82-
func runLogin(ctx context.Context) (err error) {
91+
func runLogin(ctx context.Context, cmd *cobra.Command) (err error) {
92+
accountsURL := getEnvOrDefault("CLOUDQUERY_ACCOUNTS_URL", defaultAccountsURL)
93+
apiURL := getEnvOrDefault("CLOUDQUERY_API_URL", defaultAPIURL)
94+
8395
mux := http.NewServeMux()
8496
refreshToken := ""
8597
gotToken := make(chan struct{})
@@ -147,7 +159,27 @@ func runLogin(ctx context.Context) (err error) {
147159
return fmt.Errorf("failed to save refresh token: %w", err)
148160
}
149161

150-
fmt.Println("CLI successfully authenticated.")
162+
if cmd.Flags().Changed("team") {
163+
team := cmd.Flag("team").Value.String()
164+
token, err := auth.GetToken()
165+
if err != nil {
166+
return fmt.Errorf("failed to get auth token: %w", err)
167+
}
168+
cl, err := auth.NewClient(apiURL, token)
169+
if err != nil {
170+
return fmt.Errorf("failed to create API client: %w", err)
171+
}
172+
err = cl.ValidateTeam(ctx, team)
173+
if err != nil {
174+
return fmt.Errorf("failed to set team: %w", err)
175+
}
176+
err = config.SetValue("team", team)
177+
if err != nil {
178+
return fmt.Errorf("failed to set team: %w", err)
179+
}
180+
}
181+
182+
cmd.Println("CLI successfully authenticated.")
151183

152184
return nil
153185
}

cli/cmd/logout.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"syscall"
9+
10+
"github.com/cloudquery/cloudquery/cli/internal/auth"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
const (
15+
// logout command
16+
logoutShort = "Log out of CloudQuery Hub."
17+
)
18+
19+
func newCmdLogout() *cobra.Command {
20+
loginCmd := &cobra.Command{
21+
Use: "logout",
22+
Short: logoutShort,
23+
Hidden: true,
24+
Args: cobra.MatchAll(cobra.ExactArgs(0), cobra.OnlyValidArgs),
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
// Set up a channel to listen for OS signals for graceful shutdown.
27+
ctx, cancel := context.WithCancel(cmd.Context())
28+
29+
sigChan := make(chan os.Signal, 1)
30+
signal.Notify(sigChan, syscall.SIGTERM)
31+
32+
go func() {
33+
<-sigChan
34+
cancel()
35+
}()
36+
37+
return runLogout(ctx, cmd)
38+
},
39+
}
40+
return loginCmd
41+
}
42+
43+
func runLogout(_ context.Context, cmd *cobra.Command) error {
44+
err := auth.Logout()
45+
if err != nil {
46+
return fmt.Errorf("failed to logout: %w", err)
47+
}
48+
49+
cmd.Println("CLI successfully logged out.")
50+
51+
return nil
52+
}

cli/cmd/publish.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,11 @@ This publishes a plugin version to CloudQuery Hub from a local dist directory.
3131
publishExample = `
3232
# Publish a plugin version from a local dist directory
3333
cloudquery publish my_team/my_plugin`
34-
35-
cloudQueryAPI = "https://api.cloudquery.io"
3634
)
3735

3836
func newCmdPublish() *cobra.Command {
3937
cmd := &cobra.Command{
40-
Use: "publish <team_name>/<plugin_name> [-D dist] [-u <url>]",
38+
Use: "publish <team_name>/<plugin_name> [-D dist]",
4139
Short: publishShort,
4240
Long: publishLong,
4341
Example: publishExample,
@@ -59,7 +57,6 @@ func newCmdPublish() *cobra.Command {
5957
},
6058
}
6159
cmd.Flags().StringP("dist-dir", "D", "dist", "Path to the dist directory")
62-
cmd.Flags().StringP("url", "u", cloudQueryAPI, "CloudQuery API URL")
6360
cmd.Flags().BoolP("finalize", "f", false, `Finalize the plugin version after publishing. If false, the plugin version will be marked as draft=true.`)
6461

6562
return cmd
@@ -109,11 +106,11 @@ func runPublish(ctx context.Context, cmd *cobra.Command, args []string) error {
109106
name := fmt.Sprintf("%s/%s@%s", teamName, pluginName, pkgJSON.Version)
110107
fmt.Printf("Publishing %s to CloudQuery Hub...\n", name)
111108

112-
uri := cmd.Flag("url").Value.String()
113-
c, err := cloudquery_api.NewClientWithResponses(uri, cloudquery_api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
114-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
115-
return nil
116-
}))
109+
c, err := cloudquery_api.NewClientWithResponses(getEnvOrDefault("CLOUDQUERY_API_URL", defaultAPIURL),
110+
cloudquery_api.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
111+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
112+
return nil
113+
}))
117114
if err != nil {
118115
return fmt.Errorf("failed to create hub client: %w", err)
119116
}

cli/cmd/publish_test.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
func TestPublish(t *testing.T) {
17-
t.Setenv("CQ_API_KEY", "testkey")
17+
t.Setenv("CLOUDQUERY_API_KEY", "testkey")
1818

1919
wantCalls := map[string]int{
2020
"PUT /plugins/cloudquery/source/test/versions/v1.2.3": 1,
@@ -64,7 +64,8 @@ func TestPublish(t *testing.T) {
6464
defer ts.Close()
6565

6666
cmd := NewCmdRoot()
67-
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1", "--url", ts.URL}
67+
t.Setenv("CLOUDQUERY_API_URL", ts.URL)
68+
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1"}
6869
cmd.SetArgs(args)
6970
err := cmd.Execute()
7071
if err != nil {
@@ -76,7 +77,7 @@ func TestPublish(t *testing.T) {
7677
}
7778

7879
func TestPublishFinalize(t *testing.T) {
79-
t.Setenv("CQ_API_KEY", "testkey")
80+
t.Setenv("CLOUDQUERY_API_KEY", "testkey")
8081

8182
wantCalls := map[string]int{
8283
"PUT /plugins/cloudquery/source/test/versions/v1.2.3": 1,
@@ -137,8 +138,10 @@ func TestPublishFinalize(t *testing.T) {
137138
}))
138139
defer ts.Close()
139140

141+
t.Setenv("CLOUDQUERY_API_URL", ts.URL)
142+
140143
cmd := NewCmdRoot()
141-
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1", "--url", ts.URL, "--finalize"}
144+
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1", "--finalize"}
142145
cmd.SetArgs(args)
143146
err := cmd.Execute()
144147
if err != nil {
@@ -150,7 +153,7 @@ func TestPublishFinalize(t *testing.T) {
150153
}
151154

152155
func TestPublish_Unauthorized(t *testing.T) {
153-
t.Setenv("CQ_API_KEY", "badkey")
156+
t.Setenv("CLOUDQUERY_API_KEY", "badkey")
154157

155158
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156159
w.Header().Set("Content-Type", "application/json")
@@ -159,8 +162,10 @@ func TestPublish_Unauthorized(t *testing.T) {
159162
}))
160163
defer ts.Close()
161164

165+
t.Setenv("CLOUDQUERY_API_URL", ts.URL)
166+
162167
cmd := NewCmdRoot()
163-
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1", "--url", ts.URL, "--finalize"}
168+
args := []string{"publish", "cloudquery/test", "--dist-dir", "testdata/dist-v1", "--finalize"}
164169
cmd.SetArgs(args)
165170
err := cmd.Execute()
166171
if err == nil {

cli/cmd/root.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,10 @@ func NewCmdRoot() *cobra.Command {
129129
newCmdDoc(),
130130
newCmdInstall(),
131131
NewCmdTables(),
132-
newCmdLogin(),
133132
newCmdPublish(),
133+
newCmdLogin(),
134+
newCmdLogout(),
135+
newCmdSwitch(),
134136
)
135137
cmd.CompletionOptions.HiddenDefaultCmd = true
136138
cmd.DisableAutoGenTag = true

cli/cmd/switch.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/cloudquery/cloudquery/cli/internal/auth"
10+
"github.com/cloudquery/cloudquery/cli/internal/config"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
const (
15+
switchShort = "Switches between teams."
16+
switchLong = `Switches between teams.`
17+
switchExample = `
18+
# Switch to a different team
19+
cloudquery switch my-team
20+
`
21+
)
22+
23+
func newCmdSwitch() *cobra.Command {
24+
switchCmd := &cobra.Command{
25+
Use: "switch",
26+
Short: switchShort,
27+
Long: switchLong,
28+
Example: switchExample,
29+
Args: cobra.MaximumNArgs(1),
30+
Hidden: true,
31+
RunE: runSwitch,
32+
}
33+
return switchCmd
34+
}
35+
36+
func runSwitch(cmd *cobra.Command, args []string) error {
37+
apiURL := getEnvOrDefault("CLOUDQUERY_API_URL", defaultAPIURL)
38+
39+
token, err := auth.GetToken()
40+
if err != nil {
41+
return fmt.Errorf("failed to get auth token: %w", err)
42+
}
43+
44+
cl, err := auth.NewClient(apiURL, token)
45+
if err != nil {
46+
return fmt.Errorf("failed to create API client: %w", err)
47+
}
48+
49+
if len(args) == 0 {
50+
// Print the current team context
51+
currentTeam, err := config.GetValue("team")
52+
if err != nil && !errors.Is(err, os.ErrNotExist) {
53+
return fmt.Errorf("failed to get current team: %w", err)
54+
}
55+
56+
allTeams, err := cl.ListAllTeams(cmd.Context())
57+
if err != nil {
58+
return fmt.Errorf("failed to list all teams: %w", err)
59+
}
60+
61+
if currentTeam == "" {
62+
cmd.Println("Your team is not set.")
63+
if len(allTeams) == 1 {
64+
cmd.Println("As you are currently a member of only one team, this will be used as your default team.")
65+
}
66+
} else {
67+
cmd.Printf("Your current team is set to %v.\n\n", currentTeam)
68+
}
69+
cmd.Println("Teams available to you:", strings.Join(allTeams, ", ")+"\n")
70+
cmd.Println("To switch teams, run `cloudquery switch <team>`")
71+
return nil
72+
}
73+
team := args[0]
74+
err = cl.ValidateTeam(cmd.Context(), team)
75+
if err != nil {
76+
return fmt.Errorf("failed to switch teams: %w", err)
77+
}
78+
err = config.SetValue("team", team)
79+
if err != nil {
80+
return fmt.Errorf("failed to set team value: %w", err)
81+
}
82+
cmd.Printf("Successfully switched teams to %v.\n", team)
83+
return nil
84+
}

0 commit comments

Comments
 (0)