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-vks-fzo41e6c.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "vks",
"description": "Add set-auto-upgrade-config and delete-auto-upgrade-config commands"
}
98 changes: 98 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# CLAUDE.md — GreenNode CLI

## Project overview

GreenNode CLI (`grn`) is a unified command-line tool for managing GreenNode (VNG Cloud) services. Architecture cloned from AWS CLI with hand-written commands. VKS (VNG Kubernetes Service) is the first service; other product teams add their own services by following the same pattern.

- **Repo**: `vngcloud/greennode-cli`
- **Docs**: https://vngcloud.github.io/greennode-cli/
- **PyPI**: https://pypi.org/project/grncli/

## Code conventions

- All source code text must be in **English** — error messages, descriptions, comments, docstrings, ARG_TABLE help_text
- Follow existing AWS CLI patterns: `CLIDriver` → `ServiceCommand` → `BasicCommand`
- Each command file has one class, one responsibility
- Use `display_output(result, parsed_globals)` helper for API response formatting

## VNG Cloud API quirks

- **IAM API uses camelCase**: `grantType`, `accessToken`, `expiresIn` (not snake_case OAuth2 standard)
- **VKS API pagination is 0-based**: page 0 = first page
- **`--version` conflict**: Use `--k8s-version` for Kubernetes version to avoid clash with global `--version` flag

## Adding a new command

1. Create file in `grncli/customizations/vks/<command_name>.py`
2. Extend `BasicCommand` with `NAME`, `DESCRIPTION`, `ARG_TABLE`
3. Implement `_run_main(self, parsed_args, parsed_globals)`
4. Register in `grncli/customizations/vks/__init__.py`
5. Add `validate_id()` calls for any ID args used in URLs
6. Add `--dry-run` for create/update/delete commands
7. Add `--force` + confirmation prompt for delete commands

## Adding a new service

1. Create `grncli/customizations/<service>/`
2. Write commands extending `BasicCommand`
3. Register in `grncli/handlers.py`
4. See `grncli/customizations/vks/` for reference

## Security rules

- **Credential masking**: `grn configure list` and `grn configure get` must mask `client_id`/`client_secret` (show last 4 chars only)
- **Input validation**: All cluster-id and nodegroup-id args must be validated via `validators.validate_id()` before constructing URLs — prevents path traversal
- **SSL default on**: `--no-verify-ssl` must print warning to stderr
- **Tokens in memory only**: Never write tokens to disk or logs
- **Dependency pinning**: Pin to major versions (`httpx<1.0`, `PyYAML<7.0`)

## Testing

```bash
python -m pytest tests/ -v
```

- Tests must pass on Python 3.10-3.13 × Ubuntu/macOS/Windows
- Skip Unix-only tests on Windows with `@pytest.mark.skipif(platform.system() == 'Windows', ...)`

## Git workflow

- **Do not auto commit/push** — only change source code, user will ask for commit/push when ready
- **Branches**: `main` (production), `develop` (testing), `feat/*` or `fix/*` (feature/bug branches)
- **PRs**: feature → develop (test), feature → main (release-ready)
- **Changelog**: Add fragment via `./scripts/new-change` for every change
- **Release**: `./scripts/bump-version minor` → `git push && git push --tags`
- **Main branch is protected** — cannot push directly, must use PR

## Documentation update rule

**After completing any feature or bugfix, update ALL related documentation before considering the work done:**

1. **GitHub Pages docs** (`docs/`):
- Add/update command reference page in `docs/commands/vks/<command>.md`
- Update `docs/commands/vks/index.md` command table
- Update relevant usage guides if behavior changes (pagination, dry-run, etc.)
- Update `mkdocs.yml` nav if new pages added

2. **CHANGELOG**: Add changelog fragment via `./scripts/new-change`

3. **Spec** (`docs/superpowers/specs/2026-04-10-greenode-cli-design.md`):
- Update command list in Section 4
- Update file structure in Section 2 if new files added

This is not optional. Code without docs is not done.

## Key files

| File | Purpose |
|------|---------|
| `grncli/clidriver.py` | CLIDriver + ServiceCommand — main orchestrator |
| `grncli/session.py` | Config, credentials, region, endpoints, SSL, timeouts |
| `grncli/auth.py` | TokenManager — OAuth2 Client Credentials with IAM |
| `grncli/client.py` | HTTP client with retry (3x backoff) + auto token refresh |
| `grncli/customizations/commands.py` | BasicCommand base class + display_output + help system |
| `grncli/customizations/vks/validators.py` | ID format validation |
| `grncli/data/cli.json` | Global CLI options (AWS CLI style) |
| `mkdocs.yml` | Documentation site config |
| `scripts/bump-version` | Bump version + merge changelog + commit + tag |
| `scripts/new-change` | Create changelog fragment |
36 changes: 36 additions & 0 deletions docs/commands/vks/delete-auto-upgrade-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# delete-auto-upgrade-config

## Description

Delete the auto-upgrade configuration for a cluster. This disables automatic Kubernetes version upgrades.

## Synopsis

```
grn vks delete-auto-upgrade-config
--cluster-id <value>
[--force]
```

## Options

`--cluster-id` (required)
: The ID of the cluster.

`--force` (optional)
: Skip the confirmation prompt.

## Examples

Delete auto-upgrade config with confirmation:

```bash
grn vks delete-auto-upgrade-config --cluster-id k8s-xxxxx
# Are you sure you want to delete the auto-upgrade config? (yes/no): yes
```

Delete without confirmation (for scripting):

