internal/provider/template_resource.go (522 lines of code) (raw):
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"strconv"
"github.com/antihax/optional"
backupdr "github.com/umeshkumhar/backupdr-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"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &templateResource{}
_ resource.ResourceWithConfigure = &templateResource{}
_ resource.ResourceWithImportState = &templateResource{}
)
// NewTemplateResource to create SLA Template
func NewTemplateResource() resource.Resource {
return &templateResource{}
}
// templateResource is the resource implementation.
type templateResource struct {
client *backupdr.APIClient
authCtx context.Context
}
// Metadata returns the resource type name.
func (r *templateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_template"
}
// Schema defines the schema for the resource.
func (r *templateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Templates are composed of backup policies. In policies, you can define when to run a backup, how frequently to run a backup, how long to retain the backup image for (Days, Weeks, Months, Years), and also additional configuration when the policy is applied to a different application, such as a file system, database, or VM. For more information, see [Backup template](https://cloud.google.com/backup-disaster-recovery/docs/create-plan/create-template).",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
MarkdownDescription: "This displays the backup template ID.",
},
"href": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
MarkdownDescription: "It displays the API URI for Backup Plan template.",
},
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Provide a name for the backup template.",
},
"option_href": schema.StringAttribute{
Computed: true,
MarkdownDescription: "It displays the API URI for Backup Plan template options",
},
"policy_href": schema.StringAttribute{
Computed: true,
MarkdownDescription: "This displays the backup policy ID.",
},
"description": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide a description for the backup template.",
},
// "immutable": schema.BoolAttribute{
// Optional: true,
// },
"managedbyagm": schema.BoolAttribute{
Optional: true,
},
"usedbycloudapp": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "It displays if the template is used by applications or not - true/false.",
},
"sourcename": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the source name. It should match the name value.",
},
"override": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Setting “Yes” will allow the policies set in this template to be overridden per-application. Setting “No” will enforce the policies as configured in this template without allowing any per-application overrides.",
},
"policies": schema.ListNestedAttribute{
Optional: true,
MarkdownDescription: "Provide policy details for backup template.",
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
MarkdownDescription: "Provide the unique policy ID within the template.",
},
"description": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the description for the backup policy.",
},
"name": schema.StringAttribute{
Required: true,
MarkdownDescription: "Provide a name for the backup policy.",
},
"href": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
MarkdownDescription: "Provide the API URI for backup plan template policy.",
},
"starttime": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the start time for the backup plan in decimal format: total seconds = (hours x 3600) + (minutes + 60) + seconds",
},
"endtime": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the end time for the backup plan.",
},
"priority": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the application priority. It can be medium, high or low. The default job priority is medium, but you can change the priority to high or low.",
},
"rpo": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide how often to run policy again. 24 is once per day.",
},
"rpom": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide PRP in hours. You can also set the RPO in minutes.",
},
"exclusiontype": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the exclusion type as daily, weekly, monthly, or yearly.",
},
"iscontinuous": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "provide true or false if the policy setting for continuous mode or windowed.",
},
"targetvault": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Provide the OnVault disk pool id. You can get the from the **management console** > **Manage** > **Storage Pools**, then enabling visibility of the ID column.",
},
"sourcevault": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "Provide the OnVault disk pool id. You can get the from the **management console** > **Manage** > **Storage Pools**, then enabling visibility of the ID column.",
},
"selection": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Set what days to run the scheduled job. For example, weekly jobs on Sunday - days of week as sun.",
},
"scheduletype": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Set the schedule type as daily, weekly, monthly or yearly.",
},
// "scheduling": schema.StringAttribute{
// Optional: true,
// },
"exclusion": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Specify days, days of week, month and days of month to exclude backup snapshots.",
},
"reptype": schema.StringAttribute{
Optional: true,
MarkdownDescription: "This is used for mirror policy options.",
},
"retention": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Set how long to retain an image.",
},
"retentionm": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Set the retention in days, weeks, months, or years.",
},
"encrypt": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the encryption identifier.",
},
"repeatinterval": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the interval value. Normally set to 1.",
},
"exclusioninterval": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the exclusion interval for the template. Normally set to 1.",
},
"remoteretention": schema.Int64Attribute{
Optional: true,
MarkdownDescription: "This is used for mirror policy options.",
},
"policytype": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the backup policy type. It can be snapshot, direct to OnVault, OnVault replication, mirror, and OnVault policy.",
},
"op": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Provide the operation type. Normally set to snap, DirectOnVault, or stream_snap.",
},
"verification": schema.BoolAttribute{
Optional: true,
MarkdownDescription: "Provide the verification values as true or false.",
},
"verifychoice": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Empty value by default - to be used in future versions.",
},
"truncatelog": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Enable log truncation. This may not work as required in advanced options.",
},
},
},
},
},
}
}
// Configure adds the provider configured client to the resource.
func (r *templateResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*backupdrProvider).client
r.authCtx = req.ProviderData.(*backupdrProvider).authCtx
}
// Create a new resource.
func (r *templateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Retrieve values from plan
var plan templateResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
reqSlt := backupdr.SltRest{
Name: plan.Name.ValueString(),
// Immutable: plan.Immutable.ValueBool(),
Description: plan.Description.ValueString(),
Sourcename: plan.Sourcename.ValueString(),
Override: plan.Override.ValueString(),
}
for _, pol := range plan.Policies {
reqSlt.Policies = append(reqSlt.Policies, backupdr.PolicyRest{
Name: pol.Name.ValueString(),
Description: pol.Description.ValueString(),
Priority: pol.Priority.ValueString(),
Exclusiontype: pol.Exclusiontype.ValueString(),
Iscontinuous: pol.Iscontinuous.ValueBool(),
Rpo: pol.Rpo.ValueString(),
Rpom: pol.Rpom.ValueString(),
Starttime: pol.Starttime.ValueString(),
Endtime: pol.Endtime.ValueString(),
Scheduletype: pol.Scheduletype.ValueString(),
// Scheduling: pol.Scheduling.ValueString(),
Targetvault: int32(pol.Targetvault.ValueInt64()),
Sourcevault: int32(pol.Sourcevault.ValueInt64()),
Selection: pol.Selection.ValueString(),
Exclusion: pol.Exclusion.ValueString(),
Exclusioninterval: pol.Exclusioninterval.ValueString(),
Retention: pol.Retention.ValueString(),
Retentionm: pol.Retentionm.ValueString(),
Remoteretention: int32(pol.Remoteretention.ValueInt64()),
PolicyType: pol.PolicyType.ValueString(),
Op: pol.Op.ValueString(),
Verification: pol.Verification.ValueBool(),
Repeatinterval: pol.Repeatinterval.ValueString(),
Encrypt: pol.Encrypt.ValueString(),
Reptype: pol.Reptype.ValueString(),
Verifychoice: pol.Verifychoice.ValueString(),
})
}
// Generate API request body from plan
reqBody := backupdr.SLATemplateApiCreateSltOpts{
Body: optional.NewInterface(reqSlt),
}
// Create new entity
respObject, _, err := r.client.SLATemplateApi.CreateSlt(r.authCtx, &reqBody)
if err != nil {
resp.Diagnostics.AddError(
"Error creating SLT",
"Could not create SLA Template, unexpected error: "+err.Error(),
)
return
}
// Map response body to schema and populate Computed attribute values
plan.ID = types.StringValue(respObject.Id)
plan.Href = types.StringValue(respObject.Href)
plan.OptionHref = types.StringValue(respObject.OptionHref)
plan.PolicyHref = types.StringValue(respObject.PolicyHref)
// response doesnot show policy details
sltID, _ := strconv.Atoi(respObject.Id)
respObjectPolicies, _, err := r.client.SLATemplateApi.ListPolicies(r.authCtx, int64(sltID))
if err != nil {
resp.Diagnostics.AddError(
"Error Reading SLT Policy",
"Could not read SLT policies ID: "+respObject.Id+": "+err.Error(),
)
return
}
for i, respPol := range respObjectPolicies.Items {
plan.Policies[i].ID = types.StringValue(respPol.Id)
plan.Policies[i].Href = types.StringValue(respPol.Href)
}
// Set state to fully populated data
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read resource information.
func (r *templateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Get current state
var state templateResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get refreshed values for SLT
respObject, _, err := r.client.SLATemplateApi.GetSlt(r.authCtx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Reading SLT Template",
"Could not read SLT Template with ID: "+state.ID.ValueString()+": "+err.Error(),
)
return
}
// Overwrite items with refreshed state
state.ID = types.StringValue(respObject.Id)
state.Href = types.StringValue(respObject.Href)
state.OptionHref = types.StringValue(respObject.OptionHref)
state.PolicyHref = types.StringValue(respObject.PolicyHref)
// Get refreshed values for SLT Policy
sltID, _ := strconv.Atoi(respObject.Id)
respObjectPolicies, _, err := r.client.SLATemplateApi.ListPolicies(r.authCtx, int64(sltID))
if err != nil {
resp.Diagnostics.AddError(
"Error Reading SLT Policy",
"Could not read SLT policies ID: "+respObject.Id+": "+err.Error(),
)
return
}
if len(respObjectPolicies.Items) > 0 {
for i, respPol := range respObjectPolicies.Items {
if i < len(state.Policies) {
state.Policies[i].ID = types.StringValue(respPol.Id)
state.Policies[i].Href = types.StringValue(respPol.Href)
}
}
}
// Set refreshed state
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *templateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Retrieve values from plan
var plan templateResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get current state
var state templateResourceModel
diags = req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// update SLT template
reqSlt := backupdr.SltRest{
Name: plan.Name.ValueString(),
Description: plan.Description.ValueString(),
Override: plan.Override.ValueString(),
Href: plan.Href.ValueString(),
PolicyHref: plan.PolicyHref.ValueString(),
}
// Generate API request body from plan
reqBody := backupdr.SLATemplateApiUpdateSltOpts{
Body: optional.NewInterface(reqSlt),
}
respObject, res, err := r.client.SLATemplateApi.UpdateSlt(r.authCtx, plan.ID.ValueString(), &reqBody)
if err != nil {
resp.Diagnostics.AddError(
"Error Updating SLA Template",
"Could not update template, unexpected error: "+err.Error(),
)
return
}
if res.StatusCode != 200 {
resp.Diagnostics.AddError(
"Unable to Update SLA Template",
"An unexpected error occurred when creating the BackupDR API client. "+
"If the error is not clear, please contact the provider developers.\n\n"+
"BackupDR Client Error: "+res.Status,
)
}
// Update resource state with updated items and timestamp
// Map response body to schema and populate Computed attribute values
plan.Href = types.StringValue(respObject.Href)
plan.OptionHref = types.StringValue(respObject.OptionHref)
plan.PolicyHref = types.StringValue(respObject.PolicyHref)
// create SLT template policy
if len(plan.Policies) > len(state.Policies) {
for i, pol := range plan.Policies {
tflog.Info(ctx, "------------------ Update Method: Check Create policy : "+pol.ID.ValueString())
if pol.ID.ValueString() == "" {
tflog.Info(ctx, "------------------ Update Method: Created policy : "+pol.ID.ValueString())
reqPol := backupdr.PolicyRest{
Id: pol.ID.ValueString(),
Name: pol.Name.ValueString(),
Description: pol.Description.ValueString(),
Priority: pol.Priority.ValueString(),
Exclusiontype: pol.Exclusiontype.ValueString(),
Iscontinuous: pol.Iscontinuous.ValueBool(),
Rpo: pol.Rpo.ValueString(),
Rpom: pol.Rpom.ValueString(),
Starttime: pol.Starttime.ValueString(),
Endtime: pol.Endtime.ValueString(),
Targetvault: int32(pol.Targetvault.ValueInt64()),
Sourcevault: int32(pol.Targetvault.ValueInt64()),
Scheduletype: pol.Scheduletype.ValueString(),
Selection: pol.Selection.ValueString(),
Exclusion: pol.Exclusion.ValueString(),
Exclusioninterval: pol.Exclusioninterval.ValueString(),
Retention: pol.Retention.ValueString(),
Retentionm: pol.Retentionm.ValueString(),
Remoteretention: int32(pol.Remoteretention.ValueInt64()),
PolicyType: pol.PolicyType.ValueString(),
Op: pol.Op.ValueString(),
Verification: pol.Verification.ValueBool(),
Repeatinterval: pol.Repeatinterval.ValueString(),
Encrypt: pol.Encrypt.ValueString(),
Reptype: pol.Reptype.ValueString(),
Verifychoice: pol.Verifychoice.ValueString(),
}
// Generate API request body from plan
reqPolBody := backupdr.SLATemplateApiCreatePolicyOpts{
Body: optional.NewInterface(reqPol),
}
respPol, _, err := r.client.SLATemplateApi.CreatePolicy(r.authCtx, respObject.Id, &reqPolBody)
if err != nil {
resp.Diagnostics.AddError(
"Error Creating SLT Policy",
"Could not Create SLT policies ID: "+respObject.Id+": "+err.Error(),
)
return
}
plan.Policies[i].ID = types.StringValue(respPol.Id)
plan.Policies[i].Href = types.StringValue(respPol.Href)
}
}
}
// delete SLT template policy
if len(plan.Policies) < len(state.Policies) {
missingPolicies := findMissingPolicies(plan.Policies, state.Policies)
tflog.Info(ctx, "------------------ Update Method: Check Delete policy "+fmt.Sprint(len(missingPolicies)))
for _, pol := range missingPolicies {
tflog.Info(ctx, "------------------ Update Method: Deleted policy : "+pol.ID.ValueString())
_, err := r.client.SLATemplateApi.DeletePolicy(r.authCtx, respObject.Id, pol.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Creating SLT Policy",
"Could not Create SLT policies ID: "+respObject.Id+": "+err.Error(),
)
return
}
}
}
// update SLT template policy
for i, pol := range plan.Policies {
reqPol := backupdr.PolicyRest{
Id: pol.ID.ValueString(),
Name: pol.Name.ValueString(),
Description: pol.Description.ValueString(),
Priority: pol.Priority.ValueString(),
Exclusiontype: pol.Exclusiontype.ValueString(),
Iscontinuous: pol.Iscontinuous.ValueBool(),
Rpo: pol.Rpo.ValueString(),
Rpom: pol.Rpom.ValueString(),
Starttime: pol.Starttime.ValueString(),
Endtime: pol.Endtime.ValueString(),
Targetvault: int32(pol.Targetvault.ValueInt64()),
Sourcevault: int32(pol.Targetvault.ValueInt64()),
Scheduletype: pol.Scheduletype.ValueString(),
Selection: pol.Selection.ValueString(),
Exclusion: pol.Exclusion.ValueString(),
Exclusioninterval: pol.Exclusioninterval.ValueString(),
Retention: pol.Retention.ValueString(),
Retentionm: pol.Retentionm.ValueString(),
Remoteretention: int32(pol.Remoteretention.ValueInt64()),
PolicyType: pol.PolicyType.ValueString(),
Op: pol.Op.ValueString(),
Verification: pol.Verification.ValueBool(),
Repeatinterval: pol.Repeatinterval.ValueString(),
Encrypt: pol.Encrypt.ValueString(),
Reptype: pol.Reptype.ValueString(),
Verifychoice: pol.Verifychoice.ValueString(),
}
// Generate API request body from plan
if pol.ID.ValueString() != "" {
tflog.Info(ctx, "------------------ Update Method: Update policy : "+pol.ID.ValueString())
reqPolBody := backupdr.SLATemplateApiUpdatePolicyOpts{
Body: optional.NewInterface(reqPol),
}
// ignore if there is no diff
respPol, _, err := r.client.SLATemplateApi.UpdatePolicy(r.authCtx, respObject.Id, pol.ID.ValueString(), &reqPolBody)
if err != nil {
resp.Diagnostics.AddError(
"Error Updating SLT Policy",
"Could not Update SLT policies ID: "+pol.ID.ValueString()+": "+err.Error(),
)
return
}
plan.Policies[i].Href = types.StringValue(respPol.Href)
}
}
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *templateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Retrieve values from state
var state templateResourceModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Delete existing order
_, err := r.client.SLATemplateApi.DeleteSlt(r.authCtx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error Deleting HashiCups Order",
"Could not delete order, unexpected error: "+err.Error(),
)
return
}
}
func (r *templateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Retrieve import ID and save to id attribute
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
func findMissingPolicies(list1Policies, list2Policies []policyRestModel) []policyRestModel {
var missingPolicies []policyRestModel
// Create a map of state policies for efficient lookup
list1PolicyMap := make(map[string]bool)
for _, statePolicy := range list1Policies {
list1PolicyMap[statePolicy.ID.ValueString()] = true
}
// Iterate through plan policies and check if they exist in the state map
for _, list2Policy := range list2Policies {
if _, ok := list1PolicyMap[list2Policy.ID.ValueString()]; !ok {
missingPolicies = append(missingPolicies, list2Policy)
}
}
return missingPolicies
}