diff --git a/.changelog/6845.txt b/.changelog/6845.txt new file mode 100644 index 0000000000..8b7f231855 --- /dev/null +++ b/.changelog/6845.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +`google_gke_backup_backup_plan` (beta) +``` diff --git a/google-beta/config.go b/google-beta/config.go index 651094fd2e..301d9d8988 100644 --- a/google-beta/config.go +++ b/google-beta/config.go @@ -223,6 +223,7 @@ type Config struct { FirebaseHostingBasePath string FirestoreBasePath string GameServicesBasePath string + GKEBackupBasePath string GKEHubBasePath string HealthcareBasePath string IAM2BasePath string @@ -334,6 +335,7 @@ const FirebaseBasePathKey = "Firebase" const FirebaseHostingBasePathKey = "FirebaseHosting" const FirestoreBasePathKey = "Firestore" const GameServicesBasePathKey = "GameServices" +const GKEBackupBasePathKey = "GKEBackup" const GKEHubBasePathKey = "GKEHub" const HealthcareBasePathKey = "Healthcare" const IAM2BasePathKey = "IAM2" @@ -439,6 +441,7 @@ var DefaultBasePaths = map[string]string{ FirebaseHostingBasePathKey: "https://firebasehosting.googleapis.com/v1beta1/", FirestoreBasePathKey: "https://firestore.googleapis.com/v1/", GameServicesBasePathKey: "https://gameservices.googleapis.com/v1beta/", + GKEBackupBasePathKey: "https://gkebackup.googleapis.com/v1/", GKEHubBasePathKey: "https://gkehub.googleapis.com/v1beta1/", HealthcareBasePathKey: "https://healthcare.googleapis.com/v1beta1/", IAM2BasePathKey: "https://iam.googleapis.com/v2beta/", @@ -1320,6 +1323,7 @@ func ConfigureBasePaths(c *Config) { c.FirebaseHostingBasePath = DefaultBasePaths[FirebaseHostingBasePathKey] c.FirestoreBasePath = DefaultBasePaths[FirestoreBasePathKey] c.GameServicesBasePath = DefaultBasePaths[GameServicesBasePathKey] + c.GKEBackupBasePath = DefaultBasePaths[GKEBackupBasePathKey] c.GKEHubBasePath = DefaultBasePaths[GKEHubBasePathKey] c.HealthcareBasePath = DefaultBasePaths[HealthcareBasePathKey] c.IAM2BasePath = DefaultBasePaths[IAM2BasePathKey] diff --git a/google-beta/config_test_utils.go b/google-beta/config_test_utils.go index 81d74ec883..0a751af1dc 100644 --- a/google-beta/config_test_utils.go +++ b/google-beta/config_test_utils.go @@ -69,6 +69,7 @@ func configureTestBasePaths(c *Config, url string) { c.FirebaseHostingBasePath = url c.FirestoreBasePath = url c.GameServicesBasePath = url + c.GKEBackupBasePath = url c.GKEHubBasePath = url c.HealthcareBasePath = url c.IAM2BasePath = url diff --git a/google-beta/gke_backup_operation.go b/google-beta/gke_backup_operation.go new file mode 100644 index 0000000000..4966fdd216 --- /dev/null +++ b/google-beta/gke_backup_operation.go @@ -0,0 +1,75 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "encoding/json" + "fmt" + "time" +) + +type GKEBackupOperationWaiter struct { + Config *Config + UserAgent string + Project string + CommonOperationWaiter +} + +func (w *GKEBackupOperationWaiter) QueryOp() (interface{}, error) { + if w == nil { + return nil, fmt.Errorf("Cannot query operation, it's unset or nil.") + } + // Returns the proper get. + url := fmt.Sprintf("%s%s", w.Config.GKEBackupBasePath, w.CommonOperationWaiter.Op.Name) + + return sendRequest(w.Config, "GET", w.Project, url, w.UserAgent, nil) +} + +func createGKEBackupWaiter(config *Config, op map[string]interface{}, project, activity, userAgent string) (*GKEBackupOperationWaiter, error) { + w := &GKEBackupOperationWaiter{ + Config: config, + UserAgent: userAgent, + Project: project, + } + if err := w.CommonOperationWaiter.SetOp(op); err != nil { + return nil, err + } + return w, nil +} + +// nolint: deadcode,unused +func gKEBackupOperationWaitTimeWithResponse(config *Config, op map[string]interface{}, response *map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + w, err := createGKEBackupWaiter(config, op, project, activity, userAgent) + if err != nil { + return err + } + if err := OperationWait(w, activity, timeout, config.PollInterval); err != nil { + return err + } + return json.Unmarshal([]byte(w.CommonOperationWaiter.Op.Response), response) +} + +func gKEBackupOperationWaitTime(config *Config, op map[string]interface{}, project, activity, userAgent string, timeout time.Duration) error { + if val, ok := op["name"]; !ok || val == "" { + // This was a synchronous call - there is no operation to wait for. + return nil + } + w, err := createGKEBackupWaiter(config, op, project, activity, userAgent) + if err != nil { + // If w is nil, the op was synchronous. + return err + } + return OperationWait(w, activity, timeout, config.PollInterval) +} diff --git a/google-beta/provider.go b/google-beta/provider.go index 58817b487f..befd36a401 100644 --- a/google-beta/provider.go +++ b/google-beta/provider.go @@ -557,6 +557,14 @@ func Provider() *schema.Provider { "GOOGLE_GAME_SERVICES_CUSTOM_ENDPOINT", }, DefaultBasePaths[GameServicesBasePathKey]), }, + "gke_backup_custom_endpoint": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateCustomEndpoint, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_GKE_BACKUP_CUSTOM_ENDPOINT", + }, DefaultBasePaths[GKEBackupBasePathKey]), + }, "gke_hub_custom_endpoint": { Type: schema.TypeString, Optional: true, @@ -1039,9 +1047,9 @@ func Provider() *schema.Provider { return provider } -// Generated resources: 287 +// Generated resources: 288 // Generated IAM resources: 189 -// Total generated resources: 476 +// Total generated resources: 477 func ResourceMap() map[string]*schema.Resource { resourceMap, _ := ResourceMapWithErrors() return resourceMap @@ -1348,6 +1356,7 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { "google_game_services_game_server_deployment": resourceGameServicesGameServerDeployment(), "google_game_services_game_server_config": resourceGameServicesGameServerConfig(), "google_game_services_game_server_deployment_rollout": resourceGameServicesGameServerDeploymentRollout(), + "google_gke_backup_backup_plan": resourceGKEBackupBackupPlan(), "google_gke_hub_membership": resourceGKEHubMembership(), "google_gke_hub_membership_iam_binding": ResourceIamBinding(GKEHubMembershipIamSchema, GKEHubMembershipIamUpdaterProducer, GKEHubMembershipIdParseFunc), "google_gke_hub_membership_iam_member": ResourceIamMember(GKEHubMembershipIamSchema, GKEHubMembershipIamUpdaterProducer, GKEHubMembershipIdParseFunc), @@ -1807,6 +1816,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, p *schema.Pr config.FirebaseHostingBasePath = d.Get("firebase_hosting_custom_endpoint").(string) config.FirestoreBasePath = d.Get("firestore_custom_endpoint").(string) config.GameServicesBasePath = d.Get("game_services_custom_endpoint").(string) + config.GKEBackupBasePath = d.Get("gke_backup_custom_endpoint").(string) config.GKEHubBasePath = d.Get("gke_hub_custom_endpoint").(string) config.HealthcareBasePath = d.Get("healthcare_custom_endpoint").(string) config.IAM2BasePath = d.Get("iam2_custom_endpoint").(string) diff --git a/google-beta/resource_gke_backup_backup_plan.go b/google-beta/resource_gke_backup_backup_plan.go new file mode 100644 index 0000000000..7217630782 --- /dev/null +++ b/google-beta/resource_gke_backup_backup_plan.go @@ -0,0 +1,1153 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "log" + "reflect" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGKEBackupBackupPlan() *schema.Resource { + return &schema.Resource{ + Create: resourceGKEBackupBackupPlanCreate, + Read: resourceGKEBackupBackupPlanRead, + Update: resourceGKEBackupBackupPlanUpdate, + Delete: resourceGKEBackupBackupPlanDelete, + + Importer: &schema.ResourceImporter{ + State: resourceGKEBackupBackupPlanImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(20 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), + Delete: schema.DefaultTimeout(20 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "cluster": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The source cluster from which Backups will be created via this BackupPlan.`, + }, + "location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The region of the Backup Plan.`, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: `The full name of the BackupPlan Resource.`, + }, + "backup_config": { + Type: schema.TypeList, + Optional: true, + Description: `Defines the configuration of Backups created via this BackupPlan.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "all_namespaces": { + Type: schema.TypeBool, + Optional: true, + Description: `If True, include all namespaced resources.`, + ExactlyOneOf: []string{"backup_config.0.all_namespaces", "backup_config.0.selected_namespaces", "backup_config.0.selected_applications"}, + }, + "encryption_key": { + Type: schema.TypeList, + Optional: true, + Description: `This defines a customer managed encryption key that will be used to encrypt the "config" +portion (the Kubernetes resources) of Backups created via this plan.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "gcp_kms_encryption_key": { + Type: schema.TypeString, + Required: true, + Description: `Google Cloud KMS encryption key. Format: projects/*/locations/*/keyRings/*/cryptoKeys/*`, + }, + }, + }, + }, + "include_secrets": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `This flag specifies whether Kubernetes Secret resources should be included +when they fall into the scope of Backups.`, + }, + "include_volume_data": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `This flag specifies whether volume data should be backed up when PVCs are +included in the scope of a Backup.`, + }, + "selected_applications": { + Type: schema.TypeList, + Optional: true, + Description: `A list of namespaced Kubernetes Resources.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "namespaced_names": { + Type: schema.TypeList, + Required: true, + Description: `A list of namespaced Kubernetes resources.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: `The name of a Kubernetes Resource.`, + }, + "namespace": { + Type: schema.TypeString, + Required: true, + Description: `The namespace of a Kubernetes Resource.`, + }, + }, + }, + }, + }, + }, + ExactlyOneOf: []string{"backup_config.0.all_namespaces", "backup_config.0.selected_namespaces", "backup_config.0.selected_applications"}, + }, + "selected_namespaces": { + Type: schema.TypeList, + Optional: true, + Description: `If set, include just the resources in the listed namespaces.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "namespaces": { + Type: schema.TypeList, + Required: true, + Description: `A list of Kubernetes Namespaces.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + ExactlyOneOf: []string{"backup_config.0.all_namespaces", "backup_config.0.selected_namespaces", "backup_config.0.selected_applications"}, + }, + }, + }, + }, + "backup_schedule": { + Type: schema.TypeList, + Optional: true, + Description: `Defines a schedule for automatic Backup creation via this BackupPlan.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cron_schedule": { + Type: schema.TypeString, + Optional: true, + Description: `A standard cron string that defines a repeating schedule for +creating Backups via this BackupPlan. +If this is defined, then backupRetainDays must also be defined.`, + }, + "paused": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `This flag denotes whether automatic Backup creation is paused for this BackupPlan.`, + }, + }, + }, + }, + "deactivated": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `This flag indicates whether this BackupPlan has been deactivated. +Setting this field to True locks the BackupPlan such that no further updates will be allowed +(except deletes), including the deactivated field itself. It also prevents any new Backups +from being created via this BackupPlan (including scheduled Backups).`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `User specified descriptive string for this BackupPlan.`, + }, + "labels": { + Type: schema.TypeMap, + Optional: true, + Description: `Description: A set of custom labels supplied by the user. +A list of key->value pairs. +Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }.`, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "retention_policy": { + Type: schema.TypeList, + Optional: true, + Description: `RetentionPolicy governs lifecycle of Backups created under this plan.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "backup_delete_lock_days": { + Type: schema.TypeInt, + Computed: true, + Optional: true, + Description: `Minimum age for a Backup created via this BackupPlan (in days). +Must be an integer value between 0-90 (inclusive). +A Backup created under this BackupPlan will not be deletable +until it reaches Backup's (create time + backup_delete_lock_days). +Updating this field of a BackupPlan does not affect existing Backups. +Backups created after a successful update will inherit this new value.`, + }, + "backup_retain_days": { + Type: schema.TypeInt, + Computed: true, + Optional: true, + Description: `The default maximum age of a Backup created via this BackupPlan. +This field MUST be an integer value >= 0 and <= 365. If specified, +a Backup created under this BackupPlan will be automatically deleted +after its age reaches (createTime + backupRetainDays). +If not specified, Backups created under this BackupPlan will NOT be +subject to automatic deletion. Updating this field does NOT affect +existing Backups under it. Backups created AFTER a successful update +will automatically pick up the new value. +NOTE: backupRetainDays must be >= backupDeleteLockDays. +If cronSchedule is defined, then this must be <= 360 * the creation interval.]`, + }, + "locked": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `This flag denotes whether the retention policy of this BackupPlan is locked. +If set to True, no further update is allowed on this policy, including +the locked field itself.`, + }, + }, + }, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + Description: `etag is used for optimistic concurrency control as a way to help prevent simultaneous +updates of a backup plan from overwriting each other. It is strongly suggested that +systems make use of the 'etag' in the read-modify-write cycle to perform BackupPlan updates +in order to avoid race conditions: An etag is returned in the response to backupPlans.get, +and systems are expected to put that etag in the request to backupPlans.patch or +backupPlans.delete to ensure that their change will be applied to the same version of the resource.`, + }, + "protected_pod_count": { + Type: schema.TypeInt, + Computed: true, + Description: `The number of Kubernetes Pods backed up in the last successful Backup created via this BackupPlan.`, + }, + "uid": { + Type: schema.TypeString, + Computed: true, + Description: `Server generated, unique identifier of UUID format.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + UseJSONNumber: true, + } +} + +func resourceGKEBackupBackupPlanCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + obj := make(map[string]interface{}) + nameProp, err := expandGKEBackupBackupPlanName(d.Get("name"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("name"); !isEmptyValue(reflect.ValueOf(nameProp)) && (ok || !reflect.DeepEqual(v, nameProp)) { + obj["name"] = nameProp + } + descriptionProp, err := expandGKEBackupBackupPlanDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(descriptionProp)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + clusterProp, err := expandGKEBackupBackupPlanCluster(d.Get("cluster"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("cluster"); !isEmptyValue(reflect.ValueOf(clusterProp)) && (ok || !reflect.DeepEqual(v, clusterProp)) { + obj["cluster"] = clusterProp + } + retentionPolicyProp, err := expandGKEBackupBackupPlanRetentionPolicy(d.Get("retention_policy"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("retention_policy"); !isEmptyValue(reflect.ValueOf(retentionPolicyProp)) && (ok || !reflect.DeepEqual(v, retentionPolicyProp)) { + obj["retentionPolicy"] = retentionPolicyProp + } + labelsProp, err := expandGKEBackupBackupPlanLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(labelsProp)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + backupScheduleProp, err := expandGKEBackupBackupPlanBackupSchedule(d.Get("backup_schedule"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("backup_schedule"); !isEmptyValue(reflect.ValueOf(backupScheduleProp)) && (ok || !reflect.DeepEqual(v, backupScheduleProp)) { + obj["backupSchedule"] = backupScheduleProp + } + deactivatedProp, err := expandGKEBackupBackupPlanDeactivated(d.Get("deactivated"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("deactivated"); !isEmptyValue(reflect.ValueOf(deactivatedProp)) && (ok || !reflect.DeepEqual(v, deactivatedProp)) { + obj["deactivated"] = deactivatedProp + } + backupConfigProp, err := expandGKEBackupBackupPlanBackupConfig(d.Get("backup_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("backup_config"); !isEmptyValue(reflect.ValueOf(backupConfigProp)) && (ok || !reflect.DeepEqual(v, backupConfigProp)) { + obj["backupConfig"] = backupConfigProp + } + + url, err := replaceVars(d, config, "{{GKEBackupBasePath}}projects/{{project}}/locations/{{location}}/backupPlans?backupPlanId={{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new BackupPlan: %#v", obj) + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for BackupPlan: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return fmt.Errorf("Error creating BackupPlan: %s", err) + } + + // Store the ID now + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + // Use the resource in the operation response to populate + // identity fields and d.Id() before read + var opRes map[string]interface{} + err = gKEBackupOperationWaitTimeWithResponse( + config, res, &opRes, project, "Creating BackupPlan", userAgent, + d.Timeout(schema.TimeoutCreate)) + if err != nil { + // The resource didn't actually create + d.SetId("") + + return fmt.Errorf("Error waiting to create BackupPlan: %s", err) + } + + if err := d.Set("name", flattenGKEBackupBackupPlanName(opRes["name"], d, config)); err != nil { + return err + } + + // This may have caused the ID to update - update it if so. + id, err = replaceVars(d, config, "projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + log.Printf("[DEBUG] Finished creating BackupPlan %q: %#v", d.Id(), res) + + return resourceGKEBackupBackupPlanRead(d, meta) +} + +func resourceGKEBackupBackupPlanRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + url, err := replaceVars(d, config, "{{GKEBackupBasePath}}projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for BackupPlan: %s", err) + } + billingProject = project + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "GET", billingProject, url, userAgent, nil) + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("GKEBackupBackupPlan %q", d.Id())) + } + + if err := d.Set("project", project); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + + if err := d.Set("name", flattenGKEBackupBackupPlanName(res["name"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("uid", flattenGKEBackupBackupPlanUid(res["uid"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("description", flattenGKEBackupBackupPlanDescription(res["description"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("cluster", flattenGKEBackupBackupPlanCluster(res["cluster"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("retention_policy", flattenGKEBackupBackupPlanRetentionPolicy(res["retentionPolicy"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("labels", flattenGKEBackupBackupPlanLabels(res["labels"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("backup_schedule", flattenGKEBackupBackupPlanBackupSchedule(res["backupSchedule"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("etag", flattenGKEBackupBackupPlanEtag(res["etag"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("deactivated", flattenGKEBackupBackupPlanDeactivated(res["deactivated"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("backup_config", flattenGKEBackupBackupPlanBackupConfig(res["backupConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + if err := d.Set("protected_pod_count", flattenGKEBackupBackupPlanProtectedPodCount(res["protectedPodCount"], d, config)); err != nil { + return fmt.Errorf("Error reading BackupPlan: %s", err) + } + + return nil +} + +func resourceGKEBackupBackupPlanUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for BackupPlan: %s", err) + } + billingProject = project + + obj := make(map[string]interface{}) + descriptionProp, err := expandGKEBackupBackupPlanDescription(d.Get("description"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("description"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, descriptionProp)) { + obj["description"] = descriptionProp + } + retentionPolicyProp, err := expandGKEBackupBackupPlanRetentionPolicy(d.Get("retention_policy"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("retention_policy"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, retentionPolicyProp)) { + obj["retentionPolicy"] = retentionPolicyProp + } + labelsProp, err := expandGKEBackupBackupPlanLabels(d.Get("labels"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("labels"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, labelsProp)) { + obj["labels"] = labelsProp + } + backupScheduleProp, err := expandGKEBackupBackupPlanBackupSchedule(d.Get("backup_schedule"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("backup_schedule"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, backupScheduleProp)) { + obj["backupSchedule"] = backupScheduleProp + } + deactivatedProp, err := expandGKEBackupBackupPlanDeactivated(d.Get("deactivated"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("deactivated"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, deactivatedProp)) { + obj["deactivated"] = deactivatedProp + } + backupConfigProp, err := expandGKEBackupBackupPlanBackupConfig(d.Get("backup_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("backup_config"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, backupConfigProp)) { + obj["backupConfig"] = backupConfigProp + } + + url, err := replaceVars(d, config, "{{GKEBackupBasePath}}projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return err + } + + log.Printf("[DEBUG] Updating BackupPlan %q: %#v", d.Id(), obj) + updateMask := []string{} + + if d.HasChange("description") { + updateMask = append(updateMask, "description") + } + + if d.HasChange("retention_policy") { + updateMask = append(updateMask, "retentionPolicy") + } + + if d.HasChange("labels") { + updateMask = append(updateMask, "labels") + } + + if d.HasChange("backup_schedule") { + updateMask = append(updateMask, "backupSchedule") + } + + if d.HasChange("deactivated") { + updateMask = append(updateMask, "deactivated") + } + + if d.HasChange("backup_config") { + updateMask = append(updateMask, "backupConfig") + } + // updateMask is a URL parameter but not present in the schema, so replaceVars + // won't set it + url, err = addQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) + if err != nil { + return err + } + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return fmt.Errorf("Error updating BackupPlan %q: %s", d.Id(), err) + } else { + log.Printf("[DEBUG] Finished updating BackupPlan %q: %#v", d.Id(), res) + } + + err = gKEBackupOperationWaitTime( + config, res, project, "Updating BackupPlan", userAgent, + d.Timeout(schema.TimeoutUpdate)) + + if err != nil { + return err + } + + return resourceGKEBackupBackupPlanRead(d, meta) +} + +func resourceGKEBackupBackupPlanDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + userAgent, err := generateUserAgentString(d, config.userAgent) + if err != nil { + return err + } + + billingProject := "" + + project, err := getProject(d, config) + if err != nil { + return fmt.Errorf("Error fetching project for BackupPlan: %s", err) + } + billingProject = project + + url, err := replaceVars(d, config, "{{GKEBackupBasePath}}projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return err + } + + var obj map[string]interface{} + log.Printf("[DEBUG] Deleting BackupPlan %q", d.Id()) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "DELETE", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete)) + if err != nil { + return handleNotFoundError(err, d, "BackupPlan") + } + + err = gKEBackupOperationWaitTime( + config, res, project, "Deleting BackupPlan", userAgent, + d.Timeout(schema.TimeoutDelete)) + + if err != nil { + return err + } + + log.Printf("[DEBUG] Finished deleting BackupPlan %q: %#v", d.Id(), res) + return nil +} + +func resourceGKEBackupBackupPlanImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + config := meta.(*Config) + if err := parseImportId([]string{ + "projects/(?P[^/]+)/locations/(?P[^/]+)/backupPlans/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)/(?P[^/]+)", + "(?P[^/]+)/(?P[^/]+)", + }, d, config); err != nil { + return nil, err + } + + // Replace import id for the resource id + id, err := replaceVars(d, config, "projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return nil, fmt.Errorf("Error constructing id: %s", err) + } + d.SetId(id) + + return []*schema.ResourceData{d}, nil +} + +func flattenGKEBackupBackupPlanName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + return NameFromSelfLinkStateFunc(v) +} + +func flattenGKEBackupBackupPlanUid(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanDescription(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanCluster(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanRetentionPolicy(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["backup_delete_lock_days"] = + flattenGKEBackupBackupPlanRetentionPolicyBackupDeleteLockDays(original["backupDeleteLockDays"], d, config) + transformed["backup_retain_days"] = + flattenGKEBackupBackupPlanRetentionPolicyBackupRetainDays(original["backupRetainDays"], d, config) + transformed["locked"] = + flattenGKEBackupBackupPlanRetentionPolicyLocked(original["locked"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanRetentionPolicyBackupDeleteLockDays(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := stringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenGKEBackupBackupPlanRetentionPolicyBackupRetainDays(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := stringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenGKEBackupBackupPlanRetentionPolicyLocked(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanLabels(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupSchedule(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["cron_schedule"] = + flattenGKEBackupBackupPlanBackupScheduleCronSchedule(original["cronSchedule"], d, config) + transformed["paused"] = + flattenGKEBackupBackupPlanBackupSchedulePaused(original["paused"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanBackupScheduleCronSchedule(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupSchedulePaused(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanEtag(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanDeactivated(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfig(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["include_volume_data"] = + flattenGKEBackupBackupPlanBackupConfigIncludeVolumeData(original["includeVolumeData"], d, config) + transformed["include_secrets"] = + flattenGKEBackupBackupPlanBackupConfigIncludeSecrets(original["includeSecrets"], d, config) + transformed["encryption_key"] = + flattenGKEBackupBackupPlanBackupConfigEncryptionKey(original["encryptionKey"], d, config) + transformed["all_namespaces"] = + flattenGKEBackupBackupPlanBackupConfigAllNamespaces(original["allNamespaces"], d, config) + transformed["selected_namespaces"] = + flattenGKEBackupBackupPlanBackupConfigSelectedNamespaces(original["selectedNamespaces"], d, config) + transformed["selected_applications"] = + flattenGKEBackupBackupPlanBackupConfigSelectedApplications(original["selectedApplications"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanBackupConfigIncludeVolumeData(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigIncludeSecrets(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigEncryptionKey(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["gcp_kms_encryption_key"] = + flattenGKEBackupBackupPlanBackupConfigEncryptionKeyGcpKmsEncryptionKey(original["gcpKmsEncryptionKey"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanBackupConfigEncryptionKeyGcpKmsEncryptionKey(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigAllNamespaces(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigSelectedNamespaces(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["namespaces"] = + flattenGKEBackupBackupPlanBackupConfigSelectedNamespacesNamespaces(original["namespaces"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanBackupConfigSelectedNamespacesNamespaces(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigSelectedApplications(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["namespaced_names"] = + flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNames(original["namespacedNames"], d, config) + return []interface{}{transformed} +} +func flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNames(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return v + } + l := v.([]interface{}) + transformed := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + transformed = append(transformed, map[string]interface{}{ + "namespace": flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesNamespace(original["namespace"], d, config), + "name": flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesName(original["name"], d, config), + }) + } + return transformed +} +func flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesNamespace(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesName(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenGKEBackupBackupPlanProtectedPodCount(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := stringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func expandGKEBackupBackupPlanName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return replaceVars(d, config, "projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") +} + +func expandGKEBackupBackupPlanDescription(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanCluster(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanRetentionPolicy(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedBackupDeleteLockDays, err := expandGKEBackupBackupPlanRetentionPolicyBackupDeleteLockDays(original["backup_delete_lock_days"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedBackupDeleteLockDays); val.IsValid() && !isEmptyValue(val) { + transformed["backupDeleteLockDays"] = transformedBackupDeleteLockDays + } + + transformedBackupRetainDays, err := expandGKEBackupBackupPlanRetentionPolicyBackupRetainDays(original["backup_retain_days"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedBackupRetainDays); val.IsValid() && !isEmptyValue(val) { + transformed["backupRetainDays"] = transformedBackupRetainDays + } + + transformedLocked, err := expandGKEBackupBackupPlanRetentionPolicyLocked(original["locked"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedLocked); val.IsValid() && !isEmptyValue(val) { + transformed["locked"] = transformedLocked + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanRetentionPolicyBackupDeleteLockDays(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanRetentionPolicyBackupRetainDays(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanRetentionPolicyLocked(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanLabels(v interface{}, d TerraformResourceData, config *Config) (map[string]string, error) { + if v == nil { + return map[string]string{}, nil + } + m := make(map[string]string) + for k, val := range v.(map[string]interface{}) { + m[k] = val.(string) + } + return m, nil +} + +func expandGKEBackupBackupPlanBackupSchedule(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedCronSchedule, err := expandGKEBackupBackupPlanBackupScheduleCronSchedule(original["cron_schedule"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedCronSchedule); val.IsValid() && !isEmptyValue(val) { + transformed["cronSchedule"] = transformedCronSchedule + } + + transformedPaused, err := expandGKEBackupBackupPlanBackupSchedulePaused(original["paused"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedPaused); val.IsValid() && !isEmptyValue(val) { + transformed["paused"] = transformedPaused + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanBackupScheduleCronSchedule(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupSchedulePaused(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanDeactivated(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfig(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedIncludeVolumeData, err := expandGKEBackupBackupPlanBackupConfigIncludeVolumeData(original["include_volume_data"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedIncludeVolumeData); val.IsValid() && !isEmptyValue(val) { + transformed["includeVolumeData"] = transformedIncludeVolumeData + } + + transformedIncludeSecrets, err := expandGKEBackupBackupPlanBackupConfigIncludeSecrets(original["include_secrets"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedIncludeSecrets); val.IsValid() && !isEmptyValue(val) { + transformed["includeSecrets"] = transformedIncludeSecrets + } + + transformedEncryptionKey, err := expandGKEBackupBackupPlanBackupConfigEncryptionKey(original["encryption_key"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedEncryptionKey); val.IsValid() && !isEmptyValue(val) { + transformed["encryptionKey"] = transformedEncryptionKey + } + + transformedAllNamespaces, err := expandGKEBackupBackupPlanBackupConfigAllNamespaces(original["all_namespaces"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAllNamespaces); val.IsValid() && !isEmptyValue(val) { + transformed["allNamespaces"] = transformedAllNamespaces + } + + transformedSelectedNamespaces, err := expandGKEBackupBackupPlanBackupConfigSelectedNamespaces(original["selected_namespaces"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSelectedNamespaces); val.IsValid() && !isEmptyValue(val) { + transformed["selectedNamespaces"] = transformedSelectedNamespaces + } + + transformedSelectedApplications, err := expandGKEBackupBackupPlanBackupConfigSelectedApplications(original["selected_applications"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedSelectedApplications); val.IsValid() && !isEmptyValue(val) { + transformed["selectedApplications"] = transformedSelectedApplications + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanBackupConfigIncludeVolumeData(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigIncludeSecrets(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigEncryptionKey(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedGcpKmsEncryptionKey, err := expandGKEBackupBackupPlanBackupConfigEncryptionKeyGcpKmsEncryptionKey(original["gcp_kms_encryption_key"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedGcpKmsEncryptionKey); val.IsValid() && !isEmptyValue(val) { + transformed["gcpKmsEncryptionKey"] = transformedGcpKmsEncryptionKey + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanBackupConfigEncryptionKeyGcpKmsEncryptionKey(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigAllNamespaces(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedNamespaces(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedNamespaces, err := expandGKEBackupBackupPlanBackupConfigSelectedNamespacesNamespaces(original["namespaces"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNamespaces); val.IsValid() && !isEmptyValue(val) { + transformed["namespaces"] = transformedNamespaces + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedNamespacesNamespaces(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedApplications(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedNamespacedNames, err := expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNames(original["namespaced_names"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNamespacedNames); val.IsValid() && !isEmptyValue(val) { + transformed["namespacedNames"] = transformedNamespacedNames + } + + return transformed, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNames(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + l := v.([]interface{}) + req := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedNamespace, err := expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesNamespace(original["namespace"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedNamespace); val.IsValid() && !isEmptyValue(val) { + transformed["namespace"] = transformedNamespace + } + + transformedName, err := expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesName(original["name"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedName); val.IsValid() && !isEmptyValue(val) { + transformed["name"] = transformedName + } + + req = append(req, transformed) + } + return req, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesNamespace(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandGKEBackupBackupPlanBackupConfigSelectedApplicationsNamespacedNamesName(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google-beta/resource_gke_backup_backup_plan_generated_test.go b/google-beta/resource_gke_backup_backup_plan_generated_test.go new file mode 100644 index 0000000000..c92d9543ce --- /dev/null +++ b/google-beta/resource_gke_backup_backup_plan_generated_test.go @@ -0,0 +1,319 @@ +// ---------------------------------------------------------------------------- +// +// *** AUTO GENERATED CODE *** Type: MMv1 *** +// +// ---------------------------------------------------------------------------- +// +// This file is automatically generated by Magic Modules and manual +// changes will be clobbered when the file is regenerated. +// +// Please read more about how to change this file in +// .github/CONTRIBUTING.md. +// +// ---------------------------------------------------------------------------- + +package google + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccGKEBackupBackupPlan_gkebackupBackupplanBasicExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": getTestProjectFromEnv(), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckGKEBackupBackupPlanDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccGKEBackupBackupPlan_gkebackupBackupplanBasicExample(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, + }, + }, + }) +} + +func testAccGKEBackupBackupPlan_gkebackupBackupplanBasicExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-basic-cluster%{random_suffix}" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "%{project}.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "basic" { + provider = google-beta + name = "tf-test-basic-plan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + all_namespaces = true + } +} +`, context) +} + +func TestAccGKEBackupBackupPlan_gkebackupBackupplanAutopilotExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckGKEBackupBackupPlanDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccGKEBackupBackupPlan_gkebackupBackupplanAutopilotExample(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.autopilot", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, + }, + }, + }) +} + +func testAccGKEBackupBackupPlan_gkebackupBackupplanAutopilotExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-autopilot-cluster%{random_suffix}" + location = "us-central1" + enable_autopilot = true + ip_allocation_policy { + } + release_channel { + channel = "RAPID" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "autopilot" { + provider = google-beta + name = "tf-test-autopilot-plan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + all_namespaces = true + } +} +`, context) +} + +func TestAccGKEBackupBackupPlan_gkebackupBackupplanCmekExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": getTestProjectFromEnv(), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckGKEBackupBackupPlanDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccGKEBackupBackupPlan_gkebackupBackupplanCmekExample(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.cmek", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, + }, + }, + }) +} + +func testAccGKEBackupBackupPlan_gkebackupBackupplanCmekExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-cmek-cluster%{random_suffix}" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "%{project}.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "cmek" { + provider = google-beta + name = "tf-test-cmek-plan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + selected_namespaces { + namespaces = ["default", "test"] + } + encryption_key { + gcp_kms_encryption_key = google_kms_crypto_key.crypto_key.id + } + } +} + +resource "google_kms_crypto_key" "crypto_key" { + provider = google-beta + name = "tf-test-backup-key%{random_suffix}" + key_ring = google_kms_key_ring.key_ring.id +} + +resource "google_kms_key_ring" "key_ring" { + provider = google-beta + name = "tf-test-backup-key%{random_suffix}" + location = "us-central1" +} +`, context) +} + +func TestAccGKEBackupBackupPlan_gkebackupBackupplanFullExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": getTestProjectFromEnv(), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckGKEBackupBackupPlanDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccGKEBackupBackupPlan_gkebackupBackupplanFullExample(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.full", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"location"}, + }, + }, + }) +} + +func testAccGKEBackupBackupPlan_gkebackupBackupplanFullExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-full-cluster%{random_suffix}" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "%{project}.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "full" { + provider = google-beta + name = "tf-test-full-plan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + retention_policy { + backup_delete_lock_days = 30 + backup_retain_days = 180 + } + backup_schedule { + cron_schedule = "0 9 * * 1" + } + backup_config { + include_volume_data = true + include_secrets = true + selected_applications { + namespaced_names { + name = "app1" + namespace = "ns1" + } + namespaced_names { + name = "app2" + namespace = "ns2" + } + } + } +} +`, context) +} + +func testAccCheckGKEBackupBackupPlanDestroyProducer(t *testing.T) func(s *terraform.State) error { + return func(s *terraform.State) error { + for name, rs := range s.RootModule().Resources { + if rs.Type != "google_gke_backup_backup_plan" { + continue + } + if strings.HasPrefix(name, "data.") { + continue + } + + config := googleProviderConfig(t) + + url, err := replaceVarsForTest(config, rs, "{{GKEBackupBasePath}}projects/{{project}}/locations/{{location}}/backupPlans/{{name}}") + if err != nil { + return err + } + + billingProject := "" + + if config.BillingProject != "" { + billingProject = config.BillingProject + } + + _, err = sendRequest(config, "GET", billingProject, url, config.userAgent, nil) + if err == nil { + return fmt.Errorf("GKEBackupBackupPlan still exists at %s", url) + } + } + + return nil + } +} diff --git a/google-beta/resource_gke_backup_backup_plan_test.go b/google-beta/resource_gke_backup_backup_plan_test.go new file mode 100644 index 0000000000..9390ec9c05 --- /dev/null +++ b/google-beta/resource_gke_backup_backup_plan_test.go @@ -0,0 +1,118 @@ +package google + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccGKEBackupBackupPlan_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project": getTestProjectFromEnv(), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckGKEBackupBackupPlanDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccGKEBackupBackupPlan_basic(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.backupplan", + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccGKEBackupBackupPlan_full(context), + }, + { + ResourceName: "google_gke_backup_backup_plan.backupplan", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccGKEBackupBackupPlan_basic(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-testcluster%{random_suffix}" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "%{project}.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "backupplan" { + provider = google-beta + name = "tf-test-testplan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = false + include_secrets = false + all_namespaces = true + } +} +`, context) +} + +func testAccGKEBackupBackupPlan_full(context map[string]interface{}) string { + return Nprintf(` +resource "google_container_cluster" "primary" { + provider = google-beta + name = "tf-test-testcluster%{random_suffix}" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "%{project}.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "backupplan" { + provider = google-beta + name = "tf-test-testplan%{random_suffix}" + cluster = google_container_cluster.primary.id + location = "us-central1" + retention_policy { + backup_delete_lock_days = 30 + backup_retain_days = 180 + } + backup_schedule { + cron_schedule = "0 9 * * 1" + } + backup_config { + include_volume_data = true + include_secrets = true + selected_applications { + namespaced_names { + name = "app1" + namespace = "ns1" + } + namespaced_names { + name = "app2" + namespace = "ns2" + } + } + } +} +`, context) +} diff --git a/website/docs/r/gke_backup_backup_plan.html.markdown b/website/docs/r/gke_backup_backup_plan.html.markdown new file mode 100644 index 0000000000..1b9c1250ba --- /dev/null +++ b/website/docs/r/gke_backup_backup_plan.html.markdown @@ -0,0 +1,404 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** Type: MMv1 *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in +# .github/CONTRIBUTING.md. +# +# ---------------------------------------------------------------------------- +subcategory: "Backup for GKE" +page_title: "Google: google_gke_backup_backup_plan" +description: |- + Represents a Backup Plan instance. +--- + +# google\_gke\_backup\_backup\_plan + +Represents a Backup Plan instance. + +~> **Warning:** This resource is in beta, and should be used with the terraform-provider-google-beta provider. +See [Provider Versions](https://terraform.io/docs/providers/google/guides/provider_versions.html) for more details on beta resources. + +To get more information about BackupPlan, see: + +* [API documentation](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke/reference/rest/v1/projects.locations.backupPlans) +* How-to Guides + * [Official Documentation](https://cloud.google.com/kubernetes-engine/docs/add-on/backup-for-gke) + +## Example Usage - Gkebackup Backupplan Basic + + +```hcl +resource "google_container_cluster" "primary" { + provider = google-beta + name = "basic-cluster" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "my-project-name.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "basic" { + provider = google-beta + name = "basic-plan" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + all_namespaces = true + } +} +``` + +## Example Usage - Gkebackup Backupplan Autopilot + + +```hcl +resource "google_container_cluster" "primary" { + provider = google-beta + name = "autopilot-cluster" + location = "us-central1" + enable_autopilot = true + ip_allocation_policy { + } + release_channel { + channel = "RAPID" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "autopilot" { + provider = google-beta + name = "autopilot-plan" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + all_namespaces = true + } +} +``` +## Example Usage - Gkebackup Backupplan Cmek + + +```hcl +resource "google_container_cluster" "primary" { + provider = google-beta + name = "cmek-cluster" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "my-project-name.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "cmek" { + provider = google-beta + name = "cmek-plan" + cluster = google_container_cluster.primary.id + location = "us-central1" + backup_config { + include_volume_data = true + include_secrets = true + selected_namespaces { + namespaces = ["default", "test"] + } + encryption_key { + gcp_kms_encryption_key = google_kms_crypto_key.crypto_key.id + } + } +} + +resource "google_kms_crypto_key" "crypto_key" { + provider = google-beta + name = "backup-key" + key_ring = google_kms_key_ring.key_ring.id +} + +resource "google_kms_key_ring" "key_ring" { + provider = google-beta + name = "backup-key" + location = "us-central1" +} +``` +## Example Usage - Gkebackup Backupplan Full + + +```hcl +resource "google_container_cluster" "primary" { + provider = google-beta + name = "full-cluster" + location = "us-central1" + initial_node_count = 1 + workload_identity_config { + workload_pool = "my-project-name.svc.id.goog" + } + addons_config { + gke_backup_agent_config { + enabled = true + } + } +} + +resource "google_gke_backup_backup_plan" "full" { + provider = google-beta + name = "full-plan" + cluster = google_container_cluster.primary.id + location = "us-central1" + retention_policy { + backup_delete_lock_days = 30 + backup_retain_days = 180 + } + backup_schedule { + cron_schedule = "0 9 * * 1" + } + backup_config { + include_volume_data = true + include_secrets = true + selected_applications { + namespaced_names { + name = "app1" + namespace = "ns1" + } + namespaced_names { + name = "app2" + namespace = "ns2" + } + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + + +* `name` - + (Required) + The full name of the BackupPlan Resource. + +* `cluster` - + (Required) + The source cluster from which Backups will be created via this BackupPlan. + +* `location` - + (Required) + The region of the Backup Plan. + + +- - - + + +* `description` - + (Optional) + User specified descriptive string for this BackupPlan. + +* `retention_policy` - + (Optional) + RetentionPolicy governs lifecycle of Backups created under this plan. + Structure is [documented below](#nested_retention_policy). + +* `labels` - + (Optional) + Description: A set of custom labels supplied by the user. + A list of key->value pairs. + Example: { "name": "wrench", "mass": "1.3kg", "count": "3" }. + +* `backup_schedule` - + (Optional) + Defines a schedule for automatic Backup creation via this BackupPlan. + Structure is [documented below](#nested_backup_schedule). + +* `deactivated` - + (Optional) + This flag indicates whether this BackupPlan has been deactivated. + Setting this field to True locks the BackupPlan such that no further updates will be allowed + (except deletes), including the deactivated field itself. It also prevents any new Backups + from being created via this BackupPlan (including scheduled Backups). + +* `backup_config` - + (Optional) + Defines the configuration of Backups created via this BackupPlan. + Structure is [documented below](#nested_backup_config). + +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the provider project is used. + + +The `retention_policy` block supports: + +* `backup_delete_lock_days` - + (Optional) + Minimum age for a Backup created via this BackupPlan (in days). + Must be an integer value between 0-90 (inclusive). + A Backup created under this BackupPlan will not be deletable + until it reaches Backup's (create time + backup_delete_lock_days). + Updating this field of a BackupPlan does not affect existing Backups. + Backups created after a successful update will inherit this new value. + +* `backup_retain_days` - + (Optional) + The default maximum age of a Backup created via this BackupPlan. + This field MUST be an integer value >= 0 and <= 365. If specified, + a Backup created under this BackupPlan will be automatically deleted + after its age reaches (createTime + backupRetainDays). + If not specified, Backups created under this BackupPlan will NOT be + subject to automatic deletion. Updating this field does NOT affect + existing Backups under it. Backups created AFTER a successful update + will automatically pick up the new value. + NOTE: backupRetainDays must be >= backupDeleteLockDays. + If cronSchedule is defined, then this must be <= 360 * the creation interval.] + +* `locked` - + (Optional) + This flag denotes whether the retention policy of this BackupPlan is locked. + If set to True, no further update is allowed on this policy, including + the locked field itself. + +The `backup_schedule` block supports: + +* `cron_schedule` - + (Optional) + A standard cron string that defines a repeating schedule for + creating Backups via this BackupPlan. + If this is defined, then backupRetainDays must also be defined. + +* `paused` - + (Optional) + This flag denotes whether automatic Backup creation is paused for this BackupPlan. + +The `backup_config` block supports: + +* `include_volume_data` - + (Optional) + This flag specifies whether volume data should be backed up when PVCs are + included in the scope of a Backup. + +* `include_secrets` - + (Optional) + This flag specifies whether Kubernetes Secret resources should be included + when they fall into the scope of Backups. + +* `encryption_key` - + (Optional) + This defines a customer managed encryption key that will be used to encrypt the "config" + portion (the Kubernetes resources) of Backups created via this plan. + Structure is [documented below](#nested_encryption_key). + +* `all_namespaces` - + (Optional) + If True, include all namespaced resources. + +* `selected_namespaces` - + (Optional) + If set, include just the resources in the listed namespaces. + Structure is [documented below](#nested_selected_namespaces). + +* `selected_applications` - + (Optional) + A list of namespaced Kubernetes Resources. + Structure is [documented below](#nested_selected_applications). + + +The `encryption_key` block supports: + +* `gcp_kms_encryption_key` - + (Required) + Google Cloud KMS encryption key. Format: projects/*/locations/*/keyRings/*/cryptoKeys/* + +The `selected_namespaces` block supports: + +* `namespaces` - + (Required) + A list of Kubernetes Namespaces. + +The `selected_applications` block supports: + +* `namespaced_names` - + (Required) + A list of namespaced Kubernetes resources. + Structure is [documented below](#nested_namespaced_names). + + +The `namespaced_names` block supports: + +* `namespace` - + (Required) + The namespace of a Kubernetes Resource. + +* `name` - + (Required) + The name of a Kubernetes Resource. + +## Attributes Reference + +In addition to the arguments listed above, the following computed attributes are exported: + +* `id` - an identifier for the resource with format `projects/{{project}}/locations/{{location}}/backupPlans/{{name}}` + +* `uid` - + Server generated, unique identifier of UUID format. + +* `etag` - + etag is used for optimistic concurrency control as a way to help prevent simultaneous + updates of a backup plan from overwriting each other. It is strongly suggested that + systems make use of the 'etag' in the read-modify-write cycle to perform BackupPlan updates + in order to avoid race conditions: An etag is returned in the response to backupPlans.get, + and systems are expected to put that etag in the request to backupPlans.patch or + backupPlans.delete to ensure that their change will be applied to the same version of the resource. + +* `protected_pod_count` - + The number of Kubernetes Pods backed up in the last successful Backup created via this BackupPlan. + + +## Timeouts + +This resource provides the following +[Timeouts](/docs/configuration/resources.html#timeouts) configuration options: + +- `create` - Default is 20 minutes. +- `update` - Default is 20 minutes. +- `delete` - Default is 20 minutes. + +## Import + + +BackupPlan can be imported using any of these accepted formats: + +``` +$ terraform import google_gke_backup_backup_plan.default projects/{{project}}/locations/{{location}}/backupPlans/{{name}} +$ terraform import google_gke_backup_backup_plan.default {{project}}/{{location}}/{{name}} +$ terraform import google_gke_backup_backup_plan.default {{location}}/{{name}} +``` + +## User Project Overrides + +This resource supports [User Project Overrides](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/provider_reference#user_project_override).