Skip to content

Commit 2179861

Browse files
authored
feat(configure): add project_id field to config (#13)
2 parents 51631be + 9bad10c commit 2179861

9 files changed

Lines changed: 47 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"type": "feature",
3+
"category": "configure",
4+
"description": "Add project_id field to configure wizard, configure list/get/set, and GRN_DEFAULT_PROJECT_ID env var override"
5+
}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Before using the GreenNode CLI, you need to configure your credentials. There ar
5151
export GRN_ACCESS_KEY_ID=your-client-id
5252
export GRN_SECRET_ACCESS_KEY=your-client-secret
5353
export GRN_DEFAULT_REGION=HCM-3
54+
export GRN_DEFAULT_PROJECT_ID=pro-xxxxxxxx # optional
5455
```
5556

5657
**Method 2: Interactive setup (recommended)**
@@ -64,6 +65,7 @@ GRN Client ID [None]: <your-client-id>
6465
GRN Client Secret [None]: <your-client-secret>
6566
Default region name [HCM-3]:
6667
Default output format [json]:
68+
Project ID (leave blank to auto-detect at runtime) [None]: pro-xxxxxxxx
6769
```
6870

6971
**Method 3: Credentials file (manual)**
@@ -80,6 +82,7 @@ client_secret = your-client-secret
8082
[default]
8183
region = HCM-3
8284
output = json
85+
project_id = pro-xxxxxxxx
8386
```
8487

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

docs/configuration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ 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
1617
```
1718

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.
22+
1823
Credentials are obtained from the [VNG Cloud IAM Portal](https://hcm-3.console.vngcloud.vn/iam/) under Service Accounts.
1924

2025
## Credential resolution order
@@ -31,6 +36,7 @@ Credentials are resolved in the following order (highest to lowest priority):
3136
| `GRN_ACCESS_KEY_ID` | Client ID (overrides credentials file) |
3237
| `GRN_SECRET_ACCESS_KEY` | Client Secret (overrides credentials file) |
3338
| `GRN_DEFAULT_REGION` | Default region |
39+
| `GRN_DEFAULT_PROJECT_ID` | Project ID (VNG Cloud project UUID) |
3440
| `GRN_PROFILE` | Profile name (default: "default") |
3541
| `GRN_DEFAULT_OUTPUT` | Output format |
3642

@@ -68,10 +74,12 @@ client_secret = yyy
6874
[default]
6975
region = HCM-3
7076
output = json
77+
project_id = pro-xxxxxxxx
7178

7279
[profile staging]
7380
region = HAN
7481
output = table
82+
project_id = pro-yyyyyyyy
7583
```
7684

7785
Credentials file is created with `0600` permissions (owner read/write only).
@@ -95,6 +103,7 @@ grn configure set region HAN # Set a specific value
95103
client_secret ****************c123 config-file ~/.greenode/credentials
96104
region HCM-3 config-file ~/.greenode/config
97105
output json config-file ~/.greenode/config
106+
project_id pro-xxxxxxxx config-file ~/.greenode/config
98107
```
99108

100109
## Profiles

go/cmd/configure/configure.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +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)
5152