```bash
grn vks delete-auto-upgrade-config --cluster-id k8s-xxxxx --force
```
7 changes: 7 additions & 0 deletions docs/commands/vks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ grn vks <command> [options]
| [update-nodegroup](update-nodegroup.md) | Update a node group |
| [delete-nodegroup](delete-nodegroup.md) | Delete a node group |

### Auto-Upgrade

| Command | Description |
|---------|-------------|
| [set-auto-upgrade-config](set-auto-upgrade-config.md) | Configure auto-upgrade schedule for a cluster |
| [delete-auto-upgrade-config](delete-auto-upgrade-config.md) | Delete auto-upgrade config for a cluster |

### Waiter

| Command | Description |
Expand Down
45 changes: 45 additions & 0 deletions docs/commands/vks/set-auto-upgrade-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# set-auto-upgrade-config

## Description

Configure auto-upgrade schedule for a cluster. Sets the days and time when automatic Kubernetes version upgrades will be performed.

## Synopsis

```
grn vks set-auto-upgrade-config
--cluster-id <value>
--weekdays <value>
--time <value>
```

## Options

`--cluster-id` (required)
: The ID of the cluster.

`--weekdays` (required)
: Days of the week to perform auto upgrade, comma-separated. Valid values: `Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun`. Example: `Mon,Wed,Fri`

`--time` (required)
: Time of day to perform auto upgrade in 24-hour format `HH:mm`. Example: `03:00`

## Examples

Set auto-upgrade to run on weekdays at 3 AM:

```bash
grn vks set-auto-upgrade-config \
--cluster-id k8s-xxxxx \
--weekdays Mon,Tue,Wed,Thu,Fri \
--time 03:00
```

Set auto-upgrade to run on weekends at midnight:

```bash
grn vks set-auto-upgrade-config \
--cluster-id k8s-xxxxx \
--weekdays Sat,Sun \
--time 00:00
```
4 changes: 4 additions & 0 deletions grncli/customizations/vks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def _inject_vks_operations(command_table, session, **kwargs):
from grncli.customizations.vks.update_nodegroup import UpdateNodegroupCommand
from grncli.customizations.vks.delete_nodegroup import DeleteNodegroupCommand
from grncli.customizations.vks.wait import WaitClusterActiveCommand
from grncli.customizations.vks.auto_upgrade_config import SetAutoUpgradeConfigCommand
from grncli.customizations.vks.auto_upgrade_config import DeleteAutoUpgradeConfigCommand

command_table['list-clusters'] = ListClustersCommand(session)
command_table['get-cluster'] = GetClusterCommand(session)
Expand All @@ -35,3 +37,5 @@ def _inject_vks_operations(command_table, session, **kwargs):
command_table['update-nodegroup'] = UpdateNodegroupCommand(session)
command_table['delete-nodegroup'] = DeleteNodegroupCommand(session)
command_table['wait-cluster-active'] = WaitClusterActiveCommand(session)
command_table['set-auto-upgrade-config'] = SetAutoUpgradeConfigCommand(session)
command_table['delete-auto-upgrade-config'] = DeleteAutoUpgradeConfigCommand(session)
57 changes: 57 additions & 0 deletions grncli/customizations/vks/auto_upgrade_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import annotations

from grncli.customizations.commands import BasicCommand, display_output
from grncli.customizations.vks.validators import validate_id


class SetAutoUpgradeConfigCommand(BasicCommand):
NAME = 'set-auto-upgrade-config'
DESCRIPTION = 'Configure auto-upgrade schedule for a cluster'
ARG_TABLE = [
{'name': 'cluster-id', 'help_text': 'Cluster ID', 'required': True},
{'name': 'weekdays', 'help_text': 'Days of the week (e.g. Mon,Wed,Fri)', 'required': True},
{'name': 'time', 'help_text': 'Time of day in 24h format HH:mm (e.g. 03:00)', 'required': True},
]

def _run_main(self, parsed_args, parsed_globals):
validate_id(parsed_args.cluster_id, 'cluster-id')
client = self._session.create_client('vks')
body = {
'weekdays': parsed_args.weekdays,
'time': parsed_args.time,
}
result = client.put(
f'/v1/clusters/{parsed_args.cluster_id}/auto-upgrade-config',
json=body,
)
display_output(result, parsed_globals)
return 0


class DeleteAutoUpgradeConfigCommand(BasicCommand):
NAME = 'delete-auto-upgrade-config'
DESCRIPTION = 'Delete auto-upgrade config for a cluster'
ARG_TABLE = [
{'name': 'cluster-id', 'help_text': 'Cluster ID', 'required': True},
{'name': 'force', 'help_text': 'Skip confirmation prompt',
'action': 'store_true', 'default': False},
]

def _run_main(self, parsed_args, parsed_globals):
validate_id(parsed_args.cluster_id, 'cluster-id')
client = self._session.create_client('vks')

if not parsed_args.force:
import sys
response = input(
"Are you sure you want to delete the auto-upgrade config? (yes/no): "
)
if response.strip().lower() != 'yes':
sys.stdout.write("Delete cancelled.\n")
return 0

result = client.delete(
f'/v1/clusters/{parsed_args.cluster_id}/auto-upgrade-config',
)
display_output(result, parsed_globals)
return 0
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ nav:
- create-nodegroup: commands/vks/create-nodegroup.md
- update-nodegroup: commands/vks/update-nodegroup.md
- delete-nodegroup: commands/vks/delete-nodegroup.md
- Auto-Upgrade:
- set-auto-upgrade-config: commands/vks/set-auto-upgrade-config.md
- delete-auto-upgrade-config: commands/vks/delete-auto-upgrade-config.md
- Waiter:
- wait-cluster-active: commands/vks/wait-cluster-active.md
- Development:
Expand Down
Loading