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
57 changes: 56 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.2] - 2026-06-19

### Added

- **Context auto-selection** — when exactly one context is configured and no
`CurrentContext` is set, `GetCurrentProjectID()` now automatically selects it.
Running `acloud context set` no longer requires a follow-up `acloud context use`
in the single-context case (closes #234).
- **Clear-contexts prompt on profile switch** — switching to a different credential
profile with `--profile` (or `ACLOUD_PROFILE`) now detects that existing contexts
belong to the previous profile's projects and offers to clear them interactively.
Prevents stale contexts from causing "project not found" errors after a profile
change (closes #236).

### Fixed

- **Shell completion — network sub-resources** — pressing TAB at any positional
argument slot in nested network commands now suggests the correct parent resource
IDs instead of falling back to filesystem completion. Previously, only the final
slot (the resource's own ID) was completed; all earlier slots returned nothing.
Fixed resources and their full TAB sequences:
- `subnet list/get/create <TAB>` → VPC IDs; `subnet get <vpc-id> <TAB>` → subnet IDs
- `securitygroup list/get/create <TAB>` → VPC IDs; `securitygroup get <vpc-id> <TAB>` → SG IDs
- `vpcpeering list/get/create <TAB>` → VPC IDs; `vpcpeering get <vpc-id> <TAB>` → peering IDs
- `securityrule get/list <TAB>` → VPC IDs; `… <vpc-id> <TAB>` → SG IDs; `… <sg-id> <TAB>` → rule IDs
- `vpcpeeringroute get/list <TAB>` → VPC IDs; slot 2 → peering IDs; slot 3 → route IDs

Completion is now also registered on `list` and `create` commands that
previously had no `ValidArgsFunction` at all (closes #239, PR #240).

- **Shell completion — database and storage sub-resources** — same class of fix
extended to the database and storage families (PR #241):
- `dbaas database list/create <TAB>` → DBaaS IDs; slot 2 → database names
- `dbaas user list/create <TAB>` → DBaaS IDs; slot 2 → usernames
- `dbaas grant list/create <TAB>` → DBaaS IDs; slot 2 → database names; slot 3 → grant IDs
(`create`, `list`, `get`, `delete` had no `ValidArgsFunction` at all)
- `storage backup <TAB>` (create command) → volume IDs
- `storage restore <TAB>` (create command) → backup IDs; slot 2 → volume IDs

- **Docs**: corrected zone value from `itbg1-a` to `ITBG-1` in cloud server
create examples (closes #238).

### Documentation

- Shell auto-completion sections updated in English and Italian across all
resource pages affected by the completion fixes: `subnet`, `securitygroup`,
`vpcpeering`, `securityrule`, `vpcpeeringroute`, `dbaas.database`, `dbaas.user`,
`dbaas.grant`, `storage/backup`, `storage/restore`. Each section now shows the
full hierarchical TAB sequence for every positional argument slot rather than
only the final one.
- `getting-started.md` auto-completion examples extended to cover `storage backup`
create (volume ID), `storage restore` create (backup ID then volume ID), and
database sub-resource hierarchical completion (DBaaS → database → user/grant).

## [0.5.1] - 2026-06-19

### Fixed
Expand Down Expand Up @@ -262,7 +316,8 @@ is unchanged.
- **E2e**: project `DELETE` failure now propagates correctly in the management
suite (closes #128).

[Unreleased]: https://github.com/Arubacloud/acloud-cli/compare/v0.5.1...HEAD
[Unreleased]: https://github.com/Arubacloud/acloud-cli/compare/v0.5.2...HEAD
[0.5.2]: https://github.com/Arubacloud/acloud-cli/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/Arubacloud/acloud-cli/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/Arubacloud/acloud-cli/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/Arubacloud/acloud-cli/compare/v0.3.0...v0.4.0
Expand Down
176 changes: 176 additions & 0 deletions cmd/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1047,3 +1047,179 @@ func TestCompleteVPCPeeringRouteID_TooManyArgs(t *testing.T) {
t.Errorf("expected nil completions with too many args, got %v", comps)
}
}

// ─── completeDBaaSDatabaseID parent-slot fixes ───────────────────────────────

func TestCompleteDBaaSDatabaseID_NoArgs_DelegatesToDBaaSID(t *testing.T) {
id, name := "dbaas-001", "my-dbaas"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Database/dbaas", jsonResponse(200, types.DBaaSListResponse{
Values: []types.DBaaSResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeDBaaSDatabaseID(makeProjectCmd("proj-123"), []string{}, "")
if len(completions) == 0 {
t.Errorf("expected DBaaS IDs from delegation, got none")
}
}

func TestCompleteDBaaSDatabaseID_NoArgs_NoProjectID(t *testing.T) {
cleanup := withTempHomeDir(t)
defer cleanup()

comps, _ := completeDBaaSDatabaseID(&cobra.Command{}, []string{}, "")
if comps != nil {
t.Errorf("expected nil completions without project-id, got %v", comps)
}
}

func TestCompleteDBaaSDatabaseID_TooManyArgs(t *testing.T) {
comps, _ := completeDBaaSDatabaseID(&cobra.Command{}, []string{"dbaas-001", "my-db"}, "")
if comps != nil {
t.Errorf("expected nil completions with too many args, got %v", comps)
}
}

// ─── completeDBaaSUserID parent-slot fixes ────────────────────────────────────

func TestCompleteDBaaSUserID_NoArgs_DelegatesToDBaaSID(t *testing.T) {
id, name := "dbaas-001", "my-dbaas"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Database/dbaas", jsonResponse(200, types.DBaaSListResponse{
Values: []types.DBaaSResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeDBaaSUserID(makeProjectCmd("proj-123"), []string{}, "")
if len(completions) == 0 {
t.Errorf("expected DBaaS IDs from delegation, got none")
}
}

func TestCompleteDBaaSUserID_NoArgs_NoProjectID(t *testing.T) {
cleanup := withTempHomeDir(t)
defer cleanup()

comps, _ := completeDBaaSUserID(&cobra.Command{}, []string{}, "")
if comps != nil {
t.Errorf("expected nil completions without project-id, got %v", comps)
}
}

func TestCompleteDBaaSUserID_TooManyArgs(t *testing.T) {
comps, _ := completeDBaaSUserID(&cobra.Command{}, []string{"dbaas-001", "alice"}, "")
if comps != nil {
t.Errorf("expected nil completions with too many args, got %v", comps)
}
}

// ─── completeGrantID parent-slot fixes ───────────────────────────────────────

func TestCompleteGrantID_NoArgs_DelegatesToDBaaSID(t *testing.T) {
id, name := "dbaas-001", "my-dbaas"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Database/dbaas", jsonResponse(200, types.DBaaSListResponse{
Values: []types.DBaaSResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeGrantID(makeProjectCmd("proj-123"), []string{}, "")
if len(completions) == 0 {
t.Errorf("expected DBaaS IDs from delegation, got none")
}
}

func TestCompleteGrantID_OneArg_DelegatesToDatabaseID(t *testing.T) {
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Database/dbaas/dbaas-001/databases", jsonResponse(200, types.DatabaseListResponse{
Values: []types.DatabaseResponse{
{Name: "my-db"},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeGrantID(makeProjectCmd("proj-123"), []string{"dbaas-001"}, "")
if len(completions) == 0 {
t.Errorf("expected database names from delegation, got none")
}
}

func TestCompleteGrantID_TooManyArgs(t *testing.T) {
comps, _ := completeGrantID(&cobra.Command{}, []string{"dbaas-001", "my-db", "grant-id"}, "")
if comps != nil {
t.Errorf("expected nil completions with too many args, got %v", comps)
}
}

// ─── completeStorageRestoreCreateArgs ────────────────────────────────────────

func TestCompleteStorageRestoreCreateArgs_NoArgs_DelegatesToBackupID(t *testing.T) {
id, name := "bkp-001", "my-backup"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Storage/backups", jsonResponse(200, types.StorageBackupListResponse{
Values: []types.StorageBackupResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeStorageRestoreCreateArgs(makeProjectCmd("proj-123"), []string{}, "")
if len(completions) == 0 {
t.Errorf("expected backup IDs from delegation, got none")
}
}

func TestCompleteStorageRestoreCreateArgs_OneArg_DelegatesToBlockStorageID(t *testing.T) {
id, name := "vol-001", "my-volume"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Storage/blockStorages", jsonResponse(200, types.BlockStorageListResponse{
Values: []types.BlockStorageResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeStorageRestoreCreateArgs(makeProjectCmd("proj-123"), []string{"bkp-001"}, "")
if len(completions) == 0 {
t.Errorf("expected volume IDs from delegation, got none")
}
}

func TestCompleteStorageRestoreCreateArgs_TooManyArgs(t *testing.T) {
comps, _ := completeStorageRestoreCreateArgs(&cobra.Command{}, []string{"bkp-001", "vol-001"}, "")
if comps != nil {
t.Errorf("expected nil completions with too many args, got %v", comps)
}
}

// ─── storageBackupCmd create ValidArgsFunction ───────────────────────────────

func TestCompleteBackupCreateArg_DelegatesToBlockStorageID(t *testing.T) {
id, name := "vol-001", "my-volume"
srv := newArubaTestServer(t)
srv.OnGet("/projects/proj-123/providers/Aruba.Storage/blockStorages", jsonResponse(200, types.BlockStorageListResponse{
Values: []types.BlockStorageResponse{
{Metadata: types.ResourceMetadataResponse{ID: &id, Name: &name}},
},
}))
setClientForTesting(srv.Client())
defer resetClientState()

completions, _ := completeBlockStorageID(makeProjectCmd("proj-123"), nil, "")
if len(completions) == 0 {
t.Errorf("expected volume IDs, got none")
}
}
7 changes: 6 additions & 1 deletion cmd/database.dbaas.database.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,18 @@ func init() {
dbaasDatabaseListCmd.Flags().Int("limit", 0, "Maximum number of results to return (0 = no limit)")
dbaasDatabaseListCmd.Flags().Int("offset", 0, "Number of results to skip")

dbaasDatabaseCreateCmd.ValidArgsFunction = completeDBaaSDatabaseID
dbaasDatabaseListCmd.ValidArgsFunction = completeDBaaSDatabaseID
dbaasDatabaseGetCmd.ValidArgsFunction = completeDBaaSDatabaseID
dbaasDatabaseUpdateCmd.ValidArgsFunction = completeDBaaSDatabaseID
dbaasDatabaseDeleteCmd.ValidArgsFunction = completeDBaaSDatabaseID
}

func completeDBaaSDatabaseID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) < 1 {
if len(args) == 0 {
return completeDBaaSID(cmd, args, toComplete)
}
if len(args) > 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

Expand Down
13 changes: 12 additions & 1 deletion cmd/database.dbaas.grant.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ func init() {
dbaasGrantDeleteCmd.Flags().String("project-id", "", "Project ID (uses context if not specified)")
dbaasGrantDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
dbaasGrantDeleteCmd.Flags().Bool("dry-run", false, "Validate resource exists without deleting")

dbaasGrantCreateCmd.ValidArgsFunction = completeGrantID
dbaasGrantListCmd.ValidArgsFunction = completeGrantID
dbaasGrantGetCmd.ValidArgsFunction = completeGrantID
dbaasGrantDeleteCmd.ValidArgsFunction = completeGrantID
}

// grantRef returns a Ref for a specific grant inside a database inside a DBaaS instance.
Expand All @@ -46,7 +51,13 @@ func grantRef(projectID, dbaasID, dbName, grantID string) aruba.Ref {
}

func completeGrantID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) < 2 {
if len(args) == 0 {
return completeDBaaSID(cmd, args, toComplete)
}
if len(args) == 1 {
return completeDBaaSDatabaseID(cmd, args, toComplete)
}
if len(args) > 2 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
projectID, err := GetProjectID(cmd)
Expand Down
7 changes: 6 additions & 1 deletion cmd/database.dbaas.user.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,18 @@ func init() {
dbaasUserListCmd.Flags().Int("limit", 0, "Maximum number of results to return (0 = no limit)")
dbaasUserListCmd.Flags().Int("offset", 0, "Number of results to skip")

dbaasUserCreateCmd.ValidArgsFunction = completeDBaaSUserID
dbaasUserListCmd.ValidArgsFunction = completeDBaaSUserID
dbaasUserGetCmd.ValidArgsFunction = completeDBaaSUserID
dbaasUserUpdateCmd.ValidArgsFunction = completeDBaaSUserID
dbaasUserDeleteCmd.ValidArgsFunction = completeDBaaSUserID
}

func completeDBaaSUserID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) < 1 {
if len(args) == 0 {
return completeDBaaSID(cmd, args, toComplete)
}
if len(args) > 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

Expand Down
1 change: 1 addition & 0 deletions cmd/storage.backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func init() {
storageBackupDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
storageBackupDeleteCmd.Flags().Bool("dry-run", false, "Validate resource exists without deleting")

storageBackupCmd.ValidArgsFunction = completeBlockStorageID
storageBackupGetCmd.ValidArgsFunction = completeBackupID
storageBackupUpdateCmd.ValidArgsFunction = completeBackupID
storageBackupDeleteCmd.ValidArgsFunction = completeBackupID
Expand Down
14 changes: 14 additions & 0 deletions cmd/storage.restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func init() {
storageRestoreDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
storageRestoreDeleteCmd.Flags().Bool("dry-run", false, "Validate resource exists without deleting")

storageRestoreCmd.ValidArgsFunction = completeStorageRestoreCreateArgs
storageRestoreGetCmd.ValidArgsFunction = completeRestoreID
storageRestoreUpdateCmd.ValidArgsFunction = completeRestoreID
storageRestoreDeleteCmd.ValidArgsFunction = completeRestoreID
Expand Down Expand Up @@ -96,6 +97,19 @@ func completeRestoreID(cmd *cobra.Command, args []string, toComplete string) ([]
return filterCompletions(completions, toComplete), cobra.ShellCompDirectiveNoFileComp
}

// completeStorageRestoreCreateArgs completes the two positional args of
// "storage restore [backup-id] [volume-id]": backup IDs at args[0], block
// storage (volume) IDs at args[1].
func completeStorageRestoreCreateArgs(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return completeBackupID(cmd, args, toComplete)
}
if len(args) == 1 {
return completeBlockStorageID(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}

var storageRestoreCmd = &cobra.Command{
Use: "restore [backup-id] [volume-id]",
Short: "Restore a block storage volume from a backup",
Expand Down
Loading
Loading