Skip to content

Commit 9e38e8f

Browse files
tytv2claude
andcommitted
feat(configure): auto-detect project_id during grn configure
When the user leaves the Project ID prompt blank, the wizard now calls vServer /v1/projects (using the just-entered credentials and region's vServer endpoint) and saves the single returned project. Each user is expected to have exactly one project per region, so using the first result is unambiguous. If auto-detect fails (bad creds, no network, no project), the wizard prints a warning and leaves the field blank — downstream tools (greenode-mcp-server) can still fall back to auto-detect at call time. - `go/cmd/configure/detect_project.go`: lightweight /v1/projects fetch using the existing auth.TokenManager - `go/cmd/configure/configure.go`: auto-invoke when projectID == "" - Docs + README updated with the new prompt flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 85cdf1a commit 9e38e8f

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)