diff --git a/CHANGELOG.md b/CHANGELOG.md index 9469f35..d458f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to zcp will be documented in this file. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), using [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.8] - 2026-04-09 + +### Fixed + +- **VPC create**: Correct payload structure — `cidr` is the network address (e.g. `10.1.0.1`), `size` is the mask (e.g. `16`), requires `type=Vpc`, `billing_cycle`, `plan` (from router plans), `storage_category` +- **ACL create**: Fixed to create ACL lists (name, description, vpc) instead of incorrectly sending protocol/port rule fields +- **Volume Size type**: Fixed `string` to `interface{}` — API returns number, not string +- **JSON tags**: Fixed camelCase to snake_case for `cloud_provider` across vpc, vpn, autoscale request structs +- **VPN user create**: Updated to accept `UserCreateRequest` struct with cloud_provider, region, project + +### Added + +- **VPC tier/subnet creation**: Confirmed working via `POST /networks` with `type=Vpc`, `gateway`, `netmask`, `acl_id` +- **`--cloud-provider`, `--region`, `--project` flags**: Added to network, vpc, virtualrouter, dns, vpn, autoscale create commands +- **`docs/roadmap.md`**: Feature roadmap documenting what works, what's coming, and what's blocked on platform + +### Changed + +- **VPC create flags**: Replaced old `--zone`, `--offering`, `--network-domain`, `--lb-provider` with `--cidr`, `--size`, `--plan`, `--billing-cycle`, `--storage-category`, `--cloud-provider`, `--region`, `--project` +- **ACL commands**: `zcp acl create` and `zcp vpc acl-create` now take `--name` and `--description` (matching the actual API) + +**Full Changelog**: https://github.com/zsoftly/zcp-cli/compare/0.0.7...0.0.8 + +--- + ## [0.0.7] - 2026-04-08 ### Added diff --git a/README.md b/README.md index 6bc5169..8c37707 100644 --- a/README.md +++ b/README.md @@ -288,9 +288,13 @@ zcp vm-snapshot revert # Networks zcp network list zcp network categories -zcp network create --name my-net --category +zcp network create --name my-net --category --cloud-provider nimbo --region noida --project default-124 zcp network update --name "New Name" +# VPC tier networks +zcp network create --name public-tier --cloud-provider nimbo --region noida --project default-124 \ + --vpc --type Vpc --gateway 10.1.1.1 --netmask 255.255.255.0 --acl-id + # Public IP addresses zcp ip list zcp ip allocate --network @@ -321,13 +325,20 @@ zcp portforward create \ ```bash # VPCs zcp vpc list -zcp vpc create --zone --name my-vpc --offering --cidr 10.0.0.0/8 -zcp vpc delete +zcp vpc create \ + --name my-vpc \ + --cloud-provider nimbo \ + --region noida \ + --project default-124 \ + --plan vpc-1 \ + --network-address 10.1.0.1 \ + --size 16 \ + --billing-cycle hourly \ + --storage-category nvme -# Network ACLs -zcp acl list -zcp acl create --vpc --name my-acl -zcp acl delete +# Network ACL lists +zcp acl list +zcp acl create --name my-acl --description "Allow web traffic" # Public load balancers zcp loadbalancer list @@ -362,7 +373,7 @@ zcp dns list zcp dns show # Create a domain -zcp dns create --name example.com --project my-project +zcp dns create --name example.com --project my-project --cloud-provider nimbo --region noida --dns-provider powerdns # Create a record zcp dns record-create --domain --name www --type A --content 192.0.2.1 @@ -388,7 +399,7 @@ zcp backup delete ```bash zcp autoscale list zcp autoscale get -zcp autoscale create --name my-policy --min 1 --max 5 +zcp autoscale create --name my-policy --min 1 --max 5 --cloud-provider nimbo --region noida --project default-124 zcp autoscale delete ``` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 9626031..4aeb003 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,59 +1,64 @@ -# zcp 0.0.7 Release Notes +# zcp 0.0.8 Release Notes ## What's New -### 8 new commands +### VPC create fixed -| Command | Description | -| --------------------------- | ---------------------------------------------------------------------------------------------- | -| `zcp region list` | List available regions (replaces `zone list`) | -| `zcp profile-info` | User profile, company details, time settings, API access, activity logs (2FA status via `get`) | -| `zcp vm-backup list/create` | VM backup operations | -| `zcp cloud-provider list` | List available cloud providers | -| `zcp server list` | List available servers | -| `zcp currency list` | List available currencies | -| `zcp billing-cycle list` | List available billing cycles | -| `zcp storage-category list` | List available storage categories | +VPC creation now works with the correct payload structure: -### Dead code removed +```bash +zcp vpc create \ + --name my-vpc \ + --cloud-provider nimbo \ + --region noida \ + --project default-124 \ + --plan vpc-1 \ + --network-address 10.1.0.1 \ + --size 16 \ + --billing-cycle hourly \ + --storage-category nvme +``` + +Key: `--network-address` is just the IP (not CIDR notation), `--size` is the mask separately. -11 commands and 13 API packages that still pointed at old `/restapi/` endpoints have been removed. These commands were broken since v0.0.6 and would return 403 errors: +### ACL list creation fixed -`zone`, `offering`, `resource`, `host`, `cost`, `usage`, `internal-lb`, `snapshot-policy`, `security-group`, `tag`, `admin` +`zcp vpc acl-create` and `zcp acl create` now correctly create ACL lists: -Use the STKCNSL replacements instead: +```bash +zcp vpc acl-create my-vpc --name allow-web --description "Allow HTTP" +zcp acl create my-vpc --name private-acl --description "Deny all inbound" +``` -| Old command | Replacement | -| ------------------------- | --------------------------- | -| `zcp zone list` | `zcp region list` | -| `zcp offering compute` | `zcp plan vm` | -| `zcp offering storage` | `zcp plan storage` | -| `zcp cost summary` | `zcp billing costs` | -| `zcp usage list` | `zcp billing monthly-usage` | -| `zcp tag create` | `zcp instance tag-create` | -| `zcp admin list-accounts` | Not available via API | +### Create commands gain required flags -### Auth validate fixed +`--cloud-provider`, `--region`, `--project` added to: network, vpc, virtualrouter, dns, vpn, autoscale create commands. -`zcp auth validate` now correctly hits the STKCNSL region API instead of the dead zone API. +### Volume Size type fix ---- +Volume list no longer fails when the API returns size as a number. -## 42 total commands +### Roadmap published -The CLI now has 42 commands, all backed by the STKCNSL API with zero legacy code remaining. +See `docs/roadmap.md` for what's working, what's coming, and what's blocked on the platform. --- -## Installation +## Known limitations (blocked on platform) -### Quick Install (Recommended) +These require API changes from the STKCNSL team: -**Windows:** +- **No DELETE endpoints** for VPCs, networks, virtual routers, IP addresses, or ACL lists +- **No ACL rule CRUD** — can create ACL lists but not rules inside them +- **Network create (isolated)** — `networkofferingid` not resolvable for nimbo/noida +- **DNS create** — needs admin-side `cloud_provider_setup` provisioning +- **billing cancel-service for VPCs** — returns "service not found" -```powershell -irm https://github.com/zsoftly/zcp-cli/releases/latest/download/install.ps1 | iex -``` +See `docs/roadmap.md` for full details. + +--- + +## Installation **macOS/Linux/WSL:** @@ -61,19 +66,10 @@ irm https://github.com/zsoftly/zcp-cli/releases/latest/download/install.ps1 | ie curl -fsSL https://github.com/zsoftly/zcp-cli/releases/latest/download/install.sh | bash ``` -### Manual Install - -Download the binary for your platform from the assets below, make it executable, and move it to your `PATH`. - -## Platforms +**Windows:** -| OS | Architecture | Binary | -| ------- | ------------ | ----------------------- | -| Linux | amd64 | `zcp-linux-amd64` | -| Linux | arm64 | `zcp-linux-arm64` | -| macOS | amd64 | `zcp-darwin-amd64` | -| macOS | arm64 | `zcp-darwin-arm64` | -| Windows | amd64 | `zcp-windows-amd64.exe` | -| Windows | arm64 | `zcp-windows-arm64.exe` | +```powershell +irm https://github.com/zsoftly/zcp-cli/releases/latest/download/install.ps1 | iex +``` -**Full Changelog**: https://github.com/zsoftly/zcp-cli/compare/0.0.6...0.0.7 +**Full Changelog**: https://github.com/zsoftly/zcp-cli/compare/0.0.7...0.0.8 diff --git a/docs/command-taxonomy.md b/docs/command-taxonomy.md index 63be466..fdde9b5 100644 --- a/docs/command-taxonomy.md +++ b/docs/command-taxonomy.md @@ -113,7 +113,7 @@ zcp │ ├── delete Delete a VPC │ ├── restart Restart a VPC │ ├── acl-list List ACL rules for a VPC -│ ├── acl-create-rule Create an ACL rule in a VPC +│ ├── acl-create Create a network ACL list in a VPC │ ├── acl-replace Replace the ACL on a VPC network │ └── vpn-gateway VPN gateway operations within a VPC │ ├── list List VPN gateways diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..61b2cd5 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,81 @@ +# ZCP CLI Roadmap + +Features planned, in progress, or blocked on platform support. + +--- + +## Completed (v0.0.7) + +- 42 commands covering VM, storage, networking, billing, monitoring, DNS, projects, support, and more +- Full VM lifecycle: create, start, stop, reboot, reset, tags, change-plan, change-OS, cancel +- VPC lifecycle: create, list, update, restart, ACL list create, VPN gateway create +- VPC tier/subnet creation via `POST /networks` with `type=Vpc` +- Bearer token authentication +- Global `--auto-approve` / `-y` flag for CI/CD automation +- All old STKBILL code removed, zero `/restapi/` references + +--- + +## Planned for next patch + +### CLI improvements (no platform dependency) + +- [ ] `network create` — add `--vpc`, `--type`, `--gateway`, `--netmask`, `--acl-id` flags for VPC tier creation +- [ ] `network create` — add `--acl` flag that resolves ACL name to ID automatically +- [ ] `portforward create` — add `--public-end-port` and `--private-end-port` flags (API requires them) +- [ ] `instance change-hostname` — fix request body field name (`vm_label` instead of `label`) +- [ ] `region` command — add `use` subcommand to set default region in profile +- [ ] Default `--cloud-provider`, `--region`, `--project` from profile config to reduce flag repetition + +### Blocked on STKCNSL platform + +These features require API endpoints or fixes from the STKCNSL team. + +#### Missing DELETE endpoints + +The API has no DELETE for these resource types. Resources can only be removed via `billing cancel-service` for VMs/volumes, but not for networking resources. + +- [ ] `DELETE /vpcs/{slug}` — VPC deletion +- [ ] `DELETE /networks/{slug}` — network deletion (isolated and VPC tiers) +- [ ] `DELETE /virtual-routers/{slug}` — virtual router deletion +- [ ] `DELETE /ipaddresses/{slug}` — IP address release +- [ ] `DELETE /vpcs/{slug}/network-acl-list/{id}` — ACL list deletion +- [ ] `billing cancel-service` for VPC/Virtual Router service type — currently returns "service not found" + +#### Missing ACL rule CRUD + +The UI has "Add Rule" with Number, CIDR, Action, Protocol, Traffic Type fields, but no public API endpoint exists for creating rules inside an ACL list. + +- [ ] `POST /vpcs/{slug}/network-acl-list/{acl_id}/rules` — create ACL rule +- [ ] `DELETE /vpcs/{slug}/network-acl-list/{acl_id}/rules/{rule_id}` — delete ACL rule +- [ ] `GET /vpcs/{slug}/network-acl-list/{acl_id}/rules` — list ACL rules + +#### Network create (isolated) — noida region + +`POST /networks` returns `missing parameter networkofferingid` for the nimbo/noida region. The API doesn't expose the network offering field. Likely a region configuration issue. + +- [ ] Network offering mapping for nimbo/noida + +#### DNS provisioning + +`POST /dns/domains` returns `cloud_provider_setup: DNS configuration required`. DNS needs admin-side provisioning. + +- [ ] DNS enabled for our account/region + +#### Network quota + +Only 2 VPC tier networks allowed before quota exceeded. + +- [ ] Quota increase for testing + +--- + +## Future (v0.0.9+) + +- [ ] Pagination support — `--page`, `--per-page` flags for list commands +- [ ] `--wait` flag on VPC create, volume create (poll until ready) +- [ ] JSON output improvements — consistent envelope stripping +- [ ] Shell completion for dynamic values (region slugs, plan slugs, etc.) +- [ ] `zcp config set` for default cloud-provider, region, project +- [ ] Object storage management (if API endpoint becomes available) +- [ ] Kubernetes cluster full lifecycle (create works, delete via billing cancel-service) diff --git a/internal/api/acl/acl.go b/internal/api/acl/acl.go index be3d55e..b2aac77 100644 --- a/internal/api/acl/acl.go +++ b/internal/api/acl/acl.go @@ -18,7 +18,14 @@ type NetworkACL struct { VPCSlug string `json:"vpcSlug"` } -// ACLRuleCreateRequest holds parameters for creating a Network ACL rule. +// ACLCreateRequest holds parameters for creating a Network ACL list. +type ACLCreateRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + VPC string `json:"vpc"` +} + +// ACLRuleCreateRequest holds parameters for creating a rule inside an ACL. type ACLRuleCreateRequest struct { Protocol string `json:"protocol"` CIDRList string `json:"cidrList,omitempty"` @@ -77,17 +84,13 @@ func (s *Service) List(ctx context.Context, vpcSlug string) ([]NetworkACL, error return acls, nil } -// CreateRule creates a new ACL rule in a VPC. -func (s *Service) CreateRule(ctx context.Context, vpcSlug string, req ACLRuleCreateRequest) (*ACLRule, error) { +// Create creates a new ACL list in a VPC. +func (s *Service) Create(ctx context.Context, vpcSlug string, req ACLCreateRequest) error { var env apiResponse if err := s.client.Post(ctx, "/vpcs/"+vpcSlug+"/network-acl-list", req, &env); err != nil { - return nil, fmt.Errorf("creating ACL rule in VPC %s: %w", vpcSlug, err) + return fmt.Errorf("creating ACL in VPC %s: %w", vpcSlug, err) } - var rule ACLRule - if err := json.Unmarshal(env.Data, &rule); err != nil { - return nil, fmt.Errorf("decoding created ACL rule: %w", err) - } - return &rule, nil + return nil } // ReplaceNetworkACL replaces the ACL on a network by slug. diff --git a/internal/api/acl/acl_test.go b/internal/api/acl/acl_test.go index 28dcd47..43ede54 100644 --- a/internal/api/acl/acl_test.go +++ b/internal/api/acl/acl_test.go @@ -48,26 +48,26 @@ func TestACLList(t *testing.T) { } } -func TestACLCreateRule(t *testing.T) { +func TestACLCreate(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %q, want POST", r.Method) } + if r.URL.Path != "/vpcs/my-vpc/network-acl-list" { + t.Errorf("path = %q", r.URL.Path) + } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "Success", - "data": map[string]interface{}{"slug": "rule-1", "protocol": "tcp", "action": "allow"}, + "status": "Success", + "message": "Adding network ACL list.", }) })) defer srv.Close() svc := acl.NewService(newClient(srv.URL)) - rule, err := svc.CreateRule(context.Background(), "my-vpc", acl.ACLRuleCreateRequest{Protocol: "tcp", Action: "allow"}) + err := svc.Create(context.Background(), "my-vpc", acl.ACLCreateRequest{Name: "web-acl", VPC: "my-vpc"}) if err != nil { - t.Fatalf("CreateRule() error = %v", err) - } - if rule.Slug != "rule-1" { - t.Errorf("slug = %q, want %q", rule.Slug, "rule-1") + t.Fatalf("Create() error = %v", err) } } diff --git a/internal/api/autoscale/autoscale.go b/internal/api/autoscale/autoscale.go index 7524117..2adf1d6 100644 --- a/internal/api/autoscale/autoscale.go +++ b/internal/api/autoscale/autoscale.go @@ -74,6 +74,9 @@ type CreateRequest struct { CooldownPeriod int `json:"cooldownPeriod,omitempty"` ZoneSlug string `json:"zoneSlug"` NetworkSlug string `json:"networkSlug,omitempty"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` + Project string `json:"project"` } // ChangePlanRequest holds parameters for changing an autoscale group's plan. diff --git a/internal/api/dns/dns.go b/internal/api/dns/dns.go index 7b21bfb..3994d98 100644 --- a/internal/api/dns/dns.go +++ b/internal/api/dns/dns.go @@ -41,9 +41,11 @@ type Record struct { // CreateDomainRequest holds parameters for creating a DNS domain. type CreateDomainRequest struct { - Name string `json:"name"` - Project string `json:"project"` - DNSProvider string `json:"dns_provider"` + Name string `json:"name"` + Project string `json:"project"` + DNSProvider string `json:"dns_provider"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` } // CreateRecordRequest holds parameters for creating a DNS record. diff --git a/internal/api/network/network.go b/internal/api/network/network.go index 81759e8..7993aa1 100644 --- a/internal/api/network/network.go +++ b/internal/api/network/network.go @@ -49,12 +49,15 @@ type EgressRule struct { // CreateRequest holds parameters for creating a network. type CreateRequest struct { - Name string `json:"name"` - CategorySlug string `json:"category_slug"` - ZoneSlug string `json:"zone_slug,omitempty"` - Gateway string `json:"gateway,omitempty"` - Netmask string `json:"netmask,omitempty"` - Description string `json:"description,omitempty"` + Name string `json:"name"` + CategorySlug string `json:"category_slug"` + ZoneSlug string `json:"zone_slug,omitempty"` + Gateway string `json:"gateway,omitempty"` + Netmask string `json:"netmask,omitempty"` + Description string `json:"description,omitempty"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` + Project string `json:"project"` } // UpdateRequest holds parameters for updating a network. diff --git a/internal/api/virtualrouter/virtualrouter.go b/internal/api/virtualrouter/virtualrouter.go index 2103ac3..9dafc50 100644 --- a/internal/api/virtualrouter/virtualrouter.go +++ b/internal/api/virtualrouter/virtualrouter.go @@ -28,10 +28,12 @@ type VirtualRouter struct { // CreateRequest holds parameters for creating a virtual router. type CreateRequest struct { - Name string `json:"name"` - NetworkSlug string `json:"network_slug"` - PlanSlug string `json:"plan_slug,omitempty"` - ZoneSlug string `json:"zone_slug,omitempty"` + Name string `json:"vr_name"` + NetworkSlug string `json:"network_slug"` + PlanSlug string `json:"plan,omitempty"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` + Project string `json:"project"` } type listVirtualRouterResponse struct { diff --git a/internal/api/virtualrouter/virtualrouter_test.go b/internal/api/virtualrouter/virtualrouter_test.go index d2ad674..d12221a 100644 --- a/internal/api/virtualrouter/virtualrouter_test.go +++ b/internal/api/virtualrouter/virtualrouter_test.go @@ -105,8 +105,8 @@ func TestVirtualRouterCreate(t *testing.T) { if vr.Slug != "new-router" { t.Errorf("vr.Slug = %q, want %q", vr.Slug, "new-router") } - if gotBody["name"] != "my-router" { - t.Errorf("body[name] = %v, want %q", gotBody["name"], "my-router") + if gotBody["vr_name"] != "my-router" { + t.Errorf("body[vr_name] = %v, want %q", gotBody["vr_name"], "my-router") } if gotBody["network_slug"] != "web-network" { t.Errorf("body[network_slug] = %v, want %q", gotBody["network_slug"], "web-network") diff --git a/internal/api/volume/volume.go b/internal/api/volume/volume.go index 0b00812..113752e 100644 --- a/internal/api/volume/volume.go +++ b/internal/api/volume/volume.go @@ -4,6 +4,7 @@ package volume import ( "context" + "encoding/json" "fmt" "net/url" @@ -45,7 +46,7 @@ type BillingCycle struct { // Offering represents the billing/plan offering on a volume. type Offering struct { ID string `json:"id"` - Size string `json:"size"` + Size json.Number `json:"size"` Price string `json:"price"` BillingStatus bool `json:"billing_status"` RenewAt string `json:"renew_at"` @@ -57,7 +58,7 @@ type Volume struct { ID string `json:"id"` BlockstorageID string `json:"blockstorage_id"` VirtualMachineID string `json:"virtual_machine_id"` - Size string `json:"size"` + Size json.Number `json:"size"` Name string `json:"name"` Slug string `json:"slug"` Description *string `json:"description"` diff --git a/internal/api/vpc/vpc.go b/internal/api/vpc/vpc.go index 9a649b6..b8cda86 100644 --- a/internal/api/vpc/vpc.go +++ b/internal/api/vpc/vpc.go @@ -23,13 +23,18 @@ type VPC struct { // CreateRequest holds parameters for creating a VPC. type CreateRequest struct { - Name string `json:"name"` - ZoneSlug string `json:"zoneSlug"` - VPCOfferingSlug string `json:"vpcOfferingSlug"` - CIDR string `json:"cidr"` - Description string `json:"description,omitempty"` - NetworkDomain string `json:"networkDomain,omitempty"` - PublicLoadBalancerProvider string `json:"publicLoadBalancerProvider,omitempty"` + Name string `json:"name"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` + Project string `json:"project"` + Type string `json:"type"` + BillingCycle string `json:"billing_cycle"` + CIDR string `json:"cidr"` + Size string `json:"size"` + Plan string `json:"plan"` + StorageCategory string `json:"storage_category"` + Description string `json:"description,omitempty"` + Coupon string `json:"coupon,omitempty"` } // UpdateRequest holds parameters for updating a VPC. @@ -46,17 +51,11 @@ type NetworkACL struct { Status string `json:"status"` } -// ACLRuleCreateRequest holds parameters for creating a network ACL rule. -type ACLRuleCreateRequest struct { - Protocol string `json:"protocol"` - CIDRList string `json:"cidrList,omitempty"` - StartPort int `json:"startPort,omitempty"` - EndPort int `json:"endPort,omitempty"` - TrafficType string `json:"trafficType,omitempty"` - Action string `json:"action"` - Number int `json:"number,omitempty"` - ICMPCode int `json:"icmpCode,omitempty"` - ICMPType int `json:"icmpType,omitempty"` +// ACLListCreateRequest holds parameters for creating a Network ACL list. +type ACLListCreateRequest struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + VPC string `json:"vpc"` } // ACLRule represents a single ACL rule. @@ -193,17 +192,13 @@ func (s *Service) ListACLs(ctx context.Context, vpcSlug string) ([]NetworkACL, e return acls, nil } -// CreateACLRule creates a new ACL rule in a VPC. -func (s *Service) CreateACLRule(ctx context.Context, vpcSlug string, req ACLRuleCreateRequest) (*ACLRule, error) { +// CreateACL creates a new ACL list in a VPC. +func (s *Service) CreateACL(ctx context.Context, vpcSlug string, req ACLListCreateRequest) error { var env apiResponse if err := s.client.Post(ctx, "/vpcs/"+vpcSlug+"/network-acl-list", req, &env); err != nil { - return nil, fmt.Errorf("creating ACL rule in VPC %s: %w", vpcSlug, err) - } - var rule ACLRule - if err := json.Unmarshal(env.Data, &rule); err != nil { - return nil, fmt.Errorf("decoding created ACL rule: %w", err) + return fmt.Errorf("creating ACL in VPC %s: %w", vpcSlug, err) } - return &rule, nil + return nil } // ReplaceNetworkACL replaces the ACL on a network by slug. diff --git a/internal/api/vpc/vpc_test.go b/internal/api/vpc/vpc_test.go index 4da9042..b0bd7d8 100644 --- a/internal/api/vpc/vpc_test.go +++ b/internal/api/vpc/vpc_test.go @@ -137,9 +137,15 @@ func TestVPCCreate(t *testing.T) { req := vpc.CreateRequest{ Name: "my-vpc", - ZoneSlug: "zone-1", - VPCOfferingSlug: "offering-1", - CIDR: "10.0.0.0/8", + CloudProvider: "nimbo", + Region: "noida", + Project: "default-124", + Type: "Vpc", + BillingCycle: "hourly", + CIDR: "10.0.0.1", + Size: "24", + Plan: "vpc-1", + StorageCategory: "nvme", } v, err := svc.Create(context.Background(), req) @@ -152,14 +158,32 @@ func TestVPCCreate(t *testing.T) { if gotBody["name"] != "my-vpc" { t.Errorf("body[name] = %v, want %q", gotBody["name"], "my-vpc") } - if gotBody["zoneSlug"] != "zone-1" { - t.Errorf("body[zoneSlug] = %v, want %q", gotBody["zoneSlug"], "zone-1") + if gotBody["cloud_provider"] != "nimbo" { + t.Errorf("body[cloud_provider] = %v, want %q", gotBody["cloud_provider"], "nimbo") } - if gotBody["vpcOfferingSlug"] != "offering-1" { - t.Errorf("body[vpcOfferingSlug] = %v, want %q", gotBody["vpcOfferingSlug"], "offering-1") + if gotBody["cidr"] != "10.0.0.1" { + t.Errorf("body[cidr] = %v, want %q", gotBody["cidr"], "10.0.0.1") } - if gotBody["cidr"] != "10.0.0.0/8" { - t.Errorf("body[cidr] = %v, want %q", gotBody["cidr"], "10.0.0.0/8") + if gotBody["region"] != "noida" { + t.Errorf("body[region] = %v, want %q", gotBody["region"], "noida") + } + if gotBody["project"] != "default-124" { + t.Errorf("body[project] = %v, want %q", gotBody["project"], "default-124") + } + if gotBody["type"] != "Vpc" { + t.Errorf("body[type] = %v, want %q", gotBody["type"], "Vpc") + } + if gotBody["billing_cycle"] != "hourly" { + t.Errorf("body[billing_cycle] = %v, want %q", gotBody["billing_cycle"], "hourly") + } + if gotBody["plan"] != "vpc-1" { + t.Errorf("body[plan] = %v, want %q", gotBody["plan"], "vpc-1") + } + if gotBody["storage_category"] != "nvme" { + t.Errorf("body[storage_category] = %v, want %q", gotBody["storage_category"], "nvme") + } + if gotBody["size"] != "24" { + t.Errorf("body[size] = %v, want %q", gotBody["size"], "24") } } diff --git a/internal/api/vpn/customergateway.go b/internal/api/vpn/customergateway.go index 7387a43..a96e16d 100644 --- a/internal/api/vpn/customergateway.go +++ b/internal/api/vpn/customergateway.go @@ -43,6 +43,9 @@ type CustomerGatewayRequest struct { ForceEncap bool `json:"forceencap"` SplitConnection bool `json:"splitConnection"` DPD bool `json:"dpd"` + CloudProvider string `json:"cloud_provider,omitempty"` + Region string `json:"region,omitempty"` + Project string `json:"project,omitempty"` } // CustomerGatewayService provides VPN customer gateway API operations. diff --git a/internal/api/vpn/user.go b/internal/api/vpn/user.go index 9e4615c..74f784a 100644 --- a/internal/api/vpn/user.go +++ b/internal/api/vpn/user.go @@ -18,8 +18,11 @@ type User struct { // UserCreateRequest holds parameters for creating a VPN user. type UserCreateRequest struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username"` + Password string `json:"password"` + CloudProvider string `json:"cloud_provider"` + Region string `json:"region"` + Project string `json:"project"` } // apiResponse is the STKCNSL response envelope. @@ -51,14 +54,10 @@ func (s *UserService) List(ctx context.Context) ([]User, error) { return users, nil } -// Create adds a new VPN user with the given username and password. -func (s *UserService) Create(ctx context.Context, username, password string) (*User, error) { - body := UserCreateRequest{ - Username: username, - Password: password, - } +// Create adds a new VPN user with the given request parameters. +func (s *UserService) Create(ctx context.Context, req UserCreateRequest) (*User, error) { var env apiResponse - if err := s.client.Post(ctx, "/vpn-users", body, &env); err != nil { + if err := s.client.Post(ctx, "/vpn-users", req, &env); err != nil { return nil, fmt.Errorf("creating VPN user: %w", err) } var u User diff --git a/internal/api/vpn/vpn_test.go b/internal/api/vpn/vpn_test.go index 120d320..a471393 100644 --- a/internal/api/vpn/vpn_test.go +++ b/internal/api/vpn/vpn_test.go @@ -231,7 +231,10 @@ func TestVPNUserCreate(t *testing.T) { defer srv.Close() svc := vpn.NewUserService(newClient(srv.URL)) - result, err := svc.Create(context.Background(), "carol", "p@ssw0rd") + result, err := svc.Create(context.Background(), vpn.UserCreateRequest{ + Username: "carol", + Password: "p@ssw0rd", + }) if err != nil { t.Fatalf("Create() error = %v", err) } diff --git a/internal/commands/acl.go b/internal/commands/acl.go index 9a0e980..685b233 100644 --- a/internal/commands/acl.go +++ b/internal/commands/acl.go @@ -16,7 +16,7 @@ func NewACLCmd() *cobra.Command { Short: "Manage Network ACLs", } cmd.AddCommand(newACLListCmd()) - cmd.AddCommand(newACLCreateRuleCmd()) + cmd.AddCommand(newACLCreateCmd()) cmd.AddCommand(newACLReplaceCmd()) return cmd } @@ -63,50 +63,32 @@ func runACLList(cmd *cobra.Command, vpcSlug string) error { return printer.PrintTable(headers, rows) } -func newACLCreateRuleCmd() *cobra.Command { - var protocol, cidrList, trafficType, action string - var startPort, endPort, number, icmpCode, icmpType int +func newACLCreateCmd() *cobra.Command { + var name, description string cmd := &cobra.Command{ - Use: "create-rule ", - Short: "Create a network ACL rule in a VPC", - Args: cobra.ExactArgs(1), - Example: ` zcp acl create-rule --protocol tcp --action allow --start-port 80 --end-port 80 --cidr 0.0.0.0/0 - zcp acl create-rule --protocol icmp --action deny --icmp-type 8 --icmp-code 0`, + Use: "create ", + Short: "Create a network ACL in a VPC", + Args: cobra.ExactArgs(1), + Example: ` zcp acl create my-vpc --name allow-web --description "Allow HTTP traffic"`, RunE: func(cmd *cobra.Command, args []string) error { - if protocol == "" { - return fmt.Errorf("--protocol is required") - } - if action == "" { - return fmt.Errorf("--action is required") + if name == "" { + return fmt.Errorf("--name is required") } - return runACLCreateRule(cmd, args[0], acl.ACLRuleCreateRequest{ - Protocol: protocol, - CIDRList: cidrList, - StartPort: startPort, - EndPort: endPort, - TrafficType: trafficType, - Action: action, - Number: number, - ICMPCode: icmpCode, - ICMPType: icmpType, + return runACLCreate(cmd, args[0], acl.ACLCreateRequest{ + Name: name, + Description: description, + VPC: args[0], }) }, } - cmd.Flags().StringVar(&protocol, "protocol", "", "Protocol (tcp, udp, icmp, all) (required)") - cmd.Flags().StringVar(&cidrList, "cidr", "", "CIDR list (e.g. 0.0.0.0/0)") - cmd.Flags().IntVar(&startPort, "start-port", 0, "Start port") - cmd.Flags().IntVar(&endPort, "end-port", 0, "End port") - cmd.Flags().StringVar(&trafficType, "traffic-type", "", "Traffic type (ingress, egress)") - cmd.Flags().StringVar(&action, "action", "", "Action (allow, deny) (required)") - cmd.Flags().IntVar(&number, "number", 0, "Rule number (ordering)") - cmd.Flags().IntVar(&icmpCode, "icmp-code", 0, "ICMP code") - cmd.Flags().IntVar(&icmpType, "icmp-type", 0, "ICMP type") + cmd.Flags().StringVar(&name, "name", "", "ACL name (required)") + cmd.Flags().StringVar(&description, "description", "", "ACL description") return cmd } -func runACLCreateRule(cmd *cobra.Command, vpcSlug string, req acl.ACLRuleCreateRequest) error { - _, client, printer, err := buildClientAndPrinter(cmd) +func runACLCreate(cmd *cobra.Command, vpcSlug string, req acl.ACLCreateRequest) error { + _, client, _, err := buildClientAndPrinter(cmd) if err != nil { return err } @@ -115,23 +97,12 @@ func runACLCreateRule(cmd *cobra.Command, vpcSlug string, req acl.ACLRuleCreateR ctx, cancel := context.WithTimeout(context.Background(), time.Duration(getTimeout(cmd))*time.Second) defer cancel() - rule, err := svc.CreateRule(ctx, vpcSlug, req) - if err != nil { - return fmt.Errorf("acl create-rule: %w", err) + if err := svc.Create(ctx, vpcSlug, req); err != nil { + return fmt.Errorf("acl create: %w", err) } - headers := []string{"FIELD", "VALUE"} - rows := [][]string{ - {"Slug", rule.Slug}, - {"Protocol", rule.Protocol}, - {"Action", rule.Action}, - {"CIDR List", rule.CIDRList}, - {"Start Port", fmt.Sprintf("%d", rule.StartPort)}, - {"End Port", fmt.Sprintf("%d", rule.EndPort)}, - {"Traffic Type", rule.TrafficType}, - {"Number", fmt.Sprintf("%d", rule.Number)}, - } - return printer.PrintTable(headers, rows) + fmt.Fprintf(cmd.ErrOrStderr(), "ACL %q created in VPC %q.\n", req.Name, vpcSlug) + return nil } func newACLReplaceCmd() *cobra.Command { diff --git a/internal/commands/autoscale.go b/internal/commands/autoscale.go index dc15a7d..ecb1cc3 100644 --- a/internal/commands/autoscale.go +++ b/internal/commands/autoscale.go @@ -94,13 +94,16 @@ func newAutoscaleCreateCmd() *cobra.Command { cooldownPeriod int zoneSlug string networkSlug string + cloudProvider string + region string + project string ) cmd := &cobra.Command{ Use: "create", Short: "Create a new autoscale group", - Example: ` zcp autoscale create --name web-group --plan small --template ubuntu-22 --min 1 --max 5 --zone yow-1 - zcp autoscale create --name web-group --plan small --template ubuntu-22 --min 2 --max 10 --zone yow-1 --network default --cooldown 300`, + Example: ` zcp autoscale create --name web-group --plan small --template ubuntu-22 --min 1 --max 5 --zone yow-1 --cloud-provider --region --project + zcp autoscale create --name web-group --plan small --template ubuntu-22 --min 2 --max 10 --zone yow-1 --network default --cooldown 300 --cloud-provider --region --project `, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -114,6 +117,15 @@ func newAutoscaleCreateCmd() *cobra.Command { if zoneSlug == "" { return fmt.Errorf("--zone is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } if minInstances < 0 { return fmt.Errorf("--min must be >= 0") } @@ -129,6 +141,9 @@ func newAutoscaleCreateCmd() *cobra.Command { CooldownPeriod: cooldownPeriod, ZoneSlug: zoneSlug, NetworkSlug: networkSlug, + CloudProvider: cloudProvider, + Region: region, + Project: project, }) }, } @@ -140,6 +155,9 @@ func newAutoscaleCreateCmd() *cobra.Command { cmd.Flags().IntVar(&cooldownPeriod, "cooldown", 0, "Cooldown period in seconds between scaling actions") cmd.Flags().StringVar(&zoneSlug, "zone", "", "Zone slug (required)") cmd.Flags().StringVar(&networkSlug, "network", "", "Network slug") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") return cmd } diff --git a/internal/commands/dns.go b/internal/commands/dns.go index 2371321..c10fe5d 100644 --- a/internal/commands/dns.go +++ b/internal/commands/dns.go @@ -136,12 +136,13 @@ func runDNSShow(cmd *cobra.Command, slug string) error { func newDNSCreateCmd() *cobra.Command { var name, project, dnsProvider string + var cloudProvider, region string cmd := &cobra.Command{ Use: "create", Short: "Create a DNS domain", - Example: ` zcp dns create --name example.com --project default-60 --dns-provider powerdns - zcp dns create --name example.com --project default-60`, + Example: ` zcp dns create --name example.com --project default-60 --dns-provider powerdns --cloud-provider --region + zcp dns create --name example.com --project default-60 --cloud-provider --region `, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -149,19 +150,29 @@ func newDNSCreateCmd() *cobra.Command { if project == "" { return fmt.Errorf("--project is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } if dnsProvider == "" { dnsProvider = "powerdns" } return runDNSCreate(cmd, dns.CreateDomainRequest{ - Name: name, - Project: project, - DNSProvider: dnsProvider, + Name: name, + Project: project, + DNSProvider: dnsProvider, + CloudProvider: cloudProvider, + Region: region, }) }, } cmd.Flags().StringVar(&name, "name", "", "Domain name (required, e.g. example.com)") cmd.Flags().StringVar(&project, "project", "", "Project slug (required, e.g. default-60)") cmd.Flags().StringVar(&dnsProvider, "dns-provider", "powerdns", "DNS provider (default: powerdns)") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") return cmd } diff --git a/internal/commands/network.go b/internal/commands/network.go index c7ceb5b..cd8c342 100644 --- a/internal/commands/network.go +++ b/internal/commands/network.go @@ -68,12 +68,13 @@ func runNetworkList(cmd *cobra.Command) error { func newNetworkCreateCmd() *cobra.Command { var name, categorySlug, zoneSlug, gateway, netmask, description string + var cloudProvider, region, project string cmd := &cobra.Command{ Use: "create", Short: "Create a new isolated network", - Example: ` zcp network create --name my-net --category - zcp network create --name my-net --category --gateway 10.1.1.1 --netmask 255.255.255.0`, + Example: ` zcp network create --name my-net --category --cloud-provider --region --project + zcp network create --name my-net --category --gateway 10.1.1.1 --netmask 255.255.255.0 --cloud-provider --region --project `, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -81,13 +82,25 @@ func newNetworkCreateCmd() *cobra.Command { if categorySlug == "" { return fmt.Errorf("--category is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } return runNetworkCreate(cmd, network.CreateRequest{ - Name: name, - CategorySlug: categorySlug, - ZoneSlug: zoneSlug, - Gateway: gateway, - Netmask: netmask, - Description: description, + Name: name, + CategorySlug: categorySlug, + ZoneSlug: zoneSlug, + Gateway: gateway, + Netmask: netmask, + Description: description, + CloudProvider: cloudProvider, + Region: region, + Project: project, }) }, } @@ -97,6 +110,9 @@ func newNetworkCreateCmd() *cobra.Command { cmd.Flags().StringVar(&gateway, "gateway", "", "Gateway IP") cmd.Flags().StringVar(&netmask, "netmask", "", "Netmask (e.g. 255.255.255.0)") cmd.Flags().StringVar(&description, "description", "", "Network description") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") return cmd } diff --git a/internal/commands/virtualrouter.go b/internal/commands/virtualrouter.go index d4e40e8..9f74531 100644 --- a/internal/commands/virtualrouter.go +++ b/internal/commands/virtualrouter.go @@ -70,13 +70,14 @@ func runVirtualRouterList(cmd *cobra.Command) error { } func newVirtualRouterCreateCmd() *cobra.Command { - var name, networkSlug, planSlug, zoneSlug string + var name, networkSlug, planSlug string + var cloudProvider, region, project string cmd := &cobra.Command{ Use: "create", Short: "Create a virtual router", - Example: ` zcp virtual-router create --name my-router --network - zcp vr create --name my-router --network --plan `, + Example: ` zcp virtual-router create --name my-router --network --cloud-provider nimbo --region noida --project default-124 + zcp virtual-router create --name my-router --network --plan --cloud-provider nimbo --region noida --project default-124`, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -84,18 +85,31 @@ func newVirtualRouterCreateCmd() *cobra.Command { if networkSlug == "" { return fmt.Errorf("--network is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } return runVirtualRouterCreate(cmd, virtualrouter.CreateRequest{ - Name: name, - NetworkSlug: networkSlug, - PlanSlug: planSlug, - ZoneSlug: zoneSlug, + Name: name, + NetworkSlug: networkSlug, + PlanSlug: planSlug, + CloudProvider: cloudProvider, + Region: region, + Project: project, }) }, } cmd.Flags().StringVar(&name, "name", "", "Virtual router name (required)") cmd.Flags().StringVar(&networkSlug, "network", "", "Network slug (required)") cmd.Flags().StringVar(&planSlug, "plan", "", "Virtual router plan slug") - cmd.Flags().StringVar(&zoneSlug, "zone", "", "Zone slug") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") return cmd } diff --git a/internal/commands/volume.go b/internal/commands/volume.go index 886c3bf..b6a7d3c 100644 --- a/internal/commands/volume.go +++ b/internal/commands/volume.go @@ -2,13 +2,26 @@ package commands import ( "context" + "encoding/json" "fmt" + "strings" "time" "github.com/spf13/cobra" "github.com/zsoftly/zcp-cli/internal/api/volume" ) +// formatSize normalizes a json.Number size into a clean display string. +func formatSize(size json.Number) string { + s := size.String() + if s == "" { + return "-" + } + // Strip trailing .0 for whole numbers (e.g. "50.0" -> "50") + s = strings.TrimSuffix(s, ".0") + return s +} + // NewVolumeCmd returns the 'volume' cobra command. func NewVolumeCmd() *cobra.Command { cmd := &cobra.Command{ @@ -56,7 +69,7 @@ func newVolumeListCmd() *cobra.Command { rows = append(rows, []string{ v.Slug, v.Name, - v.Size, + formatSize(v.Size), v.VolumeType, regionName, storageName, @@ -131,7 +144,7 @@ func newVolumeCreateCmd() *cobra.Command { rows := [][]string{{ vol.Slug, vol.Name, - vol.Size, + formatSize(vol.Size), vol.VolumeType, vol.CreatedAt, }} @@ -182,7 +195,7 @@ func newVolumeAttachCmd() *cobra.Command { rows := [][]string{{ vol.Slug, vol.Name, - vol.Size, + formatSize(vol.Size), vol.VirtualMachineID, }} return printer.PrintTable(headers, rows) @@ -217,7 +230,7 @@ func newVolumeDetachCmd() *cobra.Command { rows := [][]string{{ vol.Slug, vol.Name, - vol.Size, + formatSize(vol.Size), }} return printer.PrintTable(headers, rows) }, diff --git a/internal/commands/vpc.go b/internal/commands/vpc.go index c138673..5259eed 100644 --- a/internal/commands/vpc.go +++ b/internal/commands/vpc.go @@ -119,47 +119,68 @@ func runVPCGet(cmd *cobra.Command, slug string) error { } func newVPCCreateCmd() *cobra.Command { - var zoneSlug, name, offeringSlug, cidr, description, networkDomain, lbProvider string + var name, cloudProvider, region, project, billingCycle, cidr, size, plan, storageCategory, description, coupon string cmd := &cobra.Command{ Use: "create", Short: "Create a new VPC", - Example: ` zcp vpc create --zone --name my-vpc --offering --cidr 10.0.0.0/8 - zcp vpc create --zone --name my-vpc --offering --cidr 10.0.0.0/8 --description "Production VPC"`, + Example: ` zcp vpc create --name my-vpc --cloud-provider nimbo --region noida --project default-124 --plan vpc-1 --network-address 10.1.0.1 --size 16 --billing-cycle hourly --storage-category nvme + zcp vpc create --name my-vpc --cloud-provider nimbo --region noida --project default-124 --plan vpc-1 --network-address 10.1.0.1 --size 16 --billing-cycle hourly --storage-category nvme --description "Production VPC"`, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") } - if offeringSlug == "" { - return fmt.Errorf("--offering is required") + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } + if plan == "" { + return fmt.Errorf("--plan is required (see: zcp plan router)") } if cidr == "" { - return fmt.Errorf("--cidr is required") + return fmt.Errorf("--network-address is required (e.g. 10.1.0.1 — not CIDR notation)") } - if !strings.Contains(cidr, "/") { - return fmt.Errorf("--cidr must be a valid CIDR (e.g. 10.0.0.0/8)") + if size == "" { + return fmt.Errorf("--size is required (subnet mask size, e.g. 24)") } - if zoneSlug == "" { - return fmt.Errorf("--zone is required") + if billingCycle == "" { + return fmt.Errorf("--billing-cycle is required") + } + if storageCategory == "" { + return fmt.Errorf("--storage-category is required") } return runVPCCreate(cmd, vpc.CreateRequest{ - Name: name, - ZoneSlug: zoneSlug, - VPCOfferingSlug: offeringSlug, - CIDR: cidr, - Description: description, - NetworkDomain: networkDomain, - PublicLoadBalancerProvider: lbProvider, + Name: name, + CloudProvider: cloudProvider, + Region: region, + Project: project, + Type: "Vpc", // Only valid value for VPC creation + BillingCycle: billingCycle, + CIDR: cidr, + Size: size, + Plan: plan, + StorageCategory: storageCategory, + Description: description, + Coupon: coupon, }) }, } - cmd.Flags().StringVar(&zoneSlug, "zone", "", "Zone slug (required)") cmd.Flags().StringVar(&name, "name", "", "VPC name (required)") - cmd.Flags().StringVar(&offeringSlug, "offering", "", "VPC offering slug (required)") - cmd.Flags().StringVar(&cidr, "cidr", "", "CIDR block (required, e.g. 10.0.0.0/8)") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") + cmd.Flags().StringVar(&plan, "plan", "", "Plan slug (required, see: zcp plan router)") + cmd.Flags().StringVar(&cidr, "network-address", "", "Network address (required, e.g. 10.1.0.1 — not CIDR notation)") + cmd.Flags().StringVar(&size, "size", "", "Subnet mask size (required, e.g. 24)") + cmd.Flags().StringVar(&billingCycle, "billing-cycle", "", "Billing cycle: hourly, monthly (required)") + cmd.Flags().StringVar(&storageCategory, "storage-category", "", "Storage category slug (required)") cmd.Flags().StringVar(&description, "description", "", "VPC description") - cmd.Flags().StringVar(&networkDomain, "network-domain", "", "Network domain") - cmd.Flags().StringVar(&lbProvider, "lb-provider", "", "Public load balancer provider") + cmd.Flags().StringVar(&coupon, "coupon", "", "Coupon code (optional)") return cmd } @@ -372,49 +393,31 @@ func runVPCACLList(cmd *cobra.Command, vpcSlug string) error { } func newVPCACLCreateRuleCmd() *cobra.Command { - var protocol, cidrList, trafficType, action string - var startPort, endPort, number, icmpCode, icmpType int + var name, description string cmd := &cobra.Command{ - Use: "acl-create-rule ", - Short: "Create a network ACL rule in a VPC", - Args: cobra.ExactArgs(1), - Example: ` zcp vpc acl-create-rule --protocol tcp --action allow --start-port 80 --end-port 80 --cidr 0.0.0.0/0 - zcp vpc acl-create-rule --protocol icmp --action deny --icmp-type 8 --icmp-code 0`, + Use: "acl-create ", + Short: "Create a network ACL list in a VPC", + Args: cobra.ExactArgs(1), + Example: ` zcp vpc acl-create my-vpc --name allow-web --description "Allow HTTP traffic"`, RunE: func(cmd *cobra.Command, args []string) error { - if protocol == "" { - return fmt.Errorf("--protocol is required") - } - if action == "" { - return fmt.Errorf("--action is required") + if name == "" { + return fmt.Errorf("--name is required") } - return runVPCACLCreateRule(cmd, args[0], vpc.ACLRuleCreateRequest{ - Protocol: protocol, - CIDRList: cidrList, - StartPort: startPort, - EndPort: endPort, - TrafficType: trafficType, - Action: action, - Number: number, - ICMPCode: icmpCode, - ICMPType: icmpType, + return runVPCACLCreate(cmd, args[0], vpc.ACLListCreateRequest{ + Name: name, + Description: description, + VPC: args[0], }) }, } - cmd.Flags().StringVar(&protocol, "protocol", "", "Protocol (tcp, udp, icmp, all) (required)") - cmd.Flags().StringVar(&cidrList, "cidr", "", "CIDR list (e.g. 0.0.0.0/0)") - cmd.Flags().IntVar(&startPort, "start-port", 0, "Start port") - cmd.Flags().IntVar(&endPort, "end-port", 0, "End port") - cmd.Flags().StringVar(&trafficType, "traffic-type", "", "Traffic type (ingress, egress)") - cmd.Flags().StringVar(&action, "action", "", "Action (allow, deny) (required)") - cmd.Flags().IntVar(&number, "number", 0, "Rule number (ordering)") - cmd.Flags().IntVar(&icmpCode, "icmp-code", 0, "ICMP code") - cmd.Flags().IntVar(&icmpType, "icmp-type", 0, "ICMP type") + cmd.Flags().StringVar(&name, "name", "", "ACL name (required)") + cmd.Flags().StringVar(&description, "description", "", "ACL description") return cmd } -func runVPCACLCreateRule(cmd *cobra.Command, vpcSlug string, req vpc.ACLRuleCreateRequest) error { - _, client, printer, err := buildClientAndPrinter(cmd) +func runVPCACLCreate(cmd *cobra.Command, vpcSlug string, req vpc.ACLListCreateRequest) error { + _, client, _, err := buildClientAndPrinter(cmd) if err != nil { return err } @@ -423,23 +426,12 @@ func runVPCACLCreateRule(cmd *cobra.Command, vpcSlug string, req vpc.ACLRuleCrea ctx, cancel := context.WithTimeout(context.Background(), time.Duration(getTimeout(cmd))*time.Second) defer cancel() - rule, err := svc.CreateACLRule(ctx, vpcSlug, req) - if err != nil { - return fmt.Errorf("vpc acl-create-rule: %w", err) + if err := svc.CreateACL(ctx, vpcSlug, req); err != nil { + return fmt.Errorf("vpc acl-create: %w", err) } - headers := []string{"FIELD", "VALUE"} - rows := [][]string{ - {"Slug", rule.Slug}, - {"Protocol", rule.Protocol}, - {"Action", rule.Action}, - {"CIDR List", rule.CIDRList}, - {"Start Port", fmt.Sprintf("%d", rule.StartPort)}, - {"End Port", fmt.Sprintf("%d", rule.EndPort)}, - {"Traffic Type", rule.TrafficType}, - {"Number", fmt.Sprintf("%d", rule.Number)}, - } - return printer.PrintTable(headers, rows) + fmt.Fprintf(cmd.ErrOrStderr(), "ACL %q created in VPC %q.\n", req.Name, vpcSlug) + return nil } func newVPCACLReplaceCmd() *cobra.Command { diff --git a/internal/commands/vpn.go b/internal/commands/vpn.go index 9fee92a..1b21760 100644 --- a/internal/commands/vpn.go +++ b/internal/commands/vpn.go @@ -108,12 +108,13 @@ func newVPNCGCreateCmd() *cobra.Command { ikeEncryption, ikeHash, ikeVersion string espEncryption, espHash string forceEncap, splitConnection, dpd bool + cloudProvider, region, project string ) cmd := &cobra.Command{ Use: "create", Short: "Create a new VPN customer gateway", - Example: ` zcp vpn customer-gateway create --name remote-gw --gateway 203.0.113.1 --cidr 192.168.1.0/24 --psk mykey --ike-policy aes128-sha1-dh5 --esp-policy aes128-sha1`, + Example: ` zcp vpn customer-gateway create --name remote-gw --gateway 203.0.113.1 --cidr 192.168.1.0/24 --psk mykey --ike-policy aes128-sha1-dh5 --esp-policy aes128-sha1 --cloud-provider --region --project `, RunE: func(cmd *cobra.Command, args []string) error { if name == "" { return fmt.Errorf("--name is required") @@ -133,6 +134,15 @@ func newVPNCGCreateCmd() *cobra.Command { if espPolicy == "" { return fmt.Errorf("--esp-policy is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } return runVPNCGCreate(cmd, vpn.CustomerGatewayRequest{ Name: name, Gateway: gateway, @@ -150,12 +160,18 @@ func newVPNCGCreateCmd() *cobra.Command { ForceEncap: forceEncap, SplitConnection: splitConnection, DPD: dpd, + CloudProvider: cloudProvider, + Region: region, + Project: project, }) }, } addCustomerGatewayFlags(cmd, &name, &gateway, &cidr, &psk, &ikePolicy, &espPolicy, &ikeLifetime, &espLifetime, &ikeEncryption, &ikeHash, &ikeVersion, &espEncryption, &espHash, &forceEncap, &splitConnection, &dpd) + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") return cmd } @@ -357,16 +373,26 @@ func runVPNUserList(cmd *cobra.Command) error { func newVPNUserCreateCmd() *cobra.Command { var username, password string + var cloudProvider, region, project string cmd := &cobra.Command{ Use: "create", Short: "Create a new VPN user", - Example: ` zcp vpn user create --username alice - zcp vpn user create --username alice --password secret`, + Example: ` zcp vpn user create --username alice --cloud-provider --region --project + zcp vpn user create --username alice --password secret --cloud-provider --region --project `, RunE: func(cmd *cobra.Command, args []string) error { if username == "" { return fmt.Errorf("--username is required") } + if cloudProvider == "" { + return fmt.Errorf("--cloud-provider is required") + } + if region == "" { + return fmt.Errorf("--region is required") + } + if project == "" { + return fmt.Errorf("--project is required") + } if password == "" { // Prompt securely for password fmt.Fprint(os.Stderr, "Password: ") @@ -385,15 +411,24 @@ func newVPNUserCreateCmd() *cobra.Command { return fmt.Errorf("password cannot be empty") } } - return runVPNUserCreate(cmd, username, password) + return runVPNUserCreate(cmd, vpn.UserCreateRequest{ + Username: username, + Password: password, + CloudProvider: cloudProvider, + Region: region, + Project: project, + }) }, } cmd.Flags().StringVar(&username, "username", "", "VPN username (required)") cmd.Flags().StringVar(&password, "password", "", "VPN password (prompted if not provided)") + cmd.Flags().StringVar(&cloudProvider, "cloud-provider", "", "Cloud provider slug (required)") + cmd.Flags().StringVar(®ion, "region", "", "Region slug (required)") + cmd.Flags().StringVar(&project, "project", "", "Project slug (required)") return cmd } -func runVPNUserCreate(cmd *cobra.Command, username, password string) error { +func runVPNUserCreate(cmd *cobra.Command, req vpn.UserCreateRequest) error { _, client, printer, err := buildClientAndPrinter(cmd) if err != nil { return err @@ -403,7 +438,7 @@ func runVPNUserCreate(cmd *cobra.Command, username, password string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Duration(getTimeout(cmd))*time.Second) defer cancel() - u, err := svc.Create(ctx, username, password) + u, err := svc.Create(ctx, req) if err != nil { return fmt.Errorf("vpn user create: %w", err) }