diff --git a/CHANGELOG.md b/CHANGELOG.md index 19c38254..7ed032a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +### Added + +- The new [cockroach_backup_config](https://registry.terraform.io/providers/cockroachdb/cockroach/latest/docs/resources/backup_config) resource adds support for configuring [cluster backups](https://www.cockroachlabs.com/docs/cockroachcloud/managed-backups). + ## [1.9.0] - 2024-10-07 - Added support for skipping [Innovation diff --git a/docs/resources/backup_config.md b/docs/resources/backup_config.md new file mode 100644 index 00000000..c52e2706 --- /dev/null +++ b/docs/resources/backup_config.md @@ -0,0 +1,117 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "cockroach_backup_config Resource - terraform-provider-cockroach" +subcategory: "" +description: |- + The backup configuration for a cluster. + Each cluster has a backup configuration that determines if backups are enabled, + how frequently they are taken and how often they are retained for. This + resource allows configuring those settings. It's important to note that the + existence or lack of this resource doesn't not indicate whether a backup + configuration exists or not. Rather, if Terraform is managing it or not. + As a result of this, removal of the resource from the Terraform configuration + will leave the backup configuration in the state it was before the removal. + retention_days Edit Restrictions + retention_days can only be set once. Further changes require opening a support + ticket. For this reason, managing the value via terraform can require special + care. Here are some useful tips: + * Optionally, you can refrain from including retention_days in the + terraform resource to fully rely on server side management of the value and not need any + special care. + * If the initial value for retention_days in the backupconfig resource is the + default value (30), the single edit will not be used up and it can be changed one + more time. + * If the initial value set for retention_days in the backupconfig resource is + not the default, the single edit will be used up on the first apply. Further + edits will require a support ticket. + Changing the value of retention_days after using your one change will be a + multi-step operation. Here are two workflows that will work. Both of these + paths assume you already have a backup config resource that doesn't have any + updates left: + Change it and then open a ticket before applying: + + Update retention_days to the new desired value in terraform.Before applying the run, contact support to change the retention_days to the desired value.Apply the changes in terraform, a terraform READ operation will be done and see the existing value and update the tf state.Temporarily remove management of it from tf, update it via ticket and then add it back. + + Remove management of retention_days from the terraform resourceRun the apply, nothing will change but terraform is no longer managing that value.Open a support ticket to update retention_days to the desired value.Optionally, add retention_days back to the terraform config with the new value and apply the no-op update. +--- + +# cockroach_backup_config (Resource) + +The backup configuration for a cluster. + +Each cluster has a backup configuration that determines if backups are enabled, +how frequently they are taken and how often they are retained for. This +resource allows configuring those settings. It's important to note that the +existence or lack of this resource doesn't not indicate whether a backup +configuration exists or not. Rather, if Terraform is managing it or not. + +As a result of this, removal of the resource from the Terraform configuration +will leave the backup configuration in the state it was before the removal. + +### `retention_days` Edit Restrictions + +`retention_days` can only be set once. Further changes require opening a support +ticket. For this reason, managing the value via terraform can require special +care. Here are some useful tips: +* Optionally, you can refrain from including `retention_days` in the +terraform resource to fully rely on server side management of the value and not need any +special care. +* If the initial value for `retention_days` in the backup_config resource is the +default value (30), the single edit will not be used up and it can be changed one +more time. +* If the initial value set for `retention_days` in the backup_config resource is +not the default, the single edit will be used up on the first apply. Further +edits will require a support ticket. + +Changing the value of `retention_days` after using your one change will be a +multi-step operation. Here are two workflows that will work. Both of these +paths assume you already have a backup config resource that doesn't have any +updates left: + +* Change it and then open a ticket before applying: + 1. Update `retention_days` to the new desired value in terraform. + 1. Before applying the run, contact support to change the `retention_days` to the desired value. + 1. Apply the changes in terraform, a terraform READ operation will be done and see the existing value and update the tf state. +* Temporarily remove management of it from tf, update it via ticket and then add it back. + 1. Remove management of `retention_days` from the terraform resource + 1. Run the apply, nothing will change but terraform is no longer managing that value. + 1. Open a support ticket to update `retention_days` to the desired value. + 1. Optionally, add `retention_days` back to the terraform config with the new value and apply the no-op update. + +## Example Usage + +```terraform +resource "cockroach_backup_config" "backed_up_cluster" { + id = cockroach_cluster.backed_up_cluster.id + enabled = true + frequence_minutes = 60 + retention_days = 30 +} + +resource "cockroach_backup_config" "no_backups_cluster" { + id = cockroach_cluster.no_backups_cluster.id + enabled = false +} +``` + + +## Schema + +### Required + +- `enabled` (Boolean) Indicates whether the backup configuration is enabled. If set to false, no backups will be created. +- `id` (String) Cluster ID. + +### Optional + +- `frequency_minutes` (Number) The frequency of backups in minutes. Valid values are [5, 10, 15, 30, 60, 240, 1440] +- `retention_days` (Number) The number of days to retain backups for. Valid values are [2, 7, 30, 90, 365]. Can only be set once, further changes require opening a support ticket. [See more](#retention_days-edit-restrictions). + +## Import + +Import is supported using the following syntax: + +```shell +# format +terraform import cockroach_backup_config.backed_up_cluster 1f69fdd2-600a-4cfc-a9ba-16995df0d77d +``` diff --git a/examples/resources/cockroach_backup_config/import.sh b/examples/resources/cockroach_backup_config/import.sh new file mode 100644 index 00000000..df4fc244 --- /dev/null +++ b/examples/resources/cockroach_backup_config/import.sh @@ -0,0 +1,2 @@ +# format +terraform import cockroach_backup_config.backed_up_cluster 1f69fdd2-600a-4cfc-a9ba-16995df0d77d diff --git a/examples/resources/cockroach_backup_config/resource.tf b/examples/resources/cockroach_backup_config/resource.tf new file mode 100644 index 00000000..db98d04f --- /dev/null +++ b/examples/resources/cockroach_backup_config/resource.tf @@ -0,0 +1,11 @@ +resource "cockroach_backup_config" "backed_up_cluster" { + id = cockroach_cluster.backed_up_cluster.id + enabled = true + frequence_minutes = 60 + retention_days = 30 +} + +resource "cockroach_backup_config" "no_backups_cluster" { + id = cockroach_cluster.no_backups_cluster.id + enabled = false +} diff --git a/internal/provider/backup_config_resource.go b/internal/provider/backup_config_resource.go new file mode 100644 index 00000000..fb8004da --- /dev/null +++ b/internal/provider/backup_config_resource.go @@ -0,0 +1,297 @@ +/* +Copyright 2024 The Cockroach Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "fmt" + + "github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var backupConfigAttributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Cluster ID.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "enabled": schema.BoolAttribute{ + Required: true, + Description: "Indicates whether the backup configuration is enabled. If set to false, no backups will be created.", + }, + "retention_days": schema.Int64Attribute{ + Optional: true, + Computed: true, + MarkdownDescription: "The number of days to retain backups for. Valid values are [2, 7, 30, 90, 365]. Can only be set once, further changes require opening a support ticket. [See more](#retention_days-edit-restrictions).", + }, + "frequency_minutes": schema.Int64Attribute{ + Optional: true, + Computed: true, + Description: "The frequency of backups in minutes. Valid values are [5, 10, 15, 30, 60, 240, 1440]", + }, +} + +type backupConfigResource struct { + provider *provider +} + +func NewBackupConfigResource() resource.Resource { + return &backupConfigResource{} +} + +func (r *backupConfigResource) Schema( + _ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + MarkdownDescription: `The backup configuration for a cluster. + +Each cluster has a backup configuration that determines if backups are enabled, +how frequently they are taken and how often they are retained for. This +resource allows configuring those settings. It's important to note that the +existence or lack of this resource doesn't not indicate whether a backup +configuration exists or not. Rather, if Terraform is managing it or not. + +As a result of this, removal of the resource from the Terraform configuration +will leave the backup configuration in the state it was before the removal. + +` + "### `retention_days`" + ` Edit Restrictions + +` + "`retention_days`" + ` can only be set once. Further changes require opening a support +ticket. For this reason, managing the value via terraform can require special +care. Here are some useful tips: +* Optionally, you can refrain from including ` + "`retention_days`" + ` in the +terraform resource to fully rely on server side management of the value and not need any +special care. +* If the initial value for ` + "`retention_days`" + ` in the backup_config resource is the +default value (30), the single edit will not be used up and it can be changed one +more time. +* If the initial value set for ` + "`retention_days`" + ` in the backup_config resource is +not the default, the single edit will be used up on the first apply. Further +edits will require a support ticket. + +Changing the value of ` + "`retention_days`" + ` after using your one change will be a +multi-step operation. Here are two workflows that will work. Both of these +paths assume you already have a backup config resource that doesn't have any +updates left: + +* Change it and then open a ticket before applying: + 1. Update ` + "`retention_days`" + ` to the new desired value in terraform. + 1. Before applying the run, contact support to change the ` + "`retention_days`" + ` to the desired value. + 1. Apply the changes in terraform, a terraform READ operation will be done and see the existing value and update the tf state. +* Temporarily remove management of it from tf, update it via ticket and then add it back. + 1. Remove management of ` + "`retention_days`" + ` from the terraform resource + 1. Run the apply, nothing will change but terraform is no longer managing that value. + 1. Open a support ticket to update ` + "`retention_days`" + ` to the desired value. + 1. Optionally, add ` + "`retention_days`" + ` back to the terraform config with the new value and apply the no-op update. +`, + Attributes: backupConfigAttributes, + } +} + +func (r *backupConfigResource) Metadata( + _ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_backup_config" +} + +func (r *backupConfigResource) Configure( + _ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + var ok bool + if r.provider, ok = req.ProviderData.(*provider); !ok { + resp.Diagnostics.AddError("Internal provider error", + fmt.Sprintf("Error in Configure: expected %T but got %T", provider{}, req.ProviderData)) + } +} + +func (r *backupConfigResource) Create( + ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, +) { + if r.provider == nil || !r.provider.configured { + addConfigureProviderErr(&resp.Diagnostics) + return + } + + var plan ClusterBackupConfiguration + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + clusterID := plan.ID.ValueString() + + traceAPICall("GetBackupConfiguration") + remoteBackupConfig, _, err := r.provider.service.GetBackupConfiguration(ctx, clusterID) + if err != nil { + resp.Diagnostics.AddError( + "Error getting backup configuration", + fmt.Sprintf("Could not retrieve backup configuration: %s", formatAPIErrorMessage(err)), + ) + return + } + + var state ClusterBackupConfiguration + loadBackupConfigIntoTerraformState(clusterID, remoteBackupConfig, &state) + + if state != plan { + + updateRequest := &client.UpdateBackupConfigurationSpec{ + Enabled: ptr(plan.Enabled.ValueBool()), + } + + if !plan.RetentionDays.IsNull() && !plan.RetentionDays.IsUnknown() { + updateRequest.RetentionDays = ptr(int32(plan.RetentionDays.ValueInt64())) + } + + if !plan.FrequencyMinutes.IsNull() && !plan.FrequencyMinutes.IsUnknown() { + updateRequest.FrequencyMinutes = ptr(int32(plan.FrequencyMinutes.ValueInt64())) + } + + traceAPICall("UpdateBackupConfiguration") + remoteBackupConfig, _, err := r.provider.service.UpdateBackupConfiguration(ctx, clusterID, updateRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error getting backup configuration", + fmt.Sprintf("Could not retrieve backup configuration: %s", formatAPIErrorMessage(err)), + ) + return + } + + loadBackupConfigIntoTerraformState(clusterID, remoteBackupConfig, &state) + } + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +func loadBackupConfigIntoTerraformState( + clusterID string, apiBackupConfig *client.BackupConfiguration, state *ClusterBackupConfiguration, +) { + state.ID = types.StringValue(clusterID) + state.Enabled = types.BoolValue(apiBackupConfig.GetEnabled()) + state.FrequencyMinutes = types.Int64Value(int64(apiBackupConfig.GetFrequencyMinutes())) + state.RetentionDays = types.Int64Value(int64(apiBackupConfig.GetRetentionDays())) +} + +func (r *backupConfigResource) Read( + ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, +) { + if r.provider == nil || !r.provider.configured { + addConfigureProviderErr(&resp.Diagnostics) + return + } + + var state ClusterBackupConfiguration + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() || !IsKnown(state.ID) { + return + } + clusterID := state.ID.ValueString() + + traceAPICall("GetBackupConfiguration") + remoteBackupConfig, _, err := r.provider.service.GetBackupConfiguration(ctx, clusterID) + if err != nil { + resp.Diagnostics.AddError( + "Error getting backup configuration", + fmt.Sprintf("Could not retrieve backup configuration: %s", formatAPIErrorMessage(err)), + ) + return + } + + loadBackupConfigIntoTerraformState(clusterID, remoteBackupConfig, &state) + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +func (r *backupConfigResource) Update( + ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, +) { + // Get plan values + var plan ClusterBackupConfiguration + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Get current state + var state ClusterBackupConfiguration + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + clusterID := plan.ID.ValueString() + + updateRequest := &client.UpdateBackupConfigurationSpec{ + Enabled: ptr(plan.Enabled.ValueBool()), + } + + if !plan.RetentionDays.IsNull() && !plan.RetentionDays.IsUnknown() { + updateRequest.RetentionDays = ptr(int32(plan.RetentionDays.ValueInt64())) + } + + if !plan.FrequencyMinutes.IsNull() && !plan.FrequencyMinutes.IsUnknown() { + updateRequest.FrequencyMinutes = ptr(int32(plan.FrequencyMinutes.ValueInt64())) + } + + traceAPICall("UpdateBackupConfiguration") + remoteBackupConfig, _, err := r.provider.service.UpdateBackupConfiguration(ctx, clusterID, updateRequest) + if err != nil { + resp.Diagnostics.AddError( + "Error updating backup configuration", + fmt.Sprintf("Could not update backup configuration: %s", formatAPIErrorMessage(err)), + ) + return + } + + loadBackupConfigIntoTerraformState(clusterID, remoteBackupConfig, &state) + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +// Delete +func (r *backupConfigResource) Delete( + ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, +) { + // Since the backup configuration is ever existing, we don't delete it. + // Removing it from the terraform config means terraform will no longer + // manage it. In other words it will be left in the state it was before + // removal. + resp.State.RemoveResource(ctx) +} + +func (r *backupConfigResource) ImportState( + ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse, +) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/backup_config_resource_test.go b/internal/provider/backup_config_resource_test.go new file mode 100644 index 00000000..e42f1fe5 --- /dev/null +++ b/internal/provider/backup_config_resource_test.go @@ -0,0 +1,380 @@ +/* +Copyright 2024 The Cockroach Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package provider + +import ( + "context" + "fmt" + "net/http" + "os" + "regexp" + "strconv" + "testing" + + "github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client" + mock_client "github.com/cockroachdb/terraform-provider-cockroach/mock" + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// TestAccBackupConfigResource attempts to check, and update a +// real backup configuration. It will be skipped if TF_ACC isn't set. +func TestAccBackupConfigResource(t *testing.T) { + t.Parallel() + clusterName := fmt.Sprintf("%s-backup-%s", tfTestPrefix, GenerateRandomString(4)) + testBackupConfigResource(t, clusterName, false /* useMock */) +} + +// Shared Test objects +var initialBackupConfig = &client.BackupConfiguration{ + Enabled: true, + FrequencyMinutes: 60, + RetentionDays: 30, +} + +var initialBackupConfigDisabled = &client.BackupConfiguration{ + Enabled: false, + FrequencyMinutes: initialBackupConfig.FrequencyMinutes, + RetentionDays: initialBackupConfig.RetentionDays, +} + +var updatedBackupConfig = &client.BackupConfiguration{ + Enabled: true, + RetentionDays: 7, + FrequencyMinutes: 5, +} + +var secondUpdatedBackupConfig = &client.BackupConfiguration{ + Enabled: updatedBackupConfig.Enabled, + RetentionDays: updatedBackupConfig.RetentionDays, + FrequencyMinutes: 1440, +} + +// TestIntegrationBackupConfigResource attempts to check, and update a +// backup configuration, but uses a mocked API service. +func TestIntegrationBackupConfigResource(t *testing.T) { + clusterName := fmt.Sprintf("%s-logexp-%s", tfTestPrefix, GenerateRandomString(4)) + clusterID := uuid.Nil.String() + if os.Getenv(CockroachAPIKey) == "" { + os.Setenv(CockroachAPIKey, "fake") + } + + ctrl := gomock.NewController(t) + s := mock_client.NewMockService(ctrl) + defer HookGlobal(&NewService, func(c *client.Client) client.Service { + return s + })() + + cluster := client.Cluster{ + Id: uuid.Nil.String(), + Name: clusterName, + CockroachVersion: latestClusterPatchVersion, + CloudProvider: "GCP", + State: "CREATED", + Plan: "STANDARD", + Config: client.ClusterConfig{ + Serverless: &client.ServerlessClusterConfig{ + UpgradeType: client.UPGRADETYPETYPE_AUTOMATIC, + UsageLimits: &client.UsageLimits{ + ProvisionedVirtualCpus: ptr(int64(2)), + }, + RoutingId: "routing-id", + }, + }, + Regions: []client.Region{ + { + Name: "us-central1", + }, + }, + } + + httpOk := &http.Response{Status: http.StatusText(http.StatusOK)} + httpFail := &http.Response{Status: http.StatusText(http.StatusBadRequest)} + + expectUpdateSequence := func(update *client.UpdateBackupConfigurationSpec, before, after *client.BackupConfiguration) { + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(before, httpOk, nil) + s.EXPECT().UpdateBackupConfiguration(gomock.Any(), clusterID, update).Return(after, httpOk, nil) + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(after, httpOk, nil).Times(2) + } + + expectErrorSequence := func(update *client.UpdateBackupConfigurationSpec, before *client.BackupConfiguration, err error) { + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(before, httpOk, nil) + s.EXPECT().UpdateBackupConfiguration(gomock.Any(), clusterID, update).Return(before, httpFail, err) + } + + // Our cluster never changes so to avoid complexity, we'll just return it + // as many times as its asked for. + s.EXPECT().GetCluster(gomock.Any(), clusterID). + Return(&cluster, httpOk, nil).AnyTimes() + + // Step: a cluster without a backup config resource still has a default config + s.EXPECT().CreateCluster(gomock.Any(), gomock.Any()).Return(&cluster, nil, nil) + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(initialBackupConfig, httpOk, nil) + + // Step: a resource with just the mandatory enabled field + expectUpdateSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + }, initialBackupConfig, initialBackupConfig) + + // Step: disabling backups without specifying any other fields + expectUpdateSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(false), + }, initialBackupConfig, initialBackupConfigDisabled) + + // Step: reenable passing in the default values + expectUpdateSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + RetentionDays: ptr(initialBackupConfig.RetentionDays), + FrequencyMinutes: ptr(initialBackupConfig.FrequencyMinutes), + }, initialBackupConfigDisabled, initialBackupConfig) + + // Step: update frequency and retention + expectUpdateSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + RetentionDays: ptr(updatedBackupConfig.RetentionDays), + FrequencyMinutes: ptr(updatedBackupConfig.FrequencyMinutes), + }, initialBackupConfig, updatedBackupConfig) + + // Step: error case: invalid retention_days + expectErrorSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + RetentionDays: ptr(int32(12345)), + }, updatedBackupConfig, fmt.Errorf("retention_days must be one of []")) + + // Step: error case: invalid frequency_minutes + expectErrorSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + FrequencyMinutes: ptr(int32(12345)), + }, updatedBackupConfig, fmt.Errorf("frequency_minutes must be one of []")) + + // Step: remove the backup configuration resource, cluster still has the last one that was set + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(updatedBackupConfig, httpOk, nil).Times(2) + + // Step: test updating values during the create + expectUpdateSequence(&client.UpdateBackupConfigurationSpec{ + Enabled: ptr(true), + FrequencyMinutes: ptr(int32(secondUpdatedBackupConfig.FrequencyMinutes)), + }, updatedBackupConfig, secondUpdatedBackupConfig) + + // Delete phase + s.EXPECT().GetBackupConfiguration(gomock.Any(), clusterID).Return(secondUpdatedBackupConfig, httpOk, nil) + s.EXPECT().DeleteCluster(gomock.Any(), clusterID) + + testBackupConfigResource(t, clusterName, true /* useMock */) +} + +func testBackupConfigResource(t *testing.T, clusterName string, useMock bool) { + var ( + clusterResourceName = "cockroach_cluster.test" + backupConfigResourceName = "cockroach_backup_config.test" + ) + + resource.Test(t, resource.TestCase{ + IsUnitTest: useMock, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + traceMessageStep("a cluster without a backup config resource still has a default config") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: false, + }), + Check: checkBackupConfig(clusterResourceName, initialBackupConfig), + }, + { + PreConfig: func() { + traceMessageStep("a resource with just the mandatory enabled field") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + }), + Check: checkBackupConfig(clusterResourceName, initialBackupConfig), + }, + { + PreConfig: func() { + traceMessageStep("disabling backups without specifying any other fields") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: false, + }), + Check: checkBackupConfig(clusterResourceName, initialBackupConfigDisabled), + }, + { + PreConfig: func() { + traceMessageStep("reenable passing in the default values") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + retention: ptr(initialBackupConfig.RetentionDays), + frequency: ptr(initialBackupConfig.FrequencyMinutes), + }), + Check: checkBackupConfig(clusterResourceName, initialBackupConfig), + }, + { + PreConfig: func() { + traceMessageStep("update frequency and retention") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + retention: ptr(updatedBackupConfig.RetentionDays), + frequency: ptr(updatedBackupConfig.FrequencyMinutes), + }), + Check: checkBackupConfig(clusterResourceName, updatedBackupConfig), + }, + { + PreConfig: func() { + traceMessageStep("error case: invalid retention_days") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + retention: ptr(int32(12345)), + }), + Check: checkBackupConfig(clusterResourceName, updatedBackupConfig), + // Setting single line mode because error is broken across lines. + ExpectError: regexp.MustCompile(`(?s)retention_days.*must.*be.*one.*of`), + }, + { + PreConfig: func() { + traceMessageStep("error case: invalid frequency_minutes") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + frequency: ptr(int32(12345)), + }), + Check: checkBackupConfig(clusterResourceName, updatedBackupConfig), + // Setting single line mode because error is broken across lines. + ExpectError: regexp.MustCompile(`(?s)frequency_minutes.*must.*be.*one.*of`), + }, + { + PreConfig: func() { + traceMessageStep("remove the backup configuration resource, cluster still has the last one that was set") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: false, + }), + Check: checkBackupConfig(clusterResourceName, updatedBackupConfig), + }, + { + PreConfig: func() { + traceMessageStep("test updating values during the create") + }, + Config: getTestBackupConfigCreateConfig(clusterName, backupCreateConfig{ + includeBackupConfig: true, + enabled: true, + frequency: ptr(secondUpdatedBackupConfig.FrequencyMinutes), + }), + Check: resource.ComposeTestCheckFunc( + checkBackupConfig(clusterResourceName, secondUpdatedBackupConfig), + traceEndOfPlan(), + ), + }, + { + ResourceName: backupConfigResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func checkBackupConfig(clusterResourceName string, expected *client.BackupConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + p := testAccProvider.(*provider) + p.service = NewService(cl) + + clusterRs, ok := s.RootModule().Resources[clusterResourceName] + if !ok { + return fmt.Errorf("not found: %s", clusterResourceName) + } + + clusterID := clusterRs.Primary.Attributes["id"] + + traceAPICall("GetBackupConfiguration") + resp, _, err := p.service.GetBackupConfiguration(context.TODO(), clusterID) + if err != nil { + return fmt.Errorf("unexpected error during config lookup: %w", err) + } + + if *resp != *expected { + return fmt.Errorf("expected backup configuration did not match actual. expected: %v, actual %v", expected, resp) + } + + return nil + } +} + +func getTestBackupConfigClusterConfig(name string) string { + return fmt.Sprintf(` +resource "cockroach_cluster" "test" { + name = "%s" + cloud_provider = "GCP" + plan = "STANDARD" + serverless = { + usage_limits = { + provisioned_virtual_cpus = 2 + } + } + regions = [{ + name = "us-central1" + }] +} +`, name) +} + +type backupCreateConfig struct { + includeBackupConfig bool + enabled bool + retention *int32 + frequency *int32 +} + +func getTestBackupConfigCreateConfig(name string, config backupCreateConfig) string { + + tfConfig := getTestBackupConfigClusterConfig(name) + + if config.includeBackupConfig { + retentionConfig := "" + if config.retention != nil { + retentionConfig = fmt.Sprintf(`retention_days = %d`, *config.retention) + } + + frequencyConfig := "" + if config.frequency != nil { + frequencyConfig = fmt.Sprintf(`frequency_minutes = %d`, *config.frequency) + } + + tfConfig = fmt.Sprintf(` +%s +resource "cockroach_backup_config" "test" { +id = cockroach_cluster.test.id +enabled = %s +%s +%s +} +`, tfConfig, strconv.FormatBool(config.enabled), retentionConfig, frequencyConfig) + } + return tfConfig +} diff --git a/internal/provider/models.go b/internal/provider/models.go index 49d88762..f0beed93 100644 --- a/internal/provider/models.go +++ b/internal/provider/models.go @@ -271,6 +271,13 @@ type ClusterVersionDeferral struct { DeferralPolicy types.String `tfsdk:"deferral_policy"` } +type ClusterBackupConfiguration struct { + ID types.String `tfsdk:"id"` + Enabled types.Bool `tfsdk:"enabled"` + RetentionDays types.Int64 `tfsdk:"retention_days"` + FrequencyMinutes types.Int64 `tfsdk:"frequency_minutes"` +} + type ClientCACertResourceModel struct { ID types.String `tfsdk:"id"` X509PemCert types.String `tfsdk:"x509_pem_cert"` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e72723f2..96fe7490 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -147,6 +147,7 @@ func (p *provider) Resources(_ context.Context) []func() resource.Resource { NewJWTIssuerResource, NewServiceAccountResource, NewAPIKeyResource, + NewBackupConfigResource, } } diff --git a/internal/provider/service_account_resource_test.go b/internal/provider/service_account_resource_test.go index 41329507..2da87ed3 100644 --- a/internal/provider/service_account_resource_test.go +++ b/internal/provider/service_account_resource_test.go @@ -31,7 +31,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) -// TestIntegrationServiceAccountResource attempts to create, check, and destroy a +// TestAccServiceAccountResource attempts to create, check, and destroy a // real service account. It will be skipped if TF_ACC isn't set. func TestAccServiceAccountResource(t *testing.T) { t.Parallel() diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 49aab234..1f91d03f 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -18,6 +18,8 @@ import ( resource_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stretchr/testify/require" ) @@ -185,3 +187,21 @@ func traceAPICall(endpoint string) { fmt.Printf("CC API Call: %s (%s)\n", endpoint, runtime.FuncForPC(pc).Name()) } } + +func traceSupportMessageRaw(message string) { + val, exists := os.LookupEnv("TRACE_API_CALLS") + if exists && val == "1" { + fmt.Print(message) + } +} + +func traceMessageStep(message string) { + traceSupportMessageRaw(fmt.Sprintf("\n// Step: %s\n", message)) +} + +func traceEndOfPlan() resource.TestCheckFunc { + return func(s *terraform.State) error { + traceSupportMessageRaw("\n// Delete phase\n") + return nil + } +} diff --git a/mock/service.go b/mock/service.go index 0702f611..e9ae2f46 100644 --- a/mock/service.go +++ b/mock/service.go @@ -784,6 +784,22 @@ func (mr *MockServiceMockRecorder) GetApiOidcConfig(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApiOidcConfig", reflect.TypeOf((*MockService)(nil).GetApiOidcConfig), arg0, arg1) } +// GetBackupConfiguration mocks base method. +func (m *MockService) GetBackupConfiguration(arg0 context.Context, arg1 string) (*client.BackupConfiguration, *http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBackupConfiguration", arg0, arg1) + ret0, _ := ret[0].(*client.BackupConfiguration) + ret1, _ := ret[1].(*http.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetBackupConfiguration indicates an expected call of GetBackupConfiguration. +func (mr *MockServiceMockRecorder) GetBackupConfiguration(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBackupConfiguration", reflect.TypeOf((*MockService)(nil).GetBackupConfiguration), arg0, arg1) +} + // GetCMEKClusterInfo mocks base method. func (m *MockService) GetCMEKClusterInfo(arg0 context.Context, arg1 string) (*client.CMEKClusterInfo, *http.Response, error) { m.ctrl.T.Helper() @@ -1855,6 +1871,22 @@ func (mr *MockServiceMockRecorder) UpdateApiOidcConfig(arg0, arg1, arg2 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApiOidcConfig", reflect.TypeOf((*MockService)(nil).UpdateApiOidcConfig), arg0, arg1, arg2) } +// UpdateBackupConfiguration mocks base method. +func (m *MockService) UpdateBackupConfiguration(arg0 context.Context, arg1 string, arg2 *client.UpdateBackupConfigurationSpec) (*client.BackupConfiguration, *http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateBackupConfiguration", arg0, arg1, arg2) + ret0, _ := ret[0].(*client.BackupConfiguration) + ret1, _ := ret[1].(*http.Response) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// UpdateBackupConfiguration indicates an expected call of UpdateBackupConfiguration. +func (mr *MockServiceMockRecorder) UpdateBackupConfiguration(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBackupConfiguration", reflect.TypeOf((*MockService)(nil).UpdateBackupConfiguration), arg0, arg1, arg2) +} + // UpdateCMEKSpec mocks base method. func (m *MockService) UpdateCMEKSpec(arg0 context.Context, arg1 string, arg2 *client.CMEKClusterSpecification) (*client.CMEKClusterInfo, *http.Response, error) { m.ctrl.T.Helper() diff --git a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_any.go b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_any.go index 785083af..256c1275 100644 --- a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_any.go +++ b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_any.go @@ -20,7 +20,7 @@ package client // Any `Any` contains an arbitrary serialized protocol buffer message along with a URL that describes the type of the serialized message. Protobuf library provides support to pack/unpack Any values in the form of utility functions or additional generated methods of the Any type. Example 1: Pack and unpack a message in C++. Foo foo = ...; Any any; any.PackFrom(foo); ... if (any.UnpackTo(&foo)) { ... } Example 2: Pack and unpack a message in Java. Foo foo = ...; Any any = Any.pack(foo); ... if (any.is(Foo.class)) { foo = any.unpack(Foo.class); } // or ... if (any.isSameTypeAs(Foo.getDefaultInstance())) { foo = any.unpack(Foo.getDefaultInstance()); } Example 3: Pack and unpack a message in Python. foo = Foo(...) any = Any() any.Pack(foo) ... if any.Is(Foo.DESCRIPTOR): any.Unpack(foo) ... Example 4: Pack and unpack a message in Go foo := &pb.Foo{...} any, err := anypb.New(foo) if err != nil { ... } ... foo := &pb.Foo{} if err := any.UnmarshalTo(foo); err != nil { ... } The pack methods provided by protobuf library will by default use 'type.googleapis.com/full.type.name' as the type URL and the unpack methods only use the fully qualified type name after the last '/' in the type URL, for example \"foo.bar.com/x/y.z\" will yield type name \"y.z\". JSON ==== The JSON representation of an `Any` value uses the regular representation of the deserialized, embedded message, with an additional field `@type` which contains the type URL. Example: package google.profile; message Person { string first_name = 1; string last_name = 2; } { \"@type\": \"type.googleapis.com/google.profile.Person\", \"firstName\": , \"lastName\": } If the embedded message type is well-known and has a custom JSON representation, that representation will be embedded adding a field `value` which holds the custom JSON in addition to the `@type` field. Example (for message [google.protobuf.Duration][]): { \"@type\": \"type.googleapis.com/google.protobuf.Duration\", \"value\": \"1.212s\" }. type Any struct { - // A URL/resource name that uniquely identifies the type of the serialized protocol buffer message. This string must contain at least one \"/\" character. The last segment of the URL's path must represent the fully qualified name of the type (as in `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading \".\" is not accepted). In practice, teams usually precompile into the binary all types that they expect it to use in the context of Any. However, for URLs which use the scheme `http`, `https`, or no scheme, one can optionally set up a type server that maps type URLs to message definitions as follows: * If no scheme is provided, `https` is assumed. * An HTTP GET on the URL must yield a [google.protobuf.Type][] value in binary format, or produce an error. * Applications are allowed to cache lookup results based on the URL, or have them precompiled into a binary to avoid any lookup. Therefore, binary compatibility needs to be preserved on changes to types. (Use versioned type names to manage breaking changes.) Note: this functionality is not currently available in the official protobuf release, and it is not used for type URLs beginning with type.googleapis.com. Schemes other than `http`, `https` (or the empty scheme) might be used with implementation specific semantics. + // A URL/resource name that uniquely identifies the type of the serialized protocol buffer message. This string must contain at least one \"/\" character. The last segment of the URL's path must represent the fully qualified name of the type (as in `path/google.protobuf.Duration`). The name should be in a canonical form (e.g., leading \".\" is not accepted). In practice, teams usually precompile into the binary all types that they expect it to use in the context of Any. However, for URLs which use the scheme `http`, `https`, or no scheme, one can optionally set up a type server that maps type URLs to message definitions as follows: * If no scheme is provided, `https` is assumed. * An HTTP GET on the URL must yield a [google.protobuf.Type][] value in binary format, or produce an error. * Applications are allowed to cache lookup results based on the URL, or have them precompiled into a binary to avoid any lookup. Therefore, binary compatibility needs to be preserved on changes to types. (Use versioned type names to manage breaking changes.) Note: this functionality is not currently available in the official protobuf release, and it is not used for type URLs beginning with type.googleapis.com. As of May 2023, there are no widely used type server implementations and no plans to implement one. Schemes other than `http`, `https` (or the empty scheme) might be used with implementation specific semantics. Type *string `json:"@type,omitempty"` } diff --git a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_backup_configuration.go b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_backup_configuration.go new file mode 100644 index 00000000..c93f815f --- /dev/null +++ b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_backup_configuration.go @@ -0,0 +1,94 @@ +// Copyright 2023 The Cockroach Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. +// CockroachDB Cloud API +// API version: 2024-09-16 + +package client + +// BackupConfiguration struct for BackupConfiguration. +type BackupConfiguration struct { + // Indicates whether the backup configuration is enabled and can run. + Enabled bool `json:"enabled"` + // The frequency in minutes that backups are taken. Changes that are more recent may not be included in the latest backup and may be lost during a restore. + FrequencyMinutes int32 `json:"frequency_minutes"` + // The number of days backups are retained for. + RetentionDays int32 `json:"retention_days"` +} + +// NewBackupConfiguration instantiates a new BackupConfiguration object. +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewBackupConfiguration(enabled bool, frequencyMinutes int32, retentionDays int32) *BackupConfiguration { + p := BackupConfiguration{} + p.Enabled = enabled + p.FrequencyMinutes = frequencyMinutes + p.RetentionDays = retentionDays + return &p +} + +// NewBackupConfigurationWithDefaults instantiates a new BackupConfiguration object. +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewBackupConfigurationWithDefaults() *BackupConfiguration { + p := BackupConfiguration{} + return &p +} + +// GetEnabled returns the Enabled field value. +func (o *BackupConfiguration) GetEnabled() bool { + if o == nil { + var ret bool + return ret + } + + return o.Enabled +} + +// SetEnabled sets field value. +func (o *BackupConfiguration) SetEnabled(v bool) { + o.Enabled = v +} + +// GetFrequencyMinutes returns the FrequencyMinutes field value. +func (o *BackupConfiguration) GetFrequencyMinutes() int32 { + if o == nil { + var ret int32 + return ret + } + + return o.FrequencyMinutes +} + +// SetFrequencyMinutes sets field value. +func (o *BackupConfiguration) SetFrequencyMinutes(v int32) { + o.FrequencyMinutes = v +} + +// GetRetentionDays returns the RetentionDays field value. +func (o *BackupConfiguration) GetRetentionDays() int32 { + if o == nil { + var ret int32 + return ret + } + + return o.RetentionDays +} + +// SetRetentionDays sets field value. +func (o *BackupConfiguration) SetRetentionDays(v int32) { + o.RetentionDays = v +} diff --git a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_update_backup_configuration_spec.go b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_update_backup_configuration_spec.go new file mode 100644 index 00000000..8f9f2479 --- /dev/null +++ b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/model_update_backup_configuration_spec.go @@ -0,0 +1,80 @@ +// Copyright 2023 The Cockroach Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. +// CockroachDB Cloud API +// API version: 2024-09-16 + +package client + +// UpdateBackupConfigurationSpec struct for UpdateBackupConfigurationSpec. +type UpdateBackupConfigurationSpec struct { + // Indicates whether the backup configuration is enabled and can run. If set to true, frequency_minutes and retention_days must be set or will be inherited from the last enabled configuration. + Enabled *bool `json:"enabled,omitempty"` + // The frequency of backups in minutes. Can be one of [5, 10, 15, 30, 60, 240, 1440]. + FrequencyMinutes *int32 `json:"frequency_minutes,omitempty"` + // The number of days to retain backups for. Can be one of [2, 7, 30, 90, 365]. Can only be set once, further changes require opening a support ticket. + RetentionDays *int32 `json:"retention_days,omitempty"` +} + +// NewUpdateBackupConfigurationSpec instantiates a new UpdateBackupConfigurationSpec object. +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewUpdateBackupConfigurationSpec() *UpdateBackupConfigurationSpec { + p := UpdateBackupConfigurationSpec{} + return &p +} + +// GetEnabled returns the Enabled field value if set, zero value otherwise. +func (o *UpdateBackupConfigurationSpec) GetEnabled() bool { + if o == nil || o.Enabled == nil { + var ret bool + return ret + } + return *o.Enabled +} + +// SetEnabled gets a reference to the given bool and assigns it to the Enabled field. +func (o *UpdateBackupConfigurationSpec) SetEnabled(v bool) { + o.Enabled = &v +} + +// GetFrequencyMinutes returns the FrequencyMinutes field value if set, zero value otherwise. +func (o *UpdateBackupConfigurationSpec) GetFrequencyMinutes() int32 { + if o == nil || o.FrequencyMinutes == nil { + var ret int32 + return ret + } + return *o.FrequencyMinutes +} + +// SetFrequencyMinutes gets a reference to the given int32 and assigns it to the FrequencyMinutes field. +func (o *UpdateBackupConfigurationSpec) SetFrequencyMinutes(v int32) { + o.FrequencyMinutes = &v +} + +// GetRetentionDays returns the RetentionDays field value if set, zero value otherwise. +func (o *UpdateBackupConfigurationSpec) GetRetentionDays() int32 { + if o == nil || o.RetentionDays == nil { + var ret int32 + return ret + } + return *o.RetentionDays +} + +// SetRetentionDays gets a reference to the given int32 and assigns it to the RetentionDays field. +func (o *UpdateBackupConfigurationSpec) SetRetentionDays(v int32) { + o.RetentionDays = &v +} diff --git a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/service.go b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/service.go index 001a3cc4..10b95248 100644 --- a/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/service.go +++ b/vendor/github.com/cockroachdb/cockroach-cloud-sdk-go/v4/pkg/client/service.go @@ -52,6 +52,15 @@ type Service interface { // List audit logs ListAuditLogs(ctx _context.Context, options *ListAuditLogsOptions) (*ListAuditLogsResponse, *_nethttp.Response, error) + // + // BackupRestore + // + + // Get the backup configuration for a cluster + GetBackupConfiguration(ctx _context.Context, clusterId string) (*BackupConfiguration, *_nethttp.Response, error) + // Update the backup configuration for a cluster + UpdateBackupConfiguration(ctx _context.Context, clusterId string, updateBackupConfigurationSpec *UpdateBackupConfigurationSpec) (*BackupConfiguration, *_nethttp.Response, error) + // // Billing // @@ -1255,6 +1264,277 @@ func (a *ServiceImpl) ListAuditLogs( return &localVarReturnValue, localVarHTTPResponse, nil } +// GetBackupConfiguration executes the request. +func (a *ServiceImpl) GetBackupConfiguration( + ctx _context.Context, clusterId string, +) (*BackupConfiguration, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath := a.client.cfg.ServerURL + + localVarPath := localBasePath + "/api/v1/clusters/{cluster_id}/backups/config" + localVarPath = strings.Replace(localVarPath, "{"+"cluster_id"+"}", _neturl.PathEscape(parameterToString(clusterId, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + + // Determine the Content-Type header. + localVarHTTPContentTypes := []string{} + + // Set Content-Type header. + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // Determine the Accept header. + localVarHTTPHeaderAccepts := []string{"application/json"} + + // Set Accept header. + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = _ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := Error{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + var v Status + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + + var localVarReturnValue BackupConfiguration + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := Error{ + body: localVarBody, + error: err.Error(), + } + return &localVarReturnValue, localVarHTTPResponse, newErr + } + + return &localVarReturnValue, localVarHTTPResponse, nil +} + +// UpdateBackupConfiguration executes the request. +func (a *ServiceImpl) UpdateBackupConfiguration( + ctx _context.Context, clusterId string, updateBackupConfigurationSpec *UpdateBackupConfigurationSpec, +) (*BackupConfiguration, *_nethttp.Response, error) { + var ( + localVarHTTPMethod = _nethttp.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath := a.client.cfg.ServerURL + + localVarPath := localBasePath + "/api/v1/clusters/{cluster_id}/backups/config" + localVarPath = strings.Replace(localVarPath, "{"+"cluster_id"+"}", _neturl.PathEscape(parameterToString(clusterId, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := _neturl.Values{} + localVarFormParams := _neturl.Values{} + if updateBackupConfigurationSpec == nil { + return nil, nil, reportError("updateBackupConfigurationSpec is required and must be specified") + } + + // Determine the Content-Type header. + localVarHTTPContentTypes := []string{"application/json"} + + // Set Content-Type header. + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // Determine the Accept header. + localVarHTTPHeaderAccepts := []string{"application/json"} + + // Set Accept header. + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // Body params. + localVarPostBody = updateBackupConfigurationSpec + req, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return nil, localVarHTTPResponse, err + } + + localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = _ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return nil, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := Error{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v interface{} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + var v Status + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return nil, localVarHTTPResponse, newErr + } + newErr.model = v + return nil, localVarHTTPResponse, newErr + } + + var localVarReturnValue BackupConfiguration + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := Error{ + body: localVarBody, + error: err.Error(), + } + return &localVarReturnValue, localVarHTTPResponse, newErr + } + + return &localVarReturnValue, localVarHTTPResponse, nil +} + // GetInvoice executes the request. func (a *ServiceImpl) GetInvoice( ctx _context.Context, invoiceId string,