Skip to content

Commit 9a60ca9

Browse files
feat(application): update/upgrade/downgrade/plans commands (#224)
1 parent 61f08f6 commit 9a60ca9

17 files changed

Lines changed: 1993 additions & 33 deletions

File tree

.golangci.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,14 @@ linters:
33
- gosec
44
- gofumpt
55
- stylecheck
6+
7+
issues:
8+
exclude-rules:
9+
# The dashboard client surfaces user-facing CLI errors that are intentionally
10+
# written as capitalized, human-readable sentences (e.g. "Couldn't get the
11+
# list of plans: 404"), so the ST1005 "error strings should not be
12+
# capitalized" check is disabled for this file only.
13+
- path: api/dashboard/client.go
14+
linters:
15+
- stylecheck
16+
text: ST1005

api/dashboard/client.go

Lines changed: 220 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ func NewClient(clientID string) *Client {
3636
dashboardURL = strings.TrimRight(v, "/")
3737
}
3838
if dashboardURL == "" {
39-
fmt.Fprintln(os.Stderr, "fatal: ALGOLIA_DASHBOARD_URL is not set and no default was compiled in")
39+
fmt.Fprintln(
40+
os.Stderr,
41+
"fatal: ALGOLIA_DASHBOARD_URL is not set and no default was compiled in",
42+
)
4043
os.Exit(1)
4144
}
4245

@@ -54,7 +57,10 @@ func NewClient(clientID string) *Client {
5457
oauthScope = v
5558
}
5659
if oauthScope == "" {
57-
fmt.Fprintln(os.Stderr, "fatal: ALGOLIA_OAUTH_SCOPE is not set and no default was compiled in")
60+
fmt.Fprintln(
61+
os.Stderr,
62+
"fatal: ALGOLIA_OAUTH_SCOPE is not set and no default was compiled in",
63+
)
5864
os.Exit(1)
5965
}
6066

@@ -87,7 +93,10 @@ func (c *Client) SignupAuthorizeURL(codeChallenge, redirectURI string) string {
8793
return c.buildAuthorizeURL(codeChallenge, redirectURI, map[string]string{"screen": "signup"})
8894
}
8995

90-
func (c *Client) buildAuthorizeURL(codeChallenge, redirectURI string, extra map[string]string) string {
96+
func (c *Client) buildAuthorizeURL(
97+
codeChallenge, redirectURI string,
98+
extra map[string]string,
99+
) string {
91100
params := url.Values{
92101
"client_id": {c.ClientID},
93102
"response_type": {"code"},
@@ -104,7 +113,9 @@ func (c *Client) buildAuthorizeURL(codeChallenge, redirectURI string, extra map[
104113

105114
// AuthorizationCodeGrant exchanges an authorization code + PKCE code_verifier
106115
// for an access token. The redirectURI must match the one used in the authorize URL.
107-
func (c *Client) AuthorizationCodeGrant(code, codeVerifier, redirectURI string) (*OAuthTokenResponse, error) {
116+
func (c *Client) AuthorizationCodeGrant(
117+
code, codeVerifier, redirectURI string,
118+
) (*OAuthTokenResponse, error) {
108119
form := url.Values{
109120
"grant_type": {"authorization_code"},
110121
"client_id": {c.ClientID},
@@ -113,7 +124,11 @@ func (c *Client) AuthorizationCodeGrant(code, codeVerifier, redirectURI string)
113124
"redirect_uri": {redirectURI},
114125
}
115126

116-
req, err := http.NewRequest(http.MethodPost, c.DashboardURL+"/2/oauth/token", strings.NewReader(form.Encode()))
127+
req, err := http.NewRequest(
128+
http.MethodPost,
129+
c.DashboardURL+"/2/oauth/token",
130+
strings.NewReader(form.Encode()),
131+
)
117132
if err != nil {
118133
return nil, err
119134
}
@@ -146,7 +161,11 @@ func (c *Client) RefreshToken(refreshToken string) (*OAuthTokenResponse, error)
146161
"refresh_token": {refreshToken},
147162
}
148163

149-
req, err := http.NewRequest(http.MethodPost, c.DashboardURL+"/2/oauth/token", strings.NewReader(form.Encode()))
164+
req, err := http.NewRequest(
165+
http.MethodPost,
166+
c.DashboardURL+"/2/oauth/token",
167+
strings.NewReader(form.Encode()),
168+
)
150169
if err != nil {
151170
return nil, err
152171
}
@@ -178,7 +197,11 @@ func (c *Client) RevokeToken(token string) error {
178197
"token": {token},
179198
}
180199

181-
req, err := http.NewRequest(http.MethodPost, c.DashboardURL+"/2/oauth/revoke", strings.NewReader(form.Encode()))
200+
req, err := http.NewRequest(
201+
http.MethodPost,
202+
c.DashboardURL+"/2/oauth/revoke",
203+
strings.NewReader(form.Encode()),
204+
)
182205
if err != nil {
183206
return err
184207
}
@@ -246,7 +269,11 @@ func (c *Client) ListApplications(accessToken string) ([]Application, error) {
246269

247270
// GetApplication returns a single application by its ID.
248271
func (c *Client) GetApplication(accessToken, appID string) (*Application, error) {
249-
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/application/"+url.PathEscape(appID), nil)
272+
req, err := http.NewRequest(
273+
http.MethodGet,
274+
c.APIURL+"/1/application/"+url.PathEscape(appID),
275+
nil,
276+
)
250277
if err != nil {
251278
return nil, err
252279
}
@@ -299,12 +326,20 @@ func (c *Client) CreateApplication(accessToken, region, name string) (*Applicati
299326
respBody, _ := io.ReadAll(resp.Body)
300327
respStr := string(respBody)
301328

302-
if strings.Contains(strings.ToLower(respStr), "cluster") && strings.Contains(strings.ToLower(respStr), "not available") ||
329+
if strings.Contains(strings.ToLower(respStr), "cluster") &&
330+
strings.Contains(strings.ToLower(respStr), "not available") ||
303331
strings.Contains(strings.ToLower(respStr), "no cluster") {
304-
return nil, &ErrClusterUnavailable{Region: region, Message: fmt.Sprintf("no cluster available in region %q", region)}
332+
return nil, &ErrClusterUnavailable{
333+
Region: region,
334+
Message: fmt.Sprintf("no cluster available in region %q", region),
335+
}
305336
}
306337

307-
return nil, fmt.Errorf("create application failed with status %d: %s", resp.StatusCode, respStr)
338+
return nil, fmt.Errorf(
339+
"create application failed with status %d: %s",
340+
resp.StatusCode,
341+
respStr,
342+
)
308343
}
309344

310345
var singleResp SingleApplicationResponse
@@ -316,6 +351,161 @@ func (c *Client) CreateApplication(accessToken, region, name string) (*Applicati
316351
return &app, nil
317352
}
318353

354+
// UpdateApplication renames an existing application.
355+
func (c *Client) UpdateApplication(accessToken, appID, name string) (*Application, error) {
356+
payload := UpdateApplicationRequest{Name: name}
357+
body, err := json.Marshal(payload)
358+
if err != nil {
359+
return nil, err
360+
}
361+
362+
endpoint := c.APIURL + "/1/applications/" + url.PathEscape(appID)
363+
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewReader(body))
364+
if err != nil {
365+
return nil, err
366+
}
367+
c.setAPIHeaders(req, accessToken)
368+
req.Header.Set("Content-Type", "application/json")
369+
370+
resp, err := c.client.Do(req)
371+
if err != nil {
372+
return nil, fmt.Errorf("update application request failed: %w", err)
373+
}
374+
defer resp.Body.Close()
375+
376+
if resp.StatusCode == http.StatusUnauthorized {
377+
return nil, ErrSessionExpired
378+
}
379+
380+
if resp.StatusCode != http.StatusOK {
381+
respBody, _ := io.ReadAll(resp.Body)
382+
if resp.StatusCode == http.StatusNotFound {
383+
return nil, fmt.Errorf(
384+
"no existing application found; select one using 'algolia application select' or create a new one with 'algolia application create'",
385+
)
386+
}
387+
return nil, fmt.Errorf(
388+
"update application failed with status %d: %s",
389+
resp.StatusCode,
390+
string(respBody),
391+
)
392+
}
393+
394+
var singleResp SingleApplicationResponse
395+
if err := json.NewDecoder(resp.Body).Decode(&singleResp); err != nil {
396+
return nil, fmt.Errorf("failed to parse application response: %w", err)
397+
}
398+
399+
app := singleResp.Data.toApplication()
400+
return &app, nil
401+
}
402+
403+
// GetSelfServePlans returns the available plans
404+
func (c *Client) GetSelfServePlans(accessToken string) ([]Plan, error) {
405+
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/plan-templates/self-serve", nil)
406+
if err != nil {
407+
return nil, err
408+
}
409+
c.setAPIHeaders(req, accessToken)
410+
411+
resp, err := c.client.Do(req)
412+
if err != nil {
413+
return nil, fmt.Errorf("Couldn't get the list of plans: %w", err)
414+
}
415+
defer resp.Body.Close()
416+
417+
if resp.StatusCode == http.StatusUnauthorized {
418+
return nil, ErrSessionExpired
419+
}
420+
if resp.StatusCode != http.StatusOK {
421+
return nil, fmt.Errorf("Couldn't get the list of plans: %d", resp.StatusCode)
422+
}
423+
424+
var plansResp PlanTemplatesResponse
425+
if err := json.NewDecoder(resp.Body).Decode(&plansResp); err != nil {
426+
return nil, fmt.Errorf("Couldn't get the list of plans: %w", err)
427+
}
428+
429+
plans := make([]Plan, 0, len(plansResp.Data))
430+
for i := range plansResp.Data {
431+
plans = append(plans, plansResp.Data[i].toPlan())
432+
}
433+
return plans, nil
434+
}
435+
436+
// GetUser returns account-level information for the authenticated user
437+
func (c *Client) GetUser(accessToken string) (*DashboardUser, error) {
438+
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/user", nil)
439+
if err != nil {
440+
return nil, err
441+
}
442+
c.setAPIHeaders(req, accessToken)
443+
444+
resp, err := c.client.Do(req)
445+
if err != nil {
446+
return nil, fmt.Errorf("Couldn't get your account details: %w", err)
447+
}
448+
defer resp.Body.Close()
449+
450+
if resp.StatusCode == http.StatusUnauthorized {
451+
return nil, ErrSessionExpired
452+
}
453+
if resp.StatusCode != http.StatusOK {
454+
return nil, fmt.Errorf("Couldn't get your account details: %d", resp.StatusCode)
455+
}
456+
457+
var userResp userResponse
458+
if err := json.NewDecoder(resp.Body).Decode(&userResp); err != nil {
459+
return nil, fmt.Errorf("Couldn't get your account details: %w", err)
460+
}
461+
462+
user := userResp.toUser()
463+
return &user, nil
464+
}
465+
466+
// ChangeApplicationPlan changes the plan of an application
467+
func (c *Client) ChangeApplicationPlan(accessToken, appID, plan string) (*Application, error) {
468+
payload := ChangePlanRequest{Plan: plan}
469+
body, err := json.Marshal(payload)
470+
if err != nil {
471+
return nil, err
472+
}
473+
474+
endpoint := fmt.Sprintf(
475+
"%s/1/applications/%s/plan/self-serve",
476+
c.APIURL,
477+
url.PathEscape(appID),
478+
)
479+
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewReader(body))
480+
if err != nil {
481+
return nil, err
482+
}
483+
c.setAPIHeaders(req, accessToken)
484+
req.Header.Set("Content-Type", "application/json")
485+
486+
resp, err := c.client.Do(req)
487+
if err != nil {
488+
return nil, fmt.Errorf("Couldn't change your application's plan: %w", err)
489+
}
490+
defer resp.Body.Close()
491+
492+
if resp.StatusCode == http.StatusUnauthorized {
493+
return nil, ErrSessionExpired
494+
}
495+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
496+
return nil, fmt.Errorf("Couldn't change your application's plan: %d", resp.StatusCode)
497+
}
498+
499+
respBody, _ := io.ReadAll(resp.Body)
500+
var singleResp SingleApplicationResponse
501+
if err := json.Unmarshal(respBody, &singleResp); err == nil &&
502+
singleResp.Data.Attributes.ApplicationID != "" {
503+
app := singleResp.Data.toApplication()
504+
return &app, nil
505+
}
506+
return &Application{ID: appID}, nil
507+
}
508+
319509
// ListRegions returns the allowed hosting regions for application creation.
320510
func (c *Client) ListRegions(accessToken string) ([]Region, error) {
321511
req, err := http.NewRequest(http.MethodGet, c.APIURL+"/1/hosting/regions", nil)
@@ -350,7 +540,11 @@ var WriteACL = []string{
350540
}
351541

352542
// CreateAPIKey creates a new API key with the given ACL for the specified application.
353-
func (c *Client) CreateAPIKey(accessToken, appID string, acl []string, description string) (string, error) {
543+
func (c *Client) CreateAPIKey(
544+
accessToken, appID string,
545+
acl []string,
546+
description string,
547+
) (string, error) {
354548
payload := CreateAPIKeyRequest{ACL: acl, Description: description}
355549
body, err := json.Marshal(payload)
356550
if err != nil {
@@ -377,17 +571,28 @@ func (c *Client) CreateAPIKey(accessToken, appID string, acl []string, descripti
377571
}
378572

379573
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
380-
return "", fmt.Errorf("create API key failed with status %d: %s", resp.StatusCode, string(respBody))
574+
return "", fmt.Errorf(
575+
"create API key failed with status %d: %s",
576+
resp.StatusCode,
577+
string(respBody),
578+
)
381579
}
382580

383581
var keyResp CreateAPIKeyResponse
384582
if err := json.Unmarshal(respBody, &keyResp); err != nil {
385-
return "", fmt.Errorf("failed to parse API key response: %w (body: %s)", err, string(respBody))
583+
return "", fmt.Errorf(
584+
"failed to parse API key response: %w (body: %s)",
585+
err,
586+
string(respBody),
587+
)
386588
}
387589

388590
key := keyResp.Data.Attributes.Value
389591
if key == "" {
390-
return "", fmt.Errorf("API key creation succeeded but no key was returned in the response: %s", string(respBody))
592+
return "", fmt.Errorf(
593+
"API key creation succeeded but no key was returned in the response: %s",
594+
string(respBody),
595+
)
391596
}
392597

393598
return key, nil

0 commit comments

Comments
 (0)