5253
// If user entered masked value or empty, keep original
5354
if clientID == maskCred(cfg.ClientID) || clientID == "" {
@@ -76,7 +77,7 @@ func runConfigure(cmd *cobra.Command, args []string) {
7677
os.Exit(1)
7778
}
7879

79-
if err := writer.WriteConfig(profile, region, output); err != nil {
80+
if err := writer.WriteConfig(profile, region, output, projectID); err != nil {
8081
fmt.Fprintf(os.Stderr, "Error saving config: %v\n", err)
8182
os.Exit(1)
8283
}

go/cmd/configure/get.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ func runGet(cmd *cobra.Command, args []string) {
4343
value = cfg.Output
4444
case "profile":
4545
value = cfg.Profile
46+
case "project_id":
47+
value = cfg.ProjectID
4648
default:
4749
fmt.Fprintf(os.Stderr, "Unknown configuration key: %s\n", key)
4850
os.Exit(1)

go/cmd/configure/list.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func runList(cmd *cobra.Command, args []string) {
4242
resolveCredEntry("client_secret", cfg.ClientSecret, credsFile),
4343
resolveConfigEntry("region", cfg.Region, configFile),
4444
resolveConfigEntry("output", cfg.Output, configFile),
45+
resolveConfigEntry("project_id", cfg.ProjectID, configFile),
4546
}
4647

4748
// Print header
@@ -94,8 +95,9 @@ func resolveConfigEntry(name, value, configFile string) configEntry {
9495

9596
// Check if value came from env var
9697
envMap := map[string]string{
97-
"region": "GRN_DEFAULT_REGION",
98-
"output": "GRN_DEFAULT_OUTPUT",
98+
"region": "GRN_DEFAULT_REGION",
99+
"output": "GRN_DEFAULT_OUTPUT",
100+
"project_id": "GRN_DEFAULT_PROJECT_ID",
99101
}
100102
if envVar, ok := envMap[name]; ok {
101103
if os.Getenv(envVar) != "" {

go/cmd/configure/set.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ func runSet(cmd *cobra.Command, args []string) {
4343
}
4444
case "region":
4545
cfg, _ := config.LoadConfig(profile)
46-
if err := writer.WriteConfig(profile, value, cfg.Output); err != nil {
46+
if err := writer.WriteConfig(profile, value, cfg.Output, cfg.ProjectID); err != nil {
4747
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
4848
os.Exit(1)
4949
}
5050
case "output":
5151
cfg, _ := config.LoadConfig(profile)
52-
if err := writer.WriteConfig(profile, cfg.Region, value); err != nil {
52+
if err := writer.WriteConfig(profile, cfg.Region, value, cfg.ProjectID); err != nil {
53+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
54+
os.Exit(1)
55+
}
56+
case "project_id":
57+
cfg, _ := config.LoadConfig(profile)
58+
if err := writer.WriteConfig(profile, cfg.Region, cfg.Output, value); err != nil {
5359
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
5460
os.Exit(1)
5561
}

go/internal/config/config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Config struct {
2727
Region string
2828
Output string
2929
Profile string
30+
ProjectID string
3031
Regions map[string]map[string]string
3132
}
3233

@@ -105,6 +106,9 @@ func LoadConfig(profile string) (*Config, error) {
105106
if v := section.Key("output").String(); v != "" {
106107
cfg.Output = v
107108
}
109+
if v := section.Key("project_id").String(); v != "" {
110+
cfg.ProjectID = v
111+
}
108112
}
109113
}
110114

@@ -113,6 +117,11 @@ func LoadConfig(profile string) (*Config, error) {
113117
cfg.Region = v
114118
}
115119

120+
// Env var override for project_id
121+
if v := os.Getenv("GRN_DEFAULT_PROJECT_ID"); v != "" {
122+
cfg.ProjectID = v
123+
}
124+
116125
// Default output
117126
if cfg.Output == "" {
118127
cfg.Output = "json"

go/internal/config/writer.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ func (w *ConfigFileWriter) WriteCredentials(profile, clientID, clientSecret stri
4545
return w.save(cfg, filePath)
4646
}
4747

48-
// WriteConfig writes region and output for the given profile.
49-
func (w *ConfigFileWriter) WriteConfig(profile, region, output string) error {
48+
// WriteConfig writes region, output, and project_id for the given profile.
49+
// An empty projectID is written as an empty key to explicitly clear any
50+
// previously-saved value.
51+
func (w *ConfigFileWriter) WriteConfig(profile, region, output, projectID string) error {
5052
if err := w.ensureDir(); err != nil {
5153
return fmt.Errorf("failed to create config directory: %w", err)
5254
}
@@ -68,6 +70,7 @@ func (w *ConfigFileWriter) WriteConfig(profile, region, output string) error {
6870
}
6971
section.Key("region").SetValue(region)
7072
section.Key("output").SetValue(output)
73+
section.Key("project_id").SetValue(projectID)
7174

7275
return w.save(cfg, filePath)
7376
}

0 commit comments

Comments
 (0)