Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/feature-configure-iqcv1jyc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "configure",
"description": "Add project_id field to configure wizard, configure list/get/set, and GRN_DEFAULT_PROJECT_ID env var override"
}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Before using the GreenNode CLI, you need to configure your credentials. There ar
export GRN_ACCESS_KEY_ID=your-client-id
export GRN_SECRET_ACCESS_KEY=your-client-secret
export GRN_DEFAULT_REGION=HCM-3
export GRN_DEFAULT_PROJECT_ID=pro-xxxxxxxx # optional
```

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

**Method 3: Credentials file (manual)**
Expand All @@ -80,6 +82,7 @@ client_secret = your-client-secret
[default]
region = HCM-3
output = json
project_id = pro-xxxxxxxx
```

Credentials are obtained from the [VNG Cloud IAM Portal](https://hcm-3.console.vngcloud.vn/iam/) under Service Accounts.
Expand Down
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ GRN Client ID [None]: <your-client-id>
GRN Client Secret [None]: <your-client-secret>
Default region name [HCM-3]:
Default output format [json]:
Project ID (leave blank to auto-detect at runtime) [None]: pro-xxxxxxxx
```

`Project ID` is the VNG Cloud project UUID (e.g. `pro-e28d4501-...`). Each user may
have multiple projects; pick the one you work with. Leave blank to let downstream
tools (such as the GreenNode MCP Server) auto-detect at first call.

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

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

Expand Down Expand Up @@ -68,10 +74,12 @@ client_secret = yyy
[default]
region = HCM-3
output = json
project_id = pro-xxxxxxxx

[profile staging]
region = HAN
output = table
project_id = pro-yyyyyyyy
```

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

## Profiles
Expand Down
3 changes: 2 additions & 1 deletion go/cmd/configure/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func runConfigure(cmd *cobra.Command, args []string) {
clientSecret := promptWithDefault(reader, "Client Secret", maskCred(cfg.ClientSecret))
region := promptWithDefault(reader, "Default region name", cfg.Region)
output := promptWithDefault(reader, "Default output format", cfg.Output)
projectID := promptWithDefault(reader, "Project ID (leave blank to auto-detect at runtime)", cfg.ProjectID)

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

if err := writer.WriteConfig(profile, region, output); err != nil {
if err := writer.WriteConfig(profile, region, output, projectID); err != nil {
fmt.Fprintf(os.Stderr, "Error saving config: %v\n", err)
os.Exit(1)
}
Expand Down
2 changes: 2 additions & 0 deletions go/cmd/configure/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func runGet(cmd *cobra.Command, args []string) {
value = cfg.Output
case "profile":
value = cfg.Profile
case "project_id":
value = cfg.ProjectID
default:
fmt.Fprintf(os.Stderr, "Unknown configuration key: %s\n", key)
os.Exit(1)
Expand Down
6 changes: 4 additions & 2 deletions go/cmd/configure/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func runList(cmd *cobra.Command, args []string) {
resolveCredEntry("client_secret", cfg.ClientSecret, credsFile),
resolveConfigEntry("region", cfg.Region, configFile),
resolveConfigEntry("output", cfg.Output, configFile),
resolveConfigEntry("project_id", cfg.ProjectID, configFile),
}

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

// Check if value came from env var
envMap := map[string]string{
"region": "GRN_DEFAULT_REGION",
"output": "GRN_DEFAULT_OUTPUT",
"region": "GRN_DEFAULT_REGION",
"output": "GRN_DEFAULT_OUTPUT",
"project_id": "GRN_DEFAULT_PROJECT_ID",
}
if envVar, ok := envMap[name]; ok {
if os.Getenv(envVar) != "" {
Expand Down
10 changes: 8 additions & 2 deletions go/cmd/configure/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@ func runSet(cmd *cobra.Command, args []string) {
}
case "region":
cfg, _ := config.LoadConfig(profile)
if err := writer.WriteConfig(profile, value, cfg.Output); err != nil {
if err := writer.WriteConfig(profile, value, cfg.Output, cfg.ProjectID); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "output":
cfg, _ := config.LoadConfig(profile)
if err := writer.WriteConfig(profile, cfg.Region, value); err != nil {
if err := writer.WriteConfig(profile, cfg.Region, value, cfg.ProjectID); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "project_id":
cfg, _ := config.LoadConfig(profile)
if err := writer.WriteConfig(profile, cfg.Region, cfg.Output, value); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
Expand Down
9 changes: 9 additions & 0 deletions go/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Config struct {
Region string
Output string
Profile string
ProjectID string
Regions map[string]map[string]string
}

Expand Down Expand Up @@ -105,6 +106,9 @@ func LoadConfig(profile string) (*Config, error) {
if v := section.Key("output").String(); v != "" {
cfg.Output = v
}
if v := section.Key("project_id").String(); v != "" {
cfg.ProjectID = v
}
}
}

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

// Env var override for project_id
if v := os.Getenv("GRN_DEFAULT_PROJECT_ID"); v != "" {
cfg.ProjectID = v
}

// Default output
if cfg.Output == "" {
cfg.Output = "json"
Expand Down
7 changes: 5 additions & 2 deletions go/internal/config/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ func (w *ConfigFileWriter) WriteCredentials(profile, clientID, clientSecret stri
return w.save(cfg, filePath)
}

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

return w.save(cfg, filePath)
}
Expand Down
Loading