Skip to content

Commit 4e7ddd0

Browse files
authored
feat(configure): auto-detect project_id (#14)
2 parents 85cdf1a + 9e38e8f commit 4e7ddd0

5 files changed

Lines changed: 113 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "enhancement",
3+
"category": "configure",
4+
"description": "Auto-detect project_id during grn configure by calling vServer /v1/projects with the given credentials and region"
5+
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ GRN Client ID [None]: <your-client-id>
6565
GRN Client Secret [None]: <your-client-secret>
6666
Default region name [HCM-3]:
6767
Default output format [json]:
68-
Project ID (leave blank to auto-detect at runtime) [None]: pro-xxxxxxxx
68+
Project ID (leave blank to auto-detect) [None]:
69+
Fetching project_id from HCM-3...
70+
Auto-detected project_id: pro-xxxxxxxx
6971
```
7072

7173
**Method 3: Credentials file (manual)**

docs/configuration.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,19 @@ GRN Client ID [None]: <your-client-id>
1313
GRN Client Secret [None]: <your-client-secret>
1414
Default region name [HCM-3]:
1515
Default output format [json]:
16-
Project ID (leave blank to auto-detect at runtime) [None]: pro-xxxxxxxx
16+
Project ID (leave blank to auto-detect) [None]:
17+
Fetching project_id from HCM-3...
18+
Auto-detected project_id: pro-xxxxxxxx
1719
```
1820

19-
`Project ID` is the VNG Cloud project UUID (e.g. `pro-e28d4501-...`). Each user may
20-
have multiple projects; pick the one you work with. Leave blank to let downstream
21-
tools (such as the GreenNode MCP Server) auto-detect at first call.
21+
`Project ID` is the VNG Cloud project UUID for the selected region (e.g.
22+
`pro-e28d4501-...`). Leave blank and the wizard calls the vServer API with
23+
your credentials to detect and save it. Each user has one project per region,
24+
so the detection is unambiguous.
25+
26+
If auto-detect fails (network or auth error), the wizard prints a warning and
27+
leaves the field blank — downstream tools (such as the GreenNode MCP Server)
28+
can still auto-detect at first call.
2229

2330
Credentials are obtained from the [VNG Cloud IAM Portal](https://hcm-3.console.vngcloud.vn/iam/) under Service Accounts.
2431

go/cmd/configure/configure.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func runConfigure(cmd *cobra.Command, args []string) {
4848
clientSecret := promptWithDefault(reader, "Client Secret", maskCred(cfg.ClientSecret))
4949
region := promptWithDefault(reader, "Default region name", cfg.Region)
5050
output := promptWithDefault(reader, "Default output format", cfg.Output)
51-
projectID := promptWithDefault(reader, "Project ID (leave blank to auto-detect at runtime)", cfg.ProjectID)
51+
projectID := promptWithDefault(reader, "Project ID (leave blank to auto-detect)", cfg.ProjectID)
5252

5353
// If user entered masked value or empty, keep original
5454
if clientID == maskCred(cfg.ClientID) || clientID == "" {
@@ -70,6 +70,22 @@ func runConfigure(cmd *cobra.Command, args []string) {
7070
output = "json"
7171
}
7272

73+
// Auto-detect project_id when left blank
74+
if projectID == "" && clientID != "" && clientSecret != "" {
75+
if endpoint, err := vserverEndpointForRegion(region); err != nil {
76+
fmt.Fprintf(os.Stderr, "Warning: cannot determine vServer endpoint for region %q; leaving project_id blank.\n", region)
77+
} else {
78+
fmt.Printf("Fetching project_id from %s...\n", region)
79+
detected, derr := detectProjectID(clientID, clientSecret, endpoint)
80+
if derr != nil {
81+
fmt.Fprintf(os.Stderr, "Warning: auto-detect failed: %v\nLeaving project_id blank.\n", derr)
82+
} else {
83+
fmt.Printf("Auto-detected project_id: %s\n", detected)
84+
projectID = detected
85+
}
86+
}
87+
}
88+
7389
writer := config.NewConfigFileWriter()
7490

7591
if err := writer.WriteCredentials(profile, clientID, clientSecret); err != nil {

go/cmd/configure/detect_project.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package configure
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"time"
9+
10+
"github.com/vngcloud/greennode-cli/internal/auth"
11+
"github.com/vngcloud/greennode-cli/internal/config"
12+
)
13+
14+
// vserverEndpointForRegion returns the vServer base URL for a region,
15+
// looking it up in the REGIONS map.
16+
func vserverEndpointForRegion(region string) (string, error) {
17+
r, ok := config.REGIONS[region]
18+
if !ok {
19+
return "", fmt.Errorf("unknown region: %s", region)
20+
}
21+
ep, ok := r["vserver_endpoint"]
22+
if !ok {
23+
return "", fmt.Errorf("no vserver_endpoint configured for region %s", region)
24+
}
25+
return ep, nil
26+
}
27+
28+
// detectProjectTimeout is short — configure should fail fast, not hang the wizard.
29+
const detectProjectTimeout = 10 * time.Second
30+
31+
type projectsResponse struct {
32+
Projects []struct {
33+
ProjectID string `json:"projectId"`
34+
} `json:"projects"`
35+
}
36+
37+
// detectProjectID fetches the caller's project from vServer /v1/projects
38+
// using the given credentials and region's vServer endpoint.
39+
//
40+
// Returns the first projectId. Each user is expected to have exactly one
41+
// project per region; returning the first is safe by that contract.
42+
func detectProjectID(clientID, clientSecret, vserverEndpoint string) (string, error) {
43+
tm := auth.NewTokenManager(clientID, clientSecret)
44+
token, err := tm.GetToken()
45+
if err != nil {
46+
return "", fmt.Errorf("authentication failed: %w", err)
47+
}
48+
49+
req, err := http.NewRequest("GET", vserverEndpoint+"/v1/projects", nil)
50+
if err != nil {
51+
return "", err
52+
}
53+
req.Header.Set("Authorization", "Bearer "+token)
54+
55+
httpClient := &http.Client{Timeout: detectProjectTimeout}
56+
resp, err := httpClient.Do(req)
57+
if err != nil {
58+
return "", fmt.Errorf("failed to fetch projects: %w", err)
59+
}
60+
defer resp.Body.Close()
61+
62+
if resp.StatusCode != http.StatusOK {
63+
body, _ := io.ReadAll(resp.Body)
64+
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body))
65+
}
66+
67+
var parsed projectsResponse
68+
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
69+
return "", fmt.Errorf("failed to parse response: %w", err)
70+
}
71+
72+
if len(parsed.Projects) == 0 {
73+
return "", fmt.Errorf("account has no project in this region")
74+
}
75+
76+
return parsed.Projects[0].ProjectID, nil
77+
}

0 commit comments

Comments
 (0)