terraform-provider-file/internal/provider/file_local_snapshot/snapshot_resource.go

290 lines
11 KiB
Go

package file_local_snapshot
import (
"context"
"fmt"
"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/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
c "github.com/rancher/terraform-provider-file/internal/provider/file_client"
)
// The `var _` is a special Go construct that results in an unusable variable.
// The purpose of these lines is to make sure our LocalFileResource correctly implements the `resource.Resource“ interface.
// These will fail at compilation time if the implementation is not satisfied.
var _ resource.Resource = &LocalSnapshotResource{}
var _ resource.ResourceWithImportState = &LocalSnapshotResource{}
func NewLocalSnapshotResource() resource.Resource {
return &LocalSnapshotResource{}
}
type LocalSnapshotResource struct {
client c.FileClient
}
// LocalSnapshotResourceModel describes the resource data model.
type LocalSnapshotResourceModel struct {
Id types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Directory types.String `tfsdk:"directory"`
LocalSnapshot types.String `tfsdk:"snapshot"`
UpdateTrigger types.String `tfsdk:"update_trigger"`
Compress types.Bool `tfsdk:"compress"`
}
func (r *LocalSnapshotResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_local_snapshot" // file_local_snapshot
}
func (r *LocalSnapshotResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "File LocalSnapshot resource. \n" +
"This resource saves some content in state and doesn't update it until the trigger argument changes. " +
"The refresh phase doesn't update state, instead " +
"the state can only change on create or update and only when the update_trigger argument changes.",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
MarkdownDescription: "Name of the file to save. Changing this forces recreate, moving the file isn't supported.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(), // the location of the file shouldn't change
},
},
"directory": schema.StringAttribute{
MarkdownDescription: "Path of the file to save. Changing this forces recreate, moving the file isn't supported.",
Optional: true,
Computed: true, // whenever an argument has a default value it should have Computed: true
Default: stringdefault.StaticString("."),
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(), // the location of the file shouldn't change
},
},
"update_trigger": schema.StringAttribute{
MarkdownDescription: "When this argument changes the snapshot will be updated.",
Required: true,
},
"compress": schema.BoolAttribute{
MarkdownDescription: "Whether the provider should compress the contents and snapshot or not. Defaults to 'false'. " +
"When set to 'true' the provider will compress the contents and snapshot attributes using the gzip compression algorithm. " +
"Changing this attribute forces recreate, compressing snapshots which are already saved in state isn't supported. " +
"Warning! To prevent memory errors the provider generates temporary files to facilitate encoding and compression.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(), // compressing files which were previously uncompressed isn't supported
},
},
"snapshot": schema.StringAttribute{
MarkdownDescription: "Base64 encoded contents of the file specified in the name and directory fields. " +
"This data will be added on create and only updated when the update_trigger field changes. " +
"Warning! To prevent memory errors the provider generates temporary files to facilitate encoding and compression.",
Computed: true,
Sensitive: true,
},
"id": schema.StringAttribute{
MarkdownDescription: "Unique identifier for the resource. The SHA256 hash of the base64 encoded contents.",
Computed: true,
},
},
}
}
func (r *LocalSnapshotResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
// This only configures the provider, so anything here must be available in the provider package to configure.
// If you want to configure a client, do that in the Create/Read/Update/Delete functions.
if req.ProviderData == nil {
return
}
}
// We should:
// - generate reality and state to match plan in the Create function
// - update state to match reality in the Read function
// - update reality and state to match plan in the Update function (don't compare old state, just override)
// - destroy reality in the Destroy function (state is handled automatically)
func (r *LocalSnapshotResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Create Request Object: %+v", req))
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsFileClient.")
r.client = &c.OsFileClient{}
}
var plan LocalSnapshotResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
pName := plan.Name.ValueString()
pDir := plan.Directory.ValueString()
pCompress := plan.Compress.ValueBool()
name := pName
if pCompress {
err := r.client.Compress(pDir, pName, "compressed_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error compressing file: ", err.Error())
return
}
name = "compressed_" + pName
}
err := r.client.Encode(pDir, name, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error encoding file: ", err.Error())
return
}
_, encodedContents, err := r.client.Read(pDir, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error reading encoded file: ", err.Error())
return
}
plan.LocalSnapshot = types.StringValue(encodedContents)
hash, err := r.client.Hash(pDir, pName)
if err != nil {
resp.Diagnostics.AddError("Error hashing file: ", err.Error())
return
}
plan.Id = types.StringValue(hash)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
if pCompress {
err := r.client.Delete(pDir, "compressed_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error cleaning up temporary compressed file: ", err.Error())
return
}
}
err = r.client.Delete(pDir, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error cleaning up temporary encoded file: ", err.Error())
return
}
tflog.Debug(ctx, fmt.Sprintf("Create Response Object: %+v", *resp))
}
func (r *LocalSnapshotResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Read Request Object: %+v", req))
var state LocalSnapshotResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// read is a no-op
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
tflog.Debug(ctx, fmt.Sprintf("Read Response Object: %+v", *resp))
}
// a difference between the plan and the state has been found.
// we want to update reality and state to match the plan.
// our snapshot will only update if the update trigger has changed.
func (r *LocalSnapshotResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Update Request Object: %+v", req))
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsFileClient.")
r.client = &c.OsFileClient{}
}
var plan LocalSnapshotResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
pName := plan.Name.ValueString()
pDir := plan.Directory.ValueString()
pUpdateTrigger := plan.UpdateTrigger.ValueString()
var state LocalSnapshotResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
sUpdateTrigger := state.UpdateTrigger.ValueString()
sLocalSnapshot := state.LocalSnapshot.ValueString()
sCompress := state.Compress.ValueBool()
sId := state.Id.ValueString()
plan.Id = types.StringValue(sId)
if pUpdateTrigger != sUpdateTrigger {
tflog.Debug(ctx, fmt.Sprintf("Update trigger has changed from %s to %s, updating snapshot.", sUpdateTrigger, pUpdateTrigger))
name := pName
if sCompress {
err := r.client.Compress(pDir, pName, "compressed_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error compressing file: ", err.Error())
return
}
name = "compressed_" + pName
}
err := r.client.Encode(pDir, name, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error encoding file: ", err.Error())
return
}
_, encodedContents, err := r.client.Read(pDir, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error reading encoded file: ", err.Error())
return
}
plan.LocalSnapshot = types.StringValue(encodedContents)
} else {
tflog.Debug(ctx, fmt.Sprintf("Update trigger hasn't changed, keeping previous snapshot (%s).", sLocalSnapshot))
plan.LocalSnapshot = types.StringValue(sLocalSnapshot)
}
tflog.Debug(ctx, fmt.Sprintf("Setting state to this plan: \n%+v", &plan))
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
if sCompress {
err := r.client.Delete(pDir, "compressed_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error cleaning up temporary compressed file: ", err.Error())
return
}
}
err := r.client.Delete(pDir, "encoded_"+pName)
if err != nil {
resp.Diagnostics.AddError("Error cleaning up temporary encoded file: ", err.Error())
return
}
tflog.Debug(ctx, fmt.Sprintf("Update Response Object: %+v", *resp))
}
func (r *LocalSnapshotResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
tflog.Debug(ctx, fmt.Sprintf("Delete Request Object: %+v", req))
// delete is a no-op
tflog.Debug(ctx, fmt.Sprintf("Delete Response Object: %+v", *resp))
}
func (r *LocalSnapshotResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}