Skip to content

Commit 786df7d

Browse files
feat(application): add application management commands (#199)
* feat(application): add application management commands * fix: use auth.OAuthClientID instead of login
1 parent 24bb18d commit 786df7d

11 files changed

Lines changed: 697 additions & 21 deletions

File tree

api/dashboard/client.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,57 @@ func (c *Client) ListRegions(accessToken string) ([]Region, error) {
337337
return regionsResp.RegionCodes, nil
338338
}
339339

340+
// WriteACL is the set of permissions for API keys created by the CLI.
341+
var WriteACL = []string{
342+
"search", "browse", "seeUnretrievableAttributes", "listIndexes",
343+
"analytics", "logs", "addObject", "deleteObject", "deleteIndex",
344+
"settings", "editSettings", "recommendation",
345+
}
346+
347+
// CreateAPIKey creates a new API key with the given ACL for the specified application.
348+
func (c *Client) CreateAPIKey(accessToken, appID string, acl []string, description string) (string, error) {
349+
payload := CreateAPIKeyRequest{ACL: acl, Description: description}
350+
body, err := json.Marshal(payload)
351+
if err != nil {
352+
return "", err
353+
}
354+
355+
endpoint := fmt.Sprintf("%s/1/applications/%s/api-keys", c.APIURL, url.PathEscape(appID))
356+
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
357+
if err != nil {
358+
return "", err
359+
}
360+
c.setAPIHeaders(req, accessToken)
361+
req.Header.Set("Content-Type", "application/json")
362+
363+
resp, err := c.client.Do(req)
364+
if err != nil {
365+
return "", fmt.Errorf("create API key request failed: %w", err)
366+
}
367+
defer resp.Body.Close()
368+
369+
respBody, err := io.ReadAll(resp.Body)
370+
if err != nil {
371+
return "", fmt.Errorf("failed to read API key response: %w", err)
372+
}
373+
374+
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
375+
return "", fmt.Errorf("create API key failed with status %d: %s", resp.StatusCode, string(respBody))
376+
}
377+
378+
var keyResp CreateAPIKeyResponse
379+
if err := json.Unmarshal(respBody, &keyResp); err != nil {
380+
return "", fmt.Errorf("failed to parse API key response: %w (body: %s)", err, string(respBody))
381+
}
382+
383+
key := keyResp.Data.Attributes.Value
384+
if key == "" {
385+
return "", fmt.Errorf("API key creation succeeded but no key was returned in the response: %s", string(respBody))
386+
}
387+
388+
return key, nil
389+
}
390+
340391
func (c *Client) setAPIHeaders(req *http.Request, accessToken string) {
341392
req.Header.Set("Authorization", "Bearer "+accessToken)
342393
req.Header.Set("Accept", "application/vnd.api+json")

api/dashboard/types.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,29 @@ type OAuthErrorResponse struct {
8888
ErrorDescription string `json:"error_description"`
8989
}
9090

91+
// CreateAPIKeyRequest is the payload for POST /1/applications/{application_id}/api-keys.
92+
type CreateAPIKeyRequest struct {
93+
ACL []string `json:"acl"`
94+
Description string `json:"description"`
95+
}
96+
97+
// APIKeyResource is a JSON:API resource wrapper for an API key.
98+
type APIKeyResource struct {
99+
ID string `json:"id"`
100+
Type string `json:"type"`
101+
Attributes APIKeyAttributes `json:"attributes"`
102+
}
103+
104+
// APIKeyAttributes contains the actual API key fields.
105+
type APIKeyAttributes struct {
106+
Value string `json:"value"`
107+
}
108+
109+
// CreateAPIKeyResponse is the JSON:API response from POST /1/applications/{application_id}/api-keys.
110+
type CreateAPIKeyResponse struct {
111+
Data APIKeyResource `json:"data"`
112+
}
113+
91114
// toApplication flattens a JSON:API resource into a simple Application.
92115
func (r *ApplicationResource) toApplication() Application {
93116
return Application{

pkg/cmd/application/application.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package application
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/algolia/cli/pkg/cmd/application/create"
7+
"github.com/algolia/cli/pkg/cmd/application/list"
8+
"github.com/algolia/cli/pkg/cmd/application/selectapp"
9+
"github.com/algolia/cli/pkg/cmdutil"
10+
)
11+
12+
// NewApplicationCmd returns a new command for managing applications.
13+
func NewApplicationCmd(f *cmdutil.Factory) *cobra.Command {
14+
cmd := &cobra.Command{
15+
Use: "application",
16+
Aliases: []string{"app"},
17+
Short: "Manage your Algolia applications",
18+
}
19+
20+
cmd.AddCommand(create.NewCreateCmd(f))
21+
cmd.AddCommand(list.NewListCmd(f))
22+
cmd.AddCommand(selectapp.NewSelectCmd(f))
23+
24+
return cmd
25+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package create
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/MakeNowJust/heredoc"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/algolia/cli/api/dashboard"
10+
"github.com/algolia/cli/pkg/auth"
11+
"github.com/algolia/cli/pkg/cmd/shared/apputil"
12+
"github.com/algolia/cli/pkg/cmdutil"
13+
"github.com/algolia/cli/pkg/config"
14+
"github.com/algolia/cli/pkg/iostreams"
15+
"github.com/algolia/cli/pkg/validators"
16+
)
17+
18+
type CreateOptions struct {
19+
IO *iostreams.IOStreams
20+
Config config.IConfig
21+
22+
Name string
23+
Region string
24+
ProfileName string
25+
Default bool
26+
DryRun bool
27+
28+
PrintFlags *cmdutil.PrintFlags
29+
30+
NewDashboardClient func(clientID string) *dashboard.Client
31+
}
32+
33+
func NewCreateCmd(f *cmdutil.Factory) *cobra.Command {
34+
opts := &CreateOptions{
35+
IO: f.IOStreams,
36+
Config: f.Config,
37+
PrintFlags: cmdutil.NewPrintFlags(),
38+
NewDashboardClient: func(clientID string) *dashboard.Client {
39+
return dashboard.NewClient(clientID)
40+
},
41+
}
42+
43+
cmd := &cobra.Command{
44+
Use: "create",
45+
Short: "Create a new Algolia application",
46+
Long: heredoc.Doc(`
47+
Create a new Algolia application and optionally configure it as a CLI profile.
48+
Requires an active session (run "algolia auth login" first).
49+
`),
50+
Example: heredoc.Doc(`
51+
# Create an application interactively
52+
$ algolia application create
53+
54+
# Create with specific options
55+
$ algolia application create --name "My App" --region CA
56+
57+
# Create and set as default profile
58+
$ algolia application create --name "My App" --region CA --default
59+
60+
# Preview what would be created without actually creating it
61+
$ algolia application create --name "My App" --region CA --dry-run
62+
`),
63+
Args: validators.NoArgs(),
64+
Annotations: map[string]string{
65+
"skipAuthCheck": "true",
66+
},
67+
RunE: func(cmd *cobra.Command, args []string) error {
68+
return runCreateCmd(opts)
69+
},
70+
}
71+
72+
cmd.Flags().StringVar(&opts.Name, "name", "My First Application", "Name for the application")
73+
cmd.Flags().StringVar(&opts.Region, "region", "", "Region code (e.g. CA, US, EU)")
74+
cmd.Flags().StringVar(&opts.ProfileName, "profile-name", "", "Name for the CLI profile (defaults to app name)")
75+
cmd.Flags().BoolVar(&opts.Default, "default", false, "Set the new profile as the default")
76+
cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Preview the create request without sending it")
77+
78+
opts.PrintFlags.AddFlags(cmd)
79+
80+
return cmd
81+
}
82+
83+
func runCreateCmd(opts *CreateOptions) error {
84+
if opts.DryRun {
85+
summary := map[string]any{
86+
"action": "create_application",
87+
"name": opts.Name,
88+
"region": opts.Region,
89+
"default": opts.Default,
90+
"dryRun": true,
91+
}
92+
return cmdutil.PrintRunSummary(
93+
opts.IO,
94+
opts.PrintFlags,
95+
summary,
96+
fmt.Sprintf("Dry run: would create application %q in region %q", opts.Name, opts.Region),
97+
)
98+
}
99+
100+
client := opts.NewDashboardClient(auth.OAuthClientID())
101+
102+
accessToken, err := auth.EnsureAuthenticated(opts.IO, client)
103+
if err != nil {
104+
return err
105+
}
106+
107+
appDetails, err := apputil.CreateAndFetchApplication(opts.IO, client, accessToken, opts.Region, opts.Name)
108+
if err != nil {
109+
return err
110+
}
111+
112+
if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil {
113+
p, err := opts.PrintFlags.ToPrinter()
114+
if err != nil {
115+
return err
116+
}
117+
return p.Print(opts.IO, appDetails)
118+
}
119+
120+
return apputil.ConfigureProfile(opts.IO, opts.Config, appDetails, opts.ProfileName, opts.Default)
121+
}

0 commit comments

Comments
 (0)