From f58c4a8ca445e03e6fecb0a8c2c03335d9988521 Mon Sep 17 00:00:00 2001 From: Achraf Hammouda Date: Mon, 7 Sep 2026 16:36:33 +0200 Subject: [PATCH 1/2] feat(managed-database): add network update support to edit command Add --network-id and --subnet-id flags to the managed-database edit command, enabling in-place network switching between public and private networks. Usage: - Switch to private: --network-id --subnet-id - Switch to public: --network-id none --subnet-id none Network updates are sent as a separate PUT request to properly handle null values for the public network switch, since the OpenAPI spec filter in EditResource strips network fields. Signed-off-by: Achraf Hammouda --- doc/ovhcloud_cloud_managed-database_edit.md | 17 ++++++ internal/cmd/cloud_managed_database.go | 17 ++++++ .../services/cloud/cloud_managed_database.go | 61 ++++++++++++++++--- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/doc/ovhcloud_cloud_managed-database_edit.md b/doc/ovhcloud_cloud_managed-database_edit.md index 059efc890..0be2f2b20 100644 --- a/doc/ovhcloud_cloud_managed-database_edit.md +++ b/doc/ovhcloud_cloud_managed-database_edit.md @@ -20,6 +20,21 @@ There are two ways to define the edition parameters: ovhcloud cloud managed-database edit --editor --description "My database cluster" +Network update: + + You can switch a database service between public and private networks without recreating it. + + To switch from public to private network: + + ovhcloud cloud managed-database edit --network-id --subnet-id + + To switch from private to public network: + + ovhcloud cloud managed-database edit --network-id none --subnet-id none + + Note: Changing the network triggers a service rebuild. The service will be temporarily unavailable + during the transition. + ``` ovhcloud cloud managed-database edit [flags] @@ -38,7 +53,9 @@ ovhcloud cloud managed-database edit [flags] -h, --help help for edit --ip-restrictions strings IP blocks authorized to access the cluster (CIDR format) --maintenance-time string Time on which maintenances can start every day + --network-id string Private network ID (use "none" to switch to public network) --plan string Plan of the cluster + --subnet-id string Private subnet ID (use "none" to switch to public network) --version string Version of the engine deployed on the cluster ``` diff --git a/internal/cmd/cloud_managed_database.go b/internal/cmd/cloud_managed_database.go index fb14349b3..7358998a3 100644 --- a/internal/cmd/cloud_managed_database.go +++ b/internal/cmd/cloud_managed_database.go @@ -317,6 +317,21 @@ There are two ways to define the edition parameters: Note that it is also possible to override values in the presented examples using command line flags like the following: ovhcloud cloud managed-database edit --editor --description "My database cluster" + +Network update: + + You can switch a database service between public and private networks without recreating it. + + To switch from public to private network: + + ovhcloud cloud managed-database edit --network-id --subnet-id + + To switch from private to public network: + + ovhcloud cloud managed-database edit --network-id none --subnet-id none + + Note: Changing the network triggers a service rebuild. The service will be temporarily unavailable + during the transition. `, ValidArgsFunction: completion.CloudResources("/v1/cloud/project/%s/database"), Run: cloud.EditManagedDatabase, @@ -336,6 +351,8 @@ There are two ways to define the edition parameters: // Network configuration managedDatabaseEditCmd.Flags().StringSliceVar(&cloud.ManagedDatabaseSpec.CLIIPRestrictions, "ip-restrictions", nil, "IP blocks authorized to access the cluster (CIDR format)") + managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLINetworkID, "network-id", "", `Private network ID (use "none" to switch to public network)`) + managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLISubnetID, "subnet-id", "", `Private subnet ID (use "none" to switch to public network)`) // Common flags for other mean to define parameters addInteractiveEditorFlag(managedDatabaseEditCmd) diff --git a/internal/services/cloud/cloud_managed_database.go b/internal/services/cloud/cloud_managed_database.go index b828686d1..926465c40 100644 --- a/internal/services/cloud/cloud_managed_database.go +++ b/internal/services/cloud/cloud_managed_database.go @@ -87,6 +87,8 @@ var ( Engine string `json:"-"` CLIIPRestrictions []string `json:"-"` CLINodesList []string `json:"-"` + CLINetworkID string `json:"-"` + CLISubnetID string `json:"-"` } ManagedDatabaseDatabaseSpec struct { @@ -236,16 +238,44 @@ func EditManagedDatabase(cmd *cobra.Command, args []string) { ManagedDatabaseSpec.IPRestrictions = append(ManagedDatabaseSpec.IPRestrictions, managedDatabaseIPRestriction{IP: restriction}) } - // Edit resource - if err := common.EditResource( - cmd, - fmt.Sprintf("/cloud/project/{serviceName}/database/%s/{clusterId}", url.PathEscape(databaseService["engine"].(string))), - fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s", projectID, url.PathEscape(databaseService["engine"].(string)), url.PathEscape(args[0])), - ManagedDatabaseSpec, - assets.CloudOpenapiSchema, - ); err != nil { - display.OutputError(&flags.OutputFormatConfig, "%s", err) - return + // Handle network update separately since networkId/subnetId are filtered + // out by the OpenAPI spec filter in EditResource + networkChanged := ManagedDatabaseSpec.CLINetworkID != "" || ManagedDatabaseSpec.CLISubnetID != "" + + endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s", projectID, url.PathEscape(databaseService["engine"].(string)), url.PathEscape(args[0])) + + // Edit non-network fields through the standard flow + if cmd.Flags().NFlag() > 0 && (!networkChanged || cmd.Flags().NFlag() > countNetworkFlags(cmd)) { + if err := common.EditResource( + cmd, + fmt.Sprintf("/cloud/project/{serviceName}/database/%s/{clusterId}", url.PathEscape(databaseService["engine"].(string))), + endpoint, + ManagedDatabaseSpec, + assets.CloudOpenapiSchema, + ); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return + } + } + + // Send network update as a direct PUT + if networkChanged { + networkBody := map[string]any{} + if ManagedDatabaseSpec.CLINetworkID == "none" { + networkBody["networkId"] = nil + } else if ManagedDatabaseSpec.CLINetworkID != "" { + networkBody["networkId"] = ManagedDatabaseSpec.CLINetworkID + } + if ManagedDatabaseSpec.CLISubnetID == "none" { + networkBody["subnetId"] = nil + } else if ManagedDatabaseSpec.CLISubnetID != "" { + networkBody["subnetId"] = ManagedDatabaseSpec.CLISubnetID + } + if err := httpLib.Client.Put(endpoint, networkBody, nil); err != nil { + display.OutputError(&flags.OutputFormatConfig, "failed to update network: %s", err) + return + } + display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Network update triggered successfully") } } @@ -278,6 +308,17 @@ func DeleteManagedDatabase(_ *cobra.Command, args []string) { display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Managed database deleted successfully") } +func countNetworkFlags(cmd *cobra.Command) int { + count := 0 + if cmd.Flags().Changed("network-id") { + count++ + } + if cmd.Flags().Changed("subnet-id") { + count++ + } + return count +} + func ListManagedDatabaseDatabases(_ *cobra.Command, args []string) { projectID, err := getConfiguredCloudProject() if err != nil { From 2b7a4c85f30d45dacdb64975b82d9336f40965d6 Mon Sep 17 00:00:00 2001 From: Achraf Hammouda Date: Mon, 21 Sep 2026 11:51:02 +0200 Subject: [PATCH 2/2] feat(managed-database): rework network update flags - Inject network fields into EditResource via extraFields parameter instead of a separate PUT call - Replace --network-id none / --subnet-id none with --public-network flag, mutually exclusive with --network-id and --subnet-id - Add extraFields variadic parameter to common.EditResource to allow injecting fields that bypass the OpenAPI spec filter Signed-off-by: Achraf Hammouda --- doc/ovhcloud_cloud_managed-database_edit.md | 10 ++- internal/cmd/cloud_managed_database.go | 12 ++- .../services/cloud/cloud_managed_database.go | 74 +++++++------------ internal/services/common/common.go | 9 ++- 4 files changed, 48 insertions(+), 57 deletions(-) diff --git a/doc/ovhcloud_cloud_managed-database_edit.md b/doc/ovhcloud_cloud_managed-database_edit.md index 0be2f2b20..6e7aa7a39 100644 --- a/doc/ovhcloud_cloud_managed-database_edit.md +++ b/doc/ovhcloud_cloud_managed-database_edit.md @@ -30,9 +30,10 @@ Network update: To switch from private to public network: - ovhcloud cloud managed-database edit --network-id none --subnet-id none + ovhcloud cloud managed-database edit --public-network - Note: Changing the network triggers a service rebuild. The service will be temporarily unavailable + Note: --public-network is mutually exclusive with --network-id and --subnet-id. + Changing the network triggers a service rebuild. The service will be temporarily unavailable during the transition. @@ -53,9 +54,10 @@ ovhcloud cloud managed-database edit [flags] -h, --help help for edit --ip-restrictions strings IP blocks authorized to access the cluster (CIDR format) --maintenance-time string Time on which maintenances can start every day - --network-id string Private network ID (use "none" to switch to public network) + --network-id string Private network ID --plan string Plan of the cluster - --subnet-id string Private subnet ID (use "none" to switch to public network) + --public-network Switch the service to public network + --subnet-id string Private subnet ID --version string Version of the engine deployed on the cluster ``` diff --git a/internal/cmd/cloud_managed_database.go b/internal/cmd/cloud_managed_database.go index 7358998a3..dc1d3ec1b 100644 --- a/internal/cmd/cloud_managed_database.go +++ b/internal/cmd/cloud_managed_database.go @@ -328,9 +328,10 @@ Network update: To switch from private to public network: - ovhcloud cloud managed-database edit --network-id none --subnet-id none + ovhcloud cloud managed-database edit --public-network - Note: Changing the network triggers a service rebuild. The service will be temporarily unavailable + Note: --public-network is mutually exclusive with --network-id and --subnet-id. + Changing the network triggers a service rebuild. The service will be temporarily unavailable during the transition. `, ValidArgsFunction: completion.CloudResources("/v1/cloud/project/%s/database"), @@ -351,8 +352,11 @@ Network update: // Network configuration managedDatabaseEditCmd.Flags().StringSliceVar(&cloud.ManagedDatabaseSpec.CLIIPRestrictions, "ip-restrictions", nil, "IP blocks authorized to access the cluster (CIDR format)") - managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLINetworkID, "network-id", "", `Private network ID (use "none" to switch to public network)`) - managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLISubnetID, "subnet-id", "", `Private subnet ID (use "none" to switch to public network)`) + managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLINetworkID, "network-id", "", "Private network ID") + managedDatabaseEditCmd.Flags().StringVar(&cloud.ManagedDatabaseSpec.CLISubnetID, "subnet-id", "", "Private subnet ID") + managedDatabaseEditCmd.Flags().BoolVar(&cloud.ManagedDatabaseSpec.CLIPublicNetwork, "public-network", false, "Switch the service to public network") + managedDatabaseEditCmd.MarkFlagsMutuallyExclusive("public-network", "network-id") + managedDatabaseEditCmd.MarkFlagsMutuallyExclusive("public-network", "subnet-id") // Common flags for other mean to define parameters addInteractiveEditorFlag(managedDatabaseEditCmd) diff --git a/internal/services/cloud/cloud_managed_database.go b/internal/services/cloud/cloud_managed_database.go index 926465c40..7bcc932cd 100644 --- a/internal/services/cloud/cloud_managed_database.go +++ b/internal/services/cloud/cloud_managed_database.go @@ -87,8 +87,9 @@ var ( Engine string `json:"-"` CLIIPRestrictions []string `json:"-"` CLINodesList []string `json:"-"` - CLINetworkID string `json:"-"` - CLISubnetID string `json:"-"` + CLINetworkID string `json:"-"` + CLISubnetID string `json:"-"` + CLIPublicNetwork bool `json:"-"` } ManagedDatabaseDatabaseSpec struct { @@ -238,44 +239,32 @@ func EditManagedDatabase(cmd *cobra.Command, args []string) { ManagedDatabaseSpec.IPRestrictions = append(ManagedDatabaseSpec.IPRestrictions, managedDatabaseIPRestriction{IP: restriction}) } - // Handle network update separately since networkId/subnetId are filtered - // out by the OpenAPI spec filter in EditResource - networkChanged := ManagedDatabaseSpec.CLINetworkID != "" || ManagedDatabaseSpec.CLISubnetID != "" - - endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s", projectID, url.PathEscape(databaseService["engine"].(string)), url.PathEscape(args[0])) - - // Edit non-network fields through the standard flow - if cmd.Flags().NFlag() > 0 && (!networkChanged || cmd.Flags().NFlag() > countNetworkFlags(cmd)) { - if err := common.EditResource( - cmd, - fmt.Sprintf("/cloud/project/{serviceName}/database/%s/{clusterId}", url.PathEscape(databaseService["engine"].(string))), - endpoint, - ManagedDatabaseSpec, - assets.CloudOpenapiSchema, - ); err != nil { - display.OutputError(&flags.OutputFormatConfig, "%s", err) - return + // Build extra network fields that bypass the OpenAPI filter + var networkFields map[string]any + if ManagedDatabaseSpec.CLIPublicNetwork { + networkFields = map[string]any{ + "networkId": nil, + "subnetId": nil, + } + } else if ManagedDatabaseSpec.CLINetworkID != "" { + networkFields = map[string]any{ + "networkId": ManagedDatabaseSpec.CLINetworkID, + "subnetId": ManagedDatabaseSpec.CLISubnetID, } } - // Send network update as a direct PUT - if networkChanged { - networkBody := map[string]any{} - if ManagedDatabaseSpec.CLINetworkID == "none" { - networkBody["networkId"] = nil - } else if ManagedDatabaseSpec.CLINetworkID != "" { - networkBody["networkId"] = ManagedDatabaseSpec.CLINetworkID - } - if ManagedDatabaseSpec.CLISubnetID == "none" { - networkBody["subnetId"] = nil - } else if ManagedDatabaseSpec.CLISubnetID != "" { - networkBody["subnetId"] = ManagedDatabaseSpec.CLISubnetID - } - if err := httpLib.Client.Put(endpoint, networkBody, nil); err != nil { - display.OutputError(&flags.OutputFormatConfig, "failed to update network: %s", err) - return - } - display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Network update triggered successfully") + endpoint := fmt.Sprintf("/v1/cloud/project/%s/database/%s/%s", projectID, url.PathEscape(databaseService["engine"].(string)), url.PathEscape(args[0])) + + if err := common.EditResource( + cmd, + fmt.Sprintf("/cloud/project/{serviceName}/database/%s/{clusterId}", url.PathEscape(databaseService["engine"].(string))), + endpoint, + ManagedDatabaseSpec, + assets.CloudOpenapiSchema, + networkFields, + ); err != nil { + display.OutputError(&flags.OutputFormatConfig, "%s", err) + return } } @@ -308,17 +297,6 @@ func DeleteManagedDatabase(_ *cobra.Command, args []string) { display.OutputInfo(&flags.OutputFormatConfig, nil, "✅ Managed database deleted successfully") } -func countNetworkFlags(cmd *cobra.Command) int { - count := 0 - if cmd.Flags().Changed("network-id") { - count++ - } - if cmd.Flags().Changed("subnet-id") { - count++ - } - return count -} - func ListManagedDatabaseDatabases(_ *cobra.Command, args []string) { projectID, err := getConfiguredCloudProject() if err != nil { diff --git a/internal/services/common/common.go b/internal/services/common/common.go index f0689fcbf..36f615536 100644 --- a/internal/services/common/common.go +++ b/internal/services/common/common.go @@ -221,7 +221,7 @@ func CreateResource(cmd *cobra.Command, path, endpoint, defaultExample string, return createdResource, nil } -func EditResource(cmd *cobra.Command, path, url string, cliParams any, openapiSpec []byte) error { +func EditResource(cmd *cobra.Command, path, url string, cliParams any, openapiSpec []byte, extraFields ...map[string]any) error { if cmd.Flags().NFlag() == 0 { display.OutputInfo(&flags.OutputFormatConfig, nil, "🟠 No parameters given, nothing to edit") return nil @@ -282,6 +282,13 @@ func EditResource(cmd *cobra.Command, path, url string, cliParams any, openapiSp return fmt.Errorf("failed to extract writable properties: %w", err) } + // Inject extra fields that bypass the OpenAPI filter + for _, extra := range extraFields { + for k, v := range extra { + editableBody[k] = v + } + } + // If editor not needed, update the resource directly if !flags.ParametersViaEditor { if err := httpLib.Client.Put(url, editableBody, nil); err != nil {