From 5ea1ca498723dedb6f8cf8d86e1b9250a19c6867 Mon Sep 17 00:00:00 2001 From: olivier dubo Date: Thu, 9 Jul 2026 09:57:56 +0000 Subject: [PATCH] feat(cli):migrate cloud quota to API v2 Signed-off-by: olivier dubo --- doc/ovhcloud_cloud.md | 2 +- doc/ovhcloud_cloud_quota.md | 5 +- doc/ovhcloud_cloud_quota_edit.md | 42 + doc/ovhcloud_cloud_quota_get.md | 6 +- internal/assets/api-schemas/cloud_v2.json | 1209 +++++++++++++++++ internal/cmd/cloud_quota.go | 20 +- internal/cmd/cloud_quota_test.go | 126 ++ internal/services/cloud/cloud_quota.go | 42 +- .../cloud/parameter-samples/quota-edit.json | 14 + .../services/cloud/templates/cloud_quota.tmpl | 86 +- 10 files changed, 1493 insertions(+), 59 deletions(-) create mode 100644 doc/ovhcloud_cloud_quota_edit.md create mode 100644 internal/cmd/cloud_quota_test.go create mode 100644 internal/services/cloud/parameter-samples/quota-edit.json diff --git a/doc/ovhcloud_cloud.md b/doc/ovhcloud_cloud.md index 72336089..ca410344 100644 --- a/doc/ovhcloud_cloud.md +++ b/doc/ovhcloud_cloud.md @@ -43,7 +43,7 @@ Manage your projects and services in the Public Cloud universe (MKS, MPR, MRS, O * [ovhcloud cloud network](ovhcloud_cloud_network.md) - Manage networks in the given cloud project * [ovhcloud cloud operation](ovhcloud_cloud_operation.md) - List and get operations in the given cloud project * [ovhcloud cloud project](ovhcloud_cloud_project.md) - Retrieve information and manage your CloudProject services -* [ovhcloud cloud quota](ovhcloud_cloud_quota.md) - Check quotas in the given cloud project +* [ovhcloud cloud quota](ovhcloud_cloud_quota.md) - Manage quotas in the given cloud project * [ovhcloud cloud region](ovhcloud_cloud_region.md) - Check regions in the given cloud project * [ovhcloud cloud savings-plan](ovhcloud_cloud_savings-plan.md) - Manage savings plans for your cloud project * [ovhcloud cloud ssh-key](ovhcloud_cloud_ssh-key.md) - Manage SSH keys in the given cloud project diff --git a/doc/ovhcloud_cloud_quota.md b/doc/ovhcloud_cloud_quota.md index 906d2e3c..a607f1f3 100644 --- a/doc/ovhcloud_cloud_quota.md +++ b/doc/ovhcloud_cloud_quota.md @@ -1,6 +1,6 @@ ## ovhcloud cloud quota -Check quotas in the given cloud project +Manage quotas in the given cloud project ### Options @@ -31,5 +31,6 @@ Check quotas in the given cloud project ### SEE ALSO * [ovhcloud cloud](ovhcloud_cloud.md) - Manage your projects and services in the Public Cloud universe (MKS, MPR, MRS, Object Storage...) -* [ovhcloud cloud quota get](ovhcloud_cloud_quota_get.md) - Get quotas for a specific region +* [ovhcloud cloud quota edit](ovhcloud_cloud_quota_edit.md) - Update the project quota (target quota profile per region) +* [ovhcloud cloud quota get](ovhcloud_cloud_quota_get.md) - Get the project quota diff --git a/doc/ovhcloud_cloud_quota_edit.md b/doc/ovhcloud_cloud_quota_edit.md new file mode 100644 index 00000000..f1400cba --- /dev/null +++ b/doc/ovhcloud_cloud_quota_edit.md @@ -0,0 +1,42 @@ +## ovhcloud cloud quota edit + +Update the project quota (target quota profile per region) + +``` +ovhcloud cloud quota edit [flags] +``` + +### Options + +``` + --editor Use a text editor to define parameters + --from-file string File containing parameters + -h, --help help for edit + --init-file string Create a file with example parameters + --replace Replace parameters file if it already exists +``` + +### Options inherited from parent commands + +``` + --cloud-project string Cloud project ID + -d, --debug Activate debug mode (will log all HTTP requests details) + -e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution + -o, --output string Output format: json, yaml, interactive, or a custom format expression (using https://github.com/PaesslerAG/gval syntax) + Examples: + --output json + --output yaml + --output interactive + --output 'id' (to extract a single field) + --output 'nested.field.subfield' (to extract a nested field) + --output '[id, "name"]' (to extract multiple fields as an array) + --output '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object) + --output 'name+","+type' (to extract and concatenate fields in a string) + --output '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields) + --profile string Use a specific profile from the configuration file +``` + +### SEE ALSO + +* [ovhcloud cloud quota](ovhcloud_cloud_quota.md) - Manage quotas in the given cloud project + diff --git a/doc/ovhcloud_cloud_quota_get.md b/doc/ovhcloud_cloud_quota_get.md index 81303cab..7576a3e6 100644 --- a/doc/ovhcloud_cloud_quota_get.md +++ b/doc/ovhcloud_cloud_quota_get.md @@ -1,9 +1,9 @@ ## ovhcloud cloud quota get -Get quotas for a specific region +Get the project quota ``` -ovhcloud cloud quota get [flags] +ovhcloud cloud quota get [flags] ``` ### Options @@ -34,5 +34,5 @@ ovhcloud cloud quota get [flags] ### SEE ALSO -* [ovhcloud cloud quota](ovhcloud_cloud_quota.md) - Check quotas in the given cloud project +* [ovhcloud cloud quota](ovhcloud_cloud_quota.md) - Manage quotas in the given cloud project diff --git a/internal/assets/api-schemas/cloud_v2.json b/internal/assets/api-schemas/cloud_v2.json index d03d70b5..e8405b0b 100644 --- a/internal/assets/api-schemas/cloud_v2.json +++ b/internal/assets/api-schemas/cloud_v2.json @@ -1157,6 +1157,972 @@ "description": "Time (e.g., 15:04:05)", "format": "time", "example": "15:04:05" + }, + "publicCloud.quota.QuotaProfileShare": { + "type": "object", + "description": "File storage limits for a quota profile", + "properties": { + "backupSizeTotal": { + "type": "integer", + "description": "Maximum allowed total backup storage size in GB", + "readOnly": true + }, + "backups": { + "type": "integer", + "description": "Maximum allowed share backups", + "readOnly": true + }, + "shares": { + "type": "integer", + "description": "Maximum allowed shares", + "readOnly": true + }, + "sizeTotal": { + "type": "integer", + "description": "Maximum allowed total share storage size in GB", + "readOnly": true + }, + "snapshots": { + "type": "integer", + "description": "Maximum allowed share snapshots", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaUnitEnum": { + "type": "string", + "description": "Unit of measurement for quota limits and usage", + "enum": [ + "COUNT", + "GB", + "GiB", + "MB", + "MiB" + ] + }, + "publicCloud.quota.QuotaProfileNetwork": { + "type": "object", + "description": "Network limits for a quota profile", + "properties": { + "floatingIps": { + "type": "integer", + "description": "Maximum allowed floating IPs", + "readOnly": true + }, + "gateways": { + "type": "integer", + "description": "Maximum allowed gateways", + "readOnly": true + }, + "networks": { + "type": "integer", + "description": "Maximum allowed networks", + "readOnly": true + }, + "securityGroupRules": { + "type": "integer", + "description": "Maximum allowed security group rules", + "readOnly": true + }, + "securityGroups": { + "type": "integer", + "description": "Maximum allowed security groups", + "readOnly": true + }, + "subnets": { + "type": "integer", + "description": "Maximum allowed subnets", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionCurrentState": { + "type": "object", + "description": "Current state of the project quota in a single region", + "properties": { + "location": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.common.Location" + } + ], + "description": "Region where the quota applies", + "readOnly": true + }, + "profile": { + "type": "string", + "description": "Currently applied quota profile name", + "readOnly": true + }, + "usage": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsageDetails" + } + ], + "description": "Quota usage for this region", + "nullable": true, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaUpdateTargetSpec": { + "type": "object", + "description": "Update specification for the project quota", + "properties": { + "preventAutomaticQuotaUpgrade": { + "type": "boolean", + "description": "When true, automatic quota upgrades are disabled for this project" + }, + "regions": { + "type": "array", + "description": "Target quota profile per region. Regions omitted are left unchanged.", + "items": { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionTargetSpec" + } + } + }, + "required": [ + "preventAutomaticQuotaUpgrade", + "regions" + ] + }, + "publicCloud.quota.QuotaProfileCompute": { + "type": "object", + "description": "Compute limits for a quota profile", + "properties": { + "cores": { + "type": "integer", + "description": "Maximum allowed CPU cores", + "readOnly": true + }, + "instances": { + "type": "integer", + "description": "Maximum allowed instances", + "readOnly": true + }, + "memory": { + "type": "integer", + "description": "Maximum allowed memory in MB", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaUsage": { + "type": "object", + "description": "Quota usage with limit and current consumption", + "properties": { + "limit": { + "type": "integer", + "description": "Maximum allowed", + "readOnly": true + }, + "unit": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUnitEnum" + } + ], + "description": "Unit of measurement", + "readOnly": true + }, + "used": { + "type": "integer", + "description": "Currently used (may be omitted when not available)", + "nullable": true, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionKeyManager": { + "type": "object", + "description": "Per-region key manager quota usage", + "properties": { + "containers": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Containers quota usage", + "readOnly": true + }, + "secrets": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Secrets quota usage", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaTargetSpec": { + "type": "object", + "description": "Target specification for the project quota", + "properties": { + "preventAutomaticQuotaUpgrade": { + "type": "boolean", + "description": "When true, automatic quota upgrades are disabled for this project", + "readOnly": true + }, + "regions": { + "type": "array", + "description": "Target quota profile per region", + "items": { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionTargetSpec" + }, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaProfileKeypair": { + "type": "object", + "description": "Keypair limits for a quota profile", + "properties": { + "keypairs": { + "type": "integer", + "description": "Maximum allowed keypairs", + "readOnly": true + } + } + }, + "publicCloud.common.Location": { + "type": "object", + "description": "Resource location", + "properties": { + "availabilityZone": { + "type": "string", + "description": "Availability zone within the region", + "nullable": true + }, + "region": { + "type": "string", + "description": "Region code" + } + }, + "required": [ + "region" + ] + }, + "publicCloud.quota.QuotaRegionVolume": { + "type": "object", + "description": "Per-region volume quota usage", + "properties": { + "backupSizeTotal": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Volume backup storage quota usage", + "readOnly": true + }, + "backups": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Volume backups quota usage", + "readOnly": true + }, + "perVolumeSize": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaLimit" + } + ], + "description": "Maximum size per individual volume", + "readOnly": true + }, + "sizeTotal": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Total volume storage quota usage", + "readOnly": true + }, + "snapshots": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Volume snapshots quota usage", + "readOnly": true + }, + "volumes": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Volumes quota usage", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaProfileVolume": { + "type": "object", + "description": "Block storage limits for a quota profile", + "properties": { + "backupSizeTotal": { + "type": "integer", + "description": "Maximum allowed total backup storage size in GB", + "readOnly": true + }, + "backups": { + "type": "integer", + "description": "Maximum allowed volume backups", + "readOnly": true + }, + "sizeTotal": { + "type": "integer", + "description": "Maximum allowed total volume storage size in GB", + "readOnly": true + }, + "snapshots": { + "type": "integer", + "description": "Maximum allowed volume snapshots", + "readOnly": true + }, + "volumes": { + "type": "integer", + "description": "Maximum allowed volumes", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaCurrentState": { + "type": "object", + "description": "Current state of the project quota", + "properties": { + "availableProfiles": { + "type": "array", + "description": "Quota profiles available to this project", + "nullable": true, + "items": { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfile" + }, + "readOnly": true + }, + "preventAutomaticQuotaUpgrade": { + "type": "boolean", + "description": "When true, automatic quota upgrades are disabled for this project", + "readOnly": true + }, + "regions": { + "type": "array", + "description": "Current state per region", + "nullable": true, + "items": { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionCurrentState" + }, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionLoadbalancer": { + "type": "object", + "description": "Per-region load balancer quota usage", + "properties": { + "healthMonitors": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Health monitors quota usage", + "readOnly": true + }, + "l7Policies": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "L7 policies quota usage", + "readOnly": true + }, + "l7Rules": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "L7 rules quota usage", + "readOnly": true + }, + "listeners": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Listeners quota usage", + "readOnly": true + }, + "loadbalancers": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Load balancers quota usage", + "readOnly": true + }, + "members": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Pool members quota usage", + "readOnly": true + }, + "pools": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Pools quota usage", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaUsageDetails": { + "type": "object", + "description": "Quota usage across all services for a single region", + "properties": { + "compute": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionCompute" + } + ], + "description": "Compute quota usage", + "nullable": true, + "readOnly": true + }, + "keyManager": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionKeyManager" + } + ], + "description": "Key manager quota usage", + "nullable": true, + "readOnly": true + }, + "keypair": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionKeypair" + } + ], + "description": "Keypair quota usage", + "nullable": true, + "readOnly": true + }, + "loadbalancer": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionLoadbalancer" + } + ], + "description": "Load balancer quota usage", + "nullable": true, + "readOnly": true + }, + "network": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionNetwork" + } + ], + "description": "Network quota usage", + "nullable": true, + "readOnly": true + }, + "share": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionShare" + } + ], + "description": "File storage quota usage", + "nullable": true, + "readOnly": true + }, + "volume": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaRegionVolume" + } + ], + "description": "Block storage quota usage", + "nullable": true, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaProfileLoadbalancer": { + "type": "object", + "description": "Loadbalancer limits for a quota profile", + "properties": { + "healthMonitors": { + "type": "integer", + "description": "Maximum allowed health monitors", + "readOnly": true + }, + "l7Policies": { + "type": "integer", + "description": "Maximum allowed L7 policies", + "readOnly": true + }, + "l7Rules": { + "type": "integer", + "description": "Maximum allowed L7 rules", + "readOnly": true + }, + "listeners": { + "type": "integer", + "description": "Maximum allowed listeners", + "readOnly": true + }, + "loadbalancers": { + "type": "integer", + "description": "Maximum allowed loadbalancers", + "readOnly": true + }, + "members": { + "type": "integer", + "description": "Maximum allowed pool members", + "readOnly": true + }, + "pools": { + "type": "integer", + "description": "Maximum allowed pools", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaLimit": { + "type": "object", + "description": "Quota limit without usage tracking (for per-item size caps)", + "properties": { + "limit": { + "type": "integer", + "description": "Maximum allowed", + "readOnly": true + }, + "unit": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUnitEnum" + } + ], + "description": "Unit of measurement", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionKeypair": { + "type": "object", + "description": "Per-region keypair quota", + "properties": { + "keypairs": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Keypairs quota", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionNetwork": { + "type": "object", + "description": "Per-region network quota usage", + "properties": { + "floatingIps": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Floating IPs quota usage", + "readOnly": true + }, + "gateways": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Gateways quota usage", + "readOnly": true + }, + "networks": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Networks quota usage", + "readOnly": true + }, + "securityGroupRules": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Security group rules quota usage", + "readOnly": true + }, + "securityGroups": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Security groups quota usage", + "readOnly": true + }, + "subnets": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Subnets quota usage", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionCompute": { + "type": "object", + "description": "Per-region compute quota usage", + "properties": { + "cores": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "CPU cores quota usage", + "readOnly": true + }, + "instances": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Instances quota usage", + "readOnly": true + }, + "memory": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Memory quota usage in MB", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionTargetSpec": { + "type": "object", + "description": "Target specification for the project quota in a single region", + "properties": { + "location": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.common.Location" + } + ], + "description": "Region where the profile applies" + }, + "profile": { + "type": "string", + "description": "Quota profile to apply in this region. Available values are in currentState.availableProfiles." + } + }, + "required": [ + "location", + "profile" + ] + }, + "publicCloud.quota.QuotaUpdate": { + "type": "object", + "description": "Payload to update a Public Cloud project quota", + "properties": { + "checksum": { + "type": "string", + "description": "Checksum of the current resource, for optimistic concurrency" + }, + "targetSpec": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUpdateTargetSpec" + } + ], + "description": "Update specification" + } + }, + "required": [ + "checksum", + "targetSpec" + ] + }, + "publicCloud.quota.QuotaProfile": { + "type": "object", + "description": "A Public Cloud quota profile (read-only reference data)", + "properties": { + "compute": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileCompute" + } + ], + "description": "Compute limits", + "nullable": true, + "readOnly": true + }, + "keyManager": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileKeyManager" + } + ], + "description": "Key manager limits", + "nullable": true, + "readOnly": true + }, + "keypair": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileKeypair" + } + ], + "description": "Keypair limits", + "nullable": true, + "readOnly": true + }, + "loadbalancer": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileLoadbalancer" + } + ], + "description": "Loadbalancer limits", + "nullable": true, + "readOnly": true + }, + "name": { + "type": "string", + "description": "Profile name", + "readOnly": true + }, + "network": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileNetwork" + } + ], + "description": "Network limits", + "nullable": true, + "readOnly": true + }, + "share": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileShare" + } + ], + "description": "File storage limits", + "nullable": true, + "readOnly": true + }, + "volume": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaProfileVolume" + } + ], + "description": "Block storage limits", + "nullable": true, + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaProfileKeyManager": { + "type": "object", + "description": "Key manager limits for a quota profile", + "properties": { + "containers": { + "type": "integer", + "description": "Maximum allowed containers", + "readOnly": true + }, + "secrets": { + "type": "integer", + "description": "Maximum allowed secrets", + "readOnly": true + } + } + }, + "publicCloud.quota.QuotaRegionShare": { + "type": "object", + "description": "Per-region file storage quota usage", + "properties": { + "backupSizeTotal": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "File storage backup storage quota usage", + "readOnly": true + }, + "backups": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "File storage backups quota usage", + "readOnly": true + }, + "perShareSize": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaLimit" + } + ], + "description": "Maximum size per individual share", + "readOnly": true + }, + "shareNetworks": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Share networks quota usage", + "readOnly": true + }, + "shares": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "File storages quota usage", + "readOnly": true + }, + "sizeTotal": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "Total file storage size quota usage in GB", + "readOnly": true + }, + "snapshotSizeTotal": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "File storage snapshot storage quota usage", + "readOnly": true + }, + "snapshots": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUsage" + } + ], + "description": "File storage snapshots quota usage", + "readOnly": true + } + } + }, + "publicCloud.quota.Quota": { + "type": "object", + "description": "A Public Cloud project quota", + "properties": { + "checksum": { + "type": "string", + "description": "Computed hash representing the current target specification value", + "readOnly": true + }, + "createdAt": { + "type": "string", + "description": "Creation date of the quota", + "format": "date-time", + "readOnly": true + }, + "currentState": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaCurrentState" + } + ], + "description": "Current state of the quota", + "nullable": true, + "readOnly": true + }, + "currentTasks": { + "type": "array", + "description": "Ongoing asynchronous tasks related to the quota", + "nullable": true, + "items": { + "$ref": "#/components/schemas/common.CurrentTask" + }, + "readOnly": true + }, + "id": { + "type": "string", + "description": "Project ID this quota belongs to", + "readOnly": true + }, + "resourceStatus": { + "allOf": [ + { + "$ref": "#/components/schemas/common.ResourceStatusEnum" + } + ], + "description": "Quota readiness in the system", + "readOnly": true + }, + "targetSpec": { + "allOf": [ + { + "$ref": "#/components/schemas/publicCloud.quota.QuotaTargetSpec" + } + ], + "description": "Last target specification of the quota", + "readOnly": true + }, + "updatedAt": { + "type": "string", + "description": "Last update date of the quota", + "format": "date-time", + "readOnly": true + } + } } }, "securitySchemes": { @@ -3192,6 +4158,249 @@ } ] } + }, + "/publicCloud/project/{projectId}/quota": { + "get": { + "summary": "Get the project quota", + "description": "Get the project quota. When the `region` query parameter is set, the response narrows `targetSpec.regions` and `currentState.regions` to that single region. Omit it to get all regions.", + "security": [ + { + "oAuth2AuthCode": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "projectId", + "description": "Project ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "region", + "description": "Optional region filter", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/publicCloud.quota.Quota" + } + } + } + }, + "400": { + "description": "Error 400 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Common error model", + "properties": { + "class": { + "type": "string", + "description": "Class of the error" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + }, + "examples": { + "Client::BadRequest::ServiceNotAvailableInRegion": { + "value": { + "class": "Client::BadRequest::ServiceNotAvailableInRegion", + "message": "This service is not available in the selected region" + } + } + } + } + } + }, + "409": { + "description": "Error 409 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Common error model", + "properties": { + "class": { + "type": "string", + "description": "Class of the error" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + }, + "examples": { + "Client::Conflict::ResourceInInvalidState": { + "value": { + "class": "Client::Conflict::ResourceInInvalidState", + "message": "Resource cannot be modified in its current state" + } + } + } + } + } + } + }, + "tags": [ + "Quota" + ], + "x-badges": [ + { + "color": "blue", + "label": "Alpha version" + } + ], + "x-iam-actions": [ + { + "name": "publicCloudProject:apiovh:quota/get", + "required": true + } + ], + "x-response-identifier": "id" + }, + "put": { + "summary": "Update the project quota", + "description": "Update the project quota. Regions omitted from `targetSpec.regions` are left unchanged. `preventAutomaticQuotaUpgrade` is required.", + "security": [ + { + "oAuth2AuthCode": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "projectId", + "description": "Project ID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/publicCloud.quota.QuotaUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/publicCloud.quota.Quota" + } + } + } + }, + "400": { + "description": "Error 400 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Common error model", + "properties": { + "class": { + "type": "string", + "description": "Class of the error" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + }, + "examples": { + "Client::BadRequest::InvalidParameter": { + "value": { + "class": "Client::BadRequest::InvalidParameter", + "message": "Invalid parameter in the request" + } + }, + "Client::BadRequest::ServiceNotAvailableInRegion": { + "value": { + "class": "Client::BadRequest::ServiceNotAvailableInRegion", + "message": "This service is not available in the selected region" + } + } + } + } + } + }, + "409": { + "description": "Error 409 response", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Common error model", + "properties": { + "class": { + "type": "string", + "description": "Class of the error" + }, + "message": { + "type": "string", + "description": "Error message" + } + } + }, + "examples": { + "Client::Conflict::ChecksumMismatch": { + "value": { + "class": "Client::Conflict::ChecksumMismatch", + "message": "Checksum mismatch — a concurrent update was detected. Refresh the resource and retry" + } + }, + "Client::Conflict::ResourceInInvalidState": { + "value": { + "class": "Client::Conflict::ResourceInInvalidState", + "message": "Resource cannot be modified in its current state" + } + } + } + } + } + } + }, + "tags": [ + "Quota" + ], + "x-badges": [ + { + "color": "blue", + "label": "Alpha version" + } + ], + "x-iam-actions": [ + { + "name": "publicCloudProject:apiovh:quota/edit", + "required": true + } + ] + } } }, "tags": [ diff --git a/internal/cmd/cloud_quota.go b/internal/cmd/cloud_quota.go index d33c1dc2..d80883cc 100644 --- a/internal/cmd/cloud_quota.go +++ b/internal/cmd/cloud_quota.go @@ -5,6 +5,7 @@ package cmd import ( + "github.com/ovh/ovhcloud-cli/internal/assets" "github.com/ovh/ovhcloud-cli/internal/services/cloud" "github.com/spf13/cobra" ) @@ -12,16 +13,27 @@ import ( func initCloudQuotaCommand(cloudCmd *cobra.Command) { quotaCmd := &cobra.Command{ Use: "quota", - Short: "Check quotas in the given cloud project", + Short: "Manage quotas in the given cloud project", } quotaCmd.PersistentFlags().StringVar(&cloud.CloudProject, "cloud-project", "", "Cloud project ID") quotaCmd.AddCommand(&cobra.Command{ - Use: "get ", - Short: "Get quotas for a specific region", + Use: "get", + Short: "Get the project quota", Run: cloud.GetCloudQuota, - Args: cobra.ExactArgs(1), + Args: cobra.NoArgs, }) + quotaEditCmd := &cobra.Command{ + Use: "edit", + Short: "Update the project quota (target quota profile per region)", + Run: cloud.EditCloudQuota, + Args: cobra.NoArgs, + } + addParameterFileFlags(quotaEditCmd, false, assets.CloudV2OpenapiSchema, "/publicCloud/project/{projectId}/quota", "put", cloud.CloudQuotaEditExample, nil) + addInteractiveEditorFlag(quotaEditCmd) + markFlagsMutuallyExclusive(quotaEditCmd, "from-file", "editor") + quotaCmd.AddCommand(quotaEditCmd) + cloudCmd.AddCommand(quotaCmd) } diff --git a/internal/cmd/cloud_quota_test.go b/internal/cmd/cloud_quota_test.go new file mode 100644 index 00000000..5bc0c26b --- /dev/null +++ b/internal/cmd/cloud_quota_test.go @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2025 OVH SAS +// +// SPDX-License-Identifier: Apache-2.0 + +package cmd_test + +import ( + "encoding/json" + "net/http" + "os" + + "github.com/jarcoal/httpmock" + "github.com/maxatome/go-testdeep/td" + "github.com/maxatome/tdhttpmock" + "github.com/ovh/ovhcloud-cli/internal/cmd" +) + +func (ms *MockSuite) TestCloudQuotaGetCmd(assert, require *td.T) { + httpmock.RegisterResponder(http.MethodGet, + "https://eu.api.ovh.com/v2/publicCloud/project/fakeProjectID/quota", + httpmock.NewStringResponder(200, `{ + "id": "fakeProjectID", + "checksum": "abc123", + "resourceStatus": "READY", + "currentState": { + "preventAutomaticQuotaUpgrade": false, + "regions": [ + { + "location": {"region": "GRA11"}, + "profile": "default", + "usage": { + "compute": { + "instances": {"limit": 20, "unit": "COUNT", "used": 2}, + "cores": {"limit": 100, "unit": "COUNT", "used": 8}, + "memory": {"limit": 256000, "unit": "MB", "used": 16000} + } + } + } + ] + }, + "targetSpec": { + "preventAutomaticQuotaUpgrade": false, + "regions": [ + {"location": {"region": "GRA11"}, "profile": "default"} + ] + } + }`)) + + out, err := cmd.Execute("cloud", "quota", "get", "--cloud-project", "fakeProjectID", "-o", "json") + require.CmpNoError(err) + assert.Cmp(json.RawMessage(out), td.JSON(`{ + "id": "fakeProjectID", + "checksum": "abc123", + "resourceStatus": "READY", + "currentState": { + "preventAutomaticQuotaUpgrade": false, + "regions": [ + { + "location": {"region": "GRA11"}, + "profile": "default", + "usage": { + "compute": { + "instances": {"limit": 20, "unit": "COUNT", "used": 2}, + "cores": {"limit": 100, "unit": "COUNT", "used": 8}, + "memory": {"limit": 256000, "unit": "MB", "used": 16000} + } + } + } + ] + }, + "targetSpec": { + "preventAutomaticQuotaUpgrade": false, + "regions": [ + {"location": {"region": "GRA11"}, "profile": "default"} + ] + } + }`)) +} + +func (ms *MockSuite) TestCloudQuotaEditFromFileCmd(assert, require *td.T) { + // GET the current quota (without regions so the file-provided regions are + // not appended to an existing slice by the merge logic). + httpmock.RegisterResponder(http.MethodGet, + "https://eu.api.ovh.com/v2/publicCloud/project/fakeProjectID/quota", + httpmock.NewStringResponder(200, `{ + "id": "fakeProjectID", + "checksum": "abc123", + "resourceStatus": "READY", + "targetSpec": { + "preventAutomaticQuotaUpgrade": false + } + }`).Once()) + + // The PUT body is the editable subset of the merged resource. + httpmock.RegisterMatcherResponder(http.MethodPut, + "https://eu.api.ovh.com/v2/publicCloud/project/fakeProjectID/quota", + tdhttpmock.JSONBody(td.JSON(`{ + "checksum": "abc123", + "targetSpec": { + "preventAutomaticQuotaUpgrade": true, + "regions": [ + {"location": {"region": "GRA11"}, "profile": "large"} + ] + } + }`)), + httpmock.NewStringResponder(200, ``).Once()) + + tmpFile, err := os.CreateTemp("", "quota-edit-*.json") + require.CmpNoError(err) + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(`{ + "targetSpec": { + "preventAutomaticQuotaUpgrade": true, + "regions": [ + {"location": {"region": "GRA11"}, "profile": "large"} + ] + } + }`) + require.CmpNoError(err) + tmpFile.Close() + + out, err := cmd.Execute("cloud", "quota", "edit", "--cloud-project", "fakeProjectID", "--from-file", tmpFile.Name()) + require.CmpNoError(err) + assert.String(out, `✅ Resource updated successfully`) +} diff --git a/internal/services/cloud/cloud_quota.go b/internal/services/cloud/cloud_quota.go index db989cc4..5037558c 100644 --- a/internal/services/cloud/cloud_quota.go +++ b/internal/services/cloud/cloud_quota.go @@ -7,32 +7,62 @@ package cloud import ( _ "embed" "fmt" - "net/url" + "github.com/ovh/ovhcloud-cli/internal/assets" "github.com/ovh/ovhcloud-cli/internal/display" "github.com/ovh/ovhcloud-cli/internal/flags" httpLib "github.com/ovh/ovhcloud-cli/internal/http" + "github.com/ovh/ovhcloud-cli/internal/services/common" "github.com/spf13/cobra" ) var ( //go:embed templates/cloud_quota.tmpl cloudQuotaTemplate string + + //go:embed parameter-samples/quota-edit.json + CloudQuotaEditExample string + + QuotaEditSpec struct { + TargetSpec struct { + PreventAutomaticQuotaUpgrade *bool `json:"preventAutomaticQuotaUpgrade,omitempty"` + } `json:"targetSpec"` + } ) -func GetCloudQuota(_ *cobra.Command, args []string) { +func GetCloudQuota(_ *cobra.Command, _ []string) { projectID, err := getConfiguredCloudProject() if err != nil { display.OutputError(&flags.OutputFormatConfig, "%s", err) return } - url := fmt.Sprintf("/v1/cloud/project/%s/region/%s/quota", projectID, url.PathEscape(args[0])) + + endpoint := fmt.Sprintf("/v2/publicCloud/project/%s/quota", projectID) var object map[string]any - if err := httpLib.Client.Get(url, &object); err != nil { - display.OutputError(&flags.OutputFormatConfig, "error fetching quotas for region %s: %s", args[0], err) + if err := httpLib.Client.Get(endpoint, &object); err != nil { + display.OutputError(&flags.OutputFormatConfig, "error fetching quota for project %s: %s", projectID, err) return } - display.OutputObject(object, args[0], cloudQuotaTemplate, &flags.OutputFormatConfig) + display.OutputObject(object, projectID, cloudQuotaTemplate, &flags.OutputFormatConfig) +} + +func EditCloudQuota(cmd *cobra.Command, _ []string) { + projectID, err := getConfiguredCloudProject() + if err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + + if err := common.EditResource( + cmd, + "/publicCloud/project/{projectId}/quota", + fmt.Sprintf("/v2/publicCloud/project/%s/quota", projectID), + QuotaEditSpec, + assets.CloudV2OpenapiSchema, + ); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } } diff --git a/internal/services/cloud/parameter-samples/quota-edit.json b/internal/services/cloud/parameter-samples/quota-edit.json new file mode 100644 index 00000000..b26a726f --- /dev/null +++ b/internal/services/cloud/parameter-samples/quota-edit.json @@ -0,0 +1,14 @@ +{ + "checksum": "", + "targetSpec": { + "preventAutomaticQuotaUpgrade": false, + "regions": [ + { + "location": { + "region": "GRA11" + }, + "profile": "default" + } + ] + } +} diff --git a/internal/services/cloud/templates/cloud_quota.tmpl b/internal/services/cloud/templates/cloud_quota.tmpl index 929054ae..9c86075a 100644 --- a/internal/services/cloud/templates/cloud_quota.tmpl +++ b/internal/services/cloud/templates/cloud_quota.tmpl @@ -1,58 +1,58 @@ -🚀 Quotas for region {{.ServiceName}} +🚀 Quota for project {{.ServiceName}} ======= -{{if index .Result "instance"}} -## Instances +**Status**: {{.Result.resourceStatus}} +{{- with .Result.currentState}} +**Automatic quota upgrade disabled**: {{.preventAutomaticQuotaUpgrade}} +{{- range .regions}} -| Resource | Used / Available | -| --- | --- | -| Instances | {{index .Result "instance" "usedInstances"}} / {{index .Result "instance" "maxInstances"}} | -| Cores | {{index .Result "instance" "usedCores"}} / {{index .Result "instance" "maxCores"}} | -| RAM | {{index .Result "instance" "usedRAM"}} / {{index .Result "instance" "maxRAM"}} | -{{end}} - -{{if index .Result "volume"}} -## Volumes +## Region {{.location.region}} -| Resource | Used / Available | -| --- | --- | -| Volumes | {{index .Result "volume" "volumeCount"}} / {{index .Result "volume" "maxVolumeCount"}} | -| Volumes capacity | {{index .Result "volume" "usedGigabytes"}} / {{index .Result "volume" "maxGigabytes"}} GB | -| Backup volumes | {{index .Result "volume" "volumeBackupCount"}} / {{index .Result "volume" "maxVolumeBackupCount"}} | -| Backup volumes capacity | {{index .Result "volume" "usedBackupGigabytes"}} / {{index .Result "volume" "maxBackupGigabytes"}} GB | -{{end}} +**Profile**: {{.profile}} +{{- with .usage}} +{{- with .compute}} -{{if index .Result "network"}} -## Networks +### Compute -| Resource | Used / Available | +| Resource | Used / Limit | | --- | --- | -| Networks | {{index .Result "network" "usedNetworks"}} / {{index .Result "network" "maxNetworks"}} | -| Subnets | {{index .Result "network" "usedSubnets"}} / {{index .Result "network" "maxSubnets"}} | -| Gateways | {{index .Result "network" "usedGateways"}} / {{index .Result "network" "maxGateways"}} | -| Floating IPs | {{index .Result "network" "usedFloatingIPs"}} / {{index .Result "network" "maxFloatingIPs"}} | -{{end}} +| Instances | {{.instances.used}} / {{.instances.limit}} | +| Cores | {{.cores.used}} / {{.cores.limit}} | +| Memory | {{.memory.used}} / {{.memory.limit}} | +{{- end}} +{{- with .network}} -{{if index .Result "loadbalancer"}} -## Load balancers +### Network -| Resource | Used / Available | +| Resource | Used / Limit | | --- | --- | -| Load balancers | {{index .Result "loadbalancer" "usedLoadbalancers"}} / {{index .Result "loadbalancer" "maxLoadbalancers"}} | -{{end}} +| Networks | {{.networks.used}} / {{.networks.limit}} | +| Subnets | {{.subnets.used}} / {{.subnets.limit}} | +| Gateways | {{.gateways.used}} / {{.gateways.limit}} | +| Floating IPs | {{.floatingIps.used}} / {{.floatingIps.limit}} | +{{- end}} +{{- with .volume}} -{{if index .Result "keymanager"}} -## Key manager +### Block storage -| Resource | Used / Available | +| Resource | Used / Limit | | --- | --- | -| Secrets | {{index .Result "keymanager" "usedSecrets"}} / {{index .Result "keymanager" "maxSecrets"}} | -{{end}} +| Volumes | {{.volumes.used}} / {{.volumes.limit}} | +| Total size | {{.sizeTotal.used}} / {{.sizeTotal.limit}} | +| Snapshots | {{.snapshots.used}} / {{.snapshots.limit}} | +{{- end}} +{{- with .share}} -{{if index .Result "keypair"}} -## Keypair +### File storage -**Maximum allowed**: {{index .Result "keypair" "maxCount"}} -{{end}} - -💡 Use option -o json or -o yaml to get the raw output with all information \ No newline at end of file +| Resource | Used / Limit | +| --- | --- | +| Shares | {{.shares.used}} / {{.shares.limit}} | +| Total size | {{.sizeTotal.used}} / {{.sizeTotal.limit}} | +| Snapshots | {{.snapshots.used}} / {{.snapshots.limit}} | +{{- end}} +{{- end}} +{{- end}} +{{- end}} + +💡 Use option -o json or -o yaml to get the raw output with all